Compare commits

...

116 Commits

Author SHA1 Message Date
konstin
d4cfe1f2a7 Break before slice colon
**Summary** Break slices at the colon first, since the colon is separator with the lowest precedence and we're in a parenthesized context.

**Input**
```python
section_header_data = byte_array[byte_begin_index + byte_step_index * event_index : byte_begin_index + byte_step_index * (event_index + 1)]
```
**Black**
```python
section_header_data = byte_array[
    byte_begin_index
    + byte_step_index * event_index : byte_begin_index
    + byte_step_index * (event_index + 1)
]
```
**Current formatting**
```python
section_header_data = byte_array[
    byte_begin_index + byte_step_index * event_index : byte_begin_index
    + byte_step_index * (event_index + 1)
]
```
**Proposed formatting**
```python
section_header_data = byte_array[
    byte_begin_index + byte_step_index * event_index
    : byte_begin_index + byte_step_index * (event_index + 1)
]
```

This is another intentional black deviation, but i find it a clear style improvement.

This is consistent with adding a step:
```python
section_header_data2 = byte_array[
    byte_begin_index + byte_step_index * event_index
    : byte_begin_index + byte_step_index
    : section_size
]
```

As-is, this regresses trailing colon comments:

**in**
```python
c1 = "c"[
    1:  # e
    # f
    2
]
```
**out**
```python
c1 = "c"[
    1
    :  # e
    # f
    2
]
```

Fixes #7316

**Test Plan** Added the fixtures above.
2023-09-13 12:11:53 +02:00
konsti
f4c7bff36b Don't reorder parameters in function calls (#7268)
## Summary

In `f(*args, a=b, *args2, **kwargs)` the args (`*args`, `*args2`) and
keywords (`a=b`, `**kwargs`) are interleaved, which we previously didn't
handle.

Fixes #6498

**main**

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| **django** | 0.99966 | 2760 | 58 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |

**PR**

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| **django** | 0.99967 | 2760 | 53 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |


## Test Plan

New fixtures
2023-09-13 09:01:49 +00:00
konsti
56440ad835 Introduce ArgOrKeyword to keep call parameter order (#7302)
## Motivation

The `ast::Arguments` for call argument are split into positional
arguments (args) and keywords arguments (keywords). We currently assume
that call consists of first args and then keywords, which is generally
the case, but not always:

```python
f(*args, a=2, *args2, **kwargs)

class A(*args, a=2, *args2, **kwargs):
    pass
```

The consequence is accidentally reordering arguments
(https://github.com/astral-sh/ruff/pull/7268).

## Summary

`Arguments::args_and_keywords` returns an iterator of an `ArgOrKeyword`
enum that yields args and keywords in the correct order. I've fixed the
obvious `args` and `keywords` usages, but there might be some cases with
wrong assumptions remaining.

## Test Plan

The generator got new test cases, otherwise the stacked PR
(https://github.com/astral-sh/ruff/pull/7268) which uncovered this.
2023-09-13 08:45:46 +00:00
Charlie Marsh
179128dc54 Link discussion in formatter README (#7311) 2023-09-12 16:50:22 +00:00
Charlie Marsh
e7a2779402 Bump version to v0.0.289 (#7308) 2023-09-12 12:00:11 -04:00
Zanie Blue
008da95b29 Add preview documentation section (#7281)
Adds a basic documentation section for preview mode based on the FAQ
entry and versioning RFC.
2023-09-12 15:43:31 +00:00
Zanie Blue
5d4dd3e38e Set the target deployment to main during dispatched documentation deployments (#7304)
Closes #7276 by deploying to production when not triggered by a pull
request.
2023-09-12 10:34:05 -05:00
Micha Reiser
e561f5783b Fix(vscode): Respect line length ruff.toml configuration (#7306) 2023-09-12 15:31:47 +00:00
Dhruv Manilawala
ee0f1270cf Add NotebookIndex to the cache (#6863)
## Summary

This PR updates the `FileCache` to include an optional `NotebookIndex`
to support caching for Jupyter Notebooks.

We only require the index to compute the diagnostics and thus we don't
really need to store the entire `Notebook` on the `Diagnostics` struct.
This means we only need the index to be stored in the cache to
reconstruct the `Diagnostics`.

## Test Plan

Update an existing test case to run over the fixtures under
`ruff_notebook` crate where there are multiple Jupyter Notebook.

Locally, the following commands were run in order:
1. Remove the cache: `rm -rf .ruff_cache`
2. Run without cache: `cargo run --bin ruff -- check --isolated
crates/ruff_notebook/resources/test/fixtures/jupyter/unused_variable.ipynb
--no-cache`
3. Run with cache: `cargo run --bin ruff -- check --isolated
crates/ruff_notebook/resources/test/fixtures/jupyter/unused_variable.ipynb`
4. Check whether the `.ruff_cache` directory was created or not
5. Run with cache again and verify: `cargo run --bin ruff -- check
--isolated
crates/ruff_notebook/resources/test/fixtures/jupyter/unused_variable.ipynb`

## Benchmarks

https://github.com/astral-sh/ruff/pull/6863#issuecomment-1715675186

fixes: #6671
2023-09-12 18:29:03 +05:30
Tom Kuson
e7b7e4a18d Add documentation to duplicate-union-member (#7225)
## Summary

Add documentation to `duplicate-union-member` (`PYI016`) rule. Related
to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-09-12 08:56:33 -04:00
Brendon Happ
b4419c34ea Ignore @override method when enforcing bad-dunder-name rule (#7224)
## Summary

Closes #6958.

If a method has the `override` decorator, there is nothing you can do
about incorrect dunder methods, so they should be ignored.

## Test Plan

Overridden incorrect dunder method was added to the tests to verify ruff
doesn't catch it when evaluating the file. Snapshot changes are all just
line number changes
2023-09-12 11:54:40 +00:00
Micha Reiser
08f19226b9 Fix panic when formatting binary expression with two implicit concatenated string operands (#7287) 2023-09-12 09:49:51 +02:00
Micha Reiser
1e6df19a35 Bool expression comment placement (#7269) 2023-09-12 06:39:57 +00:00
Zanie Blue
c21b960fc7 Display the --preview option in the CLI help menu (#7274)
If we're going to warn on use of NURSERY in #7210 we probably ought to
show the `--preview` option in our help menus.
2023-09-11 18:09:58 -05:00
Zanie Blue
73ad2affa1 Update preview and fix documentation symbols (#7207)
I don't love the sunrise emoji and 🧪 seems nice :)

Requires #7195

---------

Co-authored-by: konsti <konstin@mailbox.org>
2023-09-11 18:08:00 -05:00
Zanie Blue
40c936922e Add "Preview" section to auto-generated release notes (#7280) 2023-09-11 18:07:47 -05:00
Charlie Marsh
874db4fb86 Invert condition for < and <= in outdated version block (#7284)
Closes https://github.com/astral-sh/ruff/issues/7258.
2023-09-11 23:02:23 +00:00
Dhruv Manilawala
a41bb2733f Add range to lexer test snapshots (#7265)
## Summary

This PR updates the lexer test snapshots to include the range value as
well. This is mainly a mechanical refactor.

### Motivation

The main motivation is so that we can verify that the ranges are valid
and do not overlap.

## Test Plan

`cargo test`
2023-09-11 19:12:46 +00:00
Zanie Blue
24b848a4ea Enable preview mode during benchmarks (#7208)
Split out of https://github.com/astral-sh/ruff/pull/7195 so benchmark
changes from enabling additional rules can be reviewed separately.
2023-09-11 14:09:33 -05:00
Zanie Blue
773ba5f816 Update the docs workflow to allow publishing a specific ref (#7278)
Related https://github.com/astral-sh/ruff/issues/7276

Our docs publishing action does not allow targetting a specific commit
when run manually which means we cannot update the documentation to
anything but the latest commit on `main`. This change allows a ref to be
provided.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2023-09-11 18:51:31 +00:00
Dhruv Manilawala
f5701fcc63 Use snapshots for remaining lexer tests (#7264)
## Summary

This PR updates the remaining lexer test cases to use the snapshots.
This is mainly a mechanical refactor.

## Motivation

The main motivation is so that when we add the token range values to the
test case output, it's easier to update the test cases.

The reason they were not using the snapshots before was because of the usage of
`test_case` macro. The macros is mainly used for different EOL test cases. If we
just generate the snapshots directly, then the snapshot name would be suffixed
with `-1`, `-2`, etc. as the test function is still the same. So, we'll create
the snapshot ourselves with the platform name for the respective EOL
test cases.

## Test Plan

`cargo test`
2023-09-12 00:16:38 +05:30
Zanie Blue
ff0feb191c Use pages deploy instead of the deprecated pages publish command to deploy the docs website (#7277)
See https://github.com/cloudflare/workers-sdk/issues/3067

Related #7276
2023-09-11 13:23:47 -05:00
Zanie Blue
6566d00295 Update rule selection to respect preview mode (#7195)
## Summary

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

Extends work in #7046 (some relevant discussion there)

Changes:

- All nursery rules are now referred to as preview rules
- Documentation for the nursery is updated to describe preview
- Adds a "PREVIEW" selector for preview rules
- This is primarily to allow `--preview --ignore PREVIEW --extend-select
FOO001,BAR200`
- Using `--preview` enables preview rules that match selectors

Notable decisions:

- Preview rules are not selectable by their rule code without enabling
preview
- Retains the "NURSERY" selector for backwards compatibility
- Nursery rules are selectable by their rule code for backwards
compatiblity

Additional work:

- Selection of preview rules without the "--preview" flag should display
a warning
- Use of deprecated nursery selection behavior should display a warning
- Nursery selection should be removed after some time

## Test Plan

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

Manual confirmation (i.e. we don't have an preview rules yet just
nursery rules so I added a preview rule for manual testing)

New unit tests

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-09-11 12:28:39 -05:00
Micha Reiser
7c9bbcf4e2 Bump version to 0.0.288 (#7271)
Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-09-11 18:18:11 +02:00
konsti
babf8d718e Fix D204 when there's a semicolon after a docstring (#7174)
## Summary

Another statement on the same line as the docstring would previous make
the D204 (newline after docstring) fix fail:

```python
class StatementOnSameLineAsDocstring:
    "After this docstring there's another statement on the same line separated by a semicolon." ;priorities=1
    def sort_services(self):
        pass
```

The fix handles this case manually:

```python
class StatementOnSameLineAsDocstring:
    "After this docstring there's another statement on the same line separated by a semicolon."

    priorities=1
    def sort_services(self):
        pass
```

Fixes #7088

## Test Plan

Added a new `D` test case
2023-09-11 15:09:47 +02:00
dependabot[bot]
878813f277 Bump tj-actions/changed-files from 38 to 39 (#7267)
Co-authored-by: repo-ranger[bot] <!-- raw HTML omitted --> (<a
Co-authored-by: renovate[bot] <!-- raw HTML omitted --> (<a
Co-authored-by: jackton1 <a
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 11:12:24 +02:00
dependabot[bot]
71c8a02ebd Bump actions/checkout from 3 to 4 (#7266)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-11 11:11:25 +02:00
konsti
3a2c3a7398 Format empty lines in stub files like black's preview style (#7206)
## Summary

Fix all but one empty line differences with the black preview style in
typeshed. The remaining differences are breaking with type comments and
trailing commas in function definitions.

I compared the empty line differences with the preview mode of black
since stable has some oddities that would have been hard to replicate
(https://github.com/psf/black/issues/3861). Additionally, it assumes the
style proposed in https://github.com/psf/black/issues/3862.

An edge case that also surfaced with typeshed are newline before
trailing module comments.

**main**

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99966 | 2760 | 58 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| **typeshed** | 0.99978 | 3496 | **2173** |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |

**PR**
| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99966 | 2760 | 58 |
| transformers | 0.99930 | 2587 | 447 |
| twine | 1.00000 | 33 | 0 |
| **typeshed** | 0.99983 | 3496 | **18** |
| warehouse | 0.99825 | 648 | 22 |
| zulip | 0.99950 | 1437 | 27 |


Closes #6723

## Test Plan

The main driver was the typeshed diff. I added new test cases for all
kinds of possible empty line combinations in stub files, test cases for
newlines before trailing module comments.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-09-11 08:03:59 +00:00
Micha Reiser
7440e54ec6 Avoid allocating in lex_decimal (#7252) 2023-09-11 06:37:25 +00:00
Charlie Marsh
0357e801ed Avoid F401 panic with noqa import name (#7260)
Closes https://github.com/astral-sh/ruff/issues/7244.
2023-09-10 16:31:25 +00:00
Ely Ronnen
69d0caabe7 fix D417 error with function docstrings with dashed lines (#7251)
## Summary

Fix https://github.com/astral-sh/ruff/issues/7250, False positive D417
for docstrings with dashed lines.

## Test Plan

Tested on the example in https://github.com/astral-sh/ruff/issues/7250
2023-09-10 17:28:44 +01:00
Micha Reiser
9cb5ce750e Remove IO based lints from linter benchmark (#7240) 2023-09-08 13:39:50 +02:00
Micha Reiser
0a07a2ca62 Extract string part and normalized string (#7219) 2023-09-08 12:56:55 +02:00
Micha Reiser
47a253fb62 Add PreviewMode option to formatter
## Summary

This PR adds the `--preview` and `--no-preview` options to the `format` command (hidden) and passes it through to the formatte. 

## Test Plan

I added the `dbg(f.options().preview())` statement in `FormatNodeRule::fmt` and verified that the option gets correctly passed to the formatter.
2023-09-08 12:04:28 +02:00
Micha Reiser
d9544a2d37 Disable default criterion features (#7241) 2023-09-08 10:03:14 +00:00
Micha Reiser
41f0aad7b3 Add FString support to binary like formatting
## Summary

This is the last part of the string - binary like formatting. It adds support for handling fstrings the same as "regular" strings.


## Test Plan

I added a test for both binary and comparison. 

Small improvements across several projects

**This PR**
| project      | similarity index  | total files       | changed files     |
|--------------|------------------:|------------------:|------------------:|
| cpython      |           0.76083 |              1789 |              1632 |
| django       |           0.99966 |              2760 |                58 |
| **transformers** |           0.99929 |              2587 |               454 |
| twine        |           1.00000 |                33 |                 0 |
| typeshed     |           0.99978 |              3496 |              2173 |
| **warehouse**    |           0.99825 |               648 |                22 |
| **zulip**        |           0.99950 |              1437 |                27 |


**Base**

| project      | similarity index  | total files       | changed files     |
|--------------|------------------:|------------------:|------------------:|
| cpython      |           0.76083 |              1789 |              1632 |
| django       |           0.99966 |              2760 |                58 |
| transformers |           0.99928 |              2587 |               454 |
| twine        |           1.00000 |                33 |                 0 |
| typeshed     |           0.99978 |              3496 |              2173 |
| warehouse    |           0.99824 |               648 |                22 |
| zulip        |           0.99948 |              1437 |                28 |


<!-- How was it tested? -->
2023-09-08 11:48:57 +02:00
qdegraaf
05951dd338 Fix inconsistent expr_lambda formatting (#6318) 2023-09-08 09:40:58 +00:00
Micha Reiser
c260762900 Formatter: Implicit concatenation in compare expressions
## Summary

This PR implements the logic for breaking implicit concatenated strings before compare expressions by building on top of  #7145 

The main change is a new `BinaryLike` enum that has the `BinaryExpression` and `CompareExpression` variants. Supporting both variants requires some downstream changes but doesn't introduce any new concepts. 

## Test Plan

I added a few more tests. The compatibility improvements are minor but we now perfectly match black on twine 🥳 


**PR**

| project      | similarity index  | total files       | changed files     |
|--------------|------------------:|------------------:|------------------:|
| cpython      |           0.76083 |              1789 |              1632 |
| django       |           0.99966 |              2760 |                58 |
| transformers |           0.99928 |              2587 |               454 |
| **twine**        |           1.00000 |                33 |                 0 | <-- improved
| typeshed     |           0.99978 |              3496 |              2173 |
| **warehouse**    |           0.99824 |               648 |                22 | <-- improved
| zulip        |           0.99948 |              1437 |                28 |


**Base**

| project      | similarity index  | total files       | changed files     |
|--------------|------------------:|------------------:|------------------:|
| cpython      |           0.76083 |              1789 |              1633 |
| django       |           0.99966 |              2760 |                58 |
| transformers |           0.99928 |              2587 |               454 |
| twine        |           0.99982 |                33 |                 1 | 
| typeshed     |           0.99978 |              3496 |              2173 |
| warehouse    |           0.99823 |               648 |                23 |
| zulip        |           0.99948 |              1437 |                28 |
2023-09-08 11:32:20 +02:00
konsti
1d5c4b0a14 Show header for formatter comment decoration info (#7228)
Show header for formatter comment decoration info

**Summary** Show a header in the formatter comment decoration debug
output that shows which node is preceding/following/enclosing
(https://github.com/astral-sh/ruff/pull/6813#issuecomment-1708119550). I
kept this intentionally condensed to make it easy to use this is a small
sidebar without vertical scrolling.

```console
$ cargo run --bin ruff_python_formatter -- --emit stdout --print-comments scratch.py
# Comment decoration: Range, Preceding, Following, Enclosing, Comment
17..20, Some((ParameterWithDefault, 6..10)), None, (Parameters, 5..22), "# a"
44..47, Some((StmtExpr, 28..39)), Some((StmtExpr, 52..60)), (StmtFunctionDef, 0..60), "# b"
77..80, None, None, (ExprList, 71..82), "# c"
{
    Node {
        kind: ParameterWithDefault,
        range: 6..10,
        source: `x=[]`,
    }: {
...
```

**Test Plan** It's debug output.
2023-09-08 09:25:06 +00:00
Micha Reiser
a352f2f092 Preserve generator parentheses in single argument call expressions (#7226) 2023-09-08 10:53:34 +02:00
Micha Reiser
e376c3ff7e Split implicit concatenated strings before binary expressions (#7145) 2023-09-08 06:51:26 +00:00
Greger
9671922e40 Do not use code location for Gitlab fingerprints. (#7203) 2023-09-08 08:25:26 +02:00
konsti
45f9fca228 Reuse locator in formatter comments (#7227)
**Summary** The comment visitor used to rebuild the locator for every
comment. Instead, we now keep the locator on the builder. Follow-up to
#6813.

**Test Plan** No formatting changes.
2023-09-07 20:08:28 +02:00
Charlie Marsh
6661be2c30 Use full range for SIM105 fixes (#7221)
Avoids inserting an accidental extra newline after the fix _and_
addresses the case of a trailing semicolon.
2023-09-07 16:16:43 +01:00
Charlie Marsh
97f945651d Change SIM118 to delete .keys() rather than replace expression (#7223)
Also improves the suggestion text. Closes
https://github.com/astral-sh/ruff/issues/7200.
2023-09-07 16:16:24 +01:00
Charlie Marsh
5cea43731e Add required space for FLY002 fixes (#7222)
Closes https://github.com/astral-sh/ruff/issues/7197.
2023-09-07 14:32:43 +00:00
Jaap Roes
7971e0b0ee Add extend-ignore-names for flake8-self (#7194)
## Summary

Add a configuration option to extend the list of names that can be
accessed without triggering SLF001.

Fixes issue #7018

## Test Plan

Manually tested by creating a python file (`test.py`):

```python
def foo(obj):
    obj._meta
```

and a `ruff.toml` file:

```toml
select = ["SLF"]

[flake8-self]
extend-ignore-names = ["_meta"]
```

Then running `cargo run -p ruff_cli -- check test.py --no-cache` (once
with the `extend-ignore-names` line comment out) to see if the
configuration option works.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-09-07 13:41:22 +00:00
Micha Reiser
842ff0212e Add Lexer emoji test case (#7213) 2023-09-07 10:02:50 +00:00
Micha Reiser
f1a4eb9c28 Use the unicode-ident crate (#7212) 2023-09-07 08:19:25 +00:00
Victor Hugo Gomes
041cdb95e0 Update identifier Unicode character validation to match Python spec (#7209)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-09-07 07:08:42 +00:00
Charlie Marsh
fda48afc23 Add required space to C416 fix (#7204)
Closes https://github.com/astral-sh/ruff/issues/7196.
2023-09-06 16:25:02 +00:00
Charlie Marsh
c1af1c291d Add required space to UP006 and UP007 fixes (#7202)
We're removing a set of brackets here so we need to pad the fix.

Closes https://github.com/astral-sh/ruff/issues/7201.
2023-09-06 16:06:02 +00:00
Micha Reiser
171b66cb43 Lexer: Add skip whitespace fastpath (#7184) 2023-09-06 16:14:01 +02:00
Charlie Marsh
fa0b6f4813 Avoid attempting to fix SIM105 violations with multi-statement lines (#7191)
I may revisit this and fix it "properly", but so rare that it's worth
disabling for now: https://github.com/astral-sh/ruff/issues/7123.
2023-09-06 13:35:46 +00:00
Charlie Marsh
eab85aea1a Use structured types for C417 comprehension target (#7190)
Rather than manually joining the arguments as a comma-separated string,
and treating that comma-separated string as a name.
2023-09-06 13:20:04 +00:00
Charlie Marsh
5b31524920 Parenthesize C417 targets if necessary (#7189)
Closes https://github.com/astral-sh/ruff/issues/7121.
2023-09-06 12:56:47 +00:00
Charlie Marsh
6d0469638c Avoid attempting to fix NPY001 with overridden builtins (#7187) 2023-09-06 12:24:37 +00:00
Charlie Marsh
29ba2bb943 Add required space when fixing C404 (#7185) 2023-09-06 13:05:39 +01:00
Charlie Marsh
f0ea40a68d Restructure signatures of flake8_comprehensions fixers (#7186) 2023-09-06 12:04:50 +00:00
Charlie Marsh
a3a531e0d4 Add alpha instructions to the ruff_python_formatter README (#7064) 2023-09-06 11:55:16 +00:00
Tom Kuson
b3e8eca871 Rename PLR1714 to repeated-equality-comparison (#7182) 2023-09-06 12:46:48 +01:00
konsti
447b7cb0e2 Formatter: Show preceding, following and enclosing nodes of comments, Attempt 2 (#6813) 2023-09-06 12:26:13 +02:00
konsti
e3114a144c Ignore single quote docstrings with newline escape (#7173) 2023-09-06 10:51:50 +02:00
Manuel Martinez
2e58ad437e build: add libcst from crates (#7179)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-09-06 08:24:28 +00:00
Dhruv Manilawala
04f2842e4f Move ExprConstant::kind to StringConstant::unicode (#7180) 2023-09-06 07:39:25 +00:00
Micha Reiser
31990b8d3f Checker: Remove unnecessary unreachable (#7181) 2023-09-06 07:21:03 +00:00
Micha Reiser
5f59101811 Memoize text width (#6552) 2023-09-06 07:10:13 +00:00
Dhruv Manilawala
fa6bff0078 Add inline documentation for Ipy* AST nodes (#7178) 2023-09-06 12:07:34 +05:30
Dhruv Manilawala
ea7c394817 Copy the starred argument as is for PLW3301 autofix (#7177) 2023-09-06 08:57:05 +05:30
Charlie Marsh
264d9159f8 Add required space when fixing UP024 (#7171) 2023-09-05 17:37:09 +00:00
Charlie Marsh
37dfb205b1 Remove autofix for ambiguous unicode rules (#7168) 2023-09-05 17:22:18 +00:00
Charlie Marsh
f8e4e1d562 Fix named expression precedence in generator (#7170) 2023-09-05 17:06:57 +00:00
Charlie Marsh
89be850b73 Add required space when fixing SIM300 (#7167) 2023-09-05 17:00:07 +00:00
Zanie Blue
d68041ba24 Fix B006 when function docstring is followed by whitespace but no newline (#7160) 2023-09-05 11:10:57 -05:00
Charlie Marsh
b60b37e866 Split within not, rather than outside of not, for PT018 (#7151) 2023-09-05 14:50:16 +00:00
konsti
5a95edab45 Use ruff line-length in format_dev (#6870) 2023-09-05 16:19:17 +02:00
Dhruv Manilawala
1adde24133 Rename parser mode from Jupyter to Ipython (#7153) 2023-09-05 14:12:26 +00:00
konsti
e02d76f070 Use insta_cmd (#6737) 2023-09-05 12:21:27 +00:00
Charlie Marsh
7ead2c17b1 Add required space when fixing C402 (#7152) 2023-09-05 12:19:33 +00:00
Charlie Marsh
e428099e4c Add required space when fixing SIM118 (#7150) 2023-09-05 11:51:34 +00:00
Charlie Marsh
7a83fd9926 Insert required space when fixing B013 (#7148) 2023-09-05 12:49:11 +01:00
Charlie Marsh
e8f78fa2cf Avoid fixing UP022 when capture_output is provided (#7149) 2023-09-05 11:44:17 +00:00
Charlie Marsh
955501f267 Use generator for UP007 autofix (#7137) 2023-09-05 11:41:53 +00:00
Micha Reiser
175b3702c3 Reduce comments.clone calls (#7144) 2023-09-05 11:32:56 +02:00
Nicholas Grisafi
40ee4909b5 Added argfile test and documentation (#7138)
Co-authored-by: konsti <konstin@mailbox.org>
2023-09-05 11:13:58 +02:00
Charlie Marsh
10a8e4a225 Remove output-file and target-version from formatter CLI (#7135) 2023-09-05 09:04:18 +00:00
konsti
0465b03282 Better formatter CLI verbose output (#7129) 2023-09-05 00:25:16 +02:00
Dhruv Manilawala
154fe7bdcc Add lexer benchmark (#7132) 2023-09-04 13:18:36 +00:00
Charlie Marsh
ece30e7c69 Preserve parentheses around partial call chains (#7109) 2023-09-04 10:57:04 +01:00
Charlie Marsh
7be28a38c5 Cache comment lookups in suite.rs (#7092) 2023-09-04 08:45:14 +00:00
Charlie Marsh
5ec73a6137 Avoid triggering N806 on TypeAlias assignments (#7119) 2023-09-04 08:44:28 +00:00
Dhruv Manilawala
1067261a55 Make SourceKind a required parameter (#7013) 2023-09-04 07:45:59 +00:00
Micha Reiser
93ca8ebbc0 Formatter: Detect line endings (#7054) 2023-09-04 08:09:31 +02:00
Charlie Marsh
834566f34f Retain parentheses when fixing SIM210 (#7118) 2023-09-03 22:39:23 +00:00
Charlie Marsh
a56121672c Add parentheses when simplifying conditions in SIM222 (#7117) 2023-09-03 22:35:59 +00:00
Charlie Marsh
32f4a96c64 Fix precedence of annotated assignments in generator (#7115) 2023-09-03 21:41:48 +00:00
Charlie Marsh
c004e03395 Add space after return when inlining number for RET504 (#7116) 2023-09-03 21:33:41 +00:00
Charlie Marsh
b57ddd54d2 Support parenthesized expressions in UP028 (#7114) 2023-09-03 21:20:09 +00:00
dalgarno
af189db5eb Deduplicate information in configuration documentation (#7108) 2023-09-03 22:17:35 +01:00
Charlie Marsh
c7217e34d7 Avoid adding duplicate text keyword to subprocess.run (#7112) 2023-09-03 21:17:04 +00:00
Charlie Marsh
d70247959c Avoid adding duplicate capture_output keyword to subprocess.run (#7113) 2023-09-03 21:14:56 +00:00
Charlie Marsh
911d4f2918 Handle parenthesized calls in PD002 (#7111) 2023-09-03 21:03:55 +00:00
Charlie Marsh
d9cf31f355 Expand F841 fixes to handle parenthesized targets (#7110) 2023-09-03 21:00:44 +00:00
Olivia Crain
7da99cc756 Fix incorrect flake8-copyright link in faq (#7093) 2023-09-03 18:00:25 +00:00
Charlie Marsh
9c3b2c3c3c Parenthesize expressins when converting to B009 (#7091) 2023-09-03 17:50:38 +01:00
Charlie Marsh
37d244d178 Add newline if B006 fix is at end-of-file (#7090) 2023-09-03 17:35:59 +01:00
Charlie Marsh
dbb34804a4 Change Option<Result> to Result<Option> in importer (#7089) 2023-09-03 16:23:42 +00:00
Charlie Marsh
3c7486817b Use symbol import for NPY003 replacement (#7083) 2023-09-03 16:53:28 +01:00
Charlie Marsh
3c3c5b5c57 Support length-2 lists in dictionary comprehension rewrites (#7081) 2023-09-03 13:34:10 +00:00
Charlie Marsh
b0d171ac19 Supported starred exceptions in length-one tuple detection (#7080) 2023-09-03 13:31:13 +00:00
Charlie Marsh
b70dde4a77 Avoid attempting to fix invalid Optional annotations (#7079) 2023-09-03 13:23:36 +00:00
Dhruv Manilawala
4eaa412370 Update LibCST (#7062)
## Summary

This PR updates the revision of `LibCST` dependency to 9c263aa897
inorder to fix https://github.com/astral-sh/ruff/issues/4899

## Test Plan

The test case including the carriage return (`\r`) character was added for
`F504` and then `cargo test`.

fixes: #4899
2023-09-03 09:11:24 +05:30
Charlie Marsh
577280c8be Rename ruff_python_formatter/README.md to CONTRIBUTING.md (#7065) 2023-09-02 16:25:23 +00:00
Charlie Marsh
45680bbb44 Avoid duplicate fixes for multi-import imports in RUF017 (#7063)
If a user has `import collections, functools, operator`, and we try to
import from `functools` and `operator`, we end up adding two identical
synthetic edits to preserve that import statement. We need to dedupe
them.

Closes https://github.com/astral-sh/ruff/issues/7059.
2023-09-02 12:58:18 +01:00
Justin Prieto
71ff6f911d Fix getattr calls on int literals (#7057) 2023-09-02 11:45:35 +00:00
Micha Reiser
c05e4628b1 Introduce Token element (#7048) 2023-09-02 10:05:47 +02:00
521 changed files with 13800 additions and 5849 deletions

View File

@@ -10,7 +10,7 @@ indent_style = space
insert_final_newline = true
indent_size = 2
[*.{rs,py}]
[*.{rs,py,pyi}]
indent_size = 4
[*.snap]

3
.github/release.yml vendored
View File

@@ -20,6 +20,9 @@ changelog:
- title: Bug Fixes
labels:
- bug
- title: Preview
labels:
- preview
- title: Other Changes
labels:
- "*"

View File

@@ -26,11 +26,11 @@ jobs:
linter: ${{ steps.changed.outputs.linter_any_changed }}
formatter: ${{ steps.changed.outputs.formatter_any_changed }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: tj-actions/changed-files@v38
- uses: tj-actions/changed-files@v39
id: changed
with:
files_yaml: |
@@ -62,7 +62,7 @@ jobs:
name: "cargo fmt"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- run: cargo fmt --all --check
@@ -71,7 +71,7 @@ jobs:
name: "cargo clippy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: |
rustup component add clippy
@@ -89,7 +89,7 @@ jobs:
runs-on: ${{ matrix.os }}
name: "cargo test | ${{ matrix.os }}"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo insta"
@@ -125,7 +125,7 @@ jobs:
runs-on: ubuntu-latest
name: "cargo fuzz"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v2
@@ -141,7 +141,7 @@ jobs:
runs-on: ubuntu-latest
name: "cargo test (wasm)"
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v3
@@ -160,7 +160,7 @@ jobs:
name: "test scripts"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup component add rustfmt
- uses: Swatinem/rust-cache@v2
@@ -182,7 +182,7 @@ jobs:
# 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' && needs.determine_changes.outputs.linter == 'true'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -227,7 +227,7 @@ jobs:
name: "cargo udeps"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install nightly Rust toolchain"
# Only pinned to make caching work, update freely
run: rustup toolchain install nightly-2023-06-08
@@ -241,7 +241,7 @@ jobs:
name: "python package"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -265,7 +265,7 @@ jobs:
name: "pre-commit"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -295,7 +295,7 @@ jobs:
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
@@ -330,7 +330,7 @@ jobs:
needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup show
- name: "Cache rust"
@@ -346,7 +346,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Checkout Branch"
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup show

View File

@@ -2,6 +2,11 @@ name: mkdocs
on:
workflow_dispatch:
inputs:
ref:
description: "The commit SHA, tag, or branch to publish. Uses the default branch if not specified."
default: ""
type: string
release:
types: [published]
@@ -12,7 +17,9 @@ jobs:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v4
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
@@ -44,4 +51,5 @@ jobs:
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages publish site --project-name=ruff-docs --branch ${GITHUB_HEAD_REF} --commit-hash ${GITHUB_SHA}
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
command: pages deploy site --project-name=ruff-docs --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}

View File

@@ -19,7 +19,7 @@ jobs:
macos-x86_64:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -42,7 +42,7 @@ jobs:
macos-universal:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -68,7 +68,7 @@ jobs:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -96,7 +96,7 @@ jobs:
matrix:
target: [x86_64, i686]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -123,7 +123,7 @@ jobs:
matrix:
target: [aarch64, armv7, s390x, ppc64le, ppc64]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -160,7 +160,7 @@ jobs:
- x86_64-unknown-linux-musl
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -196,7 +196,7 @@ jobs:
- target: armv7-unknown-linux-musleabihf
arch: armv7
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}

View File

@@ -17,7 +17,7 @@ jobs:
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@v3

View File

@@ -30,7 +30,7 @@ jobs:
sdist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -56,7 +56,7 @@ jobs:
macos-x86_64:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -94,7 +94,7 @@ jobs:
macos-universal:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -140,7 +140,7 @@ jobs:
- target: aarch64-pc-windows-msvc
arch: x64
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -186,7 +186,7 @@ jobs:
- x86_64-unknown-linux-gnu
- i686-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -243,7 +243,7 @@ jobs:
arch: ppc64
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -296,7 +296,7 @@ jobs:
- x86_64-unknown-linux-musl
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -350,7 +350,7 @@ jobs:
arch: armv7
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -398,7 +398,7 @@ jobs:
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Check tag consistency
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
@@ -464,7 +464,7 @@ jobs:
# For git tag
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: git tag
run: |
git config user.email "hey@astral.sh"

View File

@@ -23,8 +23,6 @@ repos:
- id: mdformat
additional_dependencies:
- mdformat-mkdocs
- mdformat-black
- black==23.1.0 # Must be the latest version of Black
- repo: https://github.com/igorshubovych/markdownlint-cli
rev: v0.33.0

View File

@@ -1,5 +1,20 @@
# Breaking Changes
## 0.0.288
### Remove support for emoji identifiers ([#7212](https://github.com/astral-sh/ruff/pull/7212))
Previously, Ruff supported the non-standard compliant emoji identifiers e.g. `📦 = 1`.
We decided to remove this non-standard language extension, and Ruff now reports syntax errors for emoji identifiers in your code, the same as CPython.
### Improved GitLab fingerprints ([#7203](https://github.com/astral-sh/ruff/pull/7203))
GitLab uses fingerprints to identify new, existing, or fixed violations. Previously, Ruff included the violation's position in the fingerprint. Using the location has the downside that changing any code before the violation causes the fingerprint to change, resulting in GitLab reporting one fixed and one new violation even though it is a pre-existing violation.
Ruff now uses a more stable location-agnostic fingerprint to minimize that existing violations incorrectly get marked as fixed and re-reported as new violations.
Expect GitLab to report each pre-existing violation in your project as fixed and a new violation in your Ruff upgrade PR.
## 0.0.283 / 0.284
### The target Python version now defaults to 3.8 instead of 3.10 ([#6397](https://github.com/astral-sh/ruff/pull/6397))

102
Cargo.lock generated
View File

@@ -409,9 +409,9 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5aeec2fbed4969dc38b5ca201115dd5c2614b8ef78e0a7221dd5f0977fb1552b"
checksum = "9b3238416c10f19985b52a937c5b3efc3ed7efe8f7ae263d2aab29a09bca9f57"
dependencies = [
"colored",
"libc",
@@ -420,9 +420,9 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b13f0a08d40ce7c95bdf288f725b975e62fcadfa8ba152340943bab6de43af7"
checksum = "fecc18f65b942d2b033545bb3bd8430a23eecbbe53fad3b1342fb0e5514bca7b"
dependencies = [
"codspeed",
"colored",
@@ -522,8 +522,6 @@ dependencies = [
"num-traits",
"once_cell",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_derive",
@@ -823,7 +821,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.287"
version = "0.0.289"
dependencies = [
"anyhow",
"clap",
@@ -1114,11 +1112,23 @@ dependencies = [
"lazy_static",
"linked-hash-map",
"regex",
"serde",
"similar",
"walkdir",
"yaml-rust",
]
[[package]]
name = "insta-cmd"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809d3023d1d6e8d5c2206f199251f75cb26180e41f18cb0f22dd119161cb5127"
dependencies = [
"insta",
"serde",
"serde_json",
]
[[package]]
name = "instant"
version = "0.1.12"
@@ -1269,12 +1279,12 @@ checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "libcst"
version = "0.1.0"
source = "git+https://github.com/Instagram/LibCST.git?rev=3cacca1a1029f05707e50703b49fe3dd860aa839#3cacca1a1029f05707e50703b49fe3dd860aa839"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7773d520d4292e200ab1838f2daabe2feed7549f93b0a3c7582160a09e79ffde"
dependencies = [
"chic",
"itertools",
"libcst_derive",
"once_cell",
"memchr",
"paste",
"peg",
"regex",
@@ -1284,7 +1294,8 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "0.1.0"
source = "git+https://github.com/Instagram/LibCST.git?rev=3cacca1a1029f05707e50703b49fe3dd860aa839#3cacca1a1029f05707e50703b49fe3dd860aa839"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520197c50ba477f258cd7005ec5ed3a7393693ae6bec664990c7c8d9306a7c0d"
dependencies = [
"quote",
"syn 1.0.109",
@@ -1717,34 +1728,6 @@ version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "plotters"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45"
dependencies = [
"num-traits",
"plotters-backend",
"plotters-svg",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "plotters-backend"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609"
[[package]]
name = "plotters-svg"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab"
dependencies = [
"plotters-backend",
]
[[package]]
name = "pmutil"
version = "0.5.3"
@@ -2054,7 +2037,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.287"
version = "0.0.289"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2152,7 +2135,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.287"
version = "0.0.289"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2170,6 +2153,7 @@ dependencies = [
"glob",
"ignore",
"insta",
"insta-cmd",
"is-macro",
"itertools",
"itoa",
@@ -2199,6 +2183,7 @@ dependencies = [
"similar",
"strum",
"tempfile",
"test-case",
"thiserror",
"tikv-jemallocator",
"tracing",
@@ -2319,6 +2304,7 @@ dependencies = [
"bitflags 2.4.0",
"insta",
"is-macro",
"itertools",
"memchr",
"num-bigint",
"num-traits",
@@ -2353,7 +2339,6 @@ dependencies = [
"clap",
"countme",
"insta",
"is-macro",
"itertools",
"memchr",
"once_cell",
@@ -2369,7 +2354,9 @@ dependencies = [
"serde_json",
"similar",
"smallvec",
"static_assertions",
"thiserror",
"tracing",
"unicode-width",
]
@@ -2415,10 +2402,8 @@ dependencies = [
"ruff_text_size",
"rustc-hash",
"static_assertions",
"test-case",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",
"unicode-ident",
"unicode_names2",
]
@@ -2452,6 +2437,9 @@ dependencies = [
[[package]]
name = "ruff_python_stdlib"
version = "0.0.0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ruff_python_trivia"
@@ -2464,7 +2452,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"smallvec",
"unic-ucd-ident",
"unicode-ident",
]
[[package]]
@@ -3262,17 +3250,6 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
[[package]]
name = "unic-emoji-char"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b07221e68897210270a38bde4babb655869637af0f69407f96053a34f76494d"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-category"
version = "0.9.0"
@@ -3285,17 +3262,6 @@ dependencies = [
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-ident"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
dependencies = [
"unic-char-property",
"unic-char-range",
"unic-ucd-version",
]
[[package]]
name = "unic-ucd-version"
version = "0.9.0"

View File

@@ -40,6 +40,7 @@ serde_json = { version = "1.0.93" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1", features = ["inline"] }
smallvec = { version = "1.10.0" }
static_assertions = "1.1.0"
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
syn = { version = "2.0.15" }
@@ -49,12 +50,13 @@ toml = { version = "0.7.2" }
tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-ident = "1.0.11"
unicode-width = "0.1.10"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
# v1.0.1
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false }
libcst = { version = "0.1.0", default-features = false }
[profile.release]
lto = "fat"

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.287
rev: v0.0.289
hooks:
- id: ruff
```

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.287"
version = "0.0.289"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -4,6 +4,7 @@ use std::str::FromStr;
use anyhow::anyhow;
use ruff::registry::Linter;
use ruff::settings::types::PreviewMode;
use ruff::RuleSelector;
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
@@ -331,7 +332,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec
.filter(|plugin| {
for selector in selectors {
if selector
.into_iter()
.rules(PreviewMode::Disabled)
.any(|rule| Linter::from(plugin).rules().any(|r| r == rule))
{
return true;

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.287"
version = "0.0.289"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -0,0 +1,5 @@
# Docstring followed by a newline
def foobar(foor, bar={}):
"""
"""

View File

@@ -0,0 +1,6 @@
# Docstring followed by whitespace with no newline
# Regression test for https://github.com/astral-sh/ruff/issues/7155
def foobar(foor, bar={}):
"""
"""

View File

@@ -0,0 +1,6 @@
# Docstring with no newline
def foobar(foor, bar={}):
"""
"""

View File

@@ -308,3 +308,7 @@ def single_line_func_wrong(value: dict[str, str] = {
def single_line_func_wrong(value: dict[str, str] = {}) \
: \
"""Docstring"""
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring without newline"""

View File

@@ -1,7 +1,7 @@
"""
Should emit:
B009 - Line 19, 20, 21, 22, 23, 24
B010 - Line 40, 41, 42, 43, 44, 45
B009 - Lines 19-31
B010 - Lines 40-45
"""
# Valid getattr usage
@@ -24,6 +24,16 @@ getattr(foo, r"abc123")
_ = lambda x: getattr(x, "bar")
if getattr(x, "bar"):
pass
getattr(1, "real")
getattr(1., "real")
getattr(1.0, "real")
getattr(1j, "real")
getattr(True, "real")
getattr(x := 1, "real")
getattr(x + y, "real")
getattr("foo"
"bar", "real")
# Valid setattr usage
setattr(foo, bar, None)

View File

@@ -1,3 +1,5 @@
retriable_exceptions = (FileExistsError, FileNotFoundError)
try:
pass
except (ValueError,):
@@ -6,3 +8,7 @@ except AttributeError:
pass
except (ImportError, TypeError):
pass
except (*retriable_exceptions,):
pass
except(ValueError,):
pass

View File

@@ -16,3 +16,6 @@ def f(x):
return x
print(f'Hello {dict((x,f(x)) for x in "abc")} World')
# Regression test for: https://github.com/astral-sh/ruff/issues/7086
dict((k,v)for k,v in d.iteritems() if k in only_args)

View File

@@ -11,3 +11,6 @@ f"{dict([(s,f(s)) for s in 'ab'])}"
f'{dict([(s,s) for s in "ab"]) | dict([(s,s) for s in "ab"])}'
f'{ dict([(s,s) for s in "ab"]) | dict([(s,s) for s in "ab"]) }'
# Regression test for: https://github.com/astral-sh/ruff/issues/7087
saved.append(dict([(k, v)for k,v in list(unique_instance.__dict__.items()) if k in [f.name for f in unique_instance._meta.fields]]))

View File

@@ -17,3 +17,6 @@ d = {"a": 1, "b": 2, "c": 3}
{k.foo: k for k in y}
{k["foo"]: k for k in y}
{k: v if v else None for k, v in y}
# Regression test for: https://github.com/astral-sh/ruff/issues/7196
any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])

View File

@@ -5,11 +5,13 @@ map(lambda x: str(x), nums)
list(map(lambda x: x * 2, nums))
set(map(lambda x: x % 2 == 0, nums))
dict(map(lambda v: (v, v**2), nums))
dict(map(lambda v: [v, v**2], nums))
map(lambda: "const", nums)
map(lambda _: 3.0, nums)
_ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
all(map(lambda v: isinstance(v, dict), nums))
filter(func, map(lambda v: v, nums))
list(map(lambda x, y: x * y, nums))
# When inside f-string, then the fix should be surrounded by whitespace
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
@@ -40,3 +42,8 @@ map(lambda **kwargs: len(kwargs), range(4))
# Ok because multiple arguments are allowed.
dict(map(lambda k, v: (k, v), keys, values))
# Regression test for: https://github.com/astral-sh/ruff/issues/7121
map(lambda x: x, y if y else z)
map(lambda x: x, (y if y else z))
map(lambda x: x, (x, y, z))

View File

@@ -52,3 +52,21 @@ def test_multiline():
x = 1; \
assert something and something_else
# Regression test for: https://github.com/astral-sh/ruff/issues/7143
def test_parenthesized_not():
assert not (
self.find_graph_output(node.output[0])
or self.find_graph_input(node.input[0])
or self.find_graph_output(node.input[0])
)
assert (not (
self.find_graph_output(node.output[0])
or self.find_graph_input(node.input[0])
or self.find_graph_output(node.input[0])
))
assert (not self.find_graph_output(node.output[0]) or
self.find_graph_input(node.input[0]))

View File

@@ -357,3 +357,9 @@ def foo():
def foo():
a = 1 # Comment
return a
# Regression test for: https://github.com/astral-sh/ruff/issues/7098
def mavko_debari(P_kbar):
D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
return D

View File

@@ -0,0 +1,10 @@
def foo(obj):
obj._meta # OK
def foo(obj):
obj._asdict # SLF001
def foo(obj):
obj._bar # SLF001

View File

@@ -8,6 +8,7 @@ try:
except ValueError:
pass
# SIM105
try:
foo()
@@ -110,3 +111,20 @@ try:
print()
except "not an exception":
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7123
def write_models(directory, Models):
try:
os.makedirs(model_dir);
except OSError:
pass;
try: os.makedirs(model_dir);
except OSError:
pass;
try: os.makedirs(model_dir);
except OSError:
pass; \
\
#

View File

@@ -42,3 +42,17 @@ class Foo:
def __contains__(self, key: object) -> bool:
return key in self.keys() # OK
# Regression test for: https://github.com/astral-sh/ruff/issues/7124
key in obj.keys()and foo
(key in obj.keys())and foo
key in (obj.keys())and foo
# Regression test for: https://github.com/astral-sh/ruff/issues/7200
for key in (
self.experiment.surveys[0]
.stations[0]
.keys()
):
continue

View File

@@ -13,3 +13,8 @@ def f():
return False
a = True if b else False
# Regression test for: https://github.com/astral-sh/ruff/issues/7076
samesld = True if (psl.privatesuffix(urlparse(response.url).netloc) ==
psl.privatesuffix(src.netloc)) else False

View File

@@ -152,3 +152,11 @@ if (a or [1] or True or [2]) == (a or [1]): # SIM222
if f(a or [1] or True or [2]): # SIM222
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7099
def secondToTime(s0: int) -> (int, int, int) or str:
m, s = divmod(s0, 60)
def secondToTime(s0: int) -> ((int, int, int) or str):
m, s = divmod(s0, 60)

View File

@@ -14,6 +14,8 @@ YODA >= age # SIM300
JediOrder.YODA == age # SIM300
0 < (number - 100) # SIM300
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
B<A[0][0]or B
B or(B)<A[0][0]
# OK
compare == "yoda"

View File

@@ -16,3 +16,8 @@ nok4 = "a".join([a, a, *a]) # Not OK (not a static length)
nok5 = "a".join([choice("flarp")]) # Not OK (not a simple call)
nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
# Regression test for: https://github.com/astral-sh/ruff/issues/7197
def create_file_public_url(url, filename):
return''.join([url, filename])

View File

@@ -18,3 +18,8 @@ pdf = pd.DataFrame(
)
_ = arr.astype(np.int)
# Regression test for: https://github.com/astral-sh/ruff/issues/6952
from numpy import float
float(1)

View File

@@ -1,15 +1,18 @@
import numpy as np
def func():
import numpy as np
np.round_(np.random.rand(5, 5), 2)
np.product(np.random.rand(5, 5))
np.cumproduct(np.random.rand(5, 5))
np.sometrue(np.random.rand(5, 5))
np.alltrue(np.random.rand(5, 5))
np.round_(np.random.rand(5, 5), 2)
np.product(np.random.rand(5, 5))
np.cumproduct(np.random.rand(5, 5))
np.sometrue(np.random.rand(5, 5))
np.alltrue(np.random.rand(5, 5))
from numpy import round_, product, cumproduct, sometrue, alltrue
round_(np.random.rand(5, 5), 2)
product(np.random.rand(5, 5))
cumproduct(np.random.rand(5, 5))
sometrue(np.random.rand(5, 5))
alltrue(np.random.rand(5, 5))
def func():
from numpy import round_, product, cumproduct, sometrue, alltrue
round_(np.random.rand(5, 5), 2)
product(np.random.rand(5, 5))
cumproduct(np.random.rand(5, 5))
sometrue(np.random.rand(5, 5))
alltrue(np.random.rand(5, 5))

View File

@@ -29,3 +29,5 @@ x.apply(lambda x: x.sort_values("a", inplace=True))
import torch
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call
(x.drop(["a"], axis=1, inplace=True))

View File

@@ -1,8 +1,6 @@
import collections
from collections import namedtuple
from typing import TypeVar
from typing import NewType
from typing import NamedTuple, TypedDict
from typing import TypeAlias, TypeVar, NewType, NamedTuple, TypedDict
GLOBAL: str = "foo"
@@ -21,9 +19,11 @@ def assign():
T = TypeVar("T")
UserId = NewType("UserId", int)
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
Employee = NamedTuple("Employee", [("name", str), ("id", int)])
Point2D = TypedDict('Point2D', {'in': int, 'x-y': int})
Point2D = TypedDict("Point2D", {"in": int, "x-y": int})
IntOrStr: TypeAlias = int | str
def aug_assign(rank, world_size):

View File

@@ -138,3 +138,12 @@ def scope():
class TemperatureScales(Enum):
CELSIUS = (lambda deg_c: deg_c)
FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32)
# Regression test for: https://github.com/astral-sh/ruff/issues/7141
def scope():
# E731
f = lambda: (
i := 1,
)

View File

@@ -639,3 +639,22 @@ def starts_with_space_then_this():
class SameLine: """This is a docstring on the same line"""
def same_line(): """This is a docstring on the same line"""
def single_line_docstring_with_an_escaped_backslash():
"\
"
class StatementOnSameLineAsDocstring:
"After this docstring there's another statement on the same line separated by a semicolon." ; priorities=1
def sort_services(self):
pass
class StatementOnSameLineAsDocstring:
"After this docstring there's another statement on the same line separated by a semicolon."; priorities=1
class CommentAfterDocstring:
"After this docstring there's a comment." # priorities=1
def sort_services(self):
pass

View File

@@ -128,6 +128,19 @@ def f(x, *, y, z):
"""
return x, y, z
def f(x):
"""Do something with valid description.
Args:
----
x: the value
Returns:
-------
the value
"""
return x
class Test:
def f(self, /, arg1: int) -> None:

View File

@@ -112,3 +112,11 @@ match *0, 1, *2:
import b1
import b2
# Regression test for: https://github.com/astral-sh/ruff/issues/7244
from datameta_client_lib.model_utils import ( # noqa: F401
noqa )
from datameta_client_lib.model_helpers import (
noqa )

View File

@@ -9,3 +9,8 @@ hidden = {"a": "!"}
"%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
'' % {'a''b' : ''} # F504 ("ab" not used)
# https://github.com/astral-sh/ruff/issues/4899
"" % {
'test1': '',
'test2': '',

View File

@@ -165,3 +165,9 @@ def f():
x = 1
y = 2
def f():
(x) = foo()
((x)) = foo()
(x) = (y.z) = foo()

View File

@@ -1,3 +1,6 @@
from typing import override
class Apples:
def _init_(self): # [bad-dunder-name]
pass
@@ -21,6 +24,11 @@ class Apples:
# author likely meant to call the invert dunder method
pass
@override
def _ignore__(self): # [bad-dunder-name]
# overridden dunder methods should be ignored
pass
def hello(self):
print("hello")

View File

@@ -36,3 +36,6 @@ tuples_list = [
min(min(tuples_list))
max(max(tuples_list))
# Starred argument should be copied as it is.
max(1, max(*a))

View File

@@ -91,3 +91,20 @@ def f(x: Optional[int : float]) -> None:
def f(x: Optional[str, int : float]) -> None:
...
def f(x: Optional[int, float]) -> None:
...
# Regression test for: https://github.com/astral-sh/ruff/issues/7131
class ServiceRefOrValue:
service_specification: Optional[
list[ServiceSpecificationRef]
| list[ServiceSpecification]
] = None
# Regression test for: https://github.com/astral-sh/ruff/issues/7201
class ServiceRefOrValue:
service_specification: Optional[str]is not True = None

View File

@@ -1,12 +1,10 @@
import subprocess
import subprocess as somename
from subprocess import run
from subprocess import run as anothername
# Errors
subprocess.run(["foo"], universal_newlines=True, check=True)
somename.run(["foo"], universal_newlines=True)
subprocess.run(["foo"], universal_newlines=True, text=True)
run(["foo"], universal_newlines=True, check=False)
anothername(["foo"], universal_newlines=True)
# OK
subprocess.run(["foo"], check=True)

View File

@@ -35,8 +35,19 @@ if output:
encoding="utf-8",
)
output = subprocess.run(
["foo"], stdout=subprocess.PIPE, capture_output=True, stderr=subprocess.PIPE
)
# Examples that should NOT trigger the rule
output = subprocess.run(
["foo"], stdout=subprocess.PIPE, capture_output=False, stderr=subprocess.PIPE
)
output = subprocess.run(
["foo"], capture_output=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
# OK
from foo import PIPE
subprocess.run(["foo"], stdout=PIPE, stderr=PIPE)
run(["foo"], stdout=None, stderr=PIPE)

View File

@@ -96,3 +96,11 @@ try:
pass
except (OSError, KeyError):
pass
# Regression test for: https://github.com/astral-sh/ruff/issues/7101
def get_owner_id_from_mac_address():
try:
mac_address = get_primary_mac_address()
except(IOError, OSError) as ex:
msg = 'Unable to query URL to get Owner ID: {u}\n{e}'.format(u=owner_id_url, e=ex)

View File

@@ -72,3 +72,12 @@ def f():
for x, y in z():
yield x, y
x = 1
# Regression test for: https://github.com/astral-sh/ruff/issues/7103
def _serve_method(fn):
for h in (
TaggedText.from_file(args.input)
.markup(highlight=args.region)
):
yield h

View File

@@ -1,4 +1,4 @@
# These should NOT change
# OK
def f():
for x in z:
yield

View File

@@ -178,3 +178,9 @@ if True:
if True:
if sys.version_info > (3, 0): \
expected_error = []
if sys.version_info < (3,12):
print("py3")
if sys.version_info <= (3,12):
print("py3")

View File

@@ -12,3 +12,10 @@ sum([[1, 2, 3], [4, 5, 6]],
# OK
sum([x, y])
sum([[1, 2, 3], [4, 5, 6]])
# Regression test for: https://github.com/astral-sh/ruff/issues/7059
def func():
import functools, operator
sum([x, y], [])

View File

@@ -3,14 +3,14 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Stmt};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{Ranged, TextLen, TextSize};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::autofix::codemods;
@@ -92,10 +92,8 @@ pub(crate) fn remove_argument<T: Ranged>(
) -> Result<Edit> {
// Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments
.args
.iter()
.map(Expr::range)
.chain(arguments.keywords.iter().map(Keyword::range))
.arguments_source_order()
.map(|arg| arg.range())
.filter(|range| argument.range() != *range)
.partition(|range| range.start() < argument.start());
@@ -249,6 +247,44 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
locator.line_end(start_location)
}
/// Add leading whitespace to a snippet, if it's immediately preceded an identifier or keyword.
pub(crate) fn pad_start(mut content: String, start: TextSize, locator: &Locator) -> String {
// Ex) When converting `except(ValueError,)` from a tuple to a single argument, we need to
// insert a space before the fix, to achieve `except ValueError`.
if locator
.up_to(start)
.chars()
.last()
.is_some_and(|char| char.is_ascii_alphabetic())
{
content.insert(0, ' ');
}
content
}
/// Add trailing whitespace to a snippet, if it's immediately followed by an identifier or keyword.
pub(crate) fn pad_end(mut content: String, end: TextSize, locator: &Locator) -> String {
if locator
.after(end)
.chars()
.next()
.is_some_and(|char| char.is_ascii_alphabetic())
{
content.push(' ');
}
content
}
/// Add leading or trailing whitespace to a snippet, if it's immediately preceded or followed by
/// an identifier or keyword.
pub(crate) fn pad(content: String, range: TextRange, locator: &Locator) -> String {
pad_start(
pad_end(content, range.end(), locator),
range.start(),
locator,
)
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -1195,7 +1195,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
Expr::Constant(ast::ExprConstant {
value: Constant::Int(_) | Constant::Float(_) | Constant::Complex { .. },
kind: _,
range: _,
}) => {
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
@@ -1204,7 +1203,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
Expr::Constant(ast::ExprConstant {
value: Constant::Bytes(_),
kind: _,
range: _,
}) => {
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
@@ -1213,7 +1211,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
kind,
range: _,
}) => {
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
@@ -1227,7 +1224,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_bandit::rules::hardcoded_tmp_directory(checker, expr, value);
}
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());
pyupgrade::rules::unicode_kind_prefix(checker, expr, value.unicode);
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {
@@ -1253,14 +1250,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
}) => {
if checker.enabled(Rule::IfExprWithTrueFalse) {
flake8_simplify::rules::explicit_true_false_in_ifexpr(
checker, expr, test, body, orelse,
);
flake8_simplify::rules::if_expr_with_true_false(checker, expr, test, body, orelse);
}
if checker.enabled(Rule::IfExprWithFalseTrue) {
flake8_simplify::rules::explicit_false_true_in_ifexpr(
checker, expr, test, body, orelse,
);
flake8_simplify::rules::if_expr_with_false_true(checker, expr, test, body, orelse);
}
if checker.enabled(Rule::IfExprWithTwistedArms) {
flake8_simplify::rules::twisted_arms_in_ifexpr(checker, expr, test, body, orelse);
@@ -1358,8 +1351,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::ExprAndFalse) {
flake8_simplify::rules::expr_and_false(checker, expr);
}
if checker.enabled(Rule::RepeatedEqualityComparisonTarget) {
pylint::rules::repeated_equality_comparison_target(checker, bool_op);
if checker.enabled(Rule::RepeatedEqualityComparison) {
pylint::rules::repeated_equality_comparison(checker, bool_op);
}
}
_ => {}

View File

@@ -528,11 +528,7 @@ where
&self.semantic.definitions,
);
self.semantic.push_definition(definition);
self.semantic.push_scope(match &stmt {
Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt),
_ => unreachable!("Expected Stmt::FunctionDef"),
});
self.semantic.push_scope(ScopeKind::Function(function_def));
self.deferred.functions.push(self.semantic.snapshot());
@@ -1192,7 +1188,6 @@ where
}
Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
kind: _,
range: _,
}) => {
if self.semantic.in_type_definition()

View File

@@ -85,7 +85,7 @@ pub(crate) fn check_imports(
stylist: &Stylist,
path: &Path,
package: Option<&Path>,
source_kind: Option<&SourceKind>,
source_kind: &SourceKind,
source_type: PySourceType,
) -> (Vec<Diagnostic>, Option<ImportMap>) {
// Extract all import blocks from the AST.

View File

@@ -9,6 +9,7 @@ use strum_macros::{AsRefStr, EnumIter};
use ruff_diagnostics::Violation;
use crate::registry::{AsRule, Linter};
use crate::rule_selector::is_single_rule_selector;
use crate::rules;
#[derive(PartialEq, Eq, PartialOrd, Ord)]
@@ -51,7 +52,10 @@ impl PartialEq<&str> for NoqaCode {
pub enum RuleGroup {
/// The rule has not been assigned to any specific group.
Unspecified,
/// The rule is still under development, and must be enabled explicitly.
/// The rule is unstable, and preview mode must be enabled for usage.
Preview,
/// Legacy category for unstable rules, supports backwards compatible selection.
#[deprecated(note = "Use `RuleGroup::Preview` for new rules instead")]
Nursery,
}
@@ -64,38 +68,71 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
Some(match (linter, code) {
// pycodestyle errors
(Pycodestyle, "E101") => (RuleGroup::Unspecified, rules::pycodestyle::rules::MixedSpacesAndTabs),
#[allow(deprecated)]
(Pycodestyle, "E111") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
#[allow(deprecated)]
(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
#[allow(deprecated)]
(Pycodestyle, "E113") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
#[allow(deprecated)]
(Pycodestyle, "E114") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
#[allow(deprecated)]
(Pycodestyle, "E115") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E116") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
#[allow(deprecated)]
(Pycodestyle, "E117") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::OverIndented),
#[allow(deprecated)]
(Pycodestyle, "E201") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
#[allow(deprecated)]
(Pycodestyle, "E202") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
#[allow(deprecated)]
(Pycodestyle, "E203") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
#[allow(deprecated)]
(Pycodestyle, "E211") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
#[allow(deprecated)]
(Pycodestyle, "E221") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
#[allow(deprecated)]
(Pycodestyle, "E222") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
#[allow(deprecated)]
(Pycodestyle, "E223") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
#[allow(deprecated)]
(Pycodestyle, "E224") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
#[allow(deprecated)]
(Pycodestyle, "E225") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
#[allow(deprecated)]
(Pycodestyle, "E226") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
#[allow(deprecated)]
(Pycodestyle, "E227") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
#[allow(deprecated)]
(Pycodestyle, "E228") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
#[allow(deprecated)]
(Pycodestyle, "E231") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
#[allow(deprecated)]
(Pycodestyle, "E241") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
#[allow(deprecated)]
(Pycodestyle, "E242") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterComma),
#[allow(deprecated)]
(Pycodestyle, "E251") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
#[allow(deprecated)]
(Pycodestyle, "E252") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
#[allow(deprecated)]
(Pycodestyle, "E261") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
#[allow(deprecated)]
(Pycodestyle, "E262") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
#[allow(deprecated)]
(Pycodestyle, "E265") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E266") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
#[allow(deprecated)]
(Pycodestyle, "E271") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
#[allow(deprecated)]
(Pycodestyle, "E272") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E273") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
#[allow(deprecated)]
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
#[allow(deprecated)]
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
(Pycodestyle, "E401") => (RuleGroup::Unspecified, rules::pycodestyle::rules::MultipleImportsOnOneLine),
(Pycodestyle, "E402") => (RuleGroup::Unspecified, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
@@ -176,6 +213,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots),
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
#[allow(deprecated)]
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
(Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit),
@@ -212,10 +250,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0915") => (RuleGroup::Unspecified, rules::pylint::rules::TooManyStatements),
(Pylint, "R1701") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1711") => (RuleGroup::Unspecified, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedEqualityComparisonTarget),
(Pylint, "R1714") => (RuleGroup::Unspecified, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
#[allow(deprecated)]
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
@@ -228,8 +267,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W1508") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarDefault),
(Pylint, "W1509") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessPopenPreexecFn),
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
#[allow(deprecated)]
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
#[allow(deprecated)]
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
@@ -403,6 +444,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault),
// flake8-copyright
#[allow(deprecated)]
(Flake8Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice),
// pyupgrade
@@ -815,9 +857,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional),
#[cfg(feature = "unreachable-code")] // When removing this feature gate, also update rules_selector.rs
#[allow(deprecated)]
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
#[allow(deprecated)]
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
@@ -866,8 +910,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
// refurb
#[allow(deprecated)]
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
#[allow(deprecated)]
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
_ => return None,

View File

@@ -1,4 +1,4 @@
use libcst_native::{Expression, NameOrAttribute};
use libcst_native::{Expression, NameOrAttribute, ParenthesizableWhitespace, SimpleWhitespace};
fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
match expr {
@@ -36,3 +36,17 @@ pub(crate) fn compose_module_path(module: &NameOrAttribute) -> String {
}
}
}
/// Return a [`ParenthesizableWhitespace`] containing a single space.
pub(crate) fn space() -> ParenthesizableWhitespace<'static> {
ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" "))
}
/// Ensure that a [`ParenthesizableWhitespace`] contains at least one space.
pub(crate) fn or_space(whitespace: ParenthesizableWhitespace) -> ParenthesizableWhitespace {
if whitespace == ParenthesizableWhitespace::default() {
space()
} else {
whitespace
}
}

View File

@@ -171,10 +171,8 @@ impl<'a> Importer<'a> {
at: TextSize,
semantic: &SemanticModel,
) -> Result<(Edit, String), ResolutionError> {
match self.get_symbol(symbol, at, semantic) {
Some(result) => result,
None => self.import_symbol(symbol, at, semantic),
}
self.get_symbol(symbol, at, semantic)?
.map_or_else(|| self.import_symbol(symbol, at, semantic), Ok)
}
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
@@ -183,9 +181,13 @@ impl<'a> Importer<'a> {
symbol: &ImportRequest,
at: TextSize,
semantic: &SemanticModel,
) -> Option<Result<(Edit, String), ResolutionError>> {
) -> Result<Option<(Edit, String)>, ResolutionError> {
// If the symbol is already available in the current scope, use it.
let imported_name = semantic.resolve_qualified_import_name(symbol.module, symbol.member)?;
let Some(imported_name) =
semantic.resolve_qualified_import_name(symbol.module, symbol.member)
else {
return Ok(None);
};
// If the symbol source (i.e., the import statement) comes after the current location,
// abort. For example, we could be generating an edit within a function, and the import
@@ -195,13 +197,13 @@ impl<'a> Importer<'a> {
// unclear whether should add an import statement at the start of the file, since it could
// be shadowed between the import and the current location.
if imported_name.start() > at {
return Some(Err(ResolutionError::ImportAfterUsage));
return Err(ResolutionError::ImportAfterUsage);
}
// If the symbol source (i.e., the import statement) is in a typing-only context, but we're
// in a runtime context, abort.
if imported_name.context().is_typing() && semantic.execution_context().is_runtime() {
return Some(Err(ResolutionError::IncompatibleContext));
return Err(ResolutionError::IncompatibleContext);
}
// We also add a no-op edit to force conflicts with any other fixes that might try to
@@ -224,7 +226,7 @@ impl<'a> Importer<'a> {
self.locator.slice(imported_name.range()).to_string(),
imported_name.range(),
);
Some(Ok((import_edit, imported_name.into_name())))
Ok(Some((import_edit, imported_name.into_name())))
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make

View File

@@ -81,7 +81,7 @@ pub fn check_path(
directives: &Directives,
settings: &Settings,
noqa: flags::Noqa,
source_kind: Option<&SourceKind>,
source_kind: &SourceKind,
source_type: PySourceType,
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics.
@@ -270,17 +270,17 @@ const MAX_ITERATIONS: usize = 100;
pub fn add_noqa_to_path(
path: &Path,
package: Option<&Path>,
source_kind: &SourceKind,
source_type: PySourceType,
settings: &Settings,
) -> Result<usize> {
// Read the file from disk.
let contents = std::fs::read_to_string(path)?;
let contents = source_kind.source_code();
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents, source_type.as_mode());
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(&contents);
let locator = Locator::new(contents);
// Detect the current code style (lazily).
let stylist = Stylist::from_tokens(&tokens, &locator);
@@ -310,21 +310,20 @@ pub fn add_noqa_to_path(
&directives,
settings,
flags::Noqa::Disabled,
None,
source_kind,
source_type,
);
// Log any parse errors.
if let Some(err) = error {
// TODO(dhruvmanila): This should use `SourceKind`, update when
// `--add-noqa` is supported for Jupyter notebooks.
error!(
"{}",
DisplayParseError::new(err, locator.to_source_code(), None)
DisplayParseError::new(err, locator.to_source_code(), source_kind)
);
}
// Add any missing `# noqa` pragmas.
// TODO(dhruvmanila): Add support for Jupyter Notebooks
add_noqa(
path,
&diagnostics.0,
@@ -377,7 +376,7 @@ pub fn lint_only(
&directives,
settings,
noqa,
Some(source_kind),
source_kind,
source_type,
);
@@ -471,7 +470,7 @@ pub fn lint_fix<'a>(
&directives,
settings,
noqa,
Some(source_kind),
source_kind,
source_type,
);

View File

@@ -139,14 +139,14 @@ pub fn set_up_logging(level: &LogLevel) -> Result<()> {
pub struct DisplayParseError<'a> {
error: ParseError,
source_code: SourceCode<'a, 'a>,
source_kind: Option<&'a SourceKind>,
source_kind: &'a SourceKind,
}
impl<'a> DisplayParseError<'a> {
pub fn new(
error: ParseError,
source_code: SourceCode<'a, 'a>,
source_kind: Option<&'a SourceKind>,
source_kind: &'a SourceKind,
) -> Self {
Self {
error,
@@ -171,32 +171,29 @@ impl Display for DisplayParseError<'_> {
// If we're working on a Jupyter notebook, translate the positions
// with respect to the cell and row in the cell. This is the same
// format as the `TextEmitter`.
let error_location = if let Some(jupyter_index) = self
.source_kind
.and_then(SourceKind::notebook)
.map(Notebook::index)
{
write!(
f,
"cell {cell}{colon}",
cell = jupyter_index
.cell(source_location.row.get())
.unwrap_or_default(),
colon = ":".cyan(),
)?;
let error_location =
if let Some(jupyter_index) = self.source_kind.as_ipy_notebook().map(Notebook::index) {
write!(
f,
"cell {cell}{colon}",
cell = jupyter_index
.cell(source_location.row.get())
.unwrap_or_default(),
colon = ":".cyan(),
)?;
SourceLocation {
row: OneIndexed::new(
jupyter_index
.cell_row(source_location.row.get())
.unwrap_or(1) as usize,
)
.unwrap(),
column: source_location.column,
}
} else {
source_location
};
SourceLocation {
row: OneIndexed::new(
jupyter_index
.cell_row(source_location.row.get())
.unwrap_or(1) as usize,
)
.unwrap(),
column: source_location.column,
}
} else {
source_location
};
write!(
f,

View File

@@ -1,4 +1,5 @@
use std::collections::hash_map::DefaultHasher;
use std::collections::HashSet;
use std::hash::{Hash, Hasher};
use std::io::Write;
@@ -6,8 +7,6 @@ use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde_json::json;
use ruff_source_file::SourceLocation;
use crate::fs::{relativize_path, relativize_path_to};
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
@@ -58,6 +57,7 @@ impl Serialize for SerializedMessages<'_> {
S: Serializer,
{
let mut s = serializer.serialize_seq(Some(self.messages.len()))?;
let mut fingerprints = HashSet::<u64>::with_capacity(self.messages.len());
for message in self.messages {
let start_location = message.compute_start_location();
@@ -82,10 +82,19 @@ impl Serialize for SerializedMessages<'_> {
|project_dir| relativize_path_to(message.filename(), project_dir),
);
let mut message_fingerprint = fingerprint(message, 0);
// Make sure that we do not get a fingerprint that is already in use
// by adding in the previously generated one.
while fingerprints.contains(&message_fingerprint) {
message_fingerprint = fingerprint(message, message_fingerprint);
}
fingerprints.insert(message_fingerprint);
let value = json!({
"description": format!("({}) {}", message.kind.rule().noqa_code(), message.kind.body),
"severity": "major",
"fingerprint": fingerprint(message, &start_location, &end_location),
"fingerprint": format!("{:x}", message_fingerprint),
"location": {
"path": path,
"lines": lines
@@ -100,11 +109,7 @@ impl Serialize for SerializedMessages<'_> {
}
/// Generate a unique fingerprint to identify a violation.
fn fingerprint(
message: &Message,
start_location: &SourceLocation,
end_location: &SourceLocation,
) -> String {
fn fingerprint(message: &Message, salt: u64) -> u64 {
let Message {
kind,
range: _,
@@ -115,12 +120,11 @@ fn fingerprint(
let mut hasher = DefaultHasher::new();
kind.rule().hash(&mut hasher);
start_location.hash(&mut hasher);
end_location.hash(&mut hasher);
salt.hash(&mut hasher);
kind.name.hash(&mut hasher);
file.name().hash(&mut hasher);
format!("{:x}", hasher.finish())
hasher.finish()
}
#[cfg(test)]

View File

@@ -4,7 +4,7 @@ use std::num::NonZeroUsize;
use colored::Colorize;
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_notebook::NotebookIndex;
use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
@@ -65,7 +65,7 @@ impl Emitter for GroupedEmitter {
writer,
"{}",
DisplayGroupedMessage {
jupyter_index: context.notebook(message.filename()).map(Notebook::index),
notebook_index: context.notebook_index(message.filename()),
message,
show_fix_status: self.show_fix_status,
show_source: self.show_source,
@@ -92,7 +92,7 @@ struct DisplayGroupedMessage<'a> {
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
jupyter_index: Option<&'a NotebookIndex>,
notebook_index: Option<&'a NotebookIndex>,
}
impl Display for DisplayGroupedMessage<'_> {
@@ -110,7 +110,7 @@ impl Display for DisplayGroupedMessage<'_> {
)?;
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let (row, col) = if let Some(jupyter_index) = self.jupyter_index {
let (row, col) = if let Some(jupyter_index) = self.notebook_index {
write!(
f,
"cell {cell}{sep}",
@@ -150,7 +150,7 @@ impl Display for DisplayGroupedMessage<'_> {
"{}",
MessageCodeFrame {
message,
jupyter_index: self.jupyter_index
notebook_index: self.notebook_index
}
)?;
}

View File

@@ -14,7 +14,7 @@ pub use json_lines::JsonLinesEmitter;
pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_notebook::Notebook;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use text::TextEmitter;
@@ -127,21 +127,21 @@ pub trait Emitter {
/// Context passed to [`Emitter`].
pub struct EmitterContext<'a> {
notebooks: &'a FxHashMap<String, Notebook>,
notebook_indexes: &'a FxHashMap<String, NotebookIndex>,
}
impl<'a> EmitterContext<'a> {
pub fn new(notebooks: &'a FxHashMap<String, Notebook>) -> Self {
Self { notebooks }
pub fn new(notebook_indexes: &'a FxHashMap<String, NotebookIndex>) -> Self {
Self { notebook_indexes }
}
/// Tests if the file with `name` is a jupyter notebook.
pub fn is_notebook(&self, name: &str) -> bool {
self.notebooks.contains_key(name)
self.notebook_indexes.contains_key(name)
}
pub fn notebook(&self, name: &str) -> Option<&Notebook> {
self.notebooks.get(name)
pub fn notebook_index(&self, name: &str) -> Option<&NotebookIndex> {
self.notebook_indexes.get(name)
}
}
@@ -225,8 +225,8 @@ def fibonacci(n):
emitter: &mut dyn Emitter,
messages: &[Message],
) -> String {
let source_kinds = FxHashMap::default();
let context = EmitterContext::new(&source_kinds);
let notebook_indexes = FxHashMap::default();
let context = EmitterContext::new(&notebook_indexes);
let mut output: Vec<u8> = Vec::new();
emitter.emit(&mut output, messages, &context).unwrap();

View File

@@ -7,7 +7,7 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, Sou
use bitflags::bitflags;
use colored::Colorize;
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_notebook::NotebookIndex;
use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -71,14 +71,14 @@ impl Emitter for TextEmitter {
)?;
let start_location = message.compute_start_location();
let jupyter_index = context.notebook(message.filename()).map(Notebook::index);
let notebook_index = context.notebook_index(message.filename());
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(jupyter_index) = jupyter_index {
let diagnostic_location = if let Some(notebook_index) = notebook_index {
write!(
writer,
"cell {cell}{sep}",
cell = jupyter_index
cell = notebook_index
.cell(start_location.row.get())
.unwrap_or_default(),
sep = ":".cyan(),
@@ -86,7 +86,7 @@ impl Emitter for TextEmitter {
SourceLocation {
row: OneIndexed::new(
jupyter_index
notebook_index
.cell_row(start_location.row.get())
.unwrap_or(1) as usize,
)
@@ -115,7 +115,7 @@ impl Emitter for TextEmitter {
"{}",
MessageCodeFrame {
message,
jupyter_index
notebook_index
}
)?;
}
@@ -161,7 +161,7 @@ impl Display for RuleCodeAndBody<'_> {
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
pub(crate) jupyter_index: Option<&'a NotebookIndex>,
pub(crate) notebook_index: Option<&'a NotebookIndex>,
}
impl Display for MessageCodeFrame<'_> {
@@ -186,14 +186,12 @@ impl Display for MessageCodeFrame<'_> {
let content_start_index = source_code.line_index(range.start());
let mut start_index = content_start_index.saturating_sub(2);
// If we're working on a jupyter notebook, skip the lines which are
// If we're working with a Jupyter Notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(jupyter_index) = self.jupyter_index {
let content_start_cell = jupyter_index
.cell(content_start_index.get())
.unwrap_or_default();
if let Some(index) = self.notebook_index {
let content_start_cell = index.cell(content_start_index.get()).unwrap_or_default();
while start_index < content_start_index {
if jupyter_index.cell(start_index.get()).unwrap_or_default() == content_start_cell {
if index.cell(start_index.get()).unwrap_or_default() == content_start_cell {
break;
}
start_index = start_index.saturating_add(1);
@@ -213,14 +211,12 @@ impl Display for MessageCodeFrame<'_> {
.saturating_add(2)
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
// If we're working on a jupyter notebook, skip the lines which are
// If we're working with a Jupyter Notebook, skip the lines which are
// outside of the cell containing the diagnostic.
if let Some(jupyter_index) = self.jupyter_index {
let content_end_cell = jupyter_index
.cell(content_end_index.get())
.unwrap_or_default();
if let Some(index) = self.notebook_index {
let content_end_cell = index.cell(content_end_index.get()).unwrap_or_default();
while end_index > content_end_index {
if jupyter_index.cell(end_index.get()).unwrap_or_default() == content_end_cell {
if index.cell(end_index.get()).unwrap_or_default() == content_end_cell {
break;
}
end_index = end_index.saturating_sub(1);
@@ -256,10 +252,10 @@ impl Display for MessageCodeFrame<'_> {
title: None,
slices: vec![Slice {
source: &source.text,
line_start: self.jupyter_index.map_or_else(
line_start: self.notebook_index.map_or_else(
|| start_index.get(),
|jupyter_index| {
jupyter_index
|notebook_index| {
notebook_index
.cell_row(start_index.get())
.unwrap_or_default() as usize
},

View File

@@ -59,7 +59,7 @@ impl<'a> Directive<'a> {
if text[..comment_start]
.chars()
.last()
.is_some_and(|c| c != '#')
.map_or(true, |c| c != '#')
{
continue;
}

View File

@@ -9,12 +9,16 @@ use crate::codes::RuleCodePrefix;
use crate::codes::RuleIter;
use crate::registry::{Linter, Rule, RuleNamespace};
use crate::rule_redirects::get_redirect;
use crate::settings::types::PreviewMode;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RuleSelector {
/// Select all stable rules.
/// Select all rules (includes rules in preview if enabled)
All,
/// Select all nursery rules.
/// Category to select all rules in preview (includes legacy nursery rules)
Preview,
/// Legacy category to select all rules in the "nursery" which predated preview mode
#[deprecated(note = "Use `RuleSelector::Preview` for new rules instead")]
Nursery,
/// Legacy category to select both the `mccabe` and `flake8-comprehensions` linters
/// via a single selector.
@@ -29,6 +33,11 @@ pub enum RuleSelector {
prefix: RuleCodePrefix,
redirected_from: Option<&'static str>,
},
/// Select an individual rule with a given prefix.
Rule {
prefix: RuleCodePrefix,
redirected_from: Option<&'static str>,
},
}
impl From<Linter> for RuleSelector {
@@ -43,7 +52,9 @@ impl FromStr for RuleSelector {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"ALL" => Ok(Self::All),
#[allow(deprecated)]
"NURSERY" => Ok(Self::Nursery),
"PREVIEW" => Ok(Self::Preview),
"C" => Ok(Self::C),
"T" => Ok(Self::T),
_ => {
@@ -59,16 +70,43 @@ impl FromStr for RuleSelector {
return Ok(Self::Linter(linter));
}
Ok(Self::Prefix {
prefix: RuleCodePrefix::parse(&linter, code)
.map_err(|_| ParseError::Unknown(s.to_string()))?,
redirected_from,
})
// Does the selector select a single rule?
let prefix = RuleCodePrefix::parse(&linter, code)
.map_err(|_| ParseError::Unknown(s.to_string()))?;
if is_single_rule_selector(&prefix) {
Ok(Self::Rule {
prefix,
redirected_from,
})
} else {
Ok(Self::Prefix {
prefix,
redirected_from,
})
}
}
}
}
}
/// Returns `true` if the [`RuleCodePrefix`] matches a single rule exactly
/// (e.g., `E225`, as opposed to `E2`).
pub(crate) fn is_single_rule_selector(prefix: &RuleCodePrefix) -> bool {
let mut rules = prefix.rules();
// The selector must match a single rule.
let Some(rule) = rules.next() else {
return false;
};
if rules.next().is_some() {
return false;
}
// The rule must match the selector exactly.
rule.noqa_code().suffix() == prefix.short_code()
}
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("Unknown rule selector: `{0}`")]
@@ -81,10 +119,12 @@ impl RuleSelector {
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
match self {
RuleSelector::All => ("", "ALL"),
#[allow(deprecated)]
RuleSelector::Nursery => ("", "NURSERY"),
RuleSelector::Preview => ("", "PREVIEW"),
RuleSelector::C => ("", "C"),
RuleSelector::T => ("", "T"),
RuleSelector::Prefix { prefix, .. } => {
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
(prefix.linter().common_prefix(), prefix.short_code())
}
RuleSelector::Linter(l) => (l.common_prefix(), ""),
@@ -135,27 +175,19 @@ impl Visitor<'_> for SelectorVisitor {
}
}
impl From<RuleCodePrefix> for RuleSelector {
fn from(prefix: RuleCodePrefix) -> Self {
Self::Prefix {
prefix,
redirected_from: None,
}
}
}
impl IntoIterator for &RuleSelector {
type IntoIter = RuleSelectorIter;
type Item = Rule;
fn into_iter(self) -> Self::IntoIter {
impl RuleSelector {
/// Return all matching rules, regardless of whether they're in preview.
pub fn all_rules(&self) -> impl Iterator<Item = Rule> + '_ {
match self {
RuleSelector::All => {
RuleSelectorIter::All(Rule::iter().filter(|rule| !rule.is_nursery()))
}
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
#[allow(deprecated)]
RuleSelector::Nursery => {
RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery))
}
RuleSelector::Preview => RuleSelectorIter::Nursery(
Rule::iter().filter(|rule| rule.is_preview() || rule.is_nursery()),
),
RuleSelector::C => RuleSelectorIter::Chain(
Linter::Flake8Comprehensions
.rules()
@@ -167,13 +199,28 @@ impl IntoIterator for &RuleSelector {
.chain(Linter::Flake8Print.rules()),
),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()),
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
RuleSelectorIter::Vec(prefix.clone().rules())
}
}
}
/// Returns rules matching the selector, taking into account whether preview mode is enabled.
pub fn rules(&self, preview: PreviewMode) -> impl Iterator<Item = Rule> + '_ {
#[allow(deprecated)]
self.all_rules().filter(move |rule| {
// Always include rules that are not in preview or the nursery
!(rule.is_preview() || rule.is_nursery())
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|| (matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. }) && rule.is_nursery())
// Enabling preview includes all preview or nursery rules
|| preview.is_enabled()
})
}
}
pub enum RuleSelectorIter {
All(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
All(RuleIter),
Nursery(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
Chain(std::iter::Chain<std::vec::IntoIter<Rule>, std::vec::IntoIter<Rule>>),
Vec(std::vec::IntoIter<Rule>),
@@ -192,18 +239,6 @@ impl Iterator for RuleSelectorIter {
}
}
/// A const alternative to the `impl From<RuleCodePrefix> for RuleSelector`
/// to let us keep the fields of [`RuleSelector`] private.
// Note that Rust doesn't yet support `impl const From<RuleCodePrefix> for
// RuleSelector` (see https://github.com/rust-lang/rust/issues/67792).
// TODO(martin): Remove once RuleSelector is an enum with Linter & Rule variants
pub(crate) const fn prefix_to_selector(prefix: RuleCodePrefix) -> RuleSelector {
RuleSelector::Prefix {
prefix,
redirected_from: None,
}
}
#[cfg(feature = "schemars")]
mod schema {
use itertools::Itertools;
@@ -266,18 +301,20 @@ impl RuleSelector {
pub fn specificity(&self) -> Specificity {
match self {
RuleSelector::All => Specificity::All,
RuleSelector::Preview => Specificity::All,
#[allow(deprecated)]
RuleSelector::Nursery => Specificity::All,
RuleSelector::T => Specificity::LinterGroup,
RuleSelector::C => Specificity::LinterGroup,
RuleSelector::Linter(..) => Specificity::Linter,
RuleSelector::Rule { .. } => Specificity::Rule,
RuleSelector::Prefix { prefix, .. } => {
let prefix: &'static str = prefix.short_code();
match prefix.len() {
1 => Specificity::Code1Char,
2 => Specificity::Code2Chars,
3 => Specificity::Code3Chars,
4 => Specificity::Code4Chars,
5 => Specificity::Code5Chars,
1 => Specificity::Prefix1Char,
2 => Specificity::Prefix2Chars,
3 => Specificity::Prefix3Chars,
4 => Specificity::Prefix4Chars,
_ => panic!("RuleSelector::specificity doesn't yet support codes with so many characters"),
}
}
@@ -285,16 +322,24 @@ impl RuleSelector {
}
}
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
pub enum Specificity {
/// The specificity when selecting all rules (e.g., `--select ALL`).
All,
/// The specificity when selecting a legacy linter group (e.g., `--select C` or `--select T`).
LinterGroup,
/// The specificity when selecting a linter (e.g., `--select PLE` or `--select UP`).
Linter,
Code1Char,
Code2Chars,
Code3Chars,
Code4Chars,
Code5Chars,
/// The specificity when selecting via a rule prefix with a one-character code (e.g., `--select PLE1`).
Prefix1Char,
/// The specificity when selecting via a rule prefix with a two-character code (e.g., `--select PLE12`).
Prefix2Chars,
/// The specificity when selecting via a rule prefix with a three-character code (e.g., `--select PLE123`).
Prefix3Chars,
/// The specificity when selecting via a rule prefix with a four-character code (e.g., `--select PLE1234`).
Prefix4Chars,
/// The specificity when selecting an individual rule (e.g., `--select PLE1205`).
Rule,
}
#[cfg(feature = "clap")]

View File

@@ -454,7 +454,6 @@ fn check_dynamically_typed<F>(
if let Expr::Constant(ast::ExprConstant {
range,
value: Constant::Str(string),
..
}) = annotation
{
// Quoted annotations

View File

@@ -33,6 +33,9 @@ mod tests {
#[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"))]
#[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_2.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_3.py"))]
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))]
#[test_case(Rule::RaiseLiteral, Path::new("B016.py"))]
#[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))]

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use ruff_text_size::Ranged;
@@ -81,7 +80,17 @@ pub(crate) fn getattr_with_constant(
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
format!("{}.{}", checker.locator().slice(obj), value),
if matches!(
obj,
Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) | Expr::Call(_)
) {
format!("{}.{}", checker.locator().slice(obj), value)
} else {
// Defensively parenthesize any other expressions. For example, attribute accesses
// on `int` literals must be parenthesized, e.g., `getattr(1, "real")` becomes
// `(1).real`. The same is true for named expressions and others.
format!("({}).{}", checker.locator().slice(obj), value)
},
expr.range(),
)));
}

View File

@@ -175,8 +175,15 @@ fn move_initialization(
return None;
}
Edit::insertion(content, locator.line_start(statement.start()))
} else if locator.full_line_end(statement.end()) == locator.text_len() {
// If the statement is at the end of the file, without a trailing newline, insert
// _after_ it with an extra newline.
Edit::insertion(
format!("{}{}", stylist.line_ending().as_str(), content),
locator.full_line_end(statement.end()),
)
} else {
// If the docstring is the only statement, insert _before_ it.
// If the docstring is the only statement, insert _after_ it.
Edit::insertion(content, locator.full_line_end(statement.end()))
}
} else {

View File

@@ -1,9 +1,10 @@
use ruff_python_ast::{self as ast, ExceptHandler, Expr};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::map_starred;
use ruff_python_ast::{self as ast, ExceptHandler, Expr};
use ruff_text_size::Ranged;
use crate::autofix::edits::pad;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -41,11 +42,7 @@ pub struct RedundantTupleInExceptionHandler {
impl AlwaysAutofixableViolation for RedundantTupleInExceptionHandler {
#[derive_message_formats]
fn message(&self) -> String {
let RedundantTupleInExceptionHandler { name } = self;
format!(
"A length-one tuple literal is redundant. Write `except {name}` instead of `except \
({name},)`."
)
format!("A length-one tuple literal is redundant in exception handlers")
}
fn autofix_title(&self) -> String {
@@ -70,9 +67,10 @@ pub(crate) fn redundant_tuple_in_exception_handler(
let Expr::Tuple(ast::ExprTuple { elts, .. }) = type_.as_ref() else {
continue;
};
let [elt] = &elts[..] else {
let [elt] = elts.as_slice() else {
continue;
};
let elt = map_starred(elt);
let mut diagnostic = Diagnostic::new(
RedundantTupleInExceptionHandler {
name: checker.generator().expr(elt),
@@ -80,8 +78,19 @@ pub(crate) fn redundant_tuple_in_exception_handler(
type_.range(),
);
if checker.patch(diagnostic.kind.rule()) {
// If there's no space between the `except` and the tuple, we need to insert a space,
// as in:
// ```python
// except(ValueError,):
// ```
// Otherwise, the output will be invalid syntax, since we're removing a set of
// parentheses.
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.generator().expr(elt),
pad(
checker.generator().expr(elt),
type_.range(),
checker.locator(),
),
type_.range(),
)));
}

View File

@@ -0,0 +1,26 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_1.py:3:22: B006 [*] Do not use mutable data structures for argument defaults
|
1 | # Docstring followed by a newline
2 |
3 | def foobar(foor, bar={}):
| ^^ B006
4 | """
5 | """
|
= help: Replace with `None`; initialize within function
Possible fix
1 1 | # Docstring followed by a newline
2 2 |
3 |-def foobar(foor, bar={}):
3 |+def foobar(foor, bar=None):
4 4 | """
5 5 | """
6 |+
7 |+ if bar is None:
8 |+ bar = {}

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_2.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
|
2 | # Regression test for https://github.com/astral-sh/ruff/issues/7155
3 |
4 | def foobar(foor, bar={}):
| ^^ B006
5 | """
6 | """
|
= help: Replace with `None`; initialize within function
Possible fix
1 1 | # Docstring followed by whitespace with no newline
2 2 | # Regression test for https://github.com/astral-sh/ruff/issues/7155
3 3 |
4 |-def foobar(foor, bar={}):
4 |+def foobar(foor, bar=None):
5 5 | """
6 |- """
6 |+ """
7 |+ if bar is None:
8 |+ bar = {}

View File

@@ -0,0 +1,25 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_3.py:4:22: B006 [*] Do not use mutable data structures for argument defaults
|
4 | def foobar(foor, bar={}):
| ^^ B006
5 | """
6 | """
|
= help: Replace with `None`; initialize within function
Possible fix
1 1 | # Docstring with no newline
2 2 |
3 3 |
4 |-def foobar(foor, bar={}):
4 |+def foobar(foor, bar=None):
5 |+ """
5 6 | """
6 |- """
7 |+ if bar is None:
8 |+ bar = {}

View File

@@ -476,4 +476,23 @@ B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaul
|
= help: Replace with `None`; initialize within function
B006_B008.py:313:52: B006 [*] Do not use mutable data structures for argument defaults
|
313 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
314 | """Docstring without newline"""
|
= help: Replace with `None`; initialize within function
Possible fix
310 310 | """Docstring"""
311 311 |
312 312 |
313 |-def single_line_func_wrong(value: dict[str, str] = {}):
314 |- """Docstring without newline"""
313 |+def single_line_func_wrong(value: dict[str, str] = None):
314 |+ """Docstring without newline"""
315 |+ if value is None:
316 |+ value = {}

View File

@@ -124,7 +124,7 @@ B009_B010.py:24:15: B009 [*] Do not call `getattr` with a constant attribute val
24 |+_ = lambda x: x.bar
25 25 | if getattr(x, "bar"):
26 26 | pass
27 27 |
27 27 | getattr(1, "real")
B009_B010.py:25:4: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
@@ -133,6 +133,7 @@ B009_B010.py:25:4: B009 [*] Do not call `getattr` with a constant attribute valu
25 | if getattr(x, "bar"):
| ^^^^^^^^^^^^^^^^^ B009
26 | pass
27 | getattr(1, "real")
|
= help: Replace `getattr` with attribute access
@@ -143,7 +144,176 @@ B009_B010.py:25:4: B009 [*] Do not call `getattr` with a constant attribute valu
25 |-if getattr(x, "bar"):
25 |+if x.bar:
26 26 | pass
27 27 |
28 28 | # Valid setattr usage
27 27 | getattr(1, "real")
28 28 | getattr(1., "real")
B009_B010.py:27:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
25 | if getattr(x, "bar"):
26 | pass
27 | getattr(1, "real")
| ^^^^^^^^^^^^^^^^^^ B009
28 | getattr(1., "real")
29 | getattr(1.0, "real")
|
= help: Replace `getattr` with attribute access
Suggested fix
24 24 | _ = lambda x: getattr(x, "bar")
25 25 | if getattr(x, "bar"):
26 26 | pass
27 |-getattr(1, "real")
27 |+(1).real
28 28 | getattr(1., "real")
29 29 | getattr(1.0, "real")
30 30 | getattr(1j, "real")
B009_B010.py:28:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
26 | pass
27 | getattr(1, "real")
28 | getattr(1., "real")
| ^^^^^^^^^^^^^^^^^^^ B009
29 | getattr(1.0, "real")
30 | getattr(1j, "real")
|
= help: Replace `getattr` with attribute access
Suggested fix
25 25 | if getattr(x, "bar"):
26 26 | pass
27 27 | getattr(1, "real")
28 |-getattr(1., "real")
28 |+(1.).real
29 29 | getattr(1.0, "real")
30 30 | getattr(1j, "real")
31 31 | getattr(True, "real")
B009_B010.py:29:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
27 | getattr(1, "real")
28 | getattr(1., "real")
29 | getattr(1.0, "real")
| ^^^^^^^^^^^^^^^^^^^^ B009
30 | getattr(1j, "real")
31 | getattr(True, "real")
|
= help: Replace `getattr` with attribute access
Suggested fix
26 26 | pass
27 27 | getattr(1, "real")
28 28 | getattr(1., "real")
29 |-getattr(1.0, "real")
29 |+(1.0).real
30 30 | getattr(1j, "real")
31 31 | getattr(True, "real")
32 32 | getattr(x := 1, "real")
B009_B010.py:30:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
28 | getattr(1., "real")
29 | getattr(1.0, "real")
30 | getattr(1j, "real")
| ^^^^^^^^^^^^^^^^^^^ B009
31 | getattr(True, "real")
32 | getattr(x := 1, "real")
|
= help: Replace `getattr` with attribute access
Suggested fix
27 27 | getattr(1, "real")
28 28 | getattr(1., "real")
29 29 | getattr(1.0, "real")
30 |-getattr(1j, "real")
30 |+(1j).real
31 31 | getattr(True, "real")
32 32 | getattr(x := 1, "real")
33 33 | getattr(x + y, "real")
B009_B010.py:31:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
29 | getattr(1.0, "real")
30 | getattr(1j, "real")
31 | getattr(True, "real")
| ^^^^^^^^^^^^^^^^^^^^^ B009
32 | getattr(x := 1, "real")
33 | getattr(x + y, "real")
|
= help: Replace `getattr` with attribute access
Suggested fix
28 28 | getattr(1., "real")
29 29 | getattr(1.0, "real")
30 30 | getattr(1j, "real")
31 |-getattr(True, "real")
31 |+(True).real
32 32 | getattr(x := 1, "real")
33 33 | getattr(x + y, "real")
34 34 | getattr("foo"
B009_B010.py:32:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
30 | getattr(1j, "real")
31 | getattr(True, "real")
32 | getattr(x := 1, "real")
| ^^^^^^^^^^^^^^^^^^^^^^^ B009
33 | getattr(x + y, "real")
34 | getattr("foo"
|
= help: Replace `getattr` with attribute access
Suggested fix
29 29 | getattr(1.0, "real")
30 30 | getattr(1j, "real")
31 31 | getattr(True, "real")
32 |-getattr(x := 1, "real")
32 |+(x := 1).real
33 33 | getattr(x + y, "real")
34 34 | getattr("foo"
35 35 | "bar", "real")
B009_B010.py:33:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
31 | getattr(True, "real")
32 | getattr(x := 1, "real")
33 | getattr(x + y, "real")
| ^^^^^^^^^^^^^^^^^^^^^^ B009
34 | getattr("foo"
35 | "bar", "real")
|
= help: Replace `getattr` with attribute access
Suggested fix
30 30 | getattr(1j, "real")
31 31 | getattr(True, "real")
32 32 | getattr(x := 1, "real")
33 |-getattr(x + y, "real")
33 |+(x + y).real
34 34 | getattr("foo"
35 35 | "bar", "real")
36 36 |
B009_B010.py:34:1: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
32 | getattr(x := 1, "real")
33 | getattr(x + y, "real")
34 | / getattr("foo"
35 | | "bar", "real")
| |______________________^ B009
|
= help: Replace `getattr` with attribute access
Suggested fix
31 31 | getattr(True, "real")
32 32 | getattr(x := 1, "real")
33 33 | getattr(x + y, "real")
34 |-getattr("foo"
35 |- "bar", "real")
34 |+("foo"
35 |+ "bar").real
36 36 |
37 37 |
38 38 | # Valid setattr usage

View File

@@ -1,120 +1,120 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B009_B010.py:40:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
B009_B010.py:50:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
39 | # Invalid usage
40 | setattr(foo, "bar", None)
49 | # Invalid usage
50 | setattr(foo, "bar", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B010
41 | setattr(foo, "_123abc", None)
42 | setattr(foo, "__123abc__", None)
51 | setattr(foo, "_123abc", None)
52 | setattr(foo, "__123abc__", None)
|
= help: Replace `setattr` with assignment
Suggested fix
37 37 | pass
38 38 |
39 39 | # Invalid usage
40 |-setattr(foo, "bar", None)
40 |+foo.bar = None
41 41 | setattr(foo, "_123abc", None)
42 42 | setattr(foo, "__123abc__", None)
43 43 | setattr(foo, "abc123", None)
47 47 | pass
48 48 |
49 49 | # Invalid usage
50 |-setattr(foo, "bar", None)
50 |+foo.bar = None
51 51 | setattr(foo, "_123abc", None)
52 52 | setattr(foo, "__123abc__", None)
53 53 | setattr(foo, "abc123", None)
B009_B010.py:41:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
B009_B010.py:51:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
39 | # Invalid usage
40 | setattr(foo, "bar", None)
41 | setattr(foo, "_123abc", None)
49 | # Invalid usage
50 | setattr(foo, "bar", None)
51 | setattr(foo, "_123abc", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B010
42 | setattr(foo, "__123abc__", None)
43 | setattr(foo, "abc123", None)
52 | setattr(foo, "__123abc__", None)
53 | setattr(foo, "abc123", None)
|
= help: Replace `setattr` with assignment
Suggested fix
38 38 |
39 39 | # Invalid usage
40 40 | setattr(foo, "bar", None)
41 |-setattr(foo, "_123abc", None)
41 |+foo._123abc = None
42 42 | setattr(foo, "__123abc__", None)
43 43 | setattr(foo, "abc123", None)
44 44 | setattr(foo, r"abc123", None)
48 48 |
49 49 | # Invalid usage
50 50 | setattr(foo, "bar", None)
51 |-setattr(foo, "_123abc", None)
51 |+foo._123abc = None
52 52 | setattr(foo, "__123abc__", None)
53 53 | setattr(foo, "abc123", None)
54 54 | setattr(foo, r"abc123", None)
B009_B010.py:42:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
B009_B010.py:52:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
40 | setattr(foo, "bar", None)
41 | setattr(foo, "_123abc", None)
42 | setattr(foo, "__123abc__", None)
50 | setattr(foo, "bar", None)
51 | setattr(foo, "_123abc", None)
52 | setattr(foo, "__123abc__", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B010
43 | setattr(foo, "abc123", None)
44 | setattr(foo, r"abc123", None)
53 | setattr(foo, "abc123", None)
54 | setattr(foo, r"abc123", None)
|
= help: Replace `setattr` with assignment
Suggested fix
39 39 | # Invalid usage
40 40 | setattr(foo, "bar", None)
41 41 | setattr(foo, "_123abc", None)
42 |-setattr(foo, "__123abc__", None)
42 |+foo.__123abc__ = None
43 43 | setattr(foo, "abc123", None)
44 44 | setattr(foo, r"abc123", None)
45 45 | setattr(foo.bar, r"baz", None)
49 49 | # Invalid usage
50 50 | setattr(foo, "bar", None)
51 51 | setattr(foo, "_123abc", None)
52 |-setattr(foo, "__123abc__", None)
52 |+foo.__123abc__ = None
53 53 | setattr(foo, "abc123", None)
54 54 | setattr(foo, r"abc123", None)
55 55 | setattr(foo.bar, r"baz", None)
B009_B010.py:43:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
B009_B010.py:53:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
41 | setattr(foo, "_123abc", None)
42 | setattr(foo, "__123abc__", None)
43 | setattr(foo, "abc123", None)
51 | setattr(foo, "_123abc", None)
52 | setattr(foo, "__123abc__", None)
53 | setattr(foo, "abc123", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B010
44 | setattr(foo, r"abc123", None)
45 | setattr(foo.bar, r"baz", None)
54 | setattr(foo, r"abc123", None)
55 | setattr(foo.bar, r"baz", None)
|
= help: Replace `setattr` with assignment
Suggested fix
40 40 | setattr(foo, "bar", None)
41 41 | setattr(foo, "_123abc", None)
42 42 | setattr(foo, "__123abc__", None)
43 |-setattr(foo, "abc123", None)
43 |+foo.abc123 = None
44 44 | setattr(foo, r"abc123", None)
45 45 | setattr(foo.bar, r"baz", None)
50 50 | setattr(foo, "bar", None)
51 51 | setattr(foo, "_123abc", None)
52 52 | setattr(foo, "__123abc__", None)
53 |-setattr(foo, "abc123", None)
53 |+foo.abc123 = None
54 54 | setattr(foo, r"abc123", None)
55 55 | setattr(foo.bar, r"baz", None)
B009_B010.py:44:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
B009_B010.py:54:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
42 | setattr(foo, "__123abc__", None)
43 | setattr(foo, "abc123", None)
44 | setattr(foo, r"abc123", None)
52 | setattr(foo, "__123abc__", None)
53 | setattr(foo, "abc123", None)
54 | setattr(foo, r"abc123", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B010
45 | setattr(foo.bar, r"baz", None)
55 | setattr(foo.bar, r"baz", None)
|
= help: Replace `setattr` with assignment
Suggested fix
41 41 | setattr(foo, "_123abc", None)
42 42 | setattr(foo, "__123abc__", None)
43 43 | setattr(foo, "abc123", None)
44 |-setattr(foo, r"abc123", None)
44 |+foo.abc123 = None
45 45 | setattr(foo.bar, r"baz", None)
51 51 | setattr(foo, "_123abc", None)
52 52 | setattr(foo, "__123abc__", None)
53 53 | setattr(foo, "abc123", None)
54 |-setattr(foo, r"abc123", None)
54 |+foo.abc123 = None
55 55 | setattr(foo.bar, r"baz", None)
B009_B010.py:45:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
B009_B010.py:55:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
43 | setattr(foo, "abc123", None)
44 | setattr(foo, r"abc123", None)
45 | setattr(foo.bar, r"baz", None)
53 | setattr(foo, "abc123", None)
54 | setattr(foo, r"abc123", None)
55 | setattr(foo.bar, r"baz", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B010
|
= help: Replace `setattr` with assignment
Suggested fix
42 42 | setattr(foo, "__123abc__", None)
43 43 | setattr(foo, "abc123", None)
44 44 | setattr(foo, r"abc123", None)
45 |-setattr(foo.bar, r"baz", None)
45 |+foo.bar.baz = None
52 52 | setattr(foo, "__123abc__", None)
53 53 | setattr(foo, "abc123", None)
54 54 | setattr(foo, r"abc123", None)
55 |-setattr(foo.bar, r"baz", None)
55 |+foo.bar.baz = None

View File

@@ -1,24 +1,64 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B013.py:3:8: B013 [*] A length-one tuple literal is redundant. Write `except ValueError` instead of `except (ValueError,)`.
B013.py:5:8: B013 [*] A length-one tuple literal is redundant in exception handlers
|
1 | try:
2 | pass
3 | except (ValueError,):
| ^^^^^^^^^^^^^ B013
3 | try:
4 | pass
5 | except AttributeError:
5 | except (ValueError,):
| ^^^^^^^^^^^^^ B013
6 | pass
7 | except AttributeError:
|
= help: Replace with `except ValueError`
Fix
1 1 | try:
2 2 | pass
3 |-except (ValueError,):
3 |+except ValueError:
2 2 |
3 3 | try:
4 4 | pass
5 5 | except AttributeError:
5 |-except (ValueError,):
5 |+except ValueError:
6 6 | pass
7 7 | except AttributeError:
8 8 | pass
B013.py:11:8: B013 [*] A length-one tuple literal is redundant in exception handlers
|
9 | except (ImportError, TypeError):
10 | pass
11 | except (*retriable_exceptions,):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B013
12 | pass
13 | except(ValueError,):
|
= help: Replace with `except retriable_exceptions`
Fix
8 8 | pass
9 9 | except (ImportError, TypeError):
10 10 | pass
11 |-except (*retriable_exceptions,):
11 |+except retriable_exceptions:
12 12 | pass
13 13 | except(ValueError,):
14 14 | pass
B013.py:13:7: B013 [*] A length-one tuple literal is redundant in exception handlers
|
11 | except (*retriable_exceptions,):
12 | pass
13 | except(ValueError,):
| ^^^^^^^^^^^^^ B013
14 | pass
|
= help: Replace with `except ValueError`
Fix
10 10 | pass
11 11 | except (*retriable_exceptions,):
12 12 | pass
13 |-except(ValueError,):
13 |+except ValueError:
14 14 | pass

View File

@@ -3,8 +3,8 @@ use itertools::Itertools;
use libcst_native::{
Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement,
Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket,
List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedWhitespace, RightCurlyBrace,
RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedNode, ParenthesizedWhitespace,
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
TrailingWhitespace, Tuple,
};
use ruff_python_ast::Expr;
@@ -12,9 +12,12 @@ use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{Edit, Fix};
use ruff_python_codegen::Stylist;
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::autofix::edits::pad;
use crate::cst::helpers::space;
use crate::rules::flake8_comprehensions::rules::ObjectType;
use crate::{
checkers::ast::Checker,
@@ -26,9 +29,9 @@ use crate::{
/// (C400) Convert `list(x for x in y)` to `[x for x in y]`.
pub(crate) fn fix_unnecessary_generator_list(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
let module_text = locator.slice(expr);
@@ -58,7 +61,7 @@ pub(crate) fn fix_unnecessary_generator_list(
}
/// (C401) Convert `set(x for x in y)` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_generator_set(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_generator_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -86,14 +89,14 @@ pub(crate) fn fix_unnecessary_generator_set(checker: &Checker, expr: &Expr) -> R
let content = tree.codegen_stylist(stylist);
Ok(Edit::range_replacement(
pad_expression(content, expr.range(), checker),
pad_expression(content, expr.range(), checker.locator(), checker.semantic()),
expr.range(),
))
}
/// (C402) Convert `dict((x, x) for x in range(3))` to `{x: x for x in
/// range(3)}`.
pub(crate) fn fix_unnecessary_generator_dict(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_generator_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -110,10 +113,20 @@ pub(crate) fn fix_unnecessary_generator_dict(checker: &Checker, expr: &Expr) ->
bail!("Expected tuple to contain two elements");
};
// Insert whitespace before the `for`, since we're removing parentheses, as in:
// ```python
// dict((x, x)for x in range(3))
// ```
let mut for_in = generator_exp.for_in.clone();
if for_in.whitespace_before == ParenthesizableWhitespace::default() {
for_in.whitespace_before =
ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" "));
}
tree = Expression::DictComp(Box::new(DictComp {
key: Box::new(key.clone()),
value: Box::new(value.clone()),
for_in: generator_exp.for_in.clone(),
for_in,
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
@@ -123,19 +136,24 @@ pub(crate) fn fix_unnecessary_generator_dict(checker: &Checker, expr: &Expr) ->
lpar: vec![],
rpar: vec![],
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")),
whitespace_after_colon: space(),
}));
Ok(Edit::range_replacement(
pad_expression(tree.codegen_stylist(stylist), expr.range(), checker),
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
/// (C403) Convert `set([x for x in y])` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_list_comprehension_set(
checker: &Checker,
expr: &Expr,
checker: &Checker,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -162,7 +180,12 @@ pub(crate) fn fix_unnecessary_list_comprehension_set(
}));
Ok(Edit::range_replacement(
pad_expression(tree.codegen_stylist(stylist), expr.range(), checker),
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
@@ -170,8 +193,8 @@ pub(crate) fn fix_unnecessary_list_comprehension_set(
/// (C404) Convert `dict([(i, i) for i in range(3)])` to `{i: i for i in
/// range(3)}`.
pub(crate) fn fix_unnecessary_list_comprehension_dict(
checker: &Checker,
expr: &Expr,
checker: &Checker,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -190,12 +213,22 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
bail!("Expected tuple with two elements");
};
// Insert whitespace before the `for`, since we're removing parentheses, as in:
// ```python
// dict((x, x)for x in range(3))
// ```
let mut for_in = list_comp.for_in.clone();
if for_in.whitespace_before == ParenthesizableWhitespace::default() {
for_in.whitespace_before =
ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" "));
}
tree = Expression::DictComp(Box::new(DictComp {
key: Box::new(key.clone()),
value: Box::new(value.clone()),
for_in: list_comp.for_in.clone(),
for_in,
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")),
whitespace_after_colon: space(),
lbrace: LeftCurlyBrace {
whitespace_after: call.whitespace_before_args.clone(),
},
@@ -207,7 +240,12 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
}));
Ok(Edit::range_replacement(
pad_expression(tree.codegen_stylist(stylist), expr.range(), checker),
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
@@ -256,7 +294,7 @@ fn drop_trailing_comma<'a>(
}
/// (C405) Convert `set((1, 2))` to `{1, 2}`.
pub(crate) fn fix_unnecessary_literal_set(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_literal_set(expr: &Expr, checker: &Checker) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -291,13 +329,18 @@ pub(crate) fn fix_unnecessary_literal_set(checker: &Checker, expr: &Expr) -> Res
}
Ok(Edit::range_replacement(
pad_expression(tree.codegen_stylist(stylist), expr.range(), checker),
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
/// (C406) Convert `dict([(1, 2)])` to `{1: 2}`.
pub(crate) fn fix_unnecessary_literal_dict(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_literal_dict(expr: &Expr, checker: &Checker) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -354,13 +397,18 @@ pub(crate) fn fix_unnecessary_literal_dict(checker: &Checker, expr: &Expr) -> Re
}));
Ok(Edit::range_replacement(
pad_expression(tree.codegen_stylist(stylist), expr.range(), checker),
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
),
expr.range(),
))
}
/// (C408)
pub(crate) fn fix_unnecessary_collection_call(checker: &Checker, expr: &Expr) -> Result<Edit> {
pub(crate) fn fix_unnecessary_collection_call(expr: &Expr, checker: &Checker) -> Result<Edit> {
enum Collection {
Tuple,
List,
@@ -470,7 +518,12 @@ pub(crate) fn fix_unnecessary_collection_call(checker: &Checker, expr: &Expr) ->
Ok(Edit::range_replacement(
if matches!(collection, Collection::Dict) {
pad_expression(tree.codegen_stylist(stylist), expr.range(), checker)
pad_expression(
tree.codegen_stylist(stylist),
expr.range(),
checker.locator(),
checker.semantic(),
)
} else {
tree.codegen_stylist(stylist)
},
@@ -490,19 +543,24 @@ pub(crate) fn fix_unnecessary_collection_call(checker: &Checker, expr: &Expr) ->
/// However, this is a syntax error under the f-string grammar. As such,
/// this method will pad the start and end of an expression as needed to
/// avoid producing invalid syntax.
fn pad_expression(content: String, range: TextRange, checker: &Checker) -> String {
if !checker.semantic().in_f_string() {
fn pad_expression(
content: String,
range: TextRange,
locator: &Locator,
semantic: &SemanticModel,
) -> String {
if !semantic.in_f_string() {
return content;
}
// If the expression is immediately preceded by an opening brace, then
// we need to add a space before the expression.
let prefix = checker.locator().up_to(range.start());
let prefix = locator.up_to(range.start());
let left_pad = matches!(prefix.chars().next_back(), Some('{'));
// If the expression is immediately preceded by an opening brace, then
// we need to add a space before the expression.
let suffix = checker.locator().after(range.end());
let suffix = locator.after(range.end());
let right_pad = matches!(suffix.chars().next(), Some('}'));
if left_pad && right_pad {
@@ -518,9 +576,9 @@ fn pad_expression(content: String, range: TextRange, checker: &Checker) -> Strin
/// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)`
pub(crate) fn fix_unnecessary_literal_within_tuple_call(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
@@ -568,9 +626,9 @@ pub(crate) fn fix_unnecessary_literal_within_tuple_call(
/// (C410) Convert `list([1, 2])` to `[1, 2]`
pub(crate) fn fix_unnecessary_literal_within_list_call(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
@@ -620,9 +678,9 @@ pub(crate) fn fix_unnecessary_literal_within_list_call(
/// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`.
pub(crate) fn fix_unnecessary_list_call(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
// Expr(Call(List|Tuple)))) -> Expr(List|Tuple)))
let module_text = locator.slice(expr);
@@ -642,9 +700,9 @@ pub(crate) fn fix_unnecessary_list_call(
/// (C413) Convert `reversed(sorted([2, 3, 1]))` to `sorted([2, 3, 1],
/// reverse=True)`.
pub(crate) fn fix_unnecessary_call_around_sorted(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
@@ -754,9 +812,9 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
/// (C414) Convert `sorted(list(foo))` to `sorted(foo)`
pub(crate) fn fix_unnecessary_double_cast_or_process(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
@@ -785,9 +843,9 @@ pub(crate) fn fix_unnecessary_double_cast_or_process(
/// (C416) Convert `[i for i in x]` to `list(x)`.
pub(crate) fn fix_unnecessary_comprehension(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
@@ -865,161 +923,172 @@ pub(crate) fn fix_unnecessary_comprehension(
}
Ok(Edit::range_replacement(
tree.codegen_stylist(stylist),
pad(tree.codegen_stylist(stylist), expr.range(), locator),
expr.range(),
))
}
/// (C417) Convert `map(lambda x: x * 2, bar)` to `(x * 2 for x in bar)`.
pub(crate) fn fix_unnecessary_map(
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
parent: Option<&Expr>,
object_type: ObjectType,
locator: &Locator,
stylist: &Stylist,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let arg = match_arg(call)?;
let (args, lambda_func) = match &arg.value {
Expression::Call(outer_call) => {
let inner_lambda = outer_call.args.first().unwrap().value.clone();
match &inner_lambda {
Expression::Lambda(..) => (outer_call.args.clone(), inner_lambda),
_ => {
bail!("Expected a lambda function")
}
}
let (lambda, iter) = match call.args.as_slice() {
[call] => {
let call = match_call(&call.value)?;
let [lambda, iter] = call.args.as_slice() else {
bail!("Expected two arguments");
};
let lambda = match_lambda(&lambda.value)?;
let iter = &iter.value;
(lambda, iter)
}
Expression::Lambda(..) => (call.args.clone(), arg.value.clone()),
_ => {
bail!("Expected a lambda or call")
[lambda, iter] => {
let lambda = match_lambda(&lambda.value)?;
let iter = &iter.value;
(lambda, iter)
}
_ => bail!("Expected a call or lambda"),
};
let func_body = match_lambda(&lambda_func)?;
// Format the lambda target.
let target = match lambda.params.params.as_slice() {
// Ex) `lambda: x`
[] => AssignTargetExpression::Name(Box::new(Name {
value: "_",
lpar: vec![],
rpar: vec![],
})),
// Ex) `lambda x: y`
[param] => AssignTargetExpression::Name(Box::new(param.name.clone())),
// Ex) `lambda x, y: z`
params => AssignTargetExpression::Tuple(Box::new(Tuple {
elements: params
.iter()
.map(|param| Element::Simple {
value: Expression::Name(Box::new(param.name.clone())),
comma: None,
})
.collect(),
lpar: vec![],
rpar: vec![],
})),
};
if args.len() == 2 {
if func_body.params.params.iter().any(|f| f.default.is_some()) {
bail!("Currently not supporting default values");
// Parenthesize the iterator, if necessary, as in:
// ```python
// map(lambda x: x, y if y else z)
// ```
let iter = iter.clone();
let iter = if iter.lpar().is_empty()
&& iter.rpar().is_empty()
&& matches!(iter, Expression::IfExp(_) | Expression::Lambda(_))
{
iter.with_parens(LeftParen::default(), RightParen::default())
} else {
iter
};
let compfor = Box::new(CompFor {
target,
iter,
ifs: vec![],
inner_for_in: None,
asynchronous: None,
whitespace_before: space(),
whitespace_after_for: space(),
whitespace_before_in: space(),
whitespace_after_in: space(),
});
match object_type {
ObjectType::Generator => {
tree = Expression::GeneratorExp(Box::new(GeneratorExp {
elt: lambda.body.clone(),
for_in: compfor,
lpar: vec![LeftParen::default()],
rpar: vec![RightParen::default()],
}));
}
let mut args_str = func_body
.params
.params
.iter()
.map(|f| f.name.value)
.join(", ");
if args_str.is_empty() {
args_str = "_".to_string();
}
let compfor = Box::new(CompFor {
target: AssignTargetExpression::Name(Box::new(Name {
value: args_str.as_str(),
ObjectType::List => {
tree = Expression::ListComp(Box::new(ListComp {
elt: lambda.body.clone(),
for_in: compfor,
lbracket: LeftSquareBracket::default(),
rbracket: RightSquareBracket::default(),
lpar: vec![],
rpar: vec![],
})),
iter: args.last().unwrap().value.clone(),
ifs: vec![],
inner_for_in: None,
asynchronous: None,
whitespace_before: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")),
whitespace_after_for: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
" ",
)),
whitespace_before_in: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
" ",
)),
whitespace_after_in: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")),
});
match object_type {
ObjectType::Generator => {
tree = Expression::GeneratorExp(Box::new(GeneratorExp {
elt: func_body.body.clone(),
for_in: compfor,
lpar: vec![LeftParen::default()],
rpar: vec![RightParen::default()],
}));
}
ObjectType::List => {
tree = Expression::ListComp(Box::new(ListComp {
elt: func_body.body.clone(),
for_in: compfor,
lbracket: LeftSquareBracket::default(),
rbracket: RightSquareBracket::default(),
lpar: vec![],
rpar: vec![],
}));
}
ObjectType::Set => {
tree = Expression::SetComp(Box::new(SetComp {
elt: func_body.body.clone(),
for_in: compfor,
lpar: vec![],
rpar: vec![],
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
}));
}
ObjectType::Dict => {
let (key, value) = if let Expression::Tuple(tuple) = func_body.body.as_ref() {
if tuple.elements.len() != 2 {
bail!("Expected two elements")
}
let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else {
bail!("Expected tuple to contain a key as the first element");
};
let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else {
bail!("Expected tuple to contain a key as the second element");
};
(key, value)
} else {
bail!("Expected tuple for dict comprehension")
};
tree = Expression::DictComp(Box::new(DictComp {
for_in: compfor,
lpar: vec![],
rpar: vec![],
key: Box::new(key.clone()),
value: Box::new(value.clone()),
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(
SimpleWhitespace(" "),
),
}));
}
}));
}
let mut content = tree.codegen_stylist(stylist);
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if matches!(object_type, ObjectType::Set | ObjectType::Dict) {
if parent.is_some_and(Expr::is_formatted_value_expr) {
content = format!(" {content} ");
}
ObjectType::Set => {
tree = Expression::SetComp(Box::new(SetComp {
elt: lambda.body.clone(),
for_in: compfor,
lpar: vec![],
rpar: vec![],
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
}));
}
ObjectType::Dict => {
let elements = match lambda.body.as_ref() {
Expression::Tuple(tuple) => &tuple.elements,
Expression::List(list) => &list.elements,
_ => {
bail!("Expected tuple or list for dictionary comprehension")
}
};
let [key, value] = elements.as_slice() else {
bail!("Expected container to include two elements");
};
let Element::Simple { value: key, .. } = key else {
bail!("Expected container to use a key as the first element");
};
let Element::Simple { value, .. } = value else {
bail!("Expected container to use a value as the second element");
};
Ok(Edit::range_replacement(content, expr.range()))
} else {
bail!("Should have two arguments");
tree = Expression::DictComp(Box::new(DictComp {
for_in: compfor,
lpar: vec![],
rpar: vec![],
key: Box::new(key.clone()),
value: Box::new(value.clone()),
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(
SimpleWhitespace(" "),
),
}));
}
}
let mut content = tree.codegen_stylist(stylist);
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if matches!(object_type, ObjectType::Set | ObjectType::Dict) {
if parent.is_some_and(Expr::is_formatted_value_expr) {
content = format!(" {content} ");
}
}
Ok(Edit::range_replacement(content, expr.range()))
}
/// (C418) Convert `dict({"a": 1})` to `{"a": 1}`
pub(crate) fn fix_unnecessary_literal_within_dict_call(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
@@ -1036,9 +1105,9 @@ pub(crate) fn fix_unnecessary_literal_within_dict_call(
/// (C419) Convert `[i for i in a]` into `i for i in a`
pub(crate) fn fix_unnecessary_comprehension_any_all(
expr: &Expr,
locator: &Locator,
stylist: &Stylist,
expr: &Expr,
) -> Result<Fix> {
// Expr(ListComp) -> Expr(GeneratorExp)
let module_text = locator.slice(expr);

View File

@@ -85,9 +85,9 @@ pub(crate) fn unnecessary_call_around_sorted(
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
let edit = fixes::fix_unnecessary_call_around_sorted(
expr,
checker.locator(),
checker.stylist(),
expr,
)?;
if outer.id == "reversed" {
Ok(Fix::suggested(edit))

View File

@@ -88,7 +88,7 @@ pub(crate) fn unnecessary_collection_call(
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(checker, expr).map(Fix::suggested)
fixes::fix_unnecessary_collection_call(expr, checker).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -65,7 +65,7 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) {
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension(checker.locator(), checker.stylist(), expr)
fixes::fix_unnecessary_comprehension(expr, checker.locator(), checker.stylist())
.map(Fix::suggested)
});
}

View File

@@ -91,7 +91,7 @@ pub(crate) fn unnecessary_comprehension_any_all(
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, arg.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(checker.locator(), checker.stylist(), expr)
fixes::fix_unnecessary_comprehension_any_all(expr, checker.locator(), checker.stylist())
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -133,9 +133,9 @@ pub(crate) fn unnecessary_double_cast_or_process(
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_double_cast_or_process(
expr,
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});

View File

@@ -60,7 +60,7 @@ pub(crate) fn unnecessary_generator_dict(
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_dict(checker, expr).map(Fix::suggested)
fixes::fix_unnecessary_generator_dict(expr, checker).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -62,7 +62,7 @@ pub(crate) fn unnecessary_generator_list(
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_list(checker.locator(), checker.stylist(), expr)
fixes::fix_unnecessary_generator_list(expr, checker.locator(), checker.stylist())
.map(Fix::suggested)
});
}

View File

@@ -62,7 +62,7 @@ pub(crate) fn unnecessary_generator_set(
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_set(checker, expr).map(Fix::suggested)
fixes::fix_unnecessary_generator_set(expr, checker).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -58,7 +58,7 @@ pub(crate) fn unnecessary_list_call(
let mut diagnostic = Diagnostic::new(UnnecessaryListCall, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_call(checker.locator(), checker.stylist(), expr)
fixes::fix_unnecessary_list_call(expr, checker.locator(), checker.stylist())
.map(Fix::suggested)
});
}

View File

@@ -67,7 +67,7 @@ pub(crate) fn unnecessary_list_comprehension_dict(
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_dict(checker, expr).map(Fix::suggested)
fixes::fix_unnecessary_list_comprehension_dict(expr, checker).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -60,7 +60,7 @@ pub(crate) fn unnecessary_list_comprehension_set(
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_set(checker, expr).map(Fix::suggested)
fixes::fix_unnecessary_list_comprehension_set(expr, checker).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -82,7 +82,7 @@ pub(crate) fn unnecessary_literal_dict(
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_dict(checker, expr).map(Fix::suggested));
.try_set_fix(|| fixes::fix_unnecessary_literal_dict(expr, checker).map(Fix::suggested));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -77,7 +77,7 @@ pub(crate) fn unnecessary_literal_set(
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_set(checker, expr).map(Fix::suggested));
.try_set_fix(|| fixes::fix_unnecessary_literal_set(expr, checker).map(Fix::suggested));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -94,9 +94,9 @@ pub(crate) fn unnecessary_literal_within_dict_call(
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_dict_call(
expr,
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});

View File

@@ -96,9 +96,9 @@ pub(crate) fn unnecessary_literal_within_list_call(
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_list_call(
expr,
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});

View File

@@ -98,9 +98,9 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_tuple_call(
expr,
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});

View File

@@ -224,11 +224,11 @@ pub(crate) fn unnecessary_map(
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_map(
checker.locator(),
checker.stylist(),
expr,
parent,
object_type,
checker.locator(),
checker.stylist(),
)
.map(Fix::suggested)
});

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