Compare commits

...

66 Commits

Author SHA1 Message Date
Charlie Marsh
b6d41b9560 Reorder 2023-07-20 17:40:26 -04:00
Charlie Marsh
bcec2f0c4c Move undefined-local into a post-model-building pass (#5928)
## Summary

Similar to #5852 and a bunch of related PRs -- trying to move rules that
rely on point-in-time semantic analysis to _after_ the semantic model
building.
2023-07-20 15:34:22 -04:00
qdegraaf
2cde9b8aa6 [flake8-pyi] Implement PYI017 (#5895)
## Summary

Implements `PYI017` or `Y017` from `flake8-pyi` plug-in. Mirrors
[upstream
implementation](ceab86d16b/pyi.py (L1039-L1048)).
It checks for any assignment with more than 1 target or an assignment to
anything other than a name, and raises a violation for these in stub
files.

Couldn't find a clear and concise explanation for why this is to be
avoided and what is preferred for attribute cases like:

```python
a.b = int
```
So welcome some input there, to learn and to finish up the docs.

## Test Plan

Added test cases from upstream plug-in in a fixture (both `.py` and
`.pyi`). Added a few more.

## Issue link

Refers: https://github.com/astral-sh/ruff/issues/848
2023-07-20 16:35:38 +00:00
Charlie Marsh
c948dcc203 Restore redefined-while-unused violations in classes (#5926)
## Summary

This is a regression from a recent refactor whereby we moved these
checks to a deferred pass.

Closes https://github.com/astral-sh/ruff/issues/5918.
2023-07-20 12:10:26 -04:00
Luc Khai Hai
b866cbb33d Improve slice formatting (#5922)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

- Remove space when start of slice is empty
- Treat unary op except `not` as simple expression

## Test Plan

Add some simple tests for unary op expressions in slice

Closes #5673
2023-07-20 15:05:18 +00:00
Micha Reiser
d351761f5d SimpleTokenizer: Fix infinite loop when lexing empty quotes (#5917) 2023-07-20 15:18:35 +02:00
Tom Kuson
ccc6bd5df0 Fix typo in documentation (#5914) 2023-07-20 13:06:28 +02:00
Micha Reiser
eeb8a5fe0a Avoid line break before for in comprehension if outer expression expands (#5912) 2023-07-20 10:07:22 +00:00
konsti
c2b7b46717 Extend shrinking script to also remove tokens and characters (#5898)
This shrinks a good bit more than previously, which was helpful for all
the formatter bugs. fwiw i treat this as a very ad-hoc script since it's
mainly my ecosystem bug processing companion.
2023-07-20 12:02:00 +02:00
Micha Reiser
6fd8574a0b Only run jobs if relevant files changed (#5908) 2023-07-20 10:01:08 +00:00
Micha Reiser
76e9ce6dc0 Fix SimpleTokenizer's backward lexing of # (#5878) 2023-07-20 11:54:18 +02:00
konsti
8c5f8a8aef Formatter: Small RParen refactoring (#5885)
## Summary

A bit more consistency inspired by
https://github.com/astral-sh/ruff/pull/5882#discussion_r1268182403

## Test Plan

Existing tests (refactoring)
2023-07-20 11:30:39 +02:00
konsti
92f471a666 Handle io errors gracefully (#5611)
## Summary

It can happen that we can't read a file (a python file, a jupyter
notebook or pyproject.toml), which needs to be handled and handled
consistently for all file types. Instead of using `Err` or `error!`, we
emit E602 with the io error as message and continue. This PR makes sure
we handle all three cases consistently, emit E602.

I'm not convinced that it should be possible to disable io errors, but
we now handle the regular case consistently and at least print warning
consistently.

I went with `warn!` but i can change them all to `error!`, too.

It also checks the error case when a pyproject.toml is not readable. The
error message is not very helpful, but it's now a bit clearer that
actually ruff itself failed instead vs this being a diagnostic.

## Examples

This is how an Err of `run` looks now:


![image](https://github.com/astral-sh/ruff/assets/6826232/890f7ab2-2309-4b6f-a4b3-67161947cc83)

With an unreadable file and `IOError` disabled:


![image](https://github.com/astral-sh/ruff/assets/6826232/fd3d6959-fa23-4ddf-b2e5-8d6022df54b1)

(we lint zero files but count files before linting not during so we exit
0)

I'm not sure if it should (or if we should take a different path with
manual ExitStatus), but this currently also triggers when `files` is
empty:


![image](https://github.com/astral-sh/ruff/assets/6826232/f7ede301-41b5-4743-97fd-49149f750337)

## Test Plan

Unix only: Create a temporary directory with files with permissions
`000` (not readable by the owner) and run on that directory. Since this
breaks the assumptions of most of the test code (single file, `ruff`
instead of `ruff_cli`), the test code is rather cumbersome and looks a
bit misplaced; i'm happy about suggestions to fit it in closer with the
other tests or streamline it in other ways. I added another test for
when the entire directory is not readable.
2023-07-20 11:30:14 +02:00
Micha Reiser
029fe05a5f Playground: Fix escaped quotes handling (#5906)
Co-authored-by: konsti <konstin@mailbox.org>
2023-07-20 09:25:27 +00:00
Chris Pryer
9e32585cb1 Use dangling_node_comments in lambda formatting (#5903) 2023-07-20 08:52:32 +02:00
Charlie Marsh
fe7505b738 Move undefined deletions into post-model-building pass (#5904)
## Summary

Similar to #5902, but for undefined names in deletions (e.g., `del x`
where `x` is unbound).
2023-07-20 05:14:46 +00:00
Tom Kuson
266e684192 Add flake8-fixme documentation (#5868)
## Summary

Completes documentation for the `flake8-fixme` (`FIX`) ruleset. Related
to #2646.

Tweaks the violation message. For example,

```
FIX001 Line contains FIXME
```

becomes

```
FIX001 Line contains FIXME, consider resolving the issue
```

This is because the previous message was unclear if it was warning
against the use of FIXME tags per se, or the code the FIXME tag was
annotating.


## Test Plan

`cargo test && python scripts/check_docs_formatted.py`
2023-07-20 02:21:55 +00:00
Simon Brugman
4bba0bcab8 [flake8-use-pathlib] Implement os-path-getsize and os-path-get(a|m|c)-time (PTH202-205) (#5835)
Reviving https://github.com/astral-sh/ruff/pull/2348 step by step

Pt 3. implement detection for:
- `os.path.getsize`
- `os.path.getmtime`
- `os.path.getctime`
- `os.path.getatime`
2023-07-20 02:05:13 +00:00
Simon Brugman
d35cb6942f [flake8-use-pathlib] Implement path-constructor-default-argument (PTH201) (#5833)
Reviving https://github.com/astral-sh/ruff/pull/2348 step by step

Pt 2. PTH201: Path Constructor Default Argument

- rule originates from `refurb`:
https://github.com/charliermarsh/ruff/issues/1348
- Using PTH201 rather than FURBXXX to keep all pathlib logic together
2023-07-20 01:50:54 +00:00
Victor Hugo Gomes
a37d91529b [flake8-pyi] Implement PYI026 (#5844)
## Summary
Checks for `typehint.TypeAlias` annotation in type aliases. See
[original
source](https://github.com/PyCQA/flake8-pyi/blob/main/pyi.py#L1085).
```
$ flake8 --select Y026 crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:4:1: Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "NewAny: TypeAlias = Any"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:5:1: Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "OptinalStr: TypeAlias = typing.Optional[str]"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:6:1: Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "Foo: TypeAlias = Literal['foo']"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:7:1: Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "IntOrStr: TypeAlias = int | str"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:8:1: Y026 Use typing_extensions.TypeAlias for type aliases, e.g. "AliasNone: TypeAlias = None"
```

```
$ ./target/debug/ruff --select PYI026 crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi --no-cache
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:4:1: PYI026 Use `typing.TypeAlias` for type aliases in `NewAny`, e.g. "NewAny: typing.TypeAlias = Any"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:5:1: PYI026 Use `typing.TypeAlias` for type aliases in `OptinalStr`, e.g. "OptinalStr: typing.TypeAlias = typing.Optional[str]"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:6:1: PYI026 Use `typing.TypeAlias` for type aliases in `Foo`, e.g. "Foo: typing.TypeAlias = Literal["foo"]"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:7:1: PYI026 Use `typing.TypeAlias` for type aliases in `IntOrStr`, e.g. "IntOrStr: typing.TypeAlias = int | str"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI026.pyi:8:1: PYI026 Use `typing.TypeAlias` for type aliases in `AliasNone`, e.g. "AliasNone: typing.TypeAlias = None"
Found 5 errors.
```

ref: #848 

## Test Plan

Snapshots, manual runs of flake8.
2023-07-20 01:39:55 +00:00
Charlie Marsh
963f240e46 Track unresolved references in the semantic model (#5902)
## Summary

As part of my continued quest to separate semantic model-building from
diagnostic emission, this PR moves our unresolved-reference rules to a
deferred pass. So, rather than emitting diagnostics as we encounter
unresolved references, we now track those unresolved references on the
semantic model (just like resolved references), and after traversal,
emit the relevant rules for any unresolved references.
2023-07-19 18:19:55 -04:00
Tom Kuson
23cde4d1f5 Add known problems to compare-to-empty-string documentation (#5879)
## Summary

Add known problems to `compare-to-empty-string` documentation. Related
to #5873.

Tweaked the example in the documentation to be a tad more concise and
correct (that the rule is most applicable when comparing to a `str`
variable).

## Test Plan

`python scripts/check_docs_formatted.py`
2023-07-19 18:12:27 -04:00
Charlie Marsh
9834c69c98 Remove __all__ enforcement rules out of binding phase (#5897)
## Summary

This PR moves two rules (`invalid-all-format` and `invalid-all-object`)
out of the name-binding phase, and into the dedicated pass over all
bindings that occurs at the end of the `Checker`. This is part of my
continued quest to separate the semantic model-building logic from the
actual rule enforcement.
2023-07-19 21:18:47 +00:00
Zanie Blue
b27f0fa433 Implement any_over_expr for type alias and type params (#5866)
Part of https://github.com/astral-sh/ruff/issues/5062
2023-07-19 16:17:06 -05:00
konsti
a459d8ffc7 Filter off-by-default RUF014 out of schema (#5832)
**Summary** Previously, `RUF014` would be part of ruff.schema.json
depending on whether or not the `unreachable-code` feature was active.
This caused problems for contributors who got unrelated RUF014 changes
when updating the schema without the feature active.

An alternative would be to always add `RUF014`.

**Test plan** `cargo dev generate-all` and `cargo run --bin ruff_dev
--features unreachable-code -- generate-all` now have the same effect.
2023-07-19 21:06:10 +00:00
Charlie Marsh
598549d24e Fix incorrect reference in extend-immutable-calls documentation (#5890) 2023-07-19 19:57:05 +00:00
David Cain
e1d76b60cc Add missing backtick to B034 documentation (#5889)
This is a great rule, but the documentation page shows some wonky
formatting due to a missing backtick. Fix a typo too.

Should fix display on
https://beta.ruff.rs/docs/rules/re-sub-positional-args/

<img width="1160" alt="image"
src="https://github.com/astral-sh/ruff/assets/901169/44bd76ec-9eb9-4290-ba7a-7691a7ea21d4">
2023-07-19 17:25:36 +00:00
Pedro
6f96acfd27 Rename Pynecone to Reflex (#5888)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

They just changed the name to `Reflex`

## Test Plan

Nothing
2023-07-19 18:46:49 +02:00
Micha Reiser
5a4317c688 Remove multithreading from check multiproject (#5884) 2023-07-19 16:18:30 +00:00
Charlie Marsh
5f3da9955a Rename ruff_python_whitespace to ruff_python_trivia (#5886)
## Summary

This crate now contains utilities for dealing with trivia more broadly:
whitespace, newlines, "simple" trivia lexing, etc. So renaming it to
reflect its increased responsibilities.

To avoid conflicts, I've also renamed `Token` and `TokenKind` to
`SimpleToken` and `SimpleTokenKind`.
2023-07-19 11:48:27 -04:00
Charlie Marsh
a75a6de577 Use a boxed slice for Export struct (#5887)
## Summary

The vector of names here is immutable -- we never push to it after
initialization. Boxing reduces the size of the variant from 32 bytes to
24 bytes. (See:
https://nnethercote.github.io/perf-book/type-sizes.html#boxed-slices.)
It doesn't make a difference here, since it's not the largest variant,
but it still seems like a prudent change (and I was considering adding
another field to this variant, though I may no longer do so).
2023-07-19 11:45:04 -04:00
konsti
a227775f62 Type alias stub for formatter (#5880)
**Summary** This replaces the `todo!()` with a type alias stub in the
formatter. I added the tests from
704eb40108/parser/src/parser.rs (L901-L936)
as ruff python formatter tests.

**Test Plan** None, testing is part of the actual implementation
2023-07-19 17:28:07 +02:00
konsti
a51606a10a Handle parentheses when formatting slice expressions (#5882)
**Summary** Fix the formatter crash with `x[(1) :: ]` and related code.

**Problem** For assigning comments in slices in subscripts, we need to
find the positions of the colons to assign comments before and after the
colon to the respective lower/upper/step node (or dangling in that
section). Formatting `x[(1) :: ]` was broken because we were looking for
a `:` after the `1` but didn't consider that there could be a `)`
outside the range of the lower node, which contains just the `1` and no
optional parentheses.

**Solution** Use the simple tokenizer directly and skip all closing
parentheses.

**Test Plan** I added regression tests.

Closes #5733
2023-07-19 15:25:25 +00:00
konsti
63ed7a31e8 Add message to formatter SyntaxError (#5881)
**Summary** Add a static string error message to the formatter syntax
error so we can disambiguate where the syntax error came from

**Test Plan** No fixed tests, we don't expect this to occur, but it
helped with transformers syntax error debugging:

```
Error: Failed to format node

Caused by:
    syntax error: slice first colon token was not a colon
```
2023-07-19 17:15:26 +02:00
Micha Reiser
46a17d11f3 playground: Add AST/Tokens/Formatter panels (#5859) 2023-07-19 14:46:08 +00:00
Micha Reiser
9ed7ceeb0a playground: Add left panel and use brand colors (#5838) 2023-07-19 16:33:32 +02:00
Chris Pryer
9fb8d6e999 Omit tuple parentheses inside comprehensions (#5790) 2023-07-19 12:05:38 +00:00
Chris Pryer
38678142ed Format lambda expression (#5806) 2023-07-19 11:47:56 +00:00
David Szotten
5d68ad9008 Format expr generator exp (#5804) 2023-07-19 13:01:58 +02:00
Micha Reiser
cda90d071c Upgrade cargo insta (#5872) 2023-07-19 12:56:32 +02:00
Dhruv Manilawala
7e6b472c5b Make lint_only aware of the source kind (#5876) 2023-07-19 09:29:35 +05:30
Charlie Marsh
1181d25e5a Move a few more candidate rules to the deferred Binding-only pass (#5853)
## Summary

No behavior change, but this is in theory more efficient, since we can
just iterate over the flat `Binding` vector rather than having to
iterate over binding chains via the `Scope`.
2023-07-19 00:59:02 +00:00
Charlie Marsh
626d8dc2cc Use .as_ref() in lieu of &** (#5874)
I find this less opaque (and often more succinct).
2023-07-19 00:49:13 +00:00
Charlie Marsh
7ffcd93afd Move unused deletion tracking to deferred analysis (#5852)
## Summary

This PR moves the "unused exception" rule out of the visitor and into a
deferred check. When we can base rules solely on the semantic model, we
probably should, as it greatly simplifies the `Checker` itself.
2023-07-18 20:43:12 -04:00
Charlie Marsh
2d505e2b04 Remove suite body tracking from SemanticModel (#5848)
## Summary

The `SemanticModel` currently stores the "body" of a given `Suite`,
along with the current statement index. This is used to support "next
sibling" queries, but we only use this in exactly one place -- the rule
that simplifies constructs like this to `any` or `all`:

```python
for x in y:
    if x == 0:
        return True
return False
```

Instead of tracking the state, we can just do a (slightly more
expensive) traversal, by finding the node within its parent and
returning the next node in the body.

Note that we'll only have to do this extremely rarely -- namely, for
functions that contain something like:

```python
for x in y:
    if x == 0:
        return True
```
2023-07-18 18:58:31 -04:00
Zanie Blue
a93254f026 Implement unparse for type aliases and parameters (#5869)
Part of https://github.com/astral-sh/ruff/issues/5062
2023-07-18 16:25:49 -05:00
Micha Reiser
c577045f2e perf(formatter): Use memchar for faster back tokenization (#5823) 2023-07-18 21:05:55 +00:00
Charlie Marsh
4204fc002d Remove exception-handler lexing from unused-bound-exception fix (#5851)
## Summary

The motivation here is that it will make this rule easier to rewrite as
a deferred check. Right now, we can't run this rule in the deferred
phase, because it depends on the `except_handler` to power its autofix.
Instead of lexing the `except_handler`, we can use the `SimpleTokenizer`
from the formatter, and just lex forwards and backwards.

For context, this rule detects the unused `e` in:

```python
try:
  pass
except ValueError as e:
  pass
```
2023-07-18 18:27:46 +00:00
Zanie Blue
41da52a61b Implement TokenKind for type aliases (#5870)
Part of https://github.com/astral-sh/ruff/issues/5062
2023-07-18 18:21:51 +00:00
Zanie Blue
d5c43a45b3 Implement Comparable for type aliases and parameters (#5865)
Part of https://github.com/astral-sh/ruff/issues/5062
2023-07-18 17:18:14 +00:00
Nikita Sobolev
cdfed3d50e Use relativize_path for noqa warnings (#5867)
Refs https://github.com/astral-sh/ruff/pull/5856
2023-07-18 12:44:32 -04:00
Harutaka Kawamura
68097e34e6 Update UP032 to autofix multi-line triple-quoted string (#5862)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Resolve #5854

## Test Plan

<!-- How was it tested? -->

New test cases

---------

Co-authored-by: konsti <konstin@mailbox.org>
2023-07-18 16:40:37 +00:00
Zanie Blue
f47443014e Remove tag information from RustPython-Parser dependency (#5861)
Following https://github.com/astral-sh/RustPython-Parser/pull/27 we now
cherry-pick commits onto our fork instead of rebasing our fork on top of
the upstream which means we do not overwrite history and a tag is not
necessary to preserve the pinned commit.

In the future, we may rewrite the history in our fork. If we do, we
should return to tagging the commits.
2023-07-18 10:48:51 -05:00
Zanie Blue
0eab4b3c22 Implement AnyNode and AnyNodRef for StmtTypeAlias (#5863)
Part of https://github.com/astral-sh/ruff/issues/5062
2023-07-18 10:44:55 -05:00
Charlie Marsh
c868def374 Unroll collect_call_path to speed up common cases (#5792)
## Summary

This PR just naively unrolls `collect_call_path` to handle attribute
resolutions of up to eight segments. In profiling via Instruments, it
seems to be about 4x faster for a very hot code path (4% of total
execution time on `main`, 1% here).

Profiling by running `RAYON_NUM_THREADS=1 cargo instruments -t time
--profile release-debug --time-limit 10000 -p ruff_cli -o
FromSlice.trace -- check crates/ruff/resources/test/cpython --silent -e
--no-cache --select ALL`, and modifying the linter to loop infinitely up
to the specified time (10 seconds) to increase sample size.

Before:

<img width="1792" alt="Screen Shot 2023-07-15 at 5 13 34 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/4a8b0b45-8b67-43e9-af5e-65b326928a8e">

After:

<img width="1792" alt="Screen Shot 2023-07-15 at 8 38 51 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/d8829159-2c79-4a49-ab3c-9e4e86f5b2b1">
2023-07-18 11:29:59 -04:00
konsti
5d41c832ad Formatter: Run generate.py for ElifElseClauses (#5864)
**Summary** This removes the diff for the next user of `generate.py`.
It's effectively a refactoring.

**Test Plan** No functional changes
2023-07-18 17:17:17 +02:00
Nikita Sobolev
0c7c81aa31 Add filename to noqa warnings (#5856)
## Summary

Before:

```
» ruff litestar tests --fix
warning: Invalid `# noqa` directive on line 19: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on line 65: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on line 74: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on line 22: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on line 66: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on line 75: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
```

After:

```
» cargo run --bin ruff ../litestar/litestar ../litestar/tests
    Finished dev [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/ruff ../litestar/litestar ../litestar/tests`
warning: Detected debug build without --no-cache.
warning: Invalid `# noqa` directive on /Users/sobolev/Desktop/litestar/tests/unit/test_contrib/test_sqlalchemy/models_bigint.py:19: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on /Users/sobolev/Desktop/litestar/tests/unit/test_contrib/test_sqlalchemy/models_bigint.py:65: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on /Users/sobolev/Desktop/litestar/tests/unit/test_contrib/test_sqlalchemy/models_bigint.py:74: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on /Users/sobolev/Desktop/litestar/tests/unit/test_contrib/test_sqlalchemy/models_uuid.py:22: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on /Users/sobolev/Desktop/litestar/tests/unit/test_contrib/test_sqlalchemy/models_uuid.py:66: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
warning: Invalid `# noqa` directive on /Users/sobolev/Desktop/litestar/tests/unit/test_contrib/test_sqlalchemy/models_uuid.py:75: expected a comma-separated list of codes (e.g., `# noqa: F401, F841`).
```

## Test Plan

I didn't find any existing tests with this warning.

Closes https://github.com/astral-sh/ruff/issues/5855
2023-07-18 14:08:22 +00:00
Micha Reiser
3b32e3a8fe perf(formatter): Improve is_expression_parenthesized performance (#5825) 2023-07-18 15:48:49 +02:00
Charlie Marsh
1aa851796e Add documentation to Checker (#5849)
## Summary

Documents the overall responsibilities along with the various steps in
the data flow.
2023-07-18 07:52:04 -04:00
konsti
730e6b2b4c Refactor StmtIf: Formatter and Linter (#5459)
## Summary

Previously, `StmtIf` was defined recursively as
```rust
pub struct StmtIf {
    pub range: TextRange,
    pub test: Box<Expr>,
    pub body: Vec<Stmt>,
    pub orelse: Vec<Stmt>,
}
```
Every `elif` was represented as an `orelse` with a single `StmtIf`. This
means that this representation couldn't differentiate between
```python
if cond1:
    x = 1
else:
    if cond2:
        x = 2
```
and 
```python
if cond1:
    x = 1
elif cond2:
    x = 2
```
It also makes many checks harder than they need to be because we have to
recurse just to iterate over an entire if-elif-else and because we're
lacking nodes and ranges on the `elif` and `else` branches.

We change the representation to a flat

```rust
pub struct StmtIf {
    pub range: TextRange,
    pub test: Box<Expr>,
    pub body: Vec<Stmt>,
    pub elif_else_clauses: Vec<ElifElseClause>,
}

pub struct ElifElseClause {
    pub range: TextRange,
    pub test: Option<Expr>,
    pub body: Vec<Stmt>,
}
```
where `test: Some(_)` represents an `elif` and `test: None` an else.

This representation is different tradeoff, e.g. we need to allocate the
`Vec<ElifElseClause>`, the `elif`s are now different than the `if`s
(which matters in rules where want to check both `if`s and `elif`s) and
the type system doesn't guarantee that the `test: None` else is actually
last. We're also now a bit more inconsistent since all other `else`,
those from `for`, `while` and `try`, still don't have nodes. With the
new representation some things became easier, e.g. finding the `elif`
token (we can use the start of the `ElifElseClause`) and formatting
comments for if-elif-else (no more dangling comments splitting, we only
have to insert the dangling comment after the colon manually and set
`leading_alternate_branch_comments`, everything else is taken of by
having nodes for each branch and the usual placement.rs fixups).

## Merge Plan

This PR requires coordination between the parser repo and the main ruff
repo. I've split the ruff part, into two stacked PRs which have to be
merged together (only the second one fixes all tests), the first for the
formatter to be reviewed by @michareiser and the second for the linter
to be reviewed by @charliermarsh.

* MH: Review and merge
https://github.com/astral-sh/RustPython-Parser/pull/20
* MH: Review and merge or move later in stack
https://github.com/astral-sh/RustPython-Parser/pull/21
* MH: Review and approve
https://github.com/astral-sh/RustPython-Parser/pull/22
* MH: Review and approve formatter PR
https://github.com/astral-sh/ruff/pull/5459
* CM: Review and approve linter PR
https://github.com/astral-sh/ruff/pull/5460
* Merge linter PR in formatter PR, fix ecosystem checks (ecosystem
checks can't run on the formatter PR and won't run on the linter PR, so
we need to merge them first)
 * Merge https://github.com/astral-sh/RustPython-Parser/pull/22
 * Create tag in the parser, update linter+formatter PR
 * Merge linter+formatter PR https://github.com/astral-sh/ruff/pull/5459

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-07-18 13:40:15 +02:00
Chris Pryer
167b9356fa Update from join_with example to join_comma_separated (#5843)
## Summary

Originally `join_with` was used in the formatters README.md. Now it uses

```rs
f.join_comma_separated(item.end())
    .nodes(elts.iter())
    .finish()
```

## Test Plan

None
2023-07-18 11:03:16 +02:00
konsti
d098256c96 Add a tool for shrinking failing examples (#5731)
## Summary

For formatter instabilities, the message we get look something like
this:
```text
Unstable formatting /home/konsti/ruff/target/checkouts/deepmodeling:dpdispatcher/dpdispatcher/slurm.py
@@ -47,9 +47,9 @@
-            script_header_dict["slurm_partition_line"] = (
-                NOT_YET_IMPLEMENTED_ExprJoinedStr
-            )
+            script_header_dict[
+                "slurm_partition_line"
+            ] = NOT_YET_IMPLEMENTED_ExprJoinedStr
Unstable formatting /home/konsti/ruff/target/checkouts/deepmodeling:dpdispatcher/dpdispatcher/pbs.py
@@ -26,9 +26,9 @@
-            pbs_script_header_dict["select_node_line"] += (
-                NOT_YET_IMPLEMENTED_ExprJoinedStr
-            )
+            pbs_script_header_dict[
+                "select_node_line"
+            ] += NOT_YET_IMPLEMENTED_ExprJoinedStr
``` 

For ruff crashes. you don't even get that but just the file that crashed
it. To extract the actual bug, you'd need to manually remove parts of
the file, rerun to see if the bug still occurs (and revert if it
doesn't) until you have a minimal example.

With this script, you run

```shell
cargo run --bin ruff_shrinking -- target/checkouts/deepmodeling:dpdispatcher/dpdispatcher/slurm.py target/minirepo/code.py "Unstable formatting" "target/debug/ruff_dev format-dev --stability-check target/minirepo"
```

and get

```python
class Slurm():
    def gen_script_header(self, job):
        if resources.queue_name != "":
            script_header_dict["slurm_partition_line"] = f"#SBATCH --partition {resources.queue_name}"
```

which is an nice minimal example.

I've been using this script and it would be easier for me if this were
part of main. The main disadvantage to merging is that it adds
additional dependencies.

## Test Plan

I've been using this for a number of minimization. This is an internal
helper script you only run manually. I could add a test that minimizes a
rule violation if required.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-07-18 08:03:35 +00:00
Micha Reiser
ef58287c16 playground: Merge Editor state variables (#5831)
<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR removes state variables that can be derived, merges related variables into a single state, and generally avoids `null` states. 

## Test Plan

I clicked through the playground locally
<!-- How was it tested? -->
2023-07-18 08:08:24 +02:00
Micha Reiser
9ddf40455d Upgrade playground dependencies (#5830)
<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

This PR upgrades the playground's runtime and dev dependencies

<!-- What's the purpose of the change? What does it do, and why? -->

## Test Plan

I tested the playground locally

<!-- How was it tested? -->
2023-07-18 08:00:54 +02:00
Harutaka Kawamura
a4e5e3205f Ignore directories when collecting files to lint (#5775)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

Fixes #5739

## Test Plan

<!-- How was it tested? -->

Manually tested:

```sh
$ tree dir
dir
├── dir.py
│   └── file.py
└── file.py

1 directory, 2 files

$ cargo run -p ruff_cli -- check dir --no-cache
    Finished dev [unoptimized + debuginfo] target(s) in 0.08s
     Running `target/debug/ruff check dir --no-cache`
dir/dir.py/file.py:1:7: F821 Undefined name `a`
dir/file.py:1:7: F821 Undefined name `a`
Found 2 errors.
```

Is a unit test needed?
2023-07-17 20:25:43 -05:00
Simon Brugman
17ee80363a refactor: use find_keyword ast helper more (#5847)
Use the ast helper function `find_keyword` where applicable

(found these while working on another feature)
2023-07-17 19:37:23 -04:00
368 changed files with 15337 additions and 10202 deletions

View File

@@ -2,6 +2,14 @@ name: Benchmark
on:
pull_request:
paths:
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain'
- 'crates/**'
- '!crates/ruff_dev'
- '!crates/ruff_shrinking'
workflow_dispatch:
concurrency:

View File

@@ -19,6 +19,41 @@ env:
PYTHON_VERSION: "3.11" # to build abi3 wheels
jobs:
determine_changes:
name: "Determine changes"
runs-on: ubuntu-latest
outputs:
linter: ${{ steps.linter.outputs.any_changed }}
formatter: ${{ steps.formatter.outputs.any_changed }}
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: tj-actions/changed-files@v37
id: linter
with:
files: |
Cargo.toml
Cargo.lock
crates/**
!crates/ruff_python_formatter/**
!crates/ruff_formatter/**
!crates/ruff_dev/**
!crates/ruff_shrinking/**
- uses: tj-actions/changed-files@v37
id: formatter
with:
files: |
Cargo.toml
Cargo.lock
crates/ruff_python_formatter/**
crates/ruff_formatter/**
crates/ruff_python_trivia/**
crates/ruff_python_ast/**
cargo-fmt:
name: "cargo fmt"
runs-on: ubuntu-latest
@@ -53,10 +88,12 @@ jobs:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
# cargo insta 1.30.0 fails for some reason (https://github.com/mitsuhiko/insta/issues/392)
- run: cargo install cargo-insta@=1.29.0
- name: "Install cargo insta"
uses: taiki-e/install-action@v2
with:
tool: cargo-insta
- run: pip install black[d]==23.1.0
- uses: Swatinem/rust-cache@v2
- name: "Run tests (Ubuntu)"
if: ${{ matrix.os == 'ubuntu-latest' }}
run: cargo insta test --all --all-features --unreferenced reject
@@ -135,9 +172,11 @@ jobs:
ecosystem:
name: "ecosystem"
runs-on: ubuntu-latest
needs: cargo-test
needs:
- cargo-test
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
if: github.event_name == 'pull_request'
if: github.event_name == 'pull_request' && needs.determine_changes.outputs.linter == 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -285,6 +324,8 @@ jobs:
check-formatter-stability:
name: "Check formatter stability"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true'
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"

View File

@@ -133,8 +133,8 @@ At time of writing, the repository includes the following crates:
refer to?"
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data, e.g.
the names of all built-in exceptions and which standard library types are immutable.
- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis
logic (indentation and newlines).
- `crates/ruff_python_trivia`: library crate containing Python-specific trivia utilities (e.g.,
for analyzing indentation, newlines, etc.).
- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities.
- `crates/ruff_textwrap`: library crate to indent and dedent Python source code.
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module. Powers the

408
Cargo.lock generated
View File

@@ -14,6 +14,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aho-corasick"
version = "0.7.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
dependencies = [
"memchr",
]
[[package]]
name = "aho-corasick"
version = "1.0.2"
@@ -111,9 +120,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.72"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854"
checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8"
[[package]]
name = "argfile"
@@ -126,9 +135,9 @@ dependencies = [
[[package]]
name = "assert_cmd"
version = "2.0.12"
version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88903cb14723e4d4003335bb7f8a14f27691649105346a0f0957466c096adfe6"
checksum = "86d6b683edf8d1119fe420a94f8a7e389239666aa72e65495d91c00462510151"
dependencies = [
"anstyle",
"bstr",
@@ -179,7 +188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"regex-automata",
"regex-automata 0.3.0",
"serde",
]
@@ -270,9 +279,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.3.14"
version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98330784c494e49850cb23b8e2afcca13587d2500b2e3f1f78ae20248059c9be"
checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d"
dependencies = [
"clap_builder",
"clap_derive",
@@ -281,9 +290,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.3.14"
version = "4.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e182eb5f2562a67dda37e2c57af64d720a9e010c5e860ed87c056586aeafa52e"
checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b"
dependencies = [
"anstream",
"anstyle",
@@ -334,14 +343,14 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.3.12"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050"
checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -526,10 +535,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "darling"
version = "0.20.3"
name = "ctor"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e"
checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "darling"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
dependencies = [
"darling_core",
"darling_macro",
@@ -537,27 +556,27 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.3"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621"
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
name = "darling_macro"
version = "0.20.3"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [
"darling_core",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -627,9 +646,9 @@ checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1"
[[package]]
name = "dyn-clone"
version = "1.0.12"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272"
checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30"
[[package]]
name = "either"
@@ -658,9 +677,9 @@ dependencies = [
[[package]]
name = "equivalent"
version = "1.0.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1"
[[package]]
name = "errno"
@@ -757,6 +776,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-err"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0845fa252299212f0389d64ba26f34fa32cfe41588355f21ed507c59a0f64541"
[[package]]
name = "fsevent-sys"
version = "4.1.0"
@@ -785,11 +810,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
version = "0.4.11"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df"
checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
dependencies = [
"aho-corasick",
"aho-corasick 0.7.20",
"bstr",
"fnv",
"log",
@@ -980,6 +1005,7 @@ dependencies = [
"globset",
"lazy_static",
"linked-hash-map",
"regex",
"similar",
"walkdir",
"yaml-rust",
@@ -1020,12 +1046,12 @@ dependencies = [
[[package]]
name = "is-terminal"
version = "0.4.9"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb"
dependencies = [
"hermit-abi",
"rustix 0.38.4",
"rustix 0.38.3",
"windows-sys 0.48.0",
]
@@ -1040,9 +1066,9 @@ dependencies = [
[[package]]
name = "itoa"
version = "1.0.9"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a"
[[package]]
name = "js-sys"
@@ -1179,6 +1205,15 @@ version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
[[package]]
name = "matchers"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558"
dependencies = [
"regex-automata 0.1.10",
]
[[package]]
name = "matches"
version = "0.1.10"
@@ -1294,6 +1329,16 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
dependencies = [
"overload",
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.3"
@@ -1368,10 +1413,25 @@ dependencies = [
]
[[package]]
name = "paste"
version = "1.0.14"
name = "output_vt100"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
dependencies = [
"winapi",
]
[[package]]
name = "overload"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "paste"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35"
[[package]]
name = "path-absolutize"
@@ -1498,7 +1558,7 @@ dependencies = [
"phf_shared",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -1557,9 +1617,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.4.1"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc55135a600d700580e406b4de0d59cb9ad25e344a3a091a97ded2622ec4ec6"
checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794"
[[package]]
name = "predicates"
@@ -1591,11 +1651,13 @@ dependencies = [
[[package]]
name = "pretty_assertions"
version = "1.4.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755"
dependencies = [
"ctor",
"diff",
"output_vt100",
"yansi",
]
@@ -1625,9 +1687,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.66"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
dependencies = [
"unicode-ident",
]
@@ -1647,12 +1709,12 @@ dependencies = [
[[package]]
name = "quick-junit"
version = "0.3.3"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bf780b59d590c25f8c59b44c124166a2a93587868b619fb8f5b47fb15e9ed6d"
checksum = "05b909fe9bf2abb1e3d6a97c9189a37c8105c61d03dca9ce6aace023e7d682bd"
dependencies = [
"chrono",
"indexmap 2.0.0",
"indexmap 1.9.3",
"nextest-workspace-hack",
"quick-xml",
"thiserror",
@@ -1661,18 +1723,18 @@ dependencies = [
[[package]]
name = "quick-xml"
version = "0.29.0"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.31"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
dependencies = [
"proc-macro2",
]
@@ -1745,32 +1807,47 @@ dependencies = [
[[package]]
name = "regex"
version = "1.9.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575"
checksum = "89089e897c013b3deb627116ae56a6955a72b8bed395c9526af31c9fe528b484"
dependencies = [
"aho-corasick",
"aho-corasick 1.0.2",
"memchr",
"regex-automata",
"regex-syntax",
"regex-automata 0.3.0",
"regex-syntax 0.7.3",
]
[[package]]
name = "regex-automata"
version = "0.3.3"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
dependencies = [
"aho-corasick",
"regex-syntax 0.6.29",
]
[[package]]
name = "regex-automata"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa250384981ea14565685dea16a9ccc4d1c541a13f82b9c168572264d1df8c56"
dependencies = [
"aho-corasick 1.0.2",
"memchr",
"regex-syntax",
"regex-syntax 0.7.3",
]
[[package]]
name = "regex-syntax"
version = "0.7.4"
version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab07dc67230e4a4718e70fd5c20055a4334b121f1f9db8fe63ef39ce9b8c846"
[[package]]
name = "result-like"
@@ -1852,7 +1929,7 @@ dependencies = [
"ruff_python_ast",
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_python_whitespace",
"ruff_python_trivia",
"ruff_rustpython",
"ruff_text_size",
"ruff_textwrap",
@@ -1869,6 +1946,7 @@ dependencies = [
"smallvec",
"strum",
"strum_macros",
"tempfile",
"test-case",
"thiserror",
"toml",
@@ -1927,6 +2005,7 @@ dependencies = [
"filetime",
"glob",
"ignore",
"insta",
"itertools",
"itoa",
"log",
@@ -1949,6 +2028,7 @@ dependencies = [
"shellexpand",
"similar",
"strum",
"tempfile",
"tikv-jemallocator",
"ureq",
"walkdir",
@@ -2031,7 +2111,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_textwrap",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2048,7 +2128,7 @@ dependencies = [
"num-bigint",
"num-traits",
"once_cell",
"ruff_python_whitespace",
"ruff_python_trivia",
"ruff_text_size",
"rustc-hash",
"rustpython-ast",
@@ -2072,7 +2152,7 @@ dependencies = [
"once_cell",
"ruff_formatter",
"ruff_python_ast",
"ruff_python_whitespace",
"ruff_python_trivia",
"ruff_text_size",
"rustc-hash",
"rustpython-parser",
@@ -2081,7 +2161,6 @@ dependencies = [
"similar",
"smallvec",
"thiserror",
"unic-ucd-ident",
]
[[package]]
@@ -2116,11 +2195,14 @@ name = "ruff_python_stdlib"
version = "0.0.0"
[[package]]
name = "ruff_python_whitespace"
name = "ruff_python_trivia"
version = "0.0.0"
dependencies = [
"insta",
"memchr",
"ruff_text_size",
"smallvec",
"unic-ucd-ident",
]
[[package]]
@@ -2131,10 +2213,26 @@ dependencies = [
"rustpython-parser",
]
[[package]]
name = "ruff_shrinking"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"fs-err",
"regex",
"ruff_python_ast",
"ruff_rustpython",
"rustpython-ast",
"shlex",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
dependencies = [
"schemars",
"serde",
@@ -2144,7 +2242,7 @@ dependencies = [
name = "ruff_textwrap"
version = "0.0.0"
dependencies = [
"ruff_python_whitespace",
"ruff_python_trivia",
"ruff_text_size",
]
@@ -2159,6 +2257,7 @@ dependencies = [
"ruff",
"ruff_diagnostics",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_rustpython",
"rustpython-parser",
"serde",
@@ -2199,9 +2298,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.38.4"
version = "0.38.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5"
checksum = "ac5ffa1efe7548069688cd7028f32591853cd7b5b756d41bcffd2353e4fc75b4"
dependencies = [
"bitflags 2.3.3",
"errno",
@@ -2212,13 +2311,13 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.5"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f"
dependencies = [
"log",
"ring",
"rustls-webpki 0.101.1",
"rustls-webpki",
"sct",
]
@@ -2232,20 +2331,10 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.101.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
dependencies = [
"is-macro",
"num-bigint",
@@ -2256,7 +2345,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
dependencies = [
"bitflags 2.3.3",
"itertools",
@@ -2268,7 +2357,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2280,7 +2369,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
dependencies = [
"anyhow",
"is-macro",
@@ -2303,7 +2392,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=b996b21ffca562ecb2086f632a6a0b05c245c24a#b996b21ffca562ecb2086f632a6a0b05c245c24a"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=db04fd415774032e1e2ceb03bcbf5305e0d22c8c#db04fd415774032e1e2ceb03bcbf5305e0d22c8c"
dependencies = [
"is-macro",
"memchr",
@@ -2312,15 +2401,15 @@ dependencies = [
[[package]]
name = "rustversion"
version = "1.0.14"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f"
[[package]]
name = "ryu"
version = "1.0.15"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9"
[[package]]
name = "same-file"
@@ -2363,9 +2452,9 @@ checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
@@ -2379,15 +2468,15 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.18"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed"
[[package]]
name = "serde"
version = "1.0.171"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9"
checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8"
dependencies = [
"serde_derive",
]
@@ -2405,13 +2494,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.171"
version = "1.0.166"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682"
checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2427,9 +2516,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.103"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c"
dependencies = [
"itoa",
"ryu",
@@ -2447,9 +2536,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.1.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21e47d95bc83ed33b2ecf84f4187ad1ab9685d18ff28db000c99deac8ce180e3"
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
dependencies = [
"base64",
"chrono",
@@ -2458,19 +2547,28 @@ dependencies = [
"serde",
"serde_json",
"serde_with_macros",
"time 0.3.23",
"time 0.3.22",
]
[[package]]
name = "serde_with_macros"
version = "3.1.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea3cee93715c2e266b9338b7544da68a9f24e227722ba482bd1c024367c77c65"
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
name = "sharded-slab"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31"
dependencies = [
"lazy_static",
]
[[package]]
@@ -2482,6 +2580,12 @@ dependencies = [
"dirs 5.0.1",
]
[[package]]
name = "shlex"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
[[package]]
name = "similar"
version = "2.2.1"
@@ -2496,9 +2600,9 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "smallvec"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
[[package]]
name = "spin"
@@ -2553,9 +2657,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.26"
version = "2.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970"
checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737"
dependencies = [
"proc-macro2",
"quote",
@@ -2665,7 +2769,7 @@ checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2711,9 +2815,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.23"
version = "0.3.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd"
dependencies = [
"itoa",
"serde",
@@ -2729,9 +2833,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.10"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
dependencies = [
"time-core",
]
@@ -2772,9 +2876,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.7.6"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240"
dependencies = [
"serde",
"serde_spanned",
@@ -2793,9 +2897,9 @@ dependencies = [
[[package]]
name = "toml_edit"
version = "0.19.14"
version = "0.19.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7"
dependencies = [
"indexmap 2.0.0",
"serde",
@@ -2825,7 +2929,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
]
[[package]]
@@ -2835,6 +2939,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922"
dependencies = [
"lazy_static",
"log",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]]
@@ -2915,9 +3049,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.11"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73"
[[package]]
name = "unicode-normalization"
@@ -2959,7 +3093,7 @@ dependencies = [
"log",
"once_cell",
"rustls",
"rustls-webpki 0.100.1",
"rustls-webpki",
"url",
"webpki-roots",
]
@@ -2984,9 +3118,15 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
@@ -3046,7 +3186,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
"wasm-bindgen-shared",
]
@@ -3080,7 +3220,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.26",
"syn 2.0.23",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3131,7 +3271,7 @@ version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338"
dependencies = [
"rustls-webpki 0.100.1",
"rustls-webpki",
]
[[package]]
@@ -3328,9 +3468,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.5.0"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fac9742fd1ad1bd9643b991319f72dd031016d44b77039a26977eb667141e7"
checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448"
dependencies = [
"memchr",
]

View File

@@ -21,7 +21,7 @@ filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.30.0" }
insta = { version = "1.31.0", feature = ["filters", "glob"] }
is-macro = { version = "0.2.2" }
itertools = { version = "0.10.5" }
log = { version = "0.4.17" }
@@ -52,14 +52,11 @@ wsl = { version = "0.1.0" }
# v1.0.1
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false }
# Please tag the RustPython version every time you update its revision here and in fuzz/Cargo.toml
# Tagging the version ensures that older ruff versions continue to build from source even when we rebase our RustPython fork.
# Current tag: v0.0.7
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a" , default-features = false, features = ["num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a", default-features = false }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "b996b21ffca562ecb2086f632a6a0b05c245c24a" , default-features = false, features = ["full-lexer", "num-bigint"] }
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c" , default-features = false, features = ["num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c", default-features = false }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "db04fd415774032e1e2ceb03bcbf5305e0d22c8c" , default-features = false, features = ["full-lexer", "num-bigint"] }
[profile.release]
lto = "fat"

View File

@@ -397,7 +397,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [PyTorch](https://github.com/pytorch/pytorch)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint)
- [Pynecone](https://github.com/pynecone-io/pynecone)
- [Reflex](https://github.com/reflex-dev/reflex)
- [Robyn](https://github.com/sansyrox/robyn)
- Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client))
- Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli))

View File

@@ -19,7 +19,7 @@ ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_index = { path = "../ruff_index" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_whitespace = { path = "../ruff_python_whitespace" }
ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
ruff_python_semantic = { path = "../ruff_python_semantic" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
@@ -86,6 +86,7 @@ pretty_assertions = "1.3.0"
test-case = { workspace = true }
# Disable colored output in tests
colored = { workspace = true, features = ["no-color"] }
tempfile = "3.6.0"
[features]
default = []

View File

@@ -97,3 +97,10 @@ def f():
# variable name).
for line_ in range(self.header_lines):
fp.readline()
# Regression test: visitor didn't walk the elif test
for key, value in current_crawler_tags.items():
if key:
pass
elif wanted_tag_value != value:
pass

View File

@@ -0,0 +1,14 @@
var: int
a = var # OK
b = c = int # OK
a.b = int # OK
d, e = int, str # OK
f, g, h = int, str, TypeVar("T") # OK
i: TypeAlias = int | str # OK
j: TypeAlias = int # OK

View File

@@ -0,0 +1,14 @@
var: int
a = var # OK
b = c = int # PYI017
a.b = int # PYI017
d, e = int, str # PYI017
f, g, h = int, str, TypeVar("T") # PYI017
i: TypeAlias = int | str # OK
j: TypeAlias = int # OK

View File

@@ -0,0 +1,19 @@
import typing
from typing import TypeAlias, Literal, Any
NewAny = Any
OptionalStr = typing.Optional[str]
Foo = Literal["foo"]
IntOrStr = int | str
AliasNone = None
NewAny: typing.TypeAlias = Any
OptionalStr: TypeAlias = typing.Optional[str]
Foo: typing.TypeAlias = Literal["foo"]
IntOrStr: TypeAlias = int | str
IntOrFloat: Foo = int | float
AliasNone: typing.TypeAlias = None
# these are ok
VarAlias = str
AliasFoo = Foo

View File

@@ -0,0 +1,18 @@
from typing import Literal, Any
NewAny = Any
OptionalStr = typing.Optional[str]
Foo = Literal["foo"]
IntOrStr = int | str
AliasNone = None
NewAny: typing.TypeAlias = Any
OptionalStr: TypeAlias = typing.Optional[str]
Foo: typing.TypeAlias = Literal["foo"]
IntOrStr: TypeAlias = int | str
IntOrFloat: Foo = int | float
AliasNone: typing.TypeAlias = None
# these are ok
VarAlias = str
AliasFoo = Foo

View File

@@ -100,6 +100,14 @@ if node.module0123456789:
):
print("Bad module!")
# SIM102
# Regression test for https://github.com/apache/airflow/blob/145b16caaa43f0c42bffd97344df916c602cddde/airflow/configuration.py#L1161
if a:
if b:
if c:
print("if")
elif d:
print("elif")
# OK
if a:

View File

@@ -23,7 +23,7 @@ elif a:
else:
b = 2
# OK (false negative)
# SIM108
if True:
pass
else:

View File

@@ -94,3 +94,10 @@ if result.eofs == "F":
errors = 1
else:
errors = 1
if a:
# Ignore branches with diverging comments because it means we're repeating
# the bodies because we have different reasons for each branch
x = 1
elif c:
x = 1

View File

@@ -84,3 +84,15 @@ elif func_name == "remove":
return "D"
elif func_name == "move":
return "MV"
# OK
def no_return_in_else(platform):
if platform == "linux":
return "auditwheel repair -w {dest_dir} {wheel}"
elif platform == "macos":
return "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel}"
elif platform == "windows":
return ""
else:
msg = f"Unknown platform: {platform!r}"
raise ValueError(msg)

View File

@@ -38,6 +38,15 @@ if key in a_dict:
else:
vars[idx] = "defaultß9💣26789ß9💣26789ß9💣26789ß9💣26789ß9💣26789"
# SIM401
if foo():
pass
else:
if key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "default"
###
# Negative cases
###
@@ -105,12 +114,3 @@ 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,14 @@
from pathlib import Path, PurePath
from pathlib import Path as pth
# match
_ = Path(".")
_ = pth(".")
_ = PurePath(".")
# no match
_ = Path()
print(".")
Path("file.txt")
Path(".", "folder")
PurePath(".", "folder")

View File

@@ -0,0 +1,14 @@
import os.path
from pathlib import Path
from os.path import getsize
os.path.getsize("filename")
os.path.getsize(b"filename")
os.path.getsize(Path("filename"))
os.path.getsize(__file__)
getsize("filename")
getsize(b"filename")
getsize(Path("filename"))
getsize(__file__)

View File

@@ -0,0 +1,12 @@
import os.path
from pathlib import Path
from os.path import getatime
os.path.getatime("filename")
os.path.getatime(b"filename")
os.path.getatime(Path("filename"))
getatime("filename")
getatime(b"filename")
getatime(Path("filename"))

View File

@@ -0,0 +1,13 @@
import os.path
from pathlib import Path
from os.path import getmtime
os.path.getmtime("filename")
os.path.getmtime(b"filename")
os.path.getmtime(Path("filename"))
getmtime("filename")
getmtime(b"filename")
getmtime(Path("filename"))

View File

@@ -0,0 +1,12 @@
import os.path
from pathlib import Path
from os.path import getctime
os.path.getctime("filename")
os.path.getctime(b"filename")
os.path.getctime(Path("filename"))
getctime("filename")
getctime(b"filename")
getctime(Path("filename"))

View File

@@ -1,6 +1,11 @@
if (1, 2):
pass
if (3, 4):
pass
elif foo:
pass
for _ in range(5):
if True:
pass

View File

@@ -0,0 +1,15 @@
# Regression test for branch detection from
# https://github.com/pypa/build/blob/5800521541e5e749d4429617420d1ef8cdb40b46/src/build/_importlib.py
import sys
if sys.version_info < (3, 8):
import importlib_metadata as metadata
elif sys.version_info < (3, 9, 10) or (3, 10, 0) <= sys.version_info < (3, 10, 2):
try:
import importlib_metadata as metadata
except ModuleNotFoundError:
from importlib import metadata
else:
from importlib import metadata
__all__ = ["metadata"]

View File

@@ -0,0 +1,6 @@
class Class:
def func(self):
pass
def func(self):
pass

View File

@@ -25,3 +25,17 @@ def dec(x):
def f():
dec = 1
return dec
class Class:
def f(self):
print(my_var)
my_var = 1
class Class:
my_var = 0
def f(self):
print(my_var)
my_var = 1

View File

@@ -47,3 +47,17 @@ def not_ok1():
pass
else:
pass
# Regression test for https://github.com/apache/airflow/blob/f1e1cdcc3b2826e68ba133f350300b5065bbca33/airflow/models/dag.py#L1737
def not_ok2():
if True:
print(1)
elif True:
print(2)
else:
if True:
print(3)
else:
print(4)

View File

@@ -62,6 +62,16 @@ print("foo {} ".format(x))
1111111111111111111111111111111111111111111111111111111111111111111111111,
)
"""
{}
""".format(1)
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """
{}
""".format(
111111
)
###
# Non-errors
###
@@ -99,6 +109,21 @@ r'"\N{snowman} {}".format(a)'
11111111111111111111111111111111111111111111111111111111111111111111111111,
)
"""
{}
{}
{}
""".format(
1,
2,
111111111111111111111111111111111111111111111111111111111111111111111111111111111111111,
)
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa = """{}
""".format(
111111
)
async def c():
return "{}".format(await 3)

View File

@@ -7,20 +7,20 @@ if True:
if True:
if foo:
pass
print()
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
if True:
if foo:
pass
print()
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
elif foo:
cmd = [sys.executable, "-m", "test", "-j0"]
if foo:
pass
print()
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
@@ -28,7 +28,7 @@ if True:
cmd = [sys.executable, "-m", "test.regrtest"]
if foo:
pass
print()
elif sys.version_info < (3, 3):
cmd = [sys.executable, "-m", "test.regrtest"]
else:

View File

@@ -230,6 +230,15 @@ def incorrect_multi_conditional(arg1, arg2):
raise Exception("...") # should be typeerror
def multiple_is_instance_checks(some_arg):
if isinstance(some_arg, str):
pass
elif isinstance(some_arg, int):
pass
else:
raise Exception("...") # should be typeerror
class MyCustomTypeValidation(Exception):
pass
@@ -296,6 +305,17 @@ def multiple_ifs(some_args):
pass
def else_body(obj):
if isinstance(obj, datetime.timedelta):
return "TimeDelta"
elif isinstance(obj, relativedelta.relativedelta):
return "RelativeDelta"
elif isinstance(obj, CronExpression):
return "CronExpression"
else:
raise Exception(f"Unknown object type: {obj.__class__.__name__}")
def early_return():
if isinstance(this, some_type):
if x in this:

View File

@@ -7,7 +7,7 @@ use rustpython_parser::{lexer, Mode};
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_whitespace::{is_python_whitespace, NewlineWithTrailingNewline, PythonWhitespace};
use ruff_python_trivia::{is_python_whitespace, NewlineWithTrailingNewline, PythonWhitespace};
use crate::autofix::codemods;
@@ -190,12 +190,24 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. })
| Stmt::If(ast::StmtIf { body, orelse, .. }) => {
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if is_only(body, child) || is_only(orelse, child) {
return true;
}
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if is_only(body, child)
|| elif_else_clauses
.iter()
.any(|ast::ElifElseClause { body, .. }| is_only(body, child))
{
return true;
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
//! `NoQA` enforcement and validation.
use std::path::Path;
use itertools::Itertools;
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::ast::Ranged;
@@ -16,6 +18,7 @@ use crate::settings::Settings;
pub(crate) fn check_noqa(
diagnostics: &mut Vec<Diagnostic>,
path: &Path,
locator: &Locator,
comment_ranges: &[TextRange],
noqa_line_for: &NoqaMapping,
@@ -23,10 +26,10 @@ pub(crate) fn check_noqa(
settings: &Settings,
) -> Vec<usize> {
// Identify any codes that are globally exempted (within the current file).
let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, locator);
let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, path, locator);
// Extract all `noqa` directives.
let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator);
let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
// Indices of diagnostics that were ignored by a `noqa` directive.
let mut ignored_diagnostics = vec![];

View File

@@ -5,7 +5,7 @@ use ruff_text_size::TextSize;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_whitespace::UniversalNewlines;
use ruff_python_trivia::UniversalNewlines;
use crate::comments::shebang::ShebangDirective;
use crate::registry::Rule;

View File

@@ -629,10 +629,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "014") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ArgumentDefaultInStub),
(Flake8Pyi, "015") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AssignmentDefaultInStub),
(Flake8Pyi, "016") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DuplicateUnionMember),
(Flake8Pyi, "017") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexAssignmentInStub),
(Flake8Pyi, "020") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::QuotedAnnotationInStub),
(Flake8Pyi, "021") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DocstringInStub),
(Flake8Pyi, "024") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CollectionsNamedTuple),
(Flake8Pyi, "025") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport),
(Flake8Pyi, "026") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeAliasWithoutAnnotation),
(Flake8Pyi, "029") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StrOrReprDefinedInStub),
(Flake8Pyi, "030") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnnecessaryLiteralUnion),
(Flake8Pyi, "032") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AnyEqNeAnnotation),
@@ -747,6 +749,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "122") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::BuiltinOpen),
(Flake8UsePathlib, "124") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::violations::PyPath),
(Flake8UsePathlib, "201") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory),
(Flake8UsePathlib, "202") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetsize),
(Flake8UsePathlib, "202") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetsize),
(Flake8UsePathlib, "203") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetatime),
(Flake8UsePathlib, "204") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetmtime),
(Flake8UsePathlib, "205") => (RuleGroup::Unspecified, rules::flake8_use_pathlib::rules::OsPathGetctime),
// flake8-logging-format
(Flake8LoggingFormat, "001") => (RuleGroup::Unspecified, rules::flake8_logging_format::violations::LoggingStringFormat),
@@ -782,7 +790,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "011") => (RuleGroup::Unspecified, rules::ruff::rules::StaticKeyDictComprehension),
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional),
#[cfg(feature = "unreachable-code")]
#[cfg(feature = "unreachable-code")] // When removing this feature gate, also update rules_selector.rs
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),

View File

@@ -1,4 +1,4 @@
use ruff_python_whitespace::{is_python_whitespace, Cursor};
use ruff_python_trivia::{is_python_whitespace, Cursor};
use ruff_text_size::{TextLen, TextSize};
/// A shebang directive (e.g., `#!/usr/bin/env python3`).

View File

@@ -10,7 +10,7 @@ use rustpython_parser::Tok;
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
use ruff_python_whitespace::UniversalNewlineIterator;
use ruff_python_trivia::UniversalNewlineIterator;
/// Extract doc lines (standalone comments) from a token sequence.
pub(crate) fn doc_lines_from_tokens(lxr: &[LexResult]) -> DocLines {

View File

@@ -5,7 +5,7 @@ use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
use ruff_python_whitespace::{Line, UniversalNewlineIterator, UniversalNewlines};
use ruff_python_trivia::{Line, UniversalNewlineIterator, UniversalNewlines};
use crate::docstrings::styles::SectionStyle;
use crate::docstrings::{Docstring, DocstringBody};

View File

@@ -8,7 +8,7 @@ use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_whitespace::{PythonWhitespace, UniversalNewlineIterator};
use ruff_python_trivia::{PythonWhitespace, UniversalNewlineIterator};
use ruff_textwrap::indent;
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -305,7 +305,7 @@ mod tests {
use rustpython_parser::Parse;
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_whitespace::LineEnding;
use ruff_python_trivia::LineEnding;
use super::Insertion;

View File

@@ -10,7 +10,7 @@ use serde::Serialize;
use serde_json::error::Category;
use ruff_diagnostics::Diagnostic;
use ruff_python_whitespace::{NewlineWithTrailingNewline, UniversalNewlineIterator};
use ruff_python_trivia::{NewlineWithTrailingNewline, UniversalNewlineIterator};
use ruff_text_size::{TextRange, TextSize};
use crate::autofix::source_map::{SourceMap, SourceMarker};

View File

@@ -214,6 +214,7 @@ pub fn check_path(
{
let ignored = check_noqa(
&mut diagnostics,
path,
locator,
indexer.comment_ranges(),
&directives.noqa_line_for,
@@ -320,6 +321,7 @@ pub fn lint_only(
package: Option<&Path>,
settings: &Settings,
noqa: flags::Noqa,
source_kind: Option<&SourceKind>,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> = ruff_rustpython::tokenize(contents);
@@ -352,7 +354,7 @@ pub fn lint_only(
&directives,
settings,
noqa,
None,
source_kind,
);
result.map(|(diagnostics, imports)| {

View File

@@ -13,9 +13,10 @@ use rustpython_parser::ast::Ranged;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::Locator;
use ruff_python_whitespace::LineEnding;
use ruff_python_trivia::LineEnding;
use crate::codes::NoqaCode;
use crate::fs::relativize_path;
use crate::registry::{AsRule, Rule, RuleSet};
use crate::rule_redirects::get_redirect_target;
@@ -225,6 +226,7 @@ impl FileExemption {
pub(crate) fn try_extract(
contents: &str,
comment_ranges: &[TextRange],
path: &Path,
locator: &Locator,
) -> Option<Self> {
let mut exempt_codes: Vec<NoqaCode> = vec![];
@@ -234,7 +236,8 @@ impl FileExemption {
Err(err) => {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
warn!("Invalid `# noqa` directive on line {line}: {err}");
let path_display = relativize_path(path);
warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}");
}
Ok(Some(ParsedFileExemption::All)) => {
return Some(Self::All);
@@ -437,6 +440,7 @@ pub(crate) fn add_noqa(
line_ending: LineEnding,
) -> Result<usize> {
let (count, output) = add_noqa_inner(
path,
diagnostics,
locator,
commented_lines,
@@ -448,6 +452,7 @@ pub(crate) fn add_noqa(
}
fn add_noqa_inner(
path: &Path,
diagnostics: &[Diagnostic],
locator: &Locator,
commented_ranges: &[TextRange],
@@ -460,8 +465,8 @@ fn add_noqa_inner(
// Whether the file is exempted from all checks.
// Codes that are globally exempted (within the current file).
let exemption = FileExemption::try_extract(locator.contents(), commented_ranges, locator);
let directives = NoqaDirectives::from_commented_ranges(commented_ranges, locator);
let exemption = FileExemption::try_extract(locator.contents(), commented_ranges, path, locator);
let directives = NoqaDirectives::from_commented_ranges(commented_ranges, path, locator);
// Mark any non-ignored diagnostics.
for diagnostic in diagnostics {
@@ -625,6 +630,7 @@ pub(crate) struct NoqaDirectives<'a> {
impl<'a> NoqaDirectives<'a> {
pub(crate) fn from_commented_ranges(
comment_ranges: &[TextRange],
path: &Path,
locator: &'a Locator<'a>,
) -> Self {
let mut directives = Vec::new();
@@ -634,7 +640,8 @@ impl<'a> NoqaDirectives<'a> {
Err(err) => {
#[allow(deprecated)]
let line = locator.compute_line_index(range.start());
warn!("Invalid `# noqa` directive on line {line}: {err}");
let path_display = relativize_path(path);
warn!("Invalid `# noqa` directive on {path_display}:{line}: {err}");
}
Ok(Some(directive)) => {
// noqa comments are guaranteed to be single line.
@@ -758,12 +765,14 @@ impl FromIterator<TextRange> for NoqaMapping {
#[cfg(test)]
mod tests {
use std::path::Path;
use insta::assert_debug_snapshot;
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::Locator;
use ruff_python_whitespace::LineEnding;
use ruff_python_trivia::LineEnding;
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
use crate::rules::pycodestyle::rules::AmbiguousVariableName;
@@ -946,9 +955,12 @@ mod tests {
#[test]
fn modification() {
let path = Path::new("/tmp/foo.txt");
let contents = "x = 1";
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
path,
&[],
&Locator::new(contents),
&[],
@@ -968,6 +980,7 @@ mod tests {
let contents = "x = 1";
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
path,
&diagnostics,
&Locator::new(contents),
&[],
@@ -992,6 +1005,7 @@ mod tests {
let contents = "x = 1 # noqa: E741\n";
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
path,
&diagnostics,
&Locator::new(contents),
&[TextRange::new(TextSize::from(7), TextSize::from(19))],
@@ -1016,6 +1030,7 @@ mod tests {
let contents = "x = 1 # noqa";
let noqa_line_for = NoqaMapping::default();
let (count, output) = add_noqa_inner(
path,
&diagnostics,
&Locator::new(contents),
&[TextRange::new(TextSize::from(7), TextSize::from(13))],

View File

@@ -1,4 +1,5 @@
use anyhow::Result;
use colored::Colorize;
use log::warn;
use pyproject_toml::{BuildSystem, Project};
use ruff_text_size::{TextRange, TextSize};
use serde::{Deserialize, Serialize};
@@ -22,34 +23,38 @@ struct PyProjectToml {
project: Option<Project>,
}
pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Result<Vec<Message>> {
let mut messages = vec![];
let err = match toml::from_str::<PyProjectToml>(source_file.source_text()) {
Ok(_) => return Ok(messages),
Err(err) => err,
pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Vec<Message> {
let Some(err) = toml::from_str::<PyProjectToml>(source_file.source_text()).err() else {
return Vec::default();
};
let mut messages = Vec::new();
let range = match err.span() {
// This is bad but sometimes toml and/or serde just don't give us spans
// TODO(konstin,micha): https://github.com/astral-sh/ruff/issues/4571
None => TextRange::default(),
Some(range) => {
let Ok(end) = TextSize::try_from(range.end) else {
let message = format!(
"{} is larger than 4GB, but ruff assumes all files to be smaller",
source_file.name(),
);
if settings.rules.enabled(Rule::IOError) {
let diagnostic = Diagnostic::new(
IOError {
message: "pyproject.toml is larger than 4GB".to_string(),
},
TextRange::default(),
);
let diagnostic = Diagnostic::new(IOError { message }, TextRange::default());
messages.push(Message::from_diagnostic(
diagnostic,
source_file,
TextSize::default(),
));
} else {
warn!(
"{}{}{} {message}",
"Failed to lint ".bold(),
source_file.name().bold(),
":".bold()
);
}
return Ok(messages);
return messages;
};
TextRange::new(
// start <= end, so if end < 4GB follows start < 4GB
@@ -69,5 +74,5 @@ pub fn lint_pyproject_toml(source_file: SourceFile, settings: &Settings) -> Resu
));
}
Ok(messages)
messages
}

View File

@@ -245,6 +245,7 @@ impl Renamer {
| BindingKind::NamedExprAssignment
| BindingKind::UnpackedAssignment
| BindingKind::Assignment
| BindingKind::BoundException
| BindingKind::LoopVar
| BindingKind::Global
| BindingKind::Nonlocal(_)

View File

@@ -330,9 +330,12 @@ pub fn python_files_in_path(
}
if result.as_ref().map_or(true, |entry| {
if entry.depth() == 0 {
// Ignore directories
if entry.file_type().map_or(true, |ft| ft.is_dir()) {
false
} else if entry.depth() == 0 {
// Accept all files that are passed-in directly.
entry.file_type().map_or(false, |ft| ft.is_file())
true
} else {
// Otherwise, check if the file is included.
let path = entry.path();

View File

@@ -249,6 +249,9 @@ mod schema {
(!prefix.is_empty()).then(|| prefix.to_string())
})),
)
// Filter out rule gated behind `#[cfg(feature = "unreachable-code")]`, which is
// off-by-default
.filter(|prefix| prefix != "RUF014")
.sorted()
.map(Value::String)
.collect(),
@@ -342,24 +345,33 @@ mod clap_completion {
let prefix = l.common_prefix();
(!prefix.is_empty()).then(|| PossibleValue::new(prefix).help(l.name()))
})
.chain(RuleCodePrefix::iter().map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
.chain(
RuleCodePrefix::iter()
// Filter out rule gated behind `#[cfg(feature = "unreachable-code")]`, which is
// off-by-default
.filter(|p| {
format!("{}{}", p.linter().common_prefix(), p.short_code())
!= "RUF014"
})
.map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
let mut rules_iter = p.rules();
let rule1 = rules_iter.next();
let rule2 = rules_iter.next();
let mut rules_iter = p.rules();
let rule1 = rules_iter.next();
let rule2 = rules_iter.next();
let value = PossibleValue::new(format!("{prefix}{code}"));
let value = PossibleValue::new(format!("{prefix}{code}"));
if rule2.is_none() {
let rule1 = rule1.unwrap();
let name: &'static str = rule1.into();
value.help(name)
} else {
value
}
})),
if rule2.is_none() {
let rule1 = rule1.unwrap();
let name: &'static str = rule1.into();
value.help(name)
} else {
value
}
}),
),
),
))
}

View File

@@ -26,7 +26,7 @@ pub(super) fn match_function_def(
}) => (
name,
args,
returns.as_ref().map(|expr| &**expr),
returns.as_ref().map(AsRef::as_ref),
body,
decorator_list,
),

View File

@@ -1,3 +1,4 @@
use ruff_python_ast::helpers::find_keyword;
use rustpython_parser::ast::{self, Constant, Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
@@ -37,12 +38,7 @@ pub(crate) fn jinja2_autoescape_false(checker: &mut Checker, func: &Expr, keywor
matches!(call_path.as_slice(), ["jinja2", "Environment"])
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "autoescape")
}) {
if let Some(keyword) = find_keyword(keywords, "autoescape") {
match &keyword.value {
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(true),

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_false;
use ruff_python_ast::helpers::{find_keyword, is_const_false};
use crate::checkers::ast::Checker;
@@ -63,12 +63,7 @@ pub(crate) fn request_with_no_cert_validation(
_ => None,
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "verify")
}) {
if let Some(keyword) = find_keyword(keywords, "verify") {
if is_const_false(&keyword.value) {
checker.diagnostics.push(Diagnostic::new(
RequestWithNoCertValidation {

View File

@@ -2,7 +2,7 @@ use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::helpers::{find_keyword, is_const_none};
use crate::checkers::ast::Checker;
@@ -63,12 +63,7 @@ pub(crate) fn request_without_timeout(checker: &mut Checker, func: &Expr, keywor
)
})
{
if let Some(keyword) = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "timeout")
}) {
if let Some(keyword) = find_keyword(keywords, "timeout") {
if is_const_none(&keyword.value) {
checker.diagnostics.push(Diagnostic::new(
RequestWithoutTimeout { implicit: false },

View File

@@ -45,7 +45,7 @@ use crate::checkers::ast::Checker;
/// - `flake8-bugbear.extend-immutable-calls`
#[violation]
pub struct FunctionCallInDefaultArgument {
pub name: Option<String>,
name: Option<String>,
}
impl Violation for FunctionCallInDefaultArgument {
@@ -96,14 +96,16 @@ where
}
visitor::walk_expr(self, expr);
}
Expr::Lambda(_) => {}
Expr::Lambda(_) => {
// Don't recurse.
}
_ => visitor::walk_expr(self, expr),
}
}
}
/// B008
pub(crate) fn function_call_argument_default(checker: &mut Checker, arguments: &Arguments) {
pub(crate) fn function_call_in_argument_default(checker: &mut Checker, arguments: &Arguments) {
// Map immutable calls to (module, member) format.
let extend_immutable_calls: Vec<CallPath> = checker
.settings

View File

@@ -8,7 +8,7 @@ pub(crate) use duplicate_value::*;
pub(crate) use except_with_empty_tuple::*;
pub(crate) use except_with_non_exception_classes::*;
pub(crate) use f_string_docstring::*;
pub(crate) use function_call_argument_default::*;
pub(crate) use function_call_in_argument_default::*;
pub(crate) use function_uses_loop_variable::*;
pub(crate) use getattr_with_constant::*;
pub(crate) use jump_statement_in_finally::*;
@@ -42,7 +42,7 @@ mod duplicate_value;
mod except_with_empty_tuple;
mod except_with_non_exception_classes;
mod f_string_docstring;
mod function_call_argument_default;
mod function_call_in_argument_default;
mod function_uses_loop_variable;
mod getattr_with_constant;
mod jump_statement_in_finally;

View File

@@ -13,8 +13,8 @@ use crate::checkers::ast::Checker;
///
/// ## Why is this bad?
/// Passing `count`, `maxsplit`, or `flags` as positional arguments to
/// `re.sub`, re.subn`, or `re.split` can lead to confusion, as most methods in
/// the `re` module accepts `flags` as the third positional argument, while
/// `re.sub`, `re.subn`, or `re.split` can lead to confusion, as most methods in
/// the `re` module accept `flags` as the third positional argument, while
/// `re.sub`, `re.subn`, and `re.split` have different signatures.
///
/// Instead, pass `count`, `maxsplit`, and `flags` as keyword arguments.

View File

@@ -55,8 +55,6 @@ struct GroupNameFinder<'a> {
/// A flag indicating that the `group_name` variable has been overridden
/// during the visit.
overridden: bool,
/// A stack of `if` statements.
parent_ifs: Vec<&'a Stmt>,
/// A stack of counters where each counter is itself a list of usage count.
/// This is used specifically for mutually exclusive statements such as an
/// `if` or `match`.
@@ -77,7 +75,6 @@ impl<'a> GroupNameFinder<'a> {
usage_count: 0,
nested: false,
overridden: false,
parent_ifs: Vec::new(),
counter_stack: Vec::new(),
exprs: Vec::new(),
}
@@ -146,56 +143,28 @@ where
Stmt::If(ast::StmtIf {
test,
body,
orelse,
elif_else_clauses,
range: _,
}) => {
// Determine whether we're on an `if` arm (as opposed to an `elif`).
let is_if_arm = !self.parent_ifs.iter().any(|parent| {
if let Stmt::If(ast::StmtIf { orelse, .. }) = parent {
orelse.len() == 1 && &orelse[0] == stmt
} else {
false
}
});
// base if plus branches
let mut if_stack = Vec::with_capacity(1 + elif_else_clauses.len());
// Initialize the vector with the count for the if branch.
if_stack.push(0);
self.counter_stack.push(if_stack);
if is_if_arm {
// Initialize the vector with the count for current branch.
self.counter_stack.push(vec![0]);
} else {
// SAFETY: `unwrap` is safe because we're either in `elif` or
// `else` branch which can come only after an `if` branch.
// When inside an `if` branch, a new vector will be pushed
// onto the stack.
self.visit_expr(test);
self.visit_body(body);
for clause in elif_else_clauses {
self.counter_stack.last_mut().unwrap().push(0);
self.visit_elif_else_clause(clause);
}
let has_else = orelse
.first()
.map_or(false, |expr| !matches!(expr, Stmt::If(_)));
self.parent_ifs.push(stmt);
if has_else {
// There's no `Stmt::Else`; instead, the `else` contents are directly on
// the `orelse` of the `Stmt::If` node. We want to add a new counter for
// the `orelse` branch, but first, we need to visit the `if` body manually.
self.visit_expr(test);
self.visit_body(body);
// Now, we're in an `else` block.
self.counter_stack.last_mut().unwrap().push(0);
self.visit_body(orelse);
} else {
visitor::walk_stmt(self, stmt);
}
self.parent_ifs.pop();
if is_if_arm {
if let Some(last) = self.counter_stack.pop() {
// This is the max number of group usage from all the
// branches of this `if` statement.
let max_count = last.into_iter().max().unwrap_or(0);
self.increment_usage_count(max_count);
}
if let Some(last) = self.counter_stack.pop() {
// This is the max number of group usage from all the
// branches of this `if` statement.
let max_count = last.into_iter().max().unwrap_or(0);
self.increment_usage_count(max_count);
}
}
Stmt::Match(ast::StmtMatch {

View File

@@ -22,9 +22,12 @@ pub struct Options {
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
"#
)]
/// Additional callable functions to consider "immutable" when evaluating,
/// e.g., the `no-mutable-default-argument` rule (`B006`) or
/// `no-function-call-in-dataclass-defaults` rule (`RUF009`).
/// Additional callable functions to consider "immutable" when evaluating, e.g., the
/// `function-call-in-default-argument` rule (`B008`) or `function-call-in-dataclass-defaults`
/// rule (`RUF009`).
///
/// Expects to receive a list of fully-qualified names (e.g., `fastapi.Query`, rather than
/// `Query`).
pub extend_immutable_calls: Option<Vec<String>>,
}

View File

@@ -3,39 +3,114 @@ use ruff_macros::{derive_message_formats, violation};
use crate::directives::{TodoComment, TodoDirectiveKind};
/// ## What it does
/// Checks for "TODO" comments.
///
/// ## Why is this bad?
/// "TODO" comments are used to describe an issue that should be resolved
/// (usually, a missing feature, optimization, or refactoring opportunity).
///
/// Consider resolving the issue before deploying the code.
///
/// Note that if you use "TODO" comments as a form of documentation (e.g.,
/// to [provide context for future work](https://gist.github.com/dmnd/ed5d8ef8de2e4cfea174bd5dafcda382)),
/// this rule may not be appropriate for your project.
///
/// ## Example
/// ```python
/// def greet(name):
/// return f"Hello, {name}!" # TODO: Add support for custom greetings.
/// ```
#[violation]
pub struct LineContainsTodo;
impl Violation for LineContainsTodo {
#[derive_message_formats]
fn message(&self) -> String {
format!("Line contains TODO")
format!("Line contains TODO, consider resolving the issue")
}
}
/// ## What it does
/// Checks for "FIXME" comments.
///
/// ## Why is this bad?
/// "FIXME" comments are used to describe an issue that should be resolved
/// (usually, a bug or unexpected behavior).
///
/// Consider resolving the issue before deploying the code.
///
/// Note that if you use "FIXME" comments as a form of documentation, this
/// rule may not be appropriate for your project.
///
/// ## Example
/// ```python
/// def speed(distance, time):
/// return distance / time # FIXME: Raises ZeroDivisionError for time = 0.
/// ```
#[violation]
pub struct LineContainsFixme;
impl Violation for LineContainsFixme {
#[derive_message_formats]
fn message(&self) -> String {
format!("Line contains FIXME")
format!("Line contains FIXME, consider resolving the issue")
}
}
/// ## What it does
/// Checks for "XXX" comments.
///
/// ## Why is this bad?
/// "XXX" comments are used to describe an issue that should be resolved.
///
/// Consider resolving the issue before deploying the code, or, at minimum,
/// using a more descriptive comment tag (e.g, "TODO").
///
/// ## Example
/// ```python
/// def speed(distance, time):
/// return distance / time # XXX: Raises ZeroDivisionError for time = 0.
/// ```
#[violation]
pub struct LineContainsXxx;
impl Violation for LineContainsXxx {
#[derive_message_formats]
fn message(&self) -> String {
format!("Line contains XXX")
format!("Line contains XXX, consider resolving the issue")
}
}
/// ## What it does
/// Checks for "HACK" comments.
///
/// ## Why is this bad?
/// "HACK" comments are used to describe an issue that should be resolved
/// (usually, a suboptimal solution or temporary workaround).
///
/// Consider resolving the issue before deploying the code.
///
/// Note that if you use "HACK" comments as a form of documentation, this
/// rule may not be appropriate for your project.
///
/// ## Example
/// ```python
/// import os
///
///
/// def running_windows(): # HACK: Use platform module instead.
/// try:
/// os.mkdir("C:\\Windows\\System32\\")
/// except FileExistsError:
/// return True
/// else:
/// os.rmdir("C:\\Windows\\System32\\")
/// return False
/// ```
#[violation]
pub struct LineContainsHack;
impl Violation for LineContainsHack {
#[derive_message_formats]
fn message(&self) -> String {
format!("Line contains HACK")
format!("Line contains HACK, consider resolving the issue")
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_fixme/mod.rs
---
T00.py:7:3: FIX001 Line contains FIXME
T00.py:7:3: FIX001 Line contains FIXME, consider resolving the issue
|
5 | # HACK: hack
6 | # hack: hack
@@ -10,7 +10,7 @@ T00.py:7:3: FIX001 Line contains FIXME
8 | # fixme: fixme
|
T00.py:8:3: FIX001 Line contains FIXME
T00.py:8:3: FIX001 Line contains FIXME, consider resolving the issue
|
6 | # hack: hack
7 | # FIXME: fixme

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_fixme/mod.rs
---
T00.py:5:3: FIX004 Line contains HACK
T00.py:5:3: FIX004 Line contains HACK, consider resolving the issue
|
3 | # XXX: xxx
4 | # xxx: xxx
@@ -11,7 +11,7 @@ T00.py:5:3: FIX004 Line contains HACK
7 | # FIXME: fixme
|
T00.py:6:3: FIX004 Line contains HACK
T00.py:6:3: FIX004 Line contains HACK, consider resolving the issue
|
4 | # xxx: xxx
5 | # HACK: hack

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_fixme/mod.rs
---
T00.py:1:3: FIX002 Line contains TODO
T00.py:1:3: FIX002 Line contains TODO, consider resolving the issue
|
1 | # TODO: todo
| ^^^^ FIX002
@@ -9,7 +9,7 @@ T00.py:1:3: FIX002 Line contains TODO
3 | # XXX: xxx
|
T00.py:2:3: FIX002 Line contains TODO
T00.py:2:3: FIX002 Line contains TODO, consider resolving the issue
|
1 | # TODO: todo
2 | # todo: todo

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_fixme/mod.rs
---
T00.py:3:3: FIX003 Line contains XXX
T00.py:3:3: FIX003 Line contains XXX, consider resolving the issue
|
1 | # TODO: todo
2 | # todo: todo
@@ -11,7 +11,7 @@ T00.py:3:3: FIX003 Line contains XXX
5 | # HACK: hack
|
T00.py:4:3: FIX003 Line contains XXX
T00.py:4:3: FIX003 Line contains XXX, consider resolving the issue
|
2 | # todo: todo
3 | # XXX: xxx

View File

@@ -1,14 +1,6 @@
---
source: crates/ruff/src/rules/flake8_future_annotations/mod.rs
---
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
1 | def main() -> None:
2 | a_list: list[str] | None = []
| ^^^^^^^^^^^^^^^^ FA102
3 | a_list.append("hello")
|
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
|
1 | def main() -> None:
@@ -17,11 +9,12 @@ no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annot
3 | a_list.append("hello")
|
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
no_future_import_uses_union.py:2:13: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
6 | def hello(y: dict[str, int] | None) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ FA102
7 | del y
1 | def main() -> None:
2 | a_list: list[str] | None = []
| ^^^^^^^^^^^^^^^^ FA102
3 | a_list.append("hello")
|
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 585 collection
@@ -31,4 +24,11 @@ no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annot
7 | del y
|
no_future_import_uses_union.py:6:14: FA102 Missing `from __future__ import annotations`, but uses PEP 604 union
|
6 | def hello(y: dict[str, int] | None) -> None:
| ^^^^^^^^^^^^^^^^^^^^^ FA102
7 | del y
|

View File

@@ -2,7 +2,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Scope;
use ruff_python_semantic::Binding;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -53,42 +53,38 @@ impl Violation for UnconventionalImportAlias {
/// ICN001
pub(crate) fn unconventional_import_alias(
checker: &Checker,
scope: &Scope,
diagnostics: &mut Vec<Diagnostic>,
binding: &Binding,
conventions: &FxHashMap<String, String>,
) -> Option<Diagnostic> {
for (name, binding_id) in scope.all_bindings() {
let binding = checker.semantic().binding(binding_id);
let Some(qualified_name) = binding.qualified_name() else {
return None;
};
let Some(qualified_name) = binding.qualified_name() else {
continue;
};
let Some(expected_alias) = conventions.get(qualified_name) else {
return None;
};
let Some(expected_alias) = conventions.get(qualified_name) else {
continue;
};
if binding.is_alias() && name == expected_alias {
continue;
}
let mut diagnostic = Diagnostic::new(
UnconventionalImportAlias {
name: qualified_name.to_string(),
asname: expected_alias.to_string(),
},
binding.range,
);
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_available(expected_alias) {
diagnostic.try_set_fix(|| {
let (edit, rest) =
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
}
}
diagnostics.push(diagnostic);
let name = binding.name(checker.locator);
if binding.is_alias() && name == expected_alias {
return None;
}
None
let mut diagnostic = Diagnostic::new(
UnconventionalImportAlias {
name: qualified_name.to_string(),
asname: expected_alias.to_string(),
},
binding.range,
);
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_available(expected_alias) {
diagnostic.try_set_fix(|| {
let scope = &checker.semantic().scopes[binding.scope];
let (edit, rest) =
Renamer::rename(name, expected_alias, scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
}
}
Some(diagnostic)
}

View File

@@ -25,6 +25,8 @@ mod tests {
#[test_case(Rule::BadVersionInfoComparison, Path::new("PYI006.pyi"))]
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.py"))]
#[test_case(Rule::CollectionsNamedTuple, Path::new("PYI024.pyi"))]
#[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.py"))]
#[test_case(Rule::ComplexAssignmentInStub, Path::new("PYI017.pyi"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.py"))]
#[test_case(Rule::ComplexIfStatementInStub, Path::new("PYI002.pyi"))]
#[test_case(Rule::DocstringInStub, Path::new("PYI021.py"))]
@@ -87,6 +89,8 @@ mod tests {
#[test_case(Rule::UnrecognizedVersionInfoCheck, Path::new("PYI003.pyi"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.py"))]
#[test_case(Rule::WrongTupleLengthVersionComparison, Path::new("PYI005.pyi"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.py"))]
#[test_case(Rule::TypeAliasWithoutAnnotation, Path::new("PYI026.pyi"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,54 @@
use rustpython_parser::ast::{Expr, StmtAssign};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for assignments with multiple or non-name targets in stub files.
///
/// ## Why is this bad?
/// In general, stub files should be thought of as "data files" for a type
/// checker, and are not intended to be executed. As such, it's useful to
/// enforce that only a subset of Python syntax is allowed in a stub file, to
/// ensure that everything in the stub is unambiguous for the type checker.
///
/// The need to perform multi-assignment, or assignment to a non-name target,
/// likely indicates a misunderstanding of how stub files are intended to be
/// used.
///
/// ## Example
/// ```python
/// a = b = int
/// a.b = int
/// ```
///
/// Use instead:
/// ```python
/// a: TypeAlias = int
/// b: TypeAlias = int
///
///
/// class a:
/// b: int
/// ```
#[violation]
pub struct ComplexAssignmentInStub;
impl Violation for ComplexAssignmentInStub {
#[derive_message_formats]
fn message(&self) -> String {
format!("Stubs should not contain assignments to attributes or multiple targets")
}
}
/// PYI017
pub(crate) fn complex_assignment_in_stub(checker: &mut Checker, stmt: &StmtAssign) {
if matches!(stmt.targets.as_slice(), [Expr::Name(_)]) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(ComplexAssignmentInStub, stmt.range));
}

View File

@@ -1,6 +1,7 @@
pub(crate) use any_eq_ne_annotation::*;
pub(crate) use bad_version_info_comparison::*;
pub(crate) use collections_named_tuple::*;
pub(crate) use complex_assignment_in_stub::*;
pub(crate) use complex_if_statement_in_stub::*;
pub(crate) use docstring_in_stubs::*;
pub(crate) use duplicate_union_member::*;
@@ -31,6 +32,7 @@ pub(crate) use unrecognized_version_info::*;
mod any_eq_ne_annotation;
mod bad_version_info_comparison;
mod collections_named_tuple;
mod complex_assignment_in_stub;
mod complex_if_statement_in_stub;
mod docstring_in_stubs;
mod duplicate_union_member;

View File

@@ -9,6 +9,7 @@ use ruff_python_ast::source_code::Locator;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::registry::AsRule;
#[violation]
@@ -97,6 +98,47 @@ impl Violation for UnassignedSpecialVariableInStub {
}
}
/// ## What it does
/// Checks for type alias definitions that are not annotated with
/// `typing.TypeAlias`.
///
/// ## Why is this bad?
/// In Python, a type alias is defined by assigning a type to a variable (e.g.,
/// `Vector = list[float]`).
///
/// It's best to annotate type aliases with the `typing.TypeAlias` type to
/// make it clear that the statement is a type alias declaration, as opposed
/// to a normal variable assignment.
///
/// ## Example
/// ```python
/// Vector = list[float]
/// ```
///
/// Use instead:
/// ```python
/// from typing import TypeAlias
///
/// Vector: TypeAlias = list[float]
/// ```
#[violation]
pub struct TypeAliasWithoutAnnotation {
name: String,
value: String,
}
impl AlwaysAutofixableViolation for TypeAliasWithoutAnnotation {
#[derive_message_formats]
fn message(&self) -> String {
let TypeAliasWithoutAnnotation { name, value } = self;
format!("Use `typing.TypeAlias` for type alias, e.g., `{name}: typing.TypeAlias = {value}`")
}
fn autofix_title(&self) -> String {
"Add `typing.TypeAlias` annotation".to_string()
}
}
fn is_allowed_negated_math_attribute(call_path: &CallPath) -> bool {
matches!(call_path.as_slice(), ["math", "inf" | "e" | "pi" | "tau"])
}
@@ -234,22 +276,39 @@ fn is_valid_default_value_with_annotation(
/// Returns `true` if an [`Expr`] appears to be a valid PEP 604 union. (e.g. `int | None`)
fn is_valid_pep_604_union(annotation: &Expr) -> bool {
match annotation {
Expr::BinOp(ast::ExprBinOp {
left,
op: Operator::BitOr,
right,
range: _,
}) => is_valid_pep_604_union(left) && is_valid_pep_604_union(right),
Expr::Name(_)
| Expr::Subscript(_)
| Expr::Attribute(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::None,
..
}) => true,
_ => false,
/// Returns `true` if an [`Expr`] appears to be a valid PEP 604 union member.
fn is_valid_pep_604_union_member(value: &Expr) -> bool {
match value {
Expr::BinOp(ast::ExprBinOp {
left,
op: Operator::BitOr,
right,
range: _,
}) => is_valid_pep_604_union_member(left) && is_valid_pep_604_union_member(right),
Expr::Name(_)
| Expr::Subscript(_)
| Expr::Attribute(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::None,
..
}) => true,
_ => false,
}
}
// The top-level expression must be a bit-or operation.
let Expr::BinOp(ast::ExprBinOp {
left,
op: Operator::BitOr,
right,
range: _,
}) = annotation
else {
return false;
};
// The left and right operands must be valid union members.
is_valid_pep_604_union_member(left) && is_valid_pep_604_union_member(right)
}
/// Returns `true` if an [`Expr`] appears to be a valid default value without an annotation.
@@ -323,6 +382,23 @@ fn is_enum(bases: &[Expr], semantic: &SemanticModel) -> bool {
});
}
/// Returns `true` if an [`Expr`] is a value that should be annotated with `typing.TypeAlias`.
///
/// This is relatively conservative, as it's hard to reliably detect whether a right-hand side is a
/// valid type alias. In particular, this function checks for uses of `typing.Any`, `None`,
/// parameterized generics, and PEP 604-style unions.
fn is_annotatable_type_alias(value: &Expr, semantic: &SemanticModel) -> bool {
matches!(
value,
Expr::Subscript(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::None,
..
}),
) || is_valid_pep_604_union(value)
|| semantic.match_typing_expr(value, "Any")
}
/// PYI011
pub(crate) fn typed_argument_simple_defaults(checker: &mut Checker, arguments: &Arguments) {
for ArgWithDefault {
@@ -523,3 +599,40 @@ pub(crate) fn unassigned_special_variable_in_stub(
stmt.range(),
));
}
/// PIY026
pub(crate) fn type_alias_without_annotation(checker: &mut Checker, value: &Expr, targets: &[Expr]) {
let [target] = targets else {
return;
};
let Expr::Name(ast::ExprName { id, .. }) = target else {
return;
};
if !is_annotatable_type_alias(value, checker.semantic()) {
return;
}
let mut diagnostic = Diagnostic::new(
TypeAliasWithoutAnnotation {
name: id.to_string(),
value: checker.generator().expr(value),
},
target.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer.get_or_import_symbol(
&ImportRequest::import("typing", "TypeAlias"),
target.start(),
checker.semantic(),
)?;
Ok(Fix::suggested_edits(
Edit::range_replacement(format!("{id}: {binding}"), target.range()),
[import_edit],
))
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{BindingKind, FromImport, Scope};
use ruff_python_semantic::{Binding, BindingKind, FromImport};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -48,31 +48,29 @@ impl Violation for UnaliasedCollectionsAbcSetImport {
/// PYI025
pub(crate) fn unaliased_collections_abc_set_import(
checker: &Checker,
scope: &Scope,
diagnostics: &mut Vec<Diagnostic>,
) {
for (name, binding_id) in scope.all_bindings() {
let binding = checker.semantic().binding(binding_id);
let BindingKind::FromImport(FromImport { qualified_name }) = &binding.kind else {
continue;
};
if qualified_name.as_str() != "collections.abc.Set" {
continue;
}
if name == "AbstractSet" {
continue;
}
let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range);
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_available("AbstractSet") {
diagnostic.try_set_fix(|| {
let (edit, rest) =
Renamer::rename(name, "AbstractSet", scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
}
}
diagnostics.push(diagnostic);
binding: &Binding,
) -> Option<Diagnostic> {
let BindingKind::FromImport(FromImport { qualified_name }) = &binding.kind else {
return None;
};
if qualified_name.as_str() != "collections.abc.Set" {
return None;
}
let name = binding.name(checker.locator);
if name == "AbstractSet" {
return None;
}
let mut diagnostic = Diagnostic::new(UnaliasedCollectionsAbcSetImport, binding.range);
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_available("AbstractSet") {
diagnostic.try_set_fix(|| {
let scope = &checker.semantic().scopes[binding.scope];
let (edit, rest) = Renamer::rename(name, "AbstractSet", scope, checker.semantic())?;
Ok(Fix::suggested_edits(edit, rest))
});
}
}
Some(diagnostic)
}

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

View File

@@ -0,0 +1,44 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI017.pyi:4:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
2 | a = var # OK
3 |
4 | b = c = int # PYI017
| ^^^^^^^^^^^ PYI017
5 |
6 | a.b = int # PYI017
|
PYI017.pyi:6:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
4 | b = c = int # PYI017
5 |
6 | a.b = int # PYI017
| ^^^^^^^^^ PYI017
7 |
8 | d, e = int, str # PYI017
|
PYI017.pyi:8:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
6 | a.b = int # PYI017
7 |
8 | d, e = int, str # PYI017
| ^^^^^^^^^^^^^^^ PYI017
9 |
10 | f, g, h = int, str, TypeVar("T") # PYI017
|
PYI017.pyi:10:1: PYI017 Stubs should not contain assignments to attributes or multiple targets
|
8 | d, e = int, str # PYI017
9 |
10 | f, g, h = int, str, TypeVar("T") # PYI017
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI017
11 |
12 | i: TypeAlias = int | str # OK
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---

View File

@@ -0,0 +1,117 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI026.pyi:3:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `NewAny: typing.TypeAlias = Any`
|
1 | from typing import Literal, Any
2 |
3 | NewAny = Any
| ^^^^^^ PYI026
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
|
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
1 |+from typing import Literal, Any, TypeAlias
2 2 |
3 |-NewAny = Any
3 |+NewAny: TypeAlias = Any
4 4 | OptionalStr = typing.Optional[str]
5 5 | Foo = Literal["foo"]
6 6 | IntOrStr = int | str
PYI026.pyi:4:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `OptionalStr: typing.TypeAlias = typing.Optional[str]`
|
3 | NewAny = Any
4 | OptionalStr = typing.Optional[str]
| ^^^^^^^^^^^ PYI026
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
|
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
1 |+from typing import Literal, Any, TypeAlias
2 2 |
3 3 | NewAny = Any
4 |-OptionalStr = typing.Optional[str]
4 |+OptionalStr: TypeAlias = typing.Optional[str]
5 5 | Foo = Literal["foo"]
6 6 | IntOrStr = int | str
7 7 | AliasNone = None
PYI026.pyi:5:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `Foo: typing.TypeAlias = Literal["foo"]`
|
3 | NewAny = Any
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
| ^^^ PYI026
6 | IntOrStr = int | str
7 | AliasNone = None
|
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
1 |+from typing import Literal, Any, TypeAlias
2 2 |
3 3 | NewAny = Any
4 4 | OptionalStr = typing.Optional[str]
5 |-Foo = Literal["foo"]
5 |+Foo: TypeAlias = Literal["foo"]
6 6 | IntOrStr = int | str
7 7 | AliasNone = None
8 8 |
PYI026.pyi:6:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `IntOrStr: typing.TypeAlias = int | str`
|
4 | OptionalStr = typing.Optional[str]
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
| ^^^^^^^^ PYI026
7 | AliasNone = None
|
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
1 |+from typing import Literal, Any, TypeAlias
2 2 |
3 3 | NewAny = Any
4 4 | OptionalStr = typing.Optional[str]
5 5 | Foo = Literal["foo"]
6 |-IntOrStr = int | str
6 |+IntOrStr: TypeAlias = int | str
7 7 | AliasNone = None
8 8 |
9 9 | NewAny: typing.TypeAlias = Any
PYI026.pyi:7:1: PYI026 [*] Use `typing.TypeAlias` for type alias, e.g., `AliasNone: typing.TypeAlias = None`
|
5 | Foo = Literal["foo"]
6 | IntOrStr = int | str
7 | AliasNone = None
| ^^^^^^^^^ PYI026
8 |
9 | NewAny: typing.TypeAlias = Any
|
= help: Add `typing.TypeAlias` annotation
Suggested fix
1 |-from typing import Literal, Any
1 |+from typing import Literal, Any, TypeAlias
2 2 |
3 3 | NewAny = Any
4 4 | OptionalStr = typing.Optional[str]
5 5 | Foo = Literal["foo"]
6 6 | IntOrStr = int | str
7 |-AliasNone = None
7 |+AliasNone: TypeAlias = None
8 8 |
9 9 | NewAny: typing.TypeAlias = Any
10 10 | OptionalStr: TypeAlias = typing.Optional[str]

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Constant, Decorator, Expr, Keyword};
use ruff_python_ast::call_path::{collect_call_path, CallPath};
use ruff_python_ast::helpers::map_callable;
use ruff_python_semantic::SemanticModel;
use ruff_python_whitespace::PythonWhitespace;
use ruff_python_trivia::PythonWhitespace;
pub(super) fn get_mark_decorators(
decorators: &[Decorator],

View File

@@ -1,3 +1,4 @@
use ruff_python_ast::helpers::find_keyword;
use rustpython_parser::ast::{self, Expr, Keyword, Ranged, Stmt, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
@@ -74,13 +75,7 @@ pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], key
}
if checker.enabled(Rule::PytestRaisesTooBroad) {
let match_keyword = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "match")
});
let match_keyword = find_keyword(keywords, "match");
if let Some(exception) = args.first() {
if let Some(match_keyword) = match_keyword {
if is_empty_or_null_string(&match_keyword.value) {

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast;
use rustpython_parser::ast::{Expr, Ranged, Stmt};
use ruff_python_ast::source_code::Locator;
use ruff_python_whitespace::UniversalNewlines;
use ruff_python_trivia::UniversalNewlines;
/// Return `true` if a function's return statement include at least one
/// non-`None` value.

View File

@@ -1,13 +1,14 @@
use std::ops::Add;
use ruff_text_size::{TextRange, TextSize};
use rustpython_parser::ast::{self, Expr, Ranged, Stmt};
use rustpython_parser::ast::{self, ElifElseClause, Expr, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
use ruff_python_ast::helpers::{elif_else_range, is_const_false, is_const_true};
use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::stmt_if::elif_else_range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::whitespace::indentation;
use ruff_python_semantic::SemanticModel;
@@ -387,13 +388,25 @@ fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
/// RET503
fn implicit_return(checker: &mut Checker, stmt: &Stmt) {
match stmt {
Stmt::If(ast::StmtIf { body, orelse, .. }) => {
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if let Some(last_stmt) = body.last() {
implicit_return(checker, last_stmt);
}
if let Some(last_stmt) = orelse.last() {
implicit_return(checker, last_stmt);
} else {
for clause in elif_else_clauses {
if let Some(last_stmt) = clause.body.last() {
implicit_return(checker, last_stmt);
}
}
// Check if we don't have an else clause
if matches!(
elif_else_clauses.last(),
None | Some(ast::ElifElseClause { test: Some(_), .. })
) {
let mut diagnostic = Diagnostic::new(ImplicitReturn, stmt.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(indent) = indentation(checker.locator, stmt) {
@@ -564,13 +577,21 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) {
}
/// RET505, RET506, RET507, RET508
fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Branch) -> bool {
let ast::StmtIf { body, .. } = stmt;
for child in body {
fn superfluous_else_node(
checker: &mut Checker,
if_elif_body: &[Stmt],
elif_else: &ElifElseClause,
) -> bool {
let branch = if elif_else.test.is_some() {
Branch::Elif
} else {
Branch::Else
};
for child in if_elif_body {
if child.is_return_stmt() {
let diagnostic = Diagnostic::new(
SuperfluousElseReturn { branch },
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
@@ -579,7 +600,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
} else if child.is_break_stmt() {
let diagnostic = Diagnostic::new(
SuperfluousElseBreak { branch },
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
@@ -588,7 +609,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
} else if child.is_raise_stmt() {
let diagnostic = Diagnostic::new(
SuperfluousElseRaise { branch },
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
@@ -597,7 +618,7 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
} else if child.is_continue_stmt() {
let diagnostic = Diagnostic::new(
SuperfluousElseContinue { branch },
elif_else_range(stmt, checker.locator).unwrap_or_else(|| stmt.range()),
elif_else_range(elif_else, checker.locator).unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
checker.diagnostics.push(diagnostic);
@@ -609,16 +630,9 @@ fn superfluous_else_node(checker: &mut Checker, stmt: &ast::StmtIf, branch: Bran
}
/// RET505, RET506, RET507, RET508
fn superfluous_elif(checker: &mut Checker, stack: &Stack) {
for stmt in &stack.elifs {
superfluous_else_node(checker, stmt, Branch::Elif);
}
}
/// RET505, RET506, RET507, RET508
fn superfluous_else(checker: &mut Checker, stack: &Stack) {
for stmt in &stack.elses {
superfluous_else_node(checker, stmt, Branch::Else);
fn superfluous_elif_else(checker: &mut Checker, stack: &Stack) {
for (if_elif_body, elif_else) in &stack.elifs_elses {
superfluous_else_node(checker, if_elif_body, elif_else);
}
}
@@ -655,8 +669,7 @@ pub(crate) fn function(checker: &mut Checker, body: &[Stmt], returns: Option<&Ex
Rule::SuperfluousElseContinue,
Rule::SuperfluousElseBreak,
]) {
superfluous_elif(checker, &stack);
superfluous_else(checker, &stack);
superfluous_elif_else(checker, &stack);
}
// Skip any functions without return statements.

View File

@@ -70,4 +70,13 @@ RET508.py:82:9: RET508 Unnecessary `else` after `break` statement
84 | return
|
RET508.py:158:13: RET508 Unnecessary `else` after `break` statement
|
156 | if i > w:
157 | break
158 | else:
| ^^^^ RET508
159 | a = z
|

View File

@@ -1,5 +1,5 @@
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, Expr, Identifier, Stmt};
use rustpython_parser::ast::{self, ElifElseClause, Expr, Identifier, Stmt};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
@@ -8,10 +8,8 @@ use ruff_python_ast::visitor::Visitor;
pub(super) struct Stack<'a> {
/// The `return` statements in the current function.
pub(super) returns: Vec<&'a ast::StmtReturn>,
/// The `else` statements in the current function.
pub(super) elses: Vec<&'a ast::StmtIf>,
/// The `elif` statements in the current function.
pub(super) elifs: Vec<&'a ast::StmtIf>,
/// The `elif` or `else` statements in the current function.
pub(super) elifs_elses: Vec<(&'a [Stmt], &'a ElifElseClause)>,
/// The non-local variables in the current function.
pub(super) non_locals: FxHashSet<&'a str>,
/// Whether the current function is a generator.
@@ -117,27 +115,13 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
self.stack.returns.push(stmt_return);
}
Stmt::If(stmt_if) => {
let is_elif_arm = self.parents.iter().any(|parent| {
if let Stmt::If(ast::StmtIf { orelse, .. }) = parent {
orelse.len() == 1 && &orelse[0] == stmt
} else {
false
}
});
if !is_elif_arm {
let has_elif =
stmt_if.orelse.len() == 1 && stmt_if.orelse.first().unwrap().is_if_stmt();
let has_else = !stmt_if.orelse.is_empty();
if has_elif {
// `stmt` is an `if` block followed by an `elif` clause.
self.stack.elifs.push(stmt_if);
} else if has_else {
// `stmt` is an `if` block followed by an `else` clause.
self.stack.elses.push(stmt_if);
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if let Some(first) = elif_else_clauses.first() {
self.stack.elifs_elses.push((body, first));
}
}
_ => {}

View File

@@ -1,14 +1,18 @@
use log::error;
use ruff_text_size::TextRange;
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Identifier, Ranged, Stmt};
use rustpython_parser::ast::{
self, CmpOp, Constant, ElifElseClause, Expr, ExprContext, Identifier, Ranged, Stmt, StmtIf,
};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, ComparableStmt};
use ruff_python_ast::helpers::{any_over_expr, contains_effect, first_colon_range, has_comments};
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::stmt_if::if_elif_branches;
use ruff_python_semantic::SemanticModel;
use ruff_python_whitespace::UniversalNewlines;
use ruff_python_trivia::UniversalNewlines;
use crate::checkers::ast::Checker;
use crate::line_width::LineWidth;
@@ -23,16 +27,6 @@ fn compare_stmt(stmt1: &ComparableStmt, stmt2: &ComparableStmt) -> bool {
stmt1.eq(stmt2)
}
fn compare_body(body1: &[Stmt], body2: &[Stmt]) -> bool {
if body1.len() != body2.len() {
return false;
}
body1
.iter()
.zip(body2.iter())
.all(|(stmt1, stmt2)| compare_stmt(&stmt1.into(), &stmt2.into()))
}
/// ## What it does
/// Checks for nested `if` statements that can be collapsed into a single `if`
/// statement.
@@ -287,7 +281,7 @@ fn is_main_check(expr: &Expr) -> bool {
}
/// Find the last nested if statement and return the test expression and the
/// first statement.
/// last statement.
///
/// ```python
/// if xxx:
@@ -301,13 +295,13 @@ fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> {
let [Stmt::If(ast::StmtIf {
test,
body: inner_body,
orelse,
elif_else_clauses,
..
})] = body
else {
return None;
};
if !orelse.is_empty() {
if !elif_else_clauses.is_empty() {
return None;
}
find_last_nested_if(inner_body).or_else(|| {
@@ -318,30 +312,36 @@ fn find_last_nested_if(body: &[Stmt]) -> Option<(&Expr, &Stmt)> {
})
}
/// SIM102
pub(crate) fn nested_if_statements(
checker: &mut Checker,
stmt: &Stmt,
test: &Expr,
body: &[Stmt],
orelse: &[Stmt],
parent: Option<&Stmt>,
) {
// If the parent could contain a nested if-statement, abort.
if let Some(Stmt::If(ast::StmtIf { body, orelse, .. })) = parent {
if orelse.is_empty() && body.len() == 1 {
return;
}
}
fn nested_if_body(stmt_if: &StmtIf) -> Option<(&[Stmt], TextRange)> {
let StmtIf {
test,
body,
elif_else_clauses,
..
} = stmt_if;
// If this if-statement has an else clause, or more than one child, abort.
if !(orelse.is_empty() && body.len() == 1) {
return;
// It must be the last condition, otherwise there could be another `elif` or `else` that only
// depends on the outer of the two conditions
let (test, body, range) = if let Some(clause) = elif_else_clauses.last() {
if let Some(test) = &clause.test {
(test, &clause.body, clause.range())
} else {
// The last condition is an `else` (different rule)
return None;
}
} else {
(test.as_ref(), body, stmt_if.range())
};
// The nested if must be the only child, otherwise there is at least one more statement that
// only depends on the outer condition
if body.len() > 1 {
return None;
}
// Allow `if __name__ == "__main__":` statements.
if is_main_check(test) {
return;
return None;
}
// Allow `if True:` and `if False:` statements.
@@ -352,9 +352,18 @@ pub(crate) fn nested_if_statements(
..
})
) {
return;
return None;
}
Some((body, range))
}
/// SIM102
pub(crate) fn nested_if_statements(checker: &mut Checker, stmt_if: &StmtIf, parent: Option<&Stmt>) {
let Some((body, range)) = nested_if_body(stmt_if) else {
return;
};
// Find the deepest nested if-statement, to inform the range.
let Some((test, first_stmt)) = find_last_nested_if(body) else {
return;
@@ -365,12 +374,22 @@ pub(crate) fn nested_if_statements(
checker.locator,
);
// Check if the parent is already emitting a larger diagnostic including this if statement
if let Some(Stmt::If(stmt_if)) = parent {
if let Some((body, _range)) = nested_if_body(stmt_if) {
// In addition to repeating the `nested_if_body` and `find_last_nested_if` check, we
// also need to be the first child in the parent
if matches!(&body[0], Stmt::If(inner) if inner == stmt_if)
&& find_last_nested_if(body).is_some()
{
return;
}
}
}
let mut diagnostic = Diagnostic::new(
CollapsibleIf,
colon.map_or_else(
|| stmt.range(),
|colon| TextRange::new(stmt.start(), colon.end()),
),
colon.map_or(range, |colon| TextRange::new(range.start(), colon.end())),
);
if checker.patch(diagnostic.kind.rule()) {
// The fixer preserves comments in the nested body, but removes comments between
@@ -379,9 +398,9 @@ pub(crate) fn nested_if_statements(
if !checker
.indexer
.comment_ranges()
.intersects(TextRange::new(stmt.start(), nested_if.start()))
.intersects(TextRange::new(range.start(), nested_if.start()))
{
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, stmt) {
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, range) {
Ok(edit) => {
if edit
.content()
@@ -437,17 +456,43 @@ fn is_one_line_return_bool(stmts: &[Stmt]) -> Option<Bool> {
/// SIM103
pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
let Stmt::If(ast::StmtIf {
test,
body,
orelse,
test: if_test,
body: if_body,
elif_else_clauses,
range: _,
}) = stmt
else {
return;
};
// Extract an `if` or `elif` (that returns) followed by an else (that returns the same value)
let (if_test, if_body, else_body, range) = match elif_else_clauses.as_slice() {
// if-else case
[ElifElseClause {
body: else_body,
test: None,
..
}] => (if_test.as_ref(), if_body, else_body, stmt.range()),
// elif-else case
[.., ElifElseClause {
body: elif_body,
test: Some(elif_test),
range: elif_range,
}, ElifElseClause {
body: else_body,
test: None,
range: else_range,
}] => (
elif_test,
elif_body,
else_body,
TextRange::new(elif_range.start(), else_range.end()),
),
_ => return,
};
let (Some(if_return), Some(else_return)) = (
is_one_line_return_bool(body),
is_one_line_return_bool(orelse),
is_one_line_return_bool(if_body),
is_one_line_return_bool(else_body),
) else {
return;
};
@@ -458,23 +503,23 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
return;
}
let condition = checker.generator().expr(test);
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, stmt.range());
let condition = checker.generator().expr(if_test);
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range);
if checker.patch(diagnostic.kind.rule()) {
if matches!(if_return, Bool::True)
&& matches!(else_return, Bool::False)
&& !has_comments(stmt, checker.locator, checker.indexer)
&& (test.is_compare_expr() || checker.semantic().is_builtin("bool"))
&& !has_comments(&range, checker.locator, checker.indexer)
&& (if_test.is_compare_expr() || checker.semantic().is_builtin("bool"))
{
if test.is_compare_expr() {
if if_test.is_compare_expr() {
// If the condition is a comparison, we can replace it with the condition.
let node = ast::StmtReturn {
value: Some(test.clone()),
value: Some(Box::new(if_test.clone())),
range: TextRange::default(),
};
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().stmt(&node.into()),
stmt.range(),
range,
)));
} else {
// Otherwise, we need to wrap the condition in a call to `bool`. (We've already
@@ -486,7 +531,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
};
let node1 = ast::ExprCall {
func: Box::new(node.into()),
args: vec![(**test).clone()],
args: vec![if_test.clone()],
keywords: vec![],
range: TextRange::default(),
};
@@ -496,7 +541,7 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt: &Stmt) {
};
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().stmt(&node2.into()),
stmt.range(),
range,
)));
};
}
@@ -520,99 +565,71 @@ fn ternary(target_var: &Expr, body_value: &Expr, test: &Expr, orelse_value: &Exp
node1.into()
}
/// Return `true` if the `Expr` contains a reference to `${module}.${target}`.
fn contains_call_path(expr: &Expr, target: &[&str], semantic: &SemanticModel) -> bool {
/// Return `true` if the `Expr` contains a reference to any of the given `${module}.${target}`.
fn contains_call_path(expr: &Expr, targets: &[&[&str]], semantic: &SemanticModel) -> bool {
any_over_expr(expr, &|expr| {
semantic
.resolve_call_path(expr)
.map_or(false, |call_path| call_path.as_slice() == target)
semantic.resolve_call_path(expr).map_or(false, |call_path| {
targets.iter().any(|target| &call_path.as_slice() == target)
})
})
}
/// SIM108
pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) {
pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt) {
let Stmt::If(ast::StmtIf {
test,
body,
orelse,
elif_else_clauses,
range: _,
}) = stmt
else {
return;
};
if body.len() != 1 || orelse.len() != 1 {
// `test: None` to only match an `else` clause
let [ElifElseClause {
body: else_body,
test: None,
..
}] = elif_else_clauses.as_slice()
else {
return;
}
let Stmt::Assign(ast::StmtAssign {
};
let [Stmt::Assign(ast::StmtAssign {
targets: body_targets,
value: body_value,
..
}) = &body[0]
})] = body.as_slice()
else {
return;
};
let Stmt::Assign(ast::StmtAssign {
targets: orelse_targets,
value: orelse_value,
let [Stmt::Assign(ast::StmtAssign {
targets: else_targets,
value: else_value,
..
}) = &orelse[0]
})] = else_body.as_slice()
else {
return;
};
if body_targets.len() != 1 || orelse_targets.len() != 1 {
return;
}
let Expr::Name(ast::ExprName { id: body_id, .. }) = &body_targets[0] else {
let ([body_target], [else_target]) = (body_targets.as_slice(), else_targets.as_slice()) else {
return;
};
let Expr::Name(ast::ExprName { id: orelse_id, .. }) = &orelse_targets[0] else {
let Expr::Name(ast::ExprName { id: body_id, .. }) = body_target else {
return;
};
if body_id != orelse_id {
let Expr::Name(ast::ExprName { id: else_id, .. }) = else_target else {
return;
};
if body_id != else_id {
return;
}
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
if contains_call_path(test, &["sys", "version_info"], checker.semantic()) {
// Avoid suggesting ternary for `if sys.version_info >= ...`-style and
// `if sys.platform.startswith("...")`-style checks.
let ignored_call_paths: &[&[&str]] = &[&["sys", "version_info"], &["sys", "platform"]];
if contains_call_path(test, ignored_call_paths, checker.semantic()) {
return;
}
// Avoid suggesting ternary for `if sys.platform.startswith("...")`-style
// checks.
if contains_call_path(test, &["sys", "platform"], checker.semantic()) {
return;
}
// It's part of a bigger if-elif block:
// https://github.com/MartinThoma/flake8-simplify/issues/115
if let Some(Stmt::If(ast::StmtIf {
orelse: parent_orelse,
..
})) = parent
{
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
// TODO(charlie): These two cases have the same AST:
//
// if True:
// pass
// elif a:
// b = 1
// else:
// b = 2
//
// if True:
// pass
// else:
// if a:
// b = 1
// else:
// b = 2
//
// We want to flag the latter, but not the former. Right now, we flag neither.
return;
}
}
// Avoid suggesting ternary for `if (yield ...)`-style checks.
// TODO(charlie): Fix precedence handling for yields in generator.
if matches!(
@@ -622,14 +639,14 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O
return;
}
if matches!(
orelse_value.as_ref(),
else_value.as_ref(),
Expr::Yield(_) | Expr::YieldFrom(_) | Expr::Await(_)
) {
return;
}
let target_var = &body_targets[0];
let ternary = ternary(target_var, body_value, test, orelse_value);
let target_var = &body_target;
let ternary = ternary(target_var, body_value, test, else_value);
let contents = checker.generator().stmt(&ternary);
// Don't flag if the resulting expression would exceed the maximum line length.
@@ -659,135 +676,85 @@ pub(crate) fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: O
checker.diagnostics.push(diagnostic);
}
fn get_if_body_pairs<'a>(
test: &'a Expr,
body: &'a [Stmt],
orelse: &'a [Stmt],
) -> Vec<(&'a Expr, &'a [Stmt])> {
let mut pairs = vec![(test, body)];
let mut orelse = orelse;
loop {
if orelse.len() != 1 {
break;
}
let Stmt::If(ast::StmtIf {
test,
body,
orelse: orelse_orelse,
range: _,
}) = &orelse[0]
else {
break;
};
pairs.push((test, body));
orelse = orelse_orelse;
}
pairs
}
/// SIM114
pub(crate) fn if_with_same_arms(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) {
let Stmt::If(ast::StmtIf {
test,
body,
orelse,
range: _,
}) = stmt
else {
return;
};
pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_if: &StmtIf) {
let mut branches_iter = if_elif_branches(stmt_if).peekable();
while let Some(current_branch) = branches_iter.next() {
let Some(following_branch) = branches_iter.peek() else {
continue;
};
// It's part of a bigger if-elif block:
// https://github.com/MartinThoma/flake8-simplify/issues/115
if let Some(Stmt::If(ast::StmtIf {
orelse: parent_orelse,
..
})) = parent
{
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
// TODO(charlie): These two cases have the same AST:
//
// if True:
// pass
// elif a:
// b = 1
// else:
// b = 2
//
// if True:
// pass
// else:
// if a:
// b = 1
// else:
// b = 2
//
// We want to flag the latter, but not the former. Right now, we flag neither.
return;
// The bodies must have the same code ...
if current_branch.body.len() != following_branch.body.len() {
continue;
}
if !current_branch
.body
.iter()
.zip(following_branch.body.iter())
.all(|(stmt1, stmt2)| compare_stmt(&stmt1.into(), &stmt2.into()))
{
continue;
}
}
let if_body_pairs = get_if_body_pairs(test, body, orelse);
for i in 0..(if_body_pairs.len() - 1) {
let (test, body) = &if_body_pairs[i];
let (.., next_body) = &if_body_pairs[i + 1];
if compare_body(body, next_body) {
checker.diagnostics.push(Diagnostic::new(
IfWithSameArms,
TextRange::new(
if i == 0 { stmt.start() } else { test.start() },
next_body.last().unwrap().end(),
),
));
// ...and the same comments
let first_comments: Vec<_> = checker
.indexer
.comments_in_range(current_branch.range, locator)
.collect();
let second_comments: Vec<_> = checker
.indexer
.comments_in_range(following_branch.range, locator)
.collect();
if first_comments != second_comments {
continue;
}
checker.diagnostics.push(Diagnostic::new(
IfWithSameArms,
TextRange::new(
current_branch.range.start(),
following_branch.body.last().unwrap().end(),
),
));
}
}
/// SIM116
pub(crate) fn manual_dict_lookup(
checker: &mut Checker,
stmt: &Stmt,
test: &Expr,
body: &[Stmt],
orelse: &[Stmt],
parent: Option<&Stmt>,
) {
pub(crate) fn manual_dict_lookup(checker: &mut Checker, stmt_if: &StmtIf) {
// Throughout this rule:
// * Each if-statement's test must consist of a constant equality check with the same variable.
// * Each if-statement's body must consist of a single `return`.
// * Each if-statement's orelse must be either another if-statement or empty.
// * The final if-statement's orelse must be empty, or a single `return`.
// * Each if or elif statement's test must consist of a constant equality check with the same variable.
// * Each if or elif statement's body must consist of a single `return`.
// * The else clause must be empty, or a single `return`.
let StmtIf {
body,
test,
elif_else_clauses,
..
} = stmt_if;
let Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
range: _,
}) = &test
}) = test.as_ref()
else {
return;
};
let Expr::Name(ast::ExprName { id: target, .. }) = left.as_ref() else {
return;
};
if body.len() != 1 {
if ops != &[CmpOp::Eq] {
return;
}
if orelse.len() != 1 {
return;
}
if !(ops.len() == 1 && ops[0] == CmpOp::Eq) {
return;
}
if comparators.len() != 1 {
return;
}
let Expr::Constant(ast::ExprConstant {
let [Expr::Constant(ast::ExprConstant {
value: constant, ..
}) = &comparators[0]
})] = comparators.as_slice()
else {
return;
};
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else {
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
return;
};
if value.as_ref().map_or(false, |value| {
@@ -796,99 +763,60 @@ pub(crate) fn manual_dict_lookup(
return;
}
// It's part of a bigger if-elif block:
// https://github.com/MartinThoma/flake8-simplify/issues/115
if let Some(Stmt::If(ast::StmtIf {
orelse: parent_orelse,
..
})) = parent
{
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
// TODO(charlie): These two cases have the same AST:
//
// if True:
// pass
// elif a:
// b = 1
// else:
// b = 2
//
// if True:
// pass
// else:
// if a:
// b = 1
// else:
// b = 2
//
// We want to flag the latter, but not the former. Right now, we flag neither.
return;
}
}
let mut constants: FxHashSet<ComparableConstant> = FxHashSet::default();
constants.insert(constant.into());
let mut child: Option<&Stmt> = orelse.get(0);
while let Some(current) = child.take() {
let Stmt::If(ast::StmtIf {
test,
body,
orelse,
range: _,
}) = &current
else {
return;
};
if body.len() != 1 {
return;
}
if orelse.len() > 1 {
return;
}
let Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
range: _,
}) = test.as_ref()
else {
return;
};
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
return;
};
if !(id == target && matches!(ops.as_slice(), [CmpOp::Eq])) {
return;
}
let [Expr::Constant(ast::ExprConstant {
value: constant, ..
})] = comparators.as_slice()
else {
return;
};
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &body[0] else {
return;
};
if value.as_ref().map_or(false, |value| {
contains_effect(value, |id| checker.semantic().is_builtin(id))
}) {
for clause in elif_else_clauses {
let ElifElseClause { test, body, .. } = clause;
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
return;
};
constants.insert(constant.into());
if let Some(orelse) = orelse.first() {
match orelse {
Stmt::If(_) => {
child = Some(orelse);
}
Stmt::Return(_) => {
child = None;
}
_ => return,
match test.as_ref() {
// `else`
None => {
// The else must also be a single effect-free return statement
let [Stmt::Return(ast::StmtReturn { value, range: _ })] = body.as_slice() else {
return;
};
if value.as_ref().map_or(false, |value| {
contains_effect(value, |id| checker.semantic().is_builtin(id))
}) {
return;
};
}
// `elif`
Some(Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
range: _,
})) => {
let Expr::Name(ast::ExprName { id, .. }) = left.as_ref() else {
return;
};
if id != target || ops != &[CmpOp::Eq] {
return;
}
let [Expr::Constant(ast::ExprConstant {
value: constant, ..
})] = comparators.as_slice()
else {
return;
};
if value.as_ref().map_or(false, |value| {
contains_effect(value, |id| checker.semantic().is_builtin(id))
}) {
return;
};
constants.insert(constant.into());
}
// Different `elif`
_ => {
return;
}
} else {
child = None;
}
}
@@ -898,27 +826,38 @@ pub(crate) fn manual_dict_lookup(
checker.diagnostics.push(Diagnostic::new(
IfElseBlockInsteadOfDictLookup,
stmt.range(),
stmt_if.range(),
));
}
/// SIM401
pub(crate) fn use_dict_get_with_default(
checker: &mut Checker,
stmt: &Stmt,
test: &Expr,
body: &[Stmt],
orelse: &[Stmt],
parent: Option<&Stmt>,
) {
if body.len() != 1 || orelse.len() != 1 {
pub(crate) fn use_dict_get_with_default(checker: &mut Checker, stmt_if: &StmtIf) {
let StmtIf {
test,
body,
elif_else_clauses,
..
} = stmt_if;
let [body_stmt] = body.as_slice() else {
return;
}
};
let [ElifElseClause {
body: else_body,
test: None,
..
}] = elif_else_clauses.as_slice()
else {
return;
};
let [else_body_stmt] = else_body.as_slice() else {
return;
};
let Stmt::Assign(ast::StmtAssign {
targets: body_var,
value: body_value,
..
}) = &body[0]
}) = &body_stmt
else {
return;
};
@@ -929,7 +868,7 @@ pub(crate) fn use_dict_get_with_default(
targets: orelse_var,
value: orelse_value,
..
}) = &orelse[0]
}) = &else_body_stmt
else {
return;
};
@@ -941,7 +880,7 @@ pub(crate) fn use_dict_get_with_default(
ops,
comparators: test_dict,
range: _,
}) = &test
}) = test.as_ref()
else {
return;
};
@@ -949,8 +888,18 @@ pub(crate) fn use_dict_get_with_default(
return;
}
let (expected_var, expected_value, default_var, default_value) = match ops[..] {
[CmpOp::In] => (&body_var[0], body_value, &orelse_var[0], orelse_value),
[CmpOp::NotIn] => (&orelse_var[0], orelse_value, &body_var[0], body_value),
[CmpOp::In] => (
&body_var[0],
body_value,
&orelse_var[0],
orelse_value.as_ref(),
),
[CmpOp::NotIn] => (
&orelse_var[0],
orelse_value,
&body_var[0],
body_value.as_ref(),
),
_ => {
return;
}
@@ -979,37 +928,7 @@ pub(crate) fn use_dict_get_with_default(
return;
}
// It's part of a bigger if-elif block:
// https://github.com/MartinThoma/flake8-simplify/issues/115
if let Some(Stmt::If(ast::StmtIf {
orelse: parent_orelse,
..
})) = parent
{
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
// TODO(charlie): These two cases have the same AST:
//
// if True:
// pass
// elif a:
// b = 1
// else:
// b = 2
//
// if True:
// pass
// else:
// if a:
// b = 1
// else:
// b = 2
//
// We want to flag the latter, but not the former. Right now, we flag neither.
return;
}
}
let node = *default_value.clone();
let node = default_value.clone();
let node1 = *test_key.clone();
let node2 = ast::ExprAttribute {
value: expected_subscript.clone(),
@@ -1033,9 +952,9 @@ pub(crate) fn use_dict_get_with_default(
let contents = checker.generator().stmt(&node5.into());
// Don't flag if the resulting expression would exceed the maximum line length.
let line_start = checker.locator.line_start(stmt.start());
let line_start = checker.locator.line_start(stmt_if.start());
if LineWidth::new(checker.settings.tab_size)
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt_if.start())])
.add_str(&contents)
> checker.settings.line_length
{
@@ -1046,13 +965,13 @@ pub(crate) fn use_dict_get_with_default(
IfElseBlockInsteadOfDictGet {
contents: contents.clone(),
},
stmt.range(),
stmt_if.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if !has_comments(stmt, checker.locator, checker.indexer) {
if !has_comments(stmt_if, checker.locator, checker.indexer) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
contents,
stmt.range(),
stmt_if.range(),
)));
}
}

View File

@@ -127,13 +127,7 @@ fn is_dunder_method(name: &str) -> bool {
}
fn is_exception_check(stmt: &Stmt) -> bool {
let Stmt::If(ast::StmtIf {
test: _,
body,
orelse: _,
range: _,
}) = stmt
else {
let Stmt::If(ast::StmtIf { body, .. }) = stmt else {
return false;
};
matches!(body.as_slice(), [Stmt::Raise(_)])

View File

@@ -6,7 +6,7 @@ use ruff_diagnostics::{AutofixKind, Violation};
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::first_colon_range;
use ruff_python_whitespace::UniversalNewlines;
use ruff_python_trivia::UniversalNewlines;
use crate::checkers::ast::Checker;
use crate::line_width::LineWidth;

View File

@@ -5,14 +5,13 @@ use libcst_native::{
BooleanOp, BooleanOperation, CompoundStatement, Expression, If, LeftParen,
ParenthesizableWhitespace, ParenthesizedNode, RightParen, SimpleWhitespace, Statement, Suite,
};
use rustpython_parser::ast::Ranged;
use ruff_text_size::TextRange;
use crate::autofix::codemods::CodegenStylist;
use ruff_diagnostics::Edit;
use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_ast::whitespace;
use ruff_python_whitespace::PythonWhitespace;
use crate::autofix::codemods::CodegenStylist;
use crate::cst::matchers::{match_function_def, match_if, match_indented_block, match_statement};
fn parenthesize_and_operand(expr: Expression) -> Expression {
@@ -34,21 +33,19 @@ fn parenthesize_and_operand(expr: Expression) -> Expression {
pub(crate) fn fix_nested_if_statements(
locator: &Locator,
stylist: &Stylist,
stmt: &rustpython_parser::ast::Stmt,
range: TextRange,
) -> Result<Edit> {
// Infer the indentation of the outer block.
let Some(outer_indent) = whitespace::indentation(locator, stmt) else {
let Some(outer_indent) = whitespace::indentation(locator, &range) else {
bail!("Unable to fix multiline statement");
};
// Extract the module text.
let contents = locator.lines(stmt.range());
// Handle `elif` blocks differently; detect them upfront.
let is_elif = contents.trim_whitespace_start().starts_with("elif");
let contents = locator.lines(range);
// If this is an `elif`, we have to remove the `elif` keyword for now. (We'll
// restore the `el` later on.)
let is_elif = contents.starts_with("elif");
let module_text = if is_elif {
Cow::Owned(contents.replacen("elif", "if", 1))
} else {
@@ -128,6 +125,6 @@ pub(crate) fn fix_nested_if_statements(
Cow::Borrowed(module_text)
};
let range = locator.lines_range(stmt.range());
let range = locator.lines_range(range);
Ok(Edit::range_replacement(contents.to_string(), range))
}

View File

@@ -1,4 +1,4 @@
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::TextRange;
use rustpython_parser::ast::{
self, CmpOp, Comprehension, Constant, Expr, ExprContext, Ranged, Stmt, UnaryOp,
};
@@ -7,10 +7,11 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::source_code::Generator;
use ruff_python_ast::traversal;
use crate::checkers::ast::Checker;
use crate::line_width::LineWidth;
use crate::registry::{AsRule, Rule};
use crate::registry::AsRule;
/// ## What it does
/// Checks for `for` loops that can be replaced with a builtin function, like
@@ -38,7 +39,7 @@ use crate::registry::{AsRule, Rule};
/// - [Python documentation: `all`](https://docs.python.org/3/library/functions.html#all)
#[violation]
pub struct ReimplementedBuiltin {
repl: String,
replacement: String,
}
impl Violation for ReimplementedBuiltin {
@@ -46,207 +47,229 @@ impl Violation for ReimplementedBuiltin {
#[derive_message_formats]
fn message(&self) -> String {
let ReimplementedBuiltin { repl } = self;
format!("Use `{repl}` instead of `for` loop")
let ReimplementedBuiltin { replacement } = self;
format!("Use `{replacement}` instead of `for` loop")
}
fn autofix_title(&self) -> Option<String> {
let ReimplementedBuiltin { repl } = self;
Some(format!("Replace with `{repl}`"))
let ReimplementedBuiltin { replacement } = self;
Some(format!("Replace with `{replacement}`"))
}
}
/// SIM110, SIM111
pub(crate) fn convert_for_loop_to_any_all(
checker: &mut Checker,
stmt: &Stmt,
sibling: Option<&Stmt>,
) {
// There are two cases to consider:
pub(crate) fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt) {
if !checker.semantic().scope().kind.is_any_function() {
return;
}
// The `for` loop itself must consist of an `if` with a `return`.
let Some(loop_) = match_loop(stmt) else {
return;
};
// Afterwards, there are two cases to consider:
// - `for` loop with an `else: return True` or `else: return False`.
// - `for` loop followed by `return True` or `return False`
if let Some(loop_info) = return_values_for_else(stmt)
.or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling)))
{
// Check if loop_info.target, loop_info.iter, or loop_info.test contains `await`.
if contains_await(loop_info.target)
|| contains_await(loop_info.iter)
|| contains_await(loop_info.test)
{
return;
}
if loop_info.return_value && !loop_info.next_return_value {
if checker.enabled(Rule::ReimplementedBuiltin) {
let contents = return_stmt(
"any",
loop_info.test,
loop_info.target,
loop_info.iter,
checker.generator(),
);
// - `for` loop followed by `return True` or `return False`.
let Some(terminal) = match_else_return(stmt).or_else(|| {
let parent = checker.semantic().stmt_parent()?;
let suite = traversal::suite(stmt, parent)?;
let sibling = traversal::next_sibling(stmt, suite)?;
match_sibling_return(stmt, sibling)
}) else {
return;
};
// Don't flag if the resulting expression would exceed the maximum line length.
let line_start = checker.locator.line_start(stmt.start());
if LineWidth::new(checker.settings.tab_size)
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
.add_str(&contents)
> checker.settings.line_length
{
return;
}
// Check if any of the expressions contain an `await` expression.
if contains_await(loop_.target) || contains_await(loop_.iter) || contains_await(loop_.test) {
return;
}
let mut diagnostic = Diagnostic::new(
ReimplementedBuiltin {
repl: contents.clone(),
},
TextRange::new(stmt.start(), loop_info.terminal),
);
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
contents,
stmt.start(),
loop_info.terminal,
)));
}
checker.diagnostics.push(diagnostic);
match (loop_.return_value, terminal.return_value) {
// Replace with `any`.
(true, false) => {
let contents = return_stmt(
"any",
loop_.test,
loop_.target,
loop_.iter,
checker.generator(),
);
// Don't flag if the resulting expression would exceed the maximum line length.
let line_start = checker.locator.line_start(stmt.start());
if LineWidth::new(checker.settings.tab_size)
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
.add_str(&contents)
> checker.settings.line_length
{
return;
}
}
if !loop_info.return_value && loop_info.next_return_value {
if checker.enabled(Rule::ReimplementedBuiltin) {
// Invert the condition.
let test = {
if let Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::Not,
operand,
range: _,
}) = &loop_info.test
{
*operand.clone()
} else if let Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
range: _,
}) = &loop_info.test
{
if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) {
let op = match op {
CmpOp::Eq => CmpOp::NotEq,
CmpOp::NotEq => CmpOp::Eq,
CmpOp::Lt => CmpOp::GtE,
CmpOp::LtE => CmpOp::Gt,
CmpOp::Gt => CmpOp::LtE,
CmpOp::GtE => CmpOp::Lt,
CmpOp::Is => CmpOp::IsNot,
CmpOp::IsNot => CmpOp::Is,
CmpOp::In => CmpOp::NotIn,
CmpOp::NotIn => CmpOp::In,
};
let node = ast::ExprCompare {
left: left.clone(),
ops: vec![op],
comparators: vec![comparator.clone()],
range: TextRange::default(),
};
node.into()
} else {
let node = ast::ExprUnaryOp {
op: UnaryOp::Not,
operand: Box::new(loop_info.test.clone()),
range: TextRange::default(),
};
node.into()
}
let mut diagnostic = Diagnostic::new(
ReimplementedBuiltin {
replacement: contents.to_string(),
},
TextRange::new(stmt.start(), terminal.stmt.end()),
);
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("any") {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
contents,
stmt.start(),
terminal.stmt.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
// Replace with `all`.
(false, true) => {
// Invert the condition.
let test = {
if let Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::Not,
operand,
range: _,
}) = &loop_.test
{
*operand.clone()
} else if let Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
range: _,
}) = &loop_.test
{
if let ([op], [comparator]) = (ops.as_slice(), comparators.as_slice()) {
let op = match op {
CmpOp::Eq => CmpOp::NotEq,
CmpOp::NotEq => CmpOp::Eq,
CmpOp::Lt => CmpOp::GtE,
CmpOp::LtE => CmpOp::Gt,
CmpOp::Gt => CmpOp::LtE,
CmpOp::GtE => CmpOp::Lt,
CmpOp::Is => CmpOp::IsNot,
CmpOp::IsNot => CmpOp::Is,
CmpOp::In => CmpOp::NotIn,
CmpOp::NotIn => CmpOp::In,
};
let node = ast::ExprCompare {
left: left.clone(),
ops: vec![op],
comparators: vec![comparator.clone()],
range: TextRange::default(),
};
node.into()
} else {
let node = ast::ExprUnaryOp {
op: UnaryOp::Not,
operand: Box::new(loop_info.test.clone()),
operand: Box::new(loop_.test.clone()),
range: TextRange::default(),
};
node.into()
}
};
let contents = return_stmt(
"all",
&test,
loop_info.target,
loop_info.iter,
checker.generator(),
);
// Don't flag if the resulting expression would exceed the maximum line length.
let line_start = checker.locator.line_start(stmt.start());
if LineWidth::new(checker.settings.tab_size)
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
.add_str(&contents)
> checker.settings.line_length
{
return;
} else {
let node = ast::ExprUnaryOp {
op: UnaryOp::Not,
operand: Box::new(loop_.test.clone()),
range: TextRange::default(),
};
node.into()
}
};
let contents = return_stmt("all", &test, loop_.target, loop_.iter, checker.generator());
let mut diagnostic = Diagnostic::new(
ReimplementedBuiltin {
repl: contents.clone(),
},
TextRange::new(stmt.start(), loop_info.terminal),
);
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
contents,
stmt.start(),
loop_info.terminal,
)));
}
checker.diagnostics.push(diagnostic);
// Don't flag if the resulting expression would exceed the maximum line length.
let line_start = checker.locator.line_start(stmt.start());
if LineWidth::new(checker.settings.tab_size)
.add_str(&checker.locator.contents()[TextRange::new(line_start, stmt.start())])
.add_str(&contents)
> checker.settings.line_length
{
return;
}
let mut diagnostic = Diagnostic::new(
ReimplementedBuiltin {
replacement: contents.to_string(),
},
TextRange::new(stmt.start(), terminal.stmt.end()),
);
if checker.patch(diagnostic.kind.rule()) && checker.semantic().is_builtin("all") {
diagnostic.set_fix(Fix::suggested(Edit::replacement(
contents,
stmt.start(),
terminal.stmt.end(),
)));
}
checker.diagnostics.push(diagnostic);
}
_ => {}
}
}
/// Represents a `for` loop with a conditional `return`, like:
/// ```python
/// for x in y:
/// if x == 0:
/// return True
/// ```
#[derive(Debug)]
struct Loop<'a> {
/// The `return` value of the loop.
return_value: bool,
next_return_value: bool,
/// The test condition in the loop.
test: &'a Expr,
/// The target of the loop.
target: &'a Expr,
/// The iterator of the loop.
iter: &'a Expr,
terminal: TextSize,
}
/// Extract the returned boolean values a `Stmt::For` with an `else` body.
fn return_values_for_else(stmt: &Stmt) -> Option<Loop> {
/// Represents a `return` statement following a `for` loop, like:
/// ```python
/// for x in y:
/// if x == 0:
/// return True
/// return False
/// ```
///
/// Or:
/// ```python
/// for x in y:
/// if x == 0:
/// return True
/// else:
/// return False
/// ```
#[derive(Debug)]
struct Terminal<'a> {
return_value: bool,
stmt: &'a Stmt,
}
fn match_loop(stmt: &Stmt) -> Option<Loop> {
let Stmt::For(ast::StmtFor {
body,
target,
iter,
orelse,
..
body, target, iter, ..
}) = stmt
else {
return None;
};
// The loop itself should contain a single `if` statement, with an `else`
// containing a single `return True` or `return False`.
if body.len() != 1 {
return None;
}
if orelse.len() != 1 {
return None;
}
let Stmt::If(ast::StmtIf {
// The loop itself should contain a single `if` statement, with a single `return` statement in
// the body.
let [Stmt::If(ast::StmtIf {
body: nested_body,
test: nested_test,
orelse: nested_orelse,
elif_else_clauses: nested_elif_else_clauses,
range: _,
}) = &body[0]
})] = body.as_slice()
else {
return None;
};
if nested_body.len() != 1 {
return None;
}
if !nested_orelse.is_empty() {
if !nested_elif_else_clauses.is_empty() {
return None;
}
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &nested_body[0] else {
@@ -263,15 +286,35 @@ fn return_values_for_else(stmt: &Stmt) -> Option<Loop> {
return None;
};
// The `else` block has to contain a single `return True` or `return False`.
let Stmt::Return(ast::StmtReturn {
value: next_value,
range: _,
}) = &orelse[0]
else {
Some(Loop {
return_value: *value,
test: nested_test,
target,
iter,
})
}
/// If a `Stmt::For` contains an `else` with a single boolean `return`, return the [`Terminal`]
/// representing that `return`.
///
/// For example, matches the `return` in:
/// ```python
/// for x in y:
/// if x == 0:
/// return True
/// return False
/// ```
fn match_else_return(stmt: &Stmt) -> Option<Terminal> {
let Stmt::For(ast::StmtFor { orelse, .. }) = stmt else {
return None;
};
let Some(next_value) = next_value else {
// The `else` block has to contain a single `return True` or `return False`.
let [Stmt::Return(ast::StmtReturn {
value: Some(next_value),
range: _,
})] = orelse.as_slice()
else {
return None;
};
let Expr::Constant(ast::ExprConstant {
@@ -282,78 +325,41 @@ fn return_values_for_else(stmt: &Stmt) -> Option<Loop> {
return None;
};
Some(Loop {
return_value: *value,
next_return_value: *next_value,
test: nested_test,
target,
iter,
terminal: stmt.end(),
Some(Terminal {
return_value: *next_value,
stmt,
})
}
/// Extract the returned boolean values from subsequent `Stmt::For` and
/// `Stmt::Return` statements, or `None`.
fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option<Loop<'a>> {
let Stmt::For(ast::StmtFor {
body,
target,
iter,
orelse,
..
}) = stmt
else {
/// If a `Stmt::For` is followed by a boolean `return`, return the [`Terminal`] representing that
/// `return`.
///
/// For example, matches the `return` in:
/// ```python
/// for x in y:
/// if x == 0:
/// return True
/// else:
/// return False
/// ```
fn match_sibling_return<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option<Terminal<'a>> {
let Stmt::For(ast::StmtFor { orelse, .. }) = stmt else {
return None;
};
// The loop itself should contain a single `if` statement, with a single `return
// True` or `return False`.
if body.len() != 1 {
return None;
}
// The loop itself shouldn't have an `else` block.
if !orelse.is_empty() {
return None;
}
let Stmt::If(ast::StmtIf {
body: nested_body,
test: nested_test,
orelse: nested_orelse,
range: _,
}) = &body[0]
else {
return None;
};
if nested_body.len() != 1 {
return None;
}
if !nested_orelse.is_empty() {
return None;
}
let Stmt::Return(ast::StmtReturn { value, range: _ }) = &nested_body[0] else {
return None;
};
let Some(value) = value else {
return None;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Bool(value),
..
}) = value.as_ref()
else {
return None;
};
// The next statement has to be a `return True` or `return False`.
let Stmt::Return(ast::StmtReturn {
value: next_value,
value: Some(next_value),
range: _,
}) = &sibling
else {
return None;
};
let Some(next_value) = next_value else {
return None;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Bool(next_value),
..
@@ -362,13 +368,9 @@ fn return_values_for_siblings<'a>(stmt: &'a Stmt, sibling: &'a Stmt) -> Option<L
return None;
};
Some(Loop {
return_value: *value,
next_return_value: *next_value,
test: nested_test,
target,
iter,
terminal: sibling.end(),
Some(Terminal {
return_value: *next_value,
stmt: sibling,
})
}

View File

@@ -48,6 +48,31 @@ SIM102.py:7:1: SIM102 [*] Use a single `if` statement instead of nested `if` sta
12 11 | # SIM102
13 12 | if a:
SIM102.py:8:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
6 | # SIM102
7 | if a:
8 | if b:
| _____^
9 | | if c:
| |_____________^ SIM102
10 | d
|
= help: Combine `if` statements using `and`
Suggested fix
5 5 |
6 6 | # SIM102
7 7 | if a:
8 |- if b:
9 |- if c:
10 |- d
8 |+ if b and c:
9 |+ d
11 10 |
12 11 | # SIM102
13 12 | if a:
SIM102.py:15:1: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
13 | if a:
@@ -255,30 +280,56 @@ SIM102.py:97:1: SIM102 Use a single `if` statement instead of nested `if` statem
|
= help: Combine `if` statements using `and`
SIM102.py:124:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
SIM102.py:106:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
122 | if a:
123 | # SIM 102
124 | if b:
104 | # Regression test for https://github.com/apache/airflow/blob/145b16caaa43f0c42bffd97344df916c602cddde/airflow/configuration.py#L1161
105 | if a:
106 | if b:
| _____^
125 | | if c:
107 | | if c:
| |_____________^ SIM102
126 | print("foo")
127 | else:
108 | print("if")
109 | elif d:
|
= help: Combine `if` statements using `and`
Suggested fix
121 121 | # OK
122 122 | if a:
123 123 | # SIM 102
124 |- if b:
125 |- if c:
126 |- print("foo")
124 |+ if b and c:
125 |+ print("foo")
127 126 | else:
128 127 | print("bar")
129 128 |
103 103 | # SIM102
104 104 | # Regression test for https://github.com/apache/airflow/blob/145b16caaa43f0c42bffd97344df916c602cddde/airflow/configuration.py#L1161
105 105 | if a:
106 |- if b:
107 |- if c:
108 |- print("if")
106 |+ if b and c:
107 |+ print("if")
109 108 | elif d:
110 109 | print("elif")
111 110 |
SIM102.py:132:5: SIM102 [*] Use a single `if` statement instead of nested `if` statements
|
130 | if a:
131 | # SIM 102
132 | if b:
| _____^
133 | | if c:
| |_____________^ SIM102
134 | print("foo")
135 | else:
|
= help: Combine `if` statements using `and`
Suggested fix
129 129 | # OK
130 130 | if a:
131 131 | # SIM 102
132 |- if b:
133 |- if c:
134 |- print("foo")
132 |+ if b and c:
133 |+ print("foo")
135 134 | else:
136 135 | print("bar")
137 136 |

View File

@@ -25,6 +25,32 @@ SIM108.py:2:1: SIM108 [*] Use ternary operator `b = c if a else d` instead of `i
7 4 | # OK
8 5 | b = c if a else d
SIM108.py:30:5: SIM108 [*] Use ternary operator `b = 1 if a else 2` instead of `if`-`else`-block
|
28 | pass
29 | else:
30 | if a:
| _____^
31 | | b = 1
32 | | else:
33 | | b = 2
| |_____________^ SIM108
|
= help: Replace `if`-`else`-block with `b = 1 if a else 2`
Suggested fix
27 27 | if True:
28 28 | pass
29 29 | else:
30 |- if a:
31 |- b = 1
32 |- else:
33 |- b = 2
30 |+ b = 1 if a else 2
34 31 |
35 32 |
36 33 | import sys
SIM108.py:58:1: SIM108 Use ternary operator `abc = x if x > 0 else -x` instead of `if`-`else`-block
|
57 | # SIM108 (without fix due to comments)

View File

@@ -127,12 +127,11 @@ SIM114.py:38:1: SIM114 Combine `if` branches using logical `or` operator
58 | if result.eofs == "O":
|
SIM114.py:62:6: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:62:1: SIM114 Combine `if` branches using logical `or` operator
|
60 | elif result.eofs == "S":
61 | skipped = 1
62 | elif result.eofs == "F":
| ______^
62 | / elif result.eofs == "F":
63 | | errors = 1
64 | | elif result.eofs == "E":
65 | | errors = 1

View File

@@ -105,6 +105,8 @@ SIM116.py:79:1: SIM116 Use a dictionary instead of consecutive `if` statements
85 | | elif func_name == "move":
86 | | return "MV"
| |_______________^ SIM116
87 |
88 | # OK
|

View File

@@ -114,7 +114,7 @@ SIM401.py:36:1: SIM401 [*] Use `vars[idx] = a_dict.get(key, "defaultß9💣26
39 | | vars[idx] = "defaultß9💣26789ß9💣26789ß9💣26789ß9💣26789ß9💣26789"
| |___________________________________________________________________________^ SIM401
40 |
41 | ###
41 | # SIM401
|
= help: Replace with `vars[idx] = a_dict.get(key, "defaultß9💣26789ß9💣26789ß9💣26789ß9💣26789ß9💣26789")`
@@ -128,7 +128,35 @@ SIM401.py:36:1: SIM401 [*] Use `vars[idx] = a_dict.get(key, "defaultß9💣26
39 |- vars[idx] = "defaultß9💣26789ß9💣26789ß9💣26789ß9💣26789ß9💣26789"
36 |+vars[idx] = a_dict.get(key, "defaultß9💣26789ß9💣26789ß9💣26789ß9💣26789ß9💣26789")
40 37 |
41 38 | ###
42 39 | # Negative cases
41 38 | # SIM401
42 39 | if foo():
SIM401.py:45:5: SIM401 [*] Use `vars[idx] = a_dict.get(key, "default")` instead of an `if` block
|
43 | pass
44 | else:
45 | if key in a_dict:
| _____^
46 | | vars[idx] = a_dict[key]
47 | | else:
48 | | vars[idx] = "default"
| |_____________________________^ SIM401
49 |
50 | ###
|
= help: Replace with `vars[idx] = a_dict.get(key, "default")`
Suggested fix
42 42 | if foo():
43 43 | pass
44 44 | else:
45 |- if key in a_dict:
46 |- vars[idx] = a_dict[key]
47 |- else:
48 |- vars[idx] = "default"
45 |+ vars[idx] = a_dict.get(key, "default")
49 46 |
50 47 | ###
51 48 | # Negative cases

View File

@@ -4,7 +4,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{NodeId, ReferenceId, Scope};
use ruff_python_semantic::{NodeId, ResolvedReferenceId, Scope};
use crate::autofix;
use crate::checkers::ast::Checker;
@@ -180,7 +180,7 @@ struct Import<'a> {
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
qualified_name: &'a str,
/// The first reference to the imported symbol.
reference_id: ReferenceId,
reference_id: ResolvedReferenceId,
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
range: TextRange,
/// The range of the import's parent statement.

View File

@@ -4,7 +4,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::{Binding, NodeId, ReferenceId, Scope};
use ruff_python_semantic::{Binding, NodeId, ResolvedReferenceId, Scope};
use crate::autofix;
use crate::checkers::ast::Checker;
@@ -357,7 +357,7 @@ struct Import<'a> {
/// The qualified name of the import (e.g., `typing.List` for `from typing import List`).
qualified_name: &'a str,
/// The first reference to the imported symbol.
reference_id: ReferenceId,
reference_id: ResolvedReferenceId,
/// The trimmed range of the import (e.g., `List` in `from typing import List`).
range: TextRange,
/// The range of the import's parent statement.

View File

@@ -56,6 +56,11 @@ mod tests {
#[test_case(Rule::PyPath, Path::new("py_path_1.py"))]
#[test_case(Rule::PyPath, Path::new("py_path_2.py"))]
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetatime, Path::new("PTH203.py"))]
#[test_case(Rule::OsPathGetmtime, Path::new("PTH204.py"))]
#[test_case(Rule::OsPathGetctime, Path::new("PTH205.py"))]
fn rules_pypath(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,3 +1,13 @@
pub(crate) use os_path_getatime::*;
pub(crate) use os_path_getctime::*;
pub(crate) use os_path_getmtime::*;
pub(crate) use os_path_getsize::*;
pub(crate) use path_constructor_current_directory::*;
pub(crate) use replaceable_by_pathlib::*;
mod os_path_getatime;
mod os_path_getctime;
mod os_path_getmtime;
mod os_path_getsize;
mod path_constructor_current_directory;
mod replaceable_by_pathlib;

View File

@@ -0,0 +1,43 @@
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
/// ## What it does
/// Checks for uses of `os.path.getatime`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`.
///
/// When possible, using `Path` object methods such as `Path.stat()` can
/// improve readability over the `os` module's counterparts (e.g.,
/// `os.path.getsize()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
/// e.g., in hot loops.
///
/// ## Examples
/// ```python
/// os.path.getsize(__file__)
/// ```
///
/// Use instead:
/// ```python
/// Path(__file__).stat().st_size
/// ```
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
/// - [PEP 428](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[violation]
pub struct OsPathGetatime;
impl Violation for OsPathGetatime {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.getatime` should be replaced by `Path.stat().st_atime`")
}
}

View File

@@ -0,0 +1,43 @@
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
/// ## What it does
/// Checks for uses of `os.path.getatime`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`.
///
/// When possible, using `Path` object methods such as `Path.stat()` can
/// improve readability over the `os` module's counterparts (e.g.,
/// `os.path.getsize()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
/// e.g., in hot loops.
///
/// ## Examples
/// ```python
/// os.path.getsize(__file__)
/// ```
///
/// Use instead:
/// ```python
/// Path(__file__).stat().st_size
/// ```
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
/// - [PEP 428](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[violation]
pub struct OsPathGetctime;
impl Violation for OsPathGetctime {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.getctime` should be replaced by `Path.stat().st_ctime`")
}
}

View File

@@ -0,0 +1,43 @@
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
/// ## What it does
/// Checks for uses of `os.path.getatime`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`.
///
/// When possible, using `Path` object methods such as `Path.stat()` can
/// improve readability over the `os` module's counterparts (e.g.,
/// `os.path.getsize()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
/// e.g., in hot loops.
///
/// ## Examples
/// ```python
/// os.path.getsize(__file__)
/// ```
///
/// Use instead:
/// ```python
/// Path(__file__).stat().st_size
/// ```
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
/// - [PEP 428](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[violation]
pub struct OsPathGetmtime;
impl Violation for OsPathGetmtime {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.getmtime` should be replaced by `Path.stat().st_mtime`")
}
}

View File

@@ -0,0 +1,43 @@
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
/// ## What it does
/// Checks for uses of `os.path.getsize`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os`.
///
/// When possible, using `Path` object methods such as `Path.stat()` can
/// improve readability over the `os` module's counterparts (e.g.,
/// `os.path.getsize()`).
///
/// Note that `os` functions may be preferable if performance is a concern,
/// e.g., in hot loops.
///
/// ## Examples
/// ```python
/// os.path.getsize(__file__)
/// ```
///
/// Use instead:
/// ```python
/// Path(__file__).stat().st_size
/// ```
///
/// ## References
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
/// - [PEP 428](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[violation]
pub struct OsPathGetsize;
impl Violation for OsPathGetsize {
#[derive_message_formats]
fn message(&self) -> String {
format!("`os.path.getsize` should be replaced by `Path.stat().st_size`")
}
}

View File

@@ -0,0 +1,85 @@
use rustpython_parser::ast::{Constant, Expr, ExprCall, ExprConstant};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for `pathlib.Path` objects that are initialized with the current
/// directory.
///
/// ## Why is this bad?
/// The `Path()` constructor defaults to the current directory, so passing it
/// in explicitly (as `"."`) is unnecessary.
///
/// ## Example
/// ```python
/// from pathlib import Path
///
/// _ = Path(".")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// _ = Path()
/// ```
///
/// ## References
/// - [Python documentation: `Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)
#[violation]
pub struct PathConstructorCurrentDirectory;
impl AlwaysAutofixableViolation for PathConstructorCurrentDirectory {
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not pass the current directory explicitly to `Path`")
}
fn autofix_title(&self) -> String {
"Remove the current directory argument".to_string()
}
}
/// PTH201
pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &Expr, func: &Expr) {
if !checker
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["pathlib", "Path" | "PurePath"])
})
{
return;
}
let Expr::Call(ExprCall { args, keywords, .. }) = expr else {
return;
};
if !keywords.is_empty() {
return;
}
let [Expr::Constant(ExprConstant {
value: Constant::Str(value),
kind: _,
range,
})] = args.as_slice()
else {
return;
};
if value != "." {
return;
}
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, *range);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::range_deletion(*range)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -4,6 +4,9 @@ use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_use_pathlib::rules::{
OsPathGetatime, OsPathGetctime, OsPathGetmtime, OsPathGetsize,
};
use crate::rules::flake8_use_pathlib::violations::{
BuiltinOpen, OsChmod, OsGetcwd, OsMakedirs, OsMkdir, OsPathAbspath, OsPathBasename,
OsPathDirname, OsPathExists, OsPathExpanduser, OsPathIsabs, OsPathIsdir, OsPathIsfile,
@@ -41,6 +44,14 @@ pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, expr: &Expr) {
["os", "path", "dirname"] => Some(OsPathDirname.into()),
["os", "path", "samefile"] => Some(OsPathSamefile.into()),
["os", "path", "splitext"] => Some(OsPathSplitext.into()),
// PTH202
["os", "path", "getsize"] => Some(OsPathGetsize.into()),
// PTH203
["os", "path", "getatime"] => Some(OsPathGetatime.into()),
// PTH204
["os", "path", "getmtime"] => Some(OsPathGetmtime.into()),
// PTH205
["os", "path", "getctime"] => Some(OsPathGetctime.into()),
["", "open"] => Some(BuiltinOpen.into()),
["py", "path", "local"] => Some(PyPath.into()),
// Python 3.9+

View File

@@ -0,0 +1,65 @@
---
source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
---
PTH201.py:5:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
4 | # match
5 | _ = Path(".")
| ^^^ PTH201
6 | _ = pth(".")
7 | _ = PurePath(".")
|
= help: Remove the current directory argument
Fix
2 2 | from pathlib import Path as pth
3 3 |
4 4 | # match
5 |-_ = Path(".")
5 |+_ = Path()
6 6 | _ = pth(".")
7 7 | _ = PurePath(".")
8 8 |
PTH201.py:6:9: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
4 | # match
5 | _ = Path(".")
6 | _ = pth(".")
| ^^^ PTH201
7 | _ = PurePath(".")
|
= help: Remove the current directory argument
Fix
3 3 |
4 4 | # match
5 5 | _ = Path(".")
6 |-_ = pth(".")
6 |+_ = pth()
7 7 | _ = PurePath(".")
8 8 |
9 9 | # no match
PTH201.py:7:14: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
5 | _ = Path(".")
6 | _ = pth(".")
7 | _ = PurePath(".")
| ^^^ PTH201
8 |
9 | # no match
|
= help: Remove the current directory argument
Fix
4 4 | # match
5 5 | _ = Path(".")
6 6 | _ = pth(".")
7 |-_ = PurePath(".")
7 |+_ = PurePath()
8 8 |
9 9 | # no match
10 10 | _ = Path()

View File

@@ -0,0 +1,76 @@
---
source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
---
PTH202.py:6:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
6 | os.path.getsize("filename")
| ^^^^^^^^^^^^^^^ PTH202
7 | os.path.getsize(b"filename")
8 | os.path.getsize(Path("filename"))
|
PTH202.py:7:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
6 | os.path.getsize("filename")
7 | os.path.getsize(b"filename")
| ^^^^^^^^^^^^^^^ PTH202
8 | os.path.getsize(Path("filename"))
9 | os.path.getsize(__file__)
|
PTH202.py:8:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
6 | os.path.getsize("filename")
7 | os.path.getsize(b"filename")
8 | os.path.getsize(Path("filename"))
| ^^^^^^^^^^^^^^^ PTH202
9 | os.path.getsize(__file__)
|
PTH202.py:9:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
7 | os.path.getsize(b"filename")
8 | os.path.getsize(Path("filename"))
9 | os.path.getsize(__file__)
| ^^^^^^^^^^^^^^^ PTH202
10 |
11 | getsize("filename")
|
PTH202.py:11:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
9 | os.path.getsize(__file__)
10 |
11 | getsize("filename")
| ^^^^^^^ PTH202
12 | getsize(b"filename")
13 | getsize(Path("filename"))
|
PTH202.py:12:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
11 | getsize("filename")
12 | getsize(b"filename")
| ^^^^^^^ PTH202
13 | getsize(Path("filename"))
14 | getsize(__file__)
|
PTH202.py:13:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
11 | getsize("filename")
12 | getsize(b"filename")
13 | getsize(Path("filename"))
| ^^^^^^^ PTH202
14 | getsize(__file__)
|
PTH202.py:14:1: PTH202 `os.path.getsize` should be replaced by `Path.stat().st_size`
|
12 | getsize(b"filename")
13 | getsize(Path("filename"))
14 | getsize(__file__)
| ^^^^^^^ PTH202
|

View File

@@ -0,0 +1,54 @@
---
source: crates/ruff/src/rules/flake8_use_pathlib/mod.rs
---
PTH203.py:5:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
3 | from os.path import getatime
4 |
5 | os.path.getatime("filename")
| ^^^^^^^^^^^^^^^^ PTH203
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
|
PTH203.py:6:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
5 | os.path.getatime("filename")
6 | os.path.getatime(b"filename")
| ^^^^^^^^^^^^^^^^ PTH203
7 | os.path.getatime(Path("filename"))
|
PTH203.py:7:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
5 | os.path.getatime("filename")
6 | os.path.getatime(b"filename")
7 | os.path.getatime(Path("filename"))
| ^^^^^^^^^^^^^^^^ PTH203
|
PTH203.py:10:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
| ^^^^^^^^ PTH203
11 | getatime(b"filename")
12 | getatime(Path("filename"))
|
PTH203.py:11:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
11 | getatime(b"filename")
| ^^^^^^^^ PTH203
12 | getatime(Path("filename"))
|
PTH203.py:12:1: PTH203 `os.path.getatime` should be replaced by `Path.stat().st_atime`
|
10 | getatime("filename")
11 | getatime(b"filename")
12 | getatime(Path("filename"))
| ^^^^^^^^ PTH203
|

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