Compare commits

...

88 Commits

Author SHA1 Message Date
Charlie Marsh
bbeda1a8ab Remove output-file and target-version from formatter CLI 2023-09-05 10:05:03 +01: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
Charlie Marsh
2f3a950f6f Bump version to 0.0.287 (#7038) 2023-09-01 17:32:26 +01:00
Charlie Marsh
dea65536e9 Fix placement for comments within f-strings concatenations (#7047)
## Summary

Restores the dangling comment handling for f-strings, which broke with
the parenthesized expression code.

Closes https://github.com/astral-sh/ruff/issues/6898.

## Test Plan

`cargo test`

No change in any of the similarity indexes or changed file counts:

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

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99957 | 2760 | 67 |
| transformers | 0.99927 | 2587 | 468 |
| twine | 0.99982 | 33 | 1 |
| typeshed | 0.99978 | 3496 | 2173 |
| warehouse | 0.99818 | 648 | 24 |
| zulip | 0.99942 | 1437 | 32 |
2023-09-01 16:27:32 +00:00
dependabot[bot]
fbc9b5a604 Bump cloudflare/wrangler-action from 3.1.0 to 3.1.1 (#7045) 2023-09-01 15:00:11 +00:00
Zanie Blue
253a241f5d Add dependabot for cargo dependencies (#7034)
Ideally we shouldn't have to run `cargo update` manually — it requires
us to remember to do so and groups all updates into a single pull
request making it challenging to determine which upgrade introduces
regressions e.g. #6964. Here we add daily checks for cargo dependency
updates.

This pull request also simplifies dependabot configuration for GitHub
Actions versions.
2023-09-01 09:47:40 -05:00
Sergey Chudov
33806b8b7c Fixed panic in missing_copyright_notice (#7029) 2023-09-01 13:58:48 +00:00
Charlie Marsh
afcd00da56 Create ruff_notebook crate (#7039)
## Summary

This PR moves `ruff/jupyter` into its own `ruff_notebook` crate. Beyond
the move itself, there were a few challenges:

1. `ruff_notebook` relies on the source map abstraction. I've moved the
source map into `ruff_diagnostics`, since it doesn't have any
dependencies on its own and is used alongside diagnostics.
2. `ruff_notebook` has a couple tests for end-to-end linting and
autofixing. I had to leave these tests in `ruff` itself.
3. We had code in `ruff/jupyter` that relied on Python lexing, in order
to provide a more targeted error message in the event that a user saves
a `.py` file with a `.ipynb` extension. I removed this in order to avoid
a dependency on the parser, it felt like it wasn't worth retaining just
for that dependency.

## Test Plan

`cargo test`
2023-09-01 13:56:44 +00:00
Charlie Marsh
08e246764f Refactor ruff_cli's run method to return on each branch (#7040)
## Summary

I think the fallthrough here for some branches is a little confusing.
Now each branch either runs a command that returns `Result<ExitStatus>`,
or runs a command that returns `Result<()>` and then explicitly returns
`Ok(ExitStatus::SUCCESS)`.
2023-09-01 14:15:38 +01:00
Chris Pryer
0489bbc54c Match Black's formatting of trailing comments containing NBSP (#7030) 2023-09-01 14:52:59 +02:00
Charlie Marsh
60132da7bb Add a NotebookError type to avoid returning Diagnostics on error (#7035)
## Summary

This PR refactors the error-handling cases around Jupyter notebooks to
use errors rather than `Box<Diagnostics>`, which creates some oddities
in the downstream handling. So, instead of formatting errors as
diagnostics _eagerly_ (in the notebook methods), we now return errors
and convert those errors to diagnostics at the last possible moment (in
`diagnostics.rs`). This is more ergonomic, as errors can be composed and
reported-on in different ways, whereas diagnostics require a `Printer`,
etc.

See, e.g.,
https://github.com/astral-sh/ruff/pull/7013#discussion_r1311136301.

## Test Plan

Ran `cargo run` over a Python file labeled with a `.ipynb` suffix, and
saw:

```
foo.ipynb:1:1: E999 SyntaxError: Expected a Jupyter Notebook, which must be internally stored as JSON, but found a Python source file: expected value at line 1 column 1
```
2023-09-01 11:08:05 +00:00
Chris Pryer
17a44c0078 Exclude pragma comments from measured line width (#7008)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-09-01 06:34:51 +00:00
Charlie Marsh
376d3caf47 Treat empty-line separated comments as trailing statement comments (#6999)
## Summary

This PR modifies our between-statement comment handling such that
comments that are not separated by a statement by any newlines continue
to be treated as leading comments on the statement, but comments that
_are_ separated are instead formatted as trailing comments on the
preceding statement.

See, e.g., the originating snippet:

```python
DEFAULT_TEMPLATE = "flatpages/default.html"

# This view is called from FlatpageFallbackMiddleware.process_response
# when a 404 is raised, which often means CsrfViewMiddleware.process_view
# has not been called even if CsrfViewMiddleware is installed. So we need
# to use @csrf_protect, in case the template needs {% csrf_token %}.
# However, we can't just wrap this view; if no matching flatpage exists,
# or a redirect is required for authentication, the 404 needs to be returned
# without any CSRF checks. Therefore, we only
# CSRF protect the internal implementation.


def flatpage(request, url):
    pass
```

Here, we need to ensure that the `def flatpage` is precede by two empty
lines. However, we want those two empty lines to be enforced from the
_end_ of the comment block, _unless_ the comments are directly atop the
`def flatpage`.

I played with this a bit, and I think the simplest conceptual model and
implementation is to instead treat those as trailing comments on the
preceding node. The main difficulty with this approach is that, in order
to be fully compatible with Black, we'd sometimes need to insert
newlines _between_ the preceding node and its trailing comments. See,
e.g.:

```python
def func():
    ...
# comment

x = 1
```

In this case, we'd need to insert two blank lines between `def func():
...` and `# comment`, but `# comment` is trailing comment on `def
func(): ...`. So, we'd need to take this case into account in the
various nodes that _require_ newlines after them: functions, classes,
and imports. After some discussion, we've opted _not_ to support this,
and just treat these as trailing comments -- so we won't insert newlines
there. This means our handling is still identical to Black's on
Black-formatted code, but avoids moving such trailing comments on
unformatted code.

I dislike that the empty handling is so complex, and that it's split
between so many different nodes, but this is really tricky. Continuing
to treat these as leading comments is very difficult too, since we'd
need to do similar tricks for the leading comment handling in those
nodes, and influencing leading comments is even harder, since they're
all formatted _before_ the node itself.

Closes https://github.com/astral-sh/ruff/issues/6761.

## Test Plan

`cargo test`

Surprisingly, it doesn't change the similarity at all (apart from a
0.00001 change in CPython), but I manually confirmed that it did fix the
originating issue in Django.

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.76082          |
| django       | 0.99921          |
| transformers | 0.99854          |
| twine        | 0.99982          |
| typeshed     | 0.99953          |
| warehouse    | 0.99648          |
| zulip        | 0.99928          |


After:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.76081          |
| django       | 0.99921          |
| transformers | 0.99854          |
| twine        | 0.99982          |
| typeshed     | 0.99953          |
| warehouse    | 0.99648          |
| zulip        | 0.99928          |
2023-08-31 20:55:05 +00:00
Charlie Marsh
51d69b448c Improve compatibility between multi-statement PYI rules (#7024)
## Summary

This PR modifies a few of our rules related to which statements (and how
many) are allowed in function bodies within `.pyi` files, to improve
compatibility with flake8-pyi and improve the interplay dynamics between
them. Each change fixes a deviation from flake8-pyi:

- We now always trigger the multi-statement rule (PYI048) regardless of
whether one of the statements is a docstring.
- We no longer trigger the `...` rule (PYI010) if the single statement
is a docstring or a `pass` (since those are covered by other rules).
- We no longer trigger the `...` rule (PYI010) if the function body
contains multiple statements (since that's covered by PYI048).

Closes https://github.com/astral-sh/ruff/issues/7021.

## Test Plan

`cargo test`
2023-08-31 21:45:26 +01:00
Nicholas Grisafi
f7dca3d958 Add Rippling to who uses ruff (#7032)
## Summary

Adding rippling to who uses ruff section

## Test Plan

We migrated to ruff 🙂
2023-08-31 18:36:48 +00:00
Charlie Marsh
7c1aa98f43 Run cargo update (#6964) 2023-08-31 13:11:11 -05:00
Charlie Marsh
68f605e80a Fix WithItem ranges for parenthesized, non-as items (#6782)
## Summary

This PR attempts to address a problem in the parser related to the
range's of `WithItem` nodes in certain contexts -- specifically,
`WithItem` nodes in parentheses that do not have an `as` token after
them.

For example,
[here](https://play.ruff.rs/71be2d0b-2a04-4c7e-9082-e72bff152679):

```python
with (a, b):
    pass
```

The range of the `WithItem` `a` is set to the range of `(a, b)`, as is
the range of the `WithItem` `b`. In other words, when we have this kind
of sequence, we use the range of the entire parenthesized context,
rather than the ranges of the items themselves.

Note that this also applies to cases
[like](https://play.ruff.rs/c551e8e9-c3db-4b74-8cc6-7c4e3bf3713a):

```python
with (a, b, c as d):
    pass
```

You can see the issue in the parser here:

```rust
#[inline]
WithItemsNoAs: Vec<ast::WithItem> = {
    <location:@L> <all:OneOrMore<Test<"all">>> <end_location:@R> => {
        all.into_iter().map(|context_expr| ast::WithItem { context_expr, optional_vars: None, range: (location..end_location).into() }).collect()
    },
}
```

Fixing this issue is... very tricky. The naive approach is to use the
range of the `context_expr` as the range for the `WithItem`, but that
range will be incorrect when the `context_expr` is itself parenthesized.
For example, _that_ solution would fail here, since the range of the
first `WithItem` would be that of `a`, rather than `(a)`:

```python
with ((a), b):
    pass
```

The `with` parsing in general is highly precarious due to ambiguities in
the grammar. Changing it in _any_ way seems to lead to an ambiguous
grammar that LALRPOP fails to translate. Consensus seems to be that we
don't really understand _why_ the current grammar works (i.e., _how_ it
avoids these ambiguities as-is).

The solution implemented here is to avoid changing the grammar itself,
and instead change the shape of the nodes returned by various rules in
the grammar. Specifically, everywhere that we return `Expr`, we instead
return `ParenthesizedExpr`, which includes a parenthesized range and the
underlying `Expr` itself. (If an `Expr` isn't parenthesized, the ranges
will be equivalent.) In `WithItemsNoAs`, we can then use the
parenthesized range as the range for the `WithItem`.
2023-08-31 16:21:29 +01:00
Zanie Blue
96a9717c1a Add hidden --preview / --no-preview options to ruff check (#7009)
Per discussion at https://github.com/astral-sh/ruff/discussions/6998

<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

Adds a `--preview` and `--no-preview` option to the CLI for `ruff check`
and corresponding settings. The CLI options are hidden for now.

Available in the settings as `preview = true` or `preview = false`.

Does not include environment variable configuration, although we may add
it in the future.

## Test Plan

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

`cargo build`

Future work will build on this setting, such as toggling the mode during
a test.
2023-08-31 09:51:59 -05:00
magic-akari
f4ba0ea144 Allow tab_width to be configable (#7016) 2023-08-31 07:40:03 +00:00
Micha Reiser
92143afeee Group binary operators with same precedence only (#7010) 2023-08-31 09:19:45 +02:00
Micha Reiser
eb552da8a9 Avoid parenthesizing multiline strings in binary expressions (#6973) 2023-08-30 16:03:17 +02:00
Charlie Marsh
e2b2b1759f Handle keyword comments between = and value (#6883)
## Summary

This PR adds comment handling for comments between the `=` and the
`value` for keywords, as in the following cases:

```python
func(
    x  # dangling
    =  # dangling
    # dangling
    1,
    **  # dangling
    y
)
```

(Comments after the `**` were already handled in some cases, but I've
unified the handling with the `=` handling.)

Note that, previously, comments between the `**` and its value were
rendered as trailing comments on the value (so they'd appear after `y`).
This struck me as odd since it effectively re-ordered the comment with
respect to its closest AST node (the value). I've made them leading
comments, though I don't know that that's a significant improvement. I
could also imagine us leaving them where they are.
2023-08-30 09:52:51 -04:00
Chris Pryer
a3f4d7745a Use reserved width to include line suffix measurement (#6901)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-08-30 08:07:11 +00:00
Micha Reiser
edfd888bd6 Add unicode benchmark (#7002) 2023-08-30 09:57:57 +02:00
Micha Reiser
9c382e8291 Show changed files (#7003) 2023-08-30 06:47:45 +00:00
Charlie Marsh
eb2b226142 Unset after_class_docstring state on every iteration (#7001) 2023-08-30 08:20:28 +02:00
Victor Hugo Gomes
31947af6a3 Don't "flatten" nested if expressions when formatting (#6996) 2023-08-30 04:11:58 +00:00
Charlie Marsh
b404e54f33 Remove unnecessary Comment#slice calls (#6997) 2023-08-30 00:44:11 +00:00
qdegraaf
34e8de738e [perflint] Expand PERF401 and PERF402 with type checks (#6994)
## Summary

Attempt at a small improvement to two `perflint` rules using the new
type inference capabilities to only flag `PERF401` and `PERF402` for
values we infer to be lists. This makes the rule more conservative, as
it only flags values that we _know_ to be lists, but it's overall a
desirable change, as it favors false negatives over false positives for
a "nice-to-have" rule.

Closes https://github.com/astral-sh/ruff/issues/6995.

## Test Plan

Add non-list value cases and make sure all old cases are still caught.
2023-08-29 19:15:29 -04:00
qdegraaf
1550a6bfe7 [perflint] Chore: correct PERF401 fixture comments (#6993)
## Summary
Fixes comments in PERF401 fixture to correctly reflect how rule works
2023-08-29 17:16:31 -04:00
Anselm Hahn
dc4db39f78 docs: 📝 Updating the example for assert(S101) (#6986)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

Rewriting the `if`-comparison to focus on the meaning of rule ` assert
S101`.

Fixes #6984

## Test Plan

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

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
2023-08-29 16:08:08 -05:00
qdegraaf
f3aaf84a28 Move refurb/helpers utils to ruff_python_semantic for broader use (#6990)
## Summary

The utils added for `refurb` in its `helpers.rs` file could be useful
for many other plugins. (Such as the PERF4XX codes, see e.g.
https://github.com/astral-sh/ruff/pull/6132 ).

This PR moves them to `ruff_python_semantic::analyzers::typing` as
suggested in
https://github.com/astral-sh/ruff/pull/6132#issuecomment-1697910093

## Test Plan

Confirmed `refurb` and all other tests still work
2023-08-29 14:45:09 -04:00
Charlie Marsh
5de95d7054 Reuse FormatResult and FormatterIterationError in format_stdin.rs (#6985)
## Summary

Ensures that we use the same error types and messages. Also renames
those struct to `FormatCommand*` for consistency, and removes the
`FormatCommandResult::Skipped` variant in favor of skipping in the
iterator directly.
2023-08-29 17:41:53 +00:00
Charlie Marsh
8d1610d960 Implement Display on formatter structs (#6983)
Feedback from
https://github.com/astral-sh/ruff/pull/6948#discussion_r1308260021.
2023-08-29 16:57:26 +00:00
Charlie Marsh
fad23bbe60 Add a --check flag to the formatter CLI (#6982)
## Summary

Returns an exit code of 1 if any files would be reformatted:

```
ruff on  charlie/format-check:main [$?⇡] is 📦 v0.0.286 via 🐍 v3.11.2 via 🦀 v1.72.0
❯ cargo run -p ruff_cli -- format foo.py --check
   Compiling ruff_cli v0.0.286 (/Users/crmarsh/workspace/ruff/crates/ruff_cli)
    Finished dev [unoptimized + debuginfo] target(s) in 1.69s
     Running `target/debug/ruff format foo.py --check`
warning: `ruff format` is a work-in-progress, subject to change at any time, and intended only for experimentation.
1 file would be reformatted
ruff on  charlie/format-check:main [$?⇡] is 📦 v0.0.286 via 🐍 v3.11.2 via 🦀 v1.72.0 took 2s
❯ echo $?
1
```

Closes #6966.
2023-08-29 12:40:00 -04:00
Charlie Marsh
25c374856a Move stdin formatting to its own command file (#6981)
## Summary

This is similar to `commands::check` vs. `commands::check_stdin`, and
gets the logic out of the parent file (`lib.rs`). It also ensures that
we avoid formatting files that should be excluded when `--force-exclude`
is provided.
2023-08-29 16:06:10 +00:00
Charlie Marsh
34221346c1 Rename run.rs command to check.rs (#6980)
The CLI command is called "check", so this is more consistent (and
consistent with the pattern used in other commands).
2023-08-29 15:52:06 +00:00
Chris Pryer
924f10186f Update dynamic_text builder doc comment (#6978) 2023-08-29 16:29:11 +02:00
Charlie Marsh
b7634b6ede Fix typo in banned-from (#6977)
Oops...
2023-08-29 09:39:09 -04:00
Dhruv Manilawala
4d49d5e845 Add eat_char2 for the lexer (#6968)
## Summary

This PR adds a new helper method on the `Cursor` called `eat_char2`
which is similar to `eat_char` but accepts 2 characters instead of 1. It'll
`bump` the cursor twice if both characters are found on lookahead.

## Test Plan

`cargo test`
2023-08-29 17:18:02 +05:30
Micha Reiser
715d86dae9 Remove Comprehension priority (#6947) 2023-08-29 08:30:15 +02:00
Micha Reiser
adb48692d6 Use optional parentheses for tuples in return statements (#6875) 2023-08-29 08:30:05 +02:00
Charlie Marsh
19ccf1d073 Make RUF100 a suggested fix (#6967)
I made this automatic when I removed the deprecated "unspecified"
method, but I think suggested is actually more appropriate.
2023-08-29 04:28:52 +00:00
Charlie Marsh
e3c36465ec Base parameter-find lookup on range, rather than name (#6960)
## Summary

This simplifies the signature a bit and is more reliable in the
(unusual? invalid?) case of duplicate parameters.
2023-08-29 01:39:38 +00:00
Valeriy Savchenko
7e36284684 [refurb] Implement check-and-remove-from-set rule (FURB132) (#6904)
## Summary

This PR is a continuation of #6897 and #6702 and me replicating `refurb`
rules (#1348). It adds support for
[FURB132](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/set_discard.py)

## Test Plan

I included a new test + checked that all other tests pass.
2023-08-29 01:17:26 +00:00
Valeriy Savchenko
c448b4086a [refurb] Implement delete-full-slice rule (FURB131) (#6897)
## Summary

This PR is a continuation of #6702 and me replicating `refurb` rules
(#1348). It adds support for
[FURB131](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/no_del.py.

## Test Plan

I included a new test + checked that all other tests pass.
2023-08-28 20:52:38 -04:00
Charlie Marsh
3200015c06 Fix banned-from documentation (#6959)
Closes https://github.com/astral-sh/ruff/issues/6839.
2023-08-29 00:51:15 +00:00
Valeriy Savchenko
26d53c56a2 [refurb] Implement repeated-append rule (FURB113) (#6702)
## Summary

As an initial effort with replicating `refurb` rules (#1348 ), this PR
adds support for
[FURB113](https://github.com/dosisod/refurb/blob/master/refurb/checks/builtin/list_extend.py)
and adds a new category of checks.

## Test Plan

I included a new test + checked that all other tests pass.
2023-08-28 22:51:59 +00:00
Charlie Marsh
1439bb592e Report number of changed files on the format CLI (#6948)
## Summary

Very basic summary:

<img width="962" alt="Screen Shot 2023-08-28 at 1 17 37 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/53537aca-7579-44d8-855b-f4553affae50">

If you run with `--verbose`, we'll also show you the timing:

<img width="962" alt="Screen Shot 2023-08-28 at 1 17 58 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/63cbd13e-9462-4e49-b3a3-c6663a7ad41c">
2023-08-28 18:42:31 -04:00
Charlie Marsh
fc47e0dab2 Clean up some misc. code in NamedTuple and TypedDict conversion (#6957)
- Use `Option` instead of `Result` everywhere.
- Use `field` instead of `property` (to match the nomenclature of
`NamedTuple` and `TypedDict`).
- Put the violation function at the top of the file, rather than the
bottom.
2023-08-28 17:12:56 -04:00
Charlie Marsh
87aa5d6b66 Avoid panic when typename is provided as a keyword argument (#6955)
## Summary

The `typename` argument to `NamedTuple` and `TypedDict` is a required
positional argument. We assumed as much, but panicked if it was provided
as a keyword argument or otherwise omitted. This PR handles the case
gracefully.

Closes https://github.com/astral-sh/ruff/issues/6953.
2023-08-28 21:03:18 +00:00
Charlie Marsh
d1ad20c9ea Avoid invalid fix for C417 with separate keys and values (#6954)
Closes https://github.com/astral-sh/ruff/issues/6951.
2023-08-28 16:49:40 -04:00
Charlie Marsh
ecca125f9a Use SourceCodeSnippet for more diagnostic messages (#6950)
## Summary

This ensures that we truncate the messages when they break over multiple
lines, etc.
2023-08-28 16:22:49 -04:00
Charlie Marsh
005e21a139 Add line-length to E501 documentation (#6949)
Closes https://github.com/astral-sh/ruff/issues/6908.
2023-08-28 14:45:30 -04:00
dependabot[bot]
e1db036f90 ci(deps): bump tj-actions/changed-files from 37 to 38 (#6946)
Co-authored-by: jackton1 <a
Co-authored-by: repo-ranger[bot] <!-- raw HTML omitted --> (<a
Co-authored-by: repo-ranger[bot] <!-- raw HTML omitted -->
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-28 16:50:38 +00:00
Zanie Blue
9ad67b0758 Add comment to ecoystem check re. ALL rules (#6943) 2023-08-28 11:28:36 -05:00
Jelle van der Waa
af61abc747 Re-use is_magic where possible (#6945)
## Summary

Use `is_magic` where possible

## Test Plan

Unit tests
2023-08-28 15:35:34 +00:00
Charlie Marsh
ec575188c4 Narrow the supported options on the format CLI (#6944)
## Summary

Ensures that we only show supported options:

<img width="1228" alt="Screen Shot 2023-08-28 at 11 03 16 AM"
src="https://github.com/astral-sh/ruff/assets/1309177/50fb7595-dc30-43d2-a7e4-c0103acc15b9">

For now, I'm not super focused on DRYing up the CLI.
2023-08-28 15:28:22 +00:00
Charlie Marsh
aea7500c1e Allow Locator#slice to take Ranged (#6922)
## Summary

As a small quality-of-life improvement, the locator can now slice like
`locator.slice(stmt)` instead of requiring
`locator.slice(stmt.range())`.

## Test Plan

`cargo test`
2023-08-28 11:08:39 -04:00
Charlie Marsh
58f5f27dc3 Add TOML files to SourceType (#6929)
## Summary

This PR adds a higher-level enum (`SourceType`) around `PySourceType` to
allow us to use the same detection path to handle TOML files. Right now,
we have ad hoc `is_pyproject_toml` checks littered around, and some
codepaths are omitting that logic altogether (like `add_noqa`). Instead,
we should always be required to check the source type and handle TOML
files as appropriate.

This PR will also help with our pre-commit capabilities. If we add
`toml` to pre-commit (to support `pyproject.toml`), pre-commit will
start to pass _other_ files to Ruff (along with `poetry.lock` and
`Pipfile` -- see
[identify](b59996304f/identify/extensions.py (L355))).
By detecting those files and handling those cases, we avoid attempting
to parse them as Python files, which would lead to pre-commit errors.
(We tried to add `toml` to pre-commit here
(https://github.com/astral-sh/ruff-pre-commit/pull/44), but had to
revert here (https://github.com/astral-sh/ruff-pre-commit/pull/45) as it
led to the pre-commit hook attempting to parse `poetry.lock` files as
Python files.)
2023-08-28 15:01:48 +00:00
378 changed files with 20901 additions and 15094 deletions

View File

@@ -4,8 +4,10 @@ updates:
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "12:00"
timezone: "America/New_York"
commit-message:
prefix: "ci(deps)"
labels: ["internal"]
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "daily"
labels: ["internal"]

View File

@@ -30,7 +30,7 @@ jobs:
with:
fetch-depth: 0
- uses: tj-actions/changed-files@v37
- uses: tj-actions/changed-files@v38
id: changed
with:
files_yaml: |

View File

@@ -40,7 +40,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.1.0
uses: cloudflare/wrangler-action@v3.1.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -40,7 +40,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.1.0
uses: cloudflare/wrangler-action@v3.1.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -129,6 +129,7 @@ At time of writing, the repository includes the following crates:
intermediate representation. The backend for `ruff_python_formatter`.
- `crates/ruff_index`: library crate inspired by `rustc_index`.
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_notebook`: library crate for parsing and manipulating Jupyter notebooks.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an

589
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -54,7 +54,7 @@ uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "
wsl = { version = "0.1.0" }
# v1.0.1
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "3cacca1a1029f05707e50703b49fe3dd860aa839", default-features = false }
libcst = { git = "https://github.com/Instagram/LibCST.git", rev = "9c263aa8977962a870ce2770d2aa18ee0dacb344", 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.286
rev: v0.0.287
hooks:
- id: ruff
```
@@ -398,6 +398,7 @@ Ruff is used by a number of major open-source projects and companies, including:
- [Pydantic](https://github.com/pydantic/pydantic)
- [Pylint](https://github.com/PyCQA/pylint)
- [Reflex](https://github.com/reflex-dev/reflex)
- [Rippling](https://rippling.com)
- [Robyn](https://github.com/sansyrox/robyn)
- Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client))
- Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli))

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.286"
version = "0.0.287"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -18,6 +18,7 @@ name = "ruff"
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_index = { path = "../ruff_index" }
ruff_notebook = { path = "../ruff_notebook" }
ruff_macros = { path = "../ruff_macros" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
ruff_python_codegen = { path = "../ruff_python_codegen" }
@@ -64,17 +65,15 @@ schemars = { workspace = true, optional = true }
semver = { version = "1.0.16" }
serde = { workspace = true }
serde_json = { workspace = true }
serde_with = { version = "3.0.0" }
similar = { workspace = true }
smallvec = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
thiserror = { version = "1.0.43" }
thiserror = { workspace = true }
toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { workspace = true }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
[dev-dependencies]

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,5 @@ except AttributeError:
pass
except (ImportError, TypeError):
pass
except (*retriable_exceptions,):
pass

View File

@@ -5,6 +5,7 @@ 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)))
@@ -37,3 +38,6 @@ map(lambda x: lambda x: x, range(4))
map(lambda x=1: x, nums)
map(lambda *args: len(args), range(4))
map(lambda **kwargs: len(kwargs), range(4))
# Ok because multiple arguments are allowed.
dict(map(lambda k, v: (k, v), keys, values))

View File

@@ -3,16 +3,20 @@ def bar():
def foo():
"""foo""" # OK
"""foo""" # OK, docstrings are handled by another rule
def buzz():
print("buzz") # OK, not in stub file
print("buzz") # ERROR PYI010
def foo2():
123 # OK, not in a stub file
123 # ERROR PYI010
def bizz():
x = 123 # OK, not in a stub file
x = 123 # ERROR PYI010
def foo3():
pass # OK, pass is handled by another rule

View File

@@ -1,6 +1,6 @@
def bar(): ... # OK
def foo():
"""foo""" # OK, strings are handled by another rule
"""foo""" # OK, docstrings are handled by another rule
def buzz():
print("buzz") # ERROR PYI010
@@ -10,3 +10,6 @@ def foo2():
def bizz():
x = 123 # ERROR PYI010
def foo3():
pass # OK, pass is handled by another rule

View File

@@ -1,19 +1,27 @@
def bar(): # OK
...
def bar():
... # OK
def oof(): # OK, docstrings are handled by another rule
def bar():
pass # OK
def bar():
"""oof""" # OK
def oof(): # ERROR PYI048
"""oof"""
print("foo")
def foo(): # Ok not in Stub file
def foo(): # ERROR PYI048
"""foo"""
print("foo")
print("foo")
def buzz(): # Ok not in Stub file
def buzz(): # ERROR PYI048
print("fizz")
print("buzz")
print("test")

View File

@@ -1,20 +1,20 @@
def bar(): ... # OK
def bar():
... # OK
pass # OK
def bar():
"""oof""" # OK
def oof(): # OK, docstrings are handled by another rule
"""oof"""
print("foo")
def oof(): # ERROR PYI048
"""oof"""
print("foo")
def foo(): # ERROR PYI048
def foo(): # ERROR PYI048
"""foo"""
print("foo")
print("foo")
def buzz(): # ERROR PYI048
def buzz(): # ERROR PYI048
print("fizz")
print("buzz")
print("test")

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

@@ -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

@@ -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

@@ -18,11 +18,11 @@ def f():
result = []
for i in items:
if i % 2:
result.append(i) # PERF401
result.append(i) # Ok
elif i % 2:
result.append(i) # PERF401
result.append(i)
else:
result.append(i) # PERF401
result.append(i)
def f():
@@ -60,3 +60,15 @@ def f():
for i in range(20):
foo.fibonacci.append(sum(foo.fibonacci[-2:])) # OK
print(foo.fibonacci)
class Foo:
def append(self, x):
pass
def f():
items = [1, 2, 3, 4]
result = Foo()
for i in items:
result.append(i) # Ok

View File

@@ -24,3 +24,22 @@ def f():
result = {}
for i in items:
result[i].append(i * i) # OK
class Foo:
def append(self, x):
pass
def f():
items = [1, 2, 3, 4]
result = Foo()
for i in items:
result.append(i) # OK
def f():
import sys
for path in ("foo", "bar"):
sys.path.append(path) # OK

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

@@ -91,3 +91,7 @@ def f(x: Optional[int : float]) -> None:
def f(x: Optional[str, int : float]) -> None:
...
def f(x: Optional[int, float]) -> None:
...

View File

@@ -22,3 +22,4 @@ MyType = typing.NamedTuple("MyType", a=int, b=tuple[str, ...])
# unfixable
MyType = typing.NamedTuple("MyType", [("a", int)], [("b", str)])
MyType = typing.NamedTuple("MyType", [("a", int)], b=str)
MyType = typing.NamedTuple(typename="MyType", a=int, b=str)

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,6 +35,13 @@ if output:
encoding="utf-8",
)
output = subprocess.run(
["foo"], stdout=subprocess.PIPE, capture_output=True, stderr=subprocess.PIPE
)
output = subprocess.run(
["foo"], stdout=subprocess.PIPE, capture_output=False, stderr=subprocess.PIPE
)
# Examples that should NOT trigger the rule
from foo import PIPE

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

@@ -0,0 +1,169 @@
from typing import List
def func():
pass
nums = []
nums2 = []
nums3: list[int] = func()
nums4: List[int]
class C:
def append(self, x):
pass
# Errors.
# FURB113
nums.append(1)
nums.append(2)
pass
# FURB113
nums3.append(1)
nums3.append(2)
pass
# FURB113
nums4.append(1)
nums4.append(2)
pass
# FURB113
nums.append(1)
nums2.append(1)
nums.append(2)
nums.append(3)
pass
# FURB113
nums.append(1)
nums2.append(1)
nums.append(2)
# FURB113
nums3.append(1)
nums.append(3)
# FURB113
nums4.append(1)
nums4.append(2)
nums3.append(2)
pass
# FURB113
nums.append(1)
nums.append(2)
nums.append(3)
if True:
# FURB113
nums.append(1)
nums.append(2)
if True:
# FURB113
nums.append(1)
nums.append(2)
pass
if True:
# FURB113
nums.append(1)
nums2.append(1)
nums.append(2)
nums.append(3)
def yes_one(x: list[int]):
# FURB113
x.append(1)
x.append(2)
def yes_two(x: List[int]):
# FURB113
x.append(1)
x.append(2)
def yes_three(*, x: list[int]):
# FURB113
x.append(1)
x.append(2)
def yes_four(x: list[int], /):
# FURB113
x.append(1)
x.append(2)
def yes_five(x: list[int], y: list[int]):
# FURB113
x.append(1)
x.append(2)
y.append(1)
x.append(3)
def yes_six(x: list):
# FURB113
x.append(1)
x.append(2)
# Non-errors.
nums.append(1)
pass
nums.append(2)
if True:
nums.append(1)
pass
nums.append(2)
nums.append(1)
pass
nums.append(1)
nums2.append(2)
nums.copy()
nums.copy()
c = C()
c.append(1)
c.append(2)
def not_one(x):
x.append(1)
x.append(2)
def not_two(x: C):
x.append(1)
x.append(2)
# redefining a list variable with a new type shouldn't confuse ruff.
nums2 = C()
nums2.append(1)
nums2.append(2)

View File

@@ -0,0 +1,64 @@
from typing import Dict, List
names = {"key": "value"}
nums = [1, 2, 3]
x = 42
y = "hello"
# these should match
# FURB131
del nums[:]
# FURB131
del names[:]
# FURB131
del x, nums[:]
# FURB131
del y, names[:], x
def yes_one(x: list[int]):
# FURB131
del x[:]
def yes_two(x: dict[int, str]):
# FURB131
del x[:]
def yes_three(x: List[int]):
# FURB131
del x[:]
def yes_four(x: Dict[int, str]):
# FURB131
del x[:]
# these should not
del names["key"]
del nums[0]
x = 1
del x
del nums[1:2]
del nums[:2]
del nums[1:]
del nums[::2]
def no_one(param):
del param[:]

View File

@@ -0,0 +1,80 @@
from typing import Set
from some.package.name import foo, bar
s = set()
s2 = {}
s3: set[int] = foo()
# these should match
# FURB132
if "x" in s:
s.remove("x")
# FURB132
if "x" in s2:
s2.remove("x")
# FURB132
if "x" in s3:
s3.remove("x")
var = "y"
# FURB132
if var in s:
s.remove(var)
if f"{var}:{var}" in s:
s.remove(f"{var}:{var}")
def identity(x):
return x
# TODO: FURB132
if identity("x") in s2:
s2.remove(identity("x"))
# these should not
if "x" in s:
s.remove("y")
s.discard("x")
s2 = set()
if "x" in s:
s2.remove("x")
if "x" in s:
s.remove("x")
print("removed item")
if bar() in s:
# bar() might have a side effect
s.remove(bar())
if "x" in s:
s.remove("x")
else:
print("not found")
class Container:
def remove(self, item) -> None:
return
def __contains__(self, other) -> bool:
return True
c = Container()
if "x" in c:
c.remove("x")

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

@@ -8,7 +8,6 @@ use libcst_native::{
use ruff_python_ast::Stmt;
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_statement;
@@ -39,7 +38,7 @@ pub(crate) fn remove_imports<'a>(
locator: &Locator,
stylist: &Stylist,
) -> Result<Option<String>> {
let module_text = locator.slice(stmt.range());
let module_text = locator.slice(stmt);
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {
@@ -118,7 +117,7 @@ pub(crate) fn retain_imports(
locator: &Locator,
stylist: &Stylist,
) -> Result<String> {
let module_text = locator.slice(stmt.range());
let module_text = locator.slice(stmt);
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {

View File

@@ -4,17 +4,15 @@ use std::collections::BTreeSet;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use rustc_hash::{FxHashMap, FxHashSet};
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel, SourceMap};
use ruff_source_file::Locator;
use crate::autofix::source_map::SourceMap;
use crate::linter::FixTable;
use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod snippet;
pub(crate) mod source_map;
pub(crate) struct FixResult {
/// The resulting source code, after applying all fixes.
@@ -140,10 +138,9 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
mod tests {
use ruff_text_size::{Ranged, TextSize};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_diagnostics::{Diagnostic, Edit, Fix, SourceMarker};
use ruff_source_file::Locator;
use crate::autofix::source_map::SourceMarker;
use crate::autofix::{apply_fixes, FixResult};
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
@@ -207,14 +204,8 @@ print("hello world")
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 10.into(),
dest: 10.into(),
},
SourceMarker {
source: 10.into(),
dest: 21.into(),
},
SourceMarker::new(10.into(), 10.into(),),
SourceMarker::new(10.into(), 21.into(),),
]
);
}
@@ -250,14 +241,8 @@ class A(Bar):
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 8.into(),
dest: 8.into(),
},
SourceMarker {
source: 14.into(),
dest: 11.into(),
},
SourceMarker::new(8.into(), 8.into(),),
SourceMarker::new(14.into(), 11.into(),),
]
);
}
@@ -289,14 +274,8 @@ class A:
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 7.into(),
dest: 7.into()
},
SourceMarker {
source: 15.into(),
dest: 7.into()
}
SourceMarker::new(7.into(), 7.into()),
SourceMarker::new(15.into(), 7.into()),
]
);
}
@@ -332,22 +311,10 @@ class A(object):
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 8.into(),
dest: 8.into()
},
SourceMarker {
source: 16.into(),
dest: 8.into()
},
SourceMarker {
source: 22.into(),
dest: 14.into(),
},
SourceMarker {
source: 30.into(),
dest: 14.into(),
}
SourceMarker::new(8.into(), 8.into()),
SourceMarker::new(16.into(), 8.into()),
SourceMarker::new(22.into(), 14.into(),),
SourceMarker::new(30.into(), 14.into(),),
]
);
}
@@ -382,14 +349,8 @@ class A:
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 7.into(),
dest: 7.into(),
},
SourceMarker {
source: 15.into(),
dest: 7.into(),
}
SourceMarker::new(7.into(), 7.into(),),
SourceMarker::new(15.into(), 7.into(),),
]
);
}

View File

@@ -9,6 +9,10 @@ impl SourceCodeSnippet {
Self(source_code)
}
pub(crate) fn from_str(source_code: &str) -> Self {
Self(source_code.to_string())
}
/// Return the full snippet for user-facing display, or `None` if the snippet should be
/// truncated.
pub(crate) fn full_display(&self) -> Option<&str> {

View File

@@ -163,9 +163,9 @@ pub(crate) fn definitions(checker: &mut Checker) {
continue;
};
let contents = checker.locator.slice(expr.range());
let contents = checker.locator().slice(expr);
let indentation = checker.locator.slice(TextRange::new(
let indentation = checker.locator().slice(TextRange::new(
checker.locator.line_start(expr.start()),
expr.start(),
));

View File

@@ -1039,12 +1039,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::PrintfStringFormatting) {
pyupgrade::rules::printf_string_formatting(
checker,
expr,
right,
checker.locator,
);
pyupgrade::rules::printf_string_formatting(checker, expr, right);
}
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::percent(checker, expr);
@@ -1258,14 +1253,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);

View File

@@ -13,7 +13,7 @@ use crate::rules::{
flake8_django, flake8_errmsg, flake8_import_conventions, flake8_pie, flake8_pyi,
flake8_pytest_style, flake8_raise, flake8_return, flake8_simplify, flake8_slots,
flake8_tidy_imports, flake8_type_checking, mccabe, pandas_vet, pep8_naming, perflint,
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
@@ -1056,6 +1056,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::CheckAndRemoveFromSet) {
refurb::rules::check_and_remove_from_set(checker, if_);
}
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
@@ -1459,7 +1462,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::Delete(ast::StmtDelete { targets, range: _ }) => {
Stmt::Delete(delete @ ast::StmtDelete { targets, range: _ }) => {
if checker.enabled(Rule::GlobalStatement) {
for target in targets {
if let Expr::Name(ast::ExprName { id, .. }) = target {
@@ -1467,6 +1470,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
if checker.enabled(Rule::DeleteFullSlice) {
refurb::rules::delete_full_slice(checker, delete);
}
}
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
if checker.enabled(Rule::UselessComparison) {
@@ -1484,6 +1490,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::AsyncioDanglingTask) {
ruff::rules::asyncio_dangling_task(checker, value);
}
if checker.enabled(Rule::RepeatedAppend) {
refurb::rules::repeated_append(checker, stmt);
}
}
_ => {}
}

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

@@ -110,7 +110,7 @@ pub(crate) fn check_noqa(
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
if settings.rules.should_fix(diagnostic.kind.rule()) {
diagnostic
.set_fix(Fix::automatic(delete_noqa(directive.range(), locator)));
.set_fix(Fix::suggested(delete_noqa(directive.range(), locator)));
}
diagnostics.push(diagnostic);
}
@@ -174,12 +174,12 @@ pub(crate) fn check_noqa(
);
if settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
diagnostic.set_fix(Fix::automatic(delete_noqa(
diagnostic.set_fix(Fix::suggested(delete_noqa(
directive.range(),
locator,
)));
} else {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));

View File

@@ -865,6 +865,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Slots, "001") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInTupleSubclass),
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
// refurb
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
_ => return None,
})
}

View File

@@ -76,7 +76,7 @@ impl StatementVisitor<'_> for StringLinesVisitor<'_> {
}) = value.as_ref()
{
for line in UniversalNewlineIterator::with_offset(
self.locator.slice(value.range()),
self.locator.slice(value.as_ref()),
value.start(),
) {
self.string_lines.push(line.start());

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
@@ -319,7 +321,7 @@ impl<'a> Importer<'a> {
/// Add the given member to an existing `Stmt::ImportFrom` statement.
fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
let mut statement = match_statement(self.locator.slice(stmt.range()))?;
let mut statement = match_statement(self.locator.slice(stmt))?;
let import_from = match_import_from(&mut statement)?;
let aliases = match_aliases(import_from)?;
aliases.push(ImportAlias {

View File

@@ -6,7 +6,7 @@
//! [Ruff]: https://github.com/astral-sh/ruff
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub use rules::pycodestyle::rules::{IOError, SyntaxError};
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
@@ -20,7 +20,6 @@ mod doc_lines;
mod docstrings;
pub mod fs;
mod importer;
pub mod jupyter;
mod lex;
pub mod line_width;
pub mod linter;

View File

@@ -6,8 +6,6 @@ use anyhow::{anyhow, Result};
use colored::Colorize;
use itertools::Itertools;
use log::error;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{AsMode, ParseError};
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
@@ -15,7 +13,8 @@ use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{AsMode, ParseError};
use ruff_source_file::{Locator, SourceFileBuilder};
use ruff_text_size::Ranged;
@@ -82,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.
@@ -268,17 +267,20 @@ pub fn check_path(
const MAX_ITERATIONS: usize = 100;
/// Add any missing `# noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings) -> Result<usize> {
let source_type = PySourceType::from(path);
// Read the file from disk.
let contents = std::fs::read_to_string(path)?;
pub fn add_noqa_to_path(
path: &Path,
package: Option<&Path>,
source_kind: &SourceKind,
source_type: PySourceType,
settings: &Settings,
) -> Result<usize> {
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);
@@ -308,21 +310,20 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
&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,
@@ -375,7 +376,7 @@ pub fn lint_only(
&directives,
settings,
noqa,
Some(source_kind),
source_kind,
source_type,
);
@@ -469,7 +470,7 @@ pub fn lint_fix<'a>(
&directives,
settings,
noqa,
Some(source_kind),
source_kind,
source_type,
);
@@ -606,3 +607,133 @@ This indicates a bug in `{}`. If you could open an issue at:
);
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use ruff_notebook::{Notebook, NotebookError};
use crate::registry::Rule;
use crate::source_kind::SourceKind;
use crate::test::{test_contents, test_notebook_path, TestedNotebook};
use crate::{assert_messages, settings};
/// Construct a path to a Jupyter notebook in the `resources/test/fixtures/jupyter` directory.
fn notebook_path(path: impl AsRef<Path>) -> std::path::PathBuf {
Path::new("../ruff_notebook/resources/test/fixtures/jupyter").join(path)
}
#[test]
fn test_import_sorting() -> Result<(), NotebookError> {
let actual = notebook_path("isort.ipynb");
let expected = notebook_path("isort_expected.ipynb");
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
&actual,
expected,
&settings::Settings::for_rule(Rule::UnsortedImports),
)?;
assert_messages!(messages, actual, source_notebook);
Ok(())
}
#[test]
fn test_ipy_escape_command() -> Result<(), NotebookError> {
let actual = notebook_path("ipy_escape_command.ipynb");
let expected = notebook_path("ipy_escape_command_expected.ipynb");
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
&actual,
expected,
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(messages, actual, source_notebook);
Ok(())
}
#[test]
fn test_unused_variable() -> Result<(), NotebookError> {
let actual = notebook_path("unused_variable.ipynb");
let expected = notebook_path("unused_variable_expected.ipynb");
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
&actual,
expected,
&settings::Settings::for_rule(Rule::UnusedVariable),
)?;
assert_messages!(messages, actual, source_notebook);
Ok(())
}
#[test]
fn test_json_consistency() -> Result<()> {
let actual_path = notebook_path("before_fix.ipynb");
let expected_path = notebook_path("after_fix.ipynb");
let TestedNotebook {
linted_notebook: fixed_notebook,
..
} = test_notebook_path(
actual_path,
&expected_path,
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
let mut writer = Vec::new();
fixed_notebook.write(&mut writer)?;
let actual = String::from_utf8(writer)?;
let expected = std::fs::read_to_string(expected_path)?;
assert_eq!(actual, expected);
Ok(())
}
#[test_case(Path::new("before_fix.ipynb"), true; "trailing_newline")]
#[test_case(Path::new("no_trailing_newline.ipynb"), false; "no_trailing_newline")]
fn test_trailing_newline(path: &Path, trailing_newline: bool) -> Result<()> {
let notebook = Notebook::from_path(&notebook_path(path))?;
assert_eq!(notebook.trailing_newline(), trailing_newline);
let mut writer = Vec::new();
notebook.write(&mut writer)?;
let string = String::from_utf8(writer)?;
assert_eq!(string.ends_with('\n'), trailing_newline);
Ok(())
}
// Version <4.5, don't emit cell ids
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
// Version 4.5, cell ids are missing and need to be added
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
let source_notebook = Notebook::from_path(&notebook_path(path))?;
let source_kind = SourceKind::IpyNotebook(source_notebook);
let (_, transformed) = test_contents(
&source_kind,
path,
&settings::Settings::for_rule(Rule::UnusedImport),
);
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
let mut writer = Vec::new();
linted_notebook.write(&mut writer)?;
let actual = String::from_utf8(writer)?;
if has_id {
assert!(actual.contains(r#""id": ""#));
} else {
assert!(!actual.contains(r#""id":"#));
}
Ok(())
}
}

View File

@@ -12,8 +12,8 @@ use ruff_python_parser::{ParseError, ParseErrorType};
use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
use crate::fs;
use crate::jupyter::Notebook;
use crate::source_kind::SourceKind;
use ruff_notebook::Notebook;
pub static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
@@ -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

@@ -4,10 +4,10 @@ use std::num::NonZeroUsize;
use colored::Colorize;
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::message::diff::calculate_print_width;
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{

View File

@@ -14,12 +14,11 @@ 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_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use text::TextEmitter;
use crate::jupyter::Notebook;
mod azure;
mod diff;
mod github;

View File

@@ -7,11 +7,11 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, Sou
use bitflags::bitflags;
use colored::Colorize;
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::fs::relativize_path;
use crate::jupyter::{Notebook, NotebookIndex};
use crate::line_width::{LineWidthBuilder, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};

View File

@@ -196,6 +196,9 @@ pub enum Linter {
/// [Perflint](https://pypi.org/project/perflint/)
#[prefix = "PERF"]
Perflint,
/// [refurb](https://pypi.org/project/refurb/)
#[prefix = "FURB"]
Refurb,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,

View File

@@ -23,6 +23,10 @@ use ruff_text_size::Ranged;
///
/// Use instead:
/// ```python
/// if not x > 0:
/// raise ValueError("Expected positive value.")
///
/// # or even better:
/// if x <= 0:
/// raise ValueError("Expected positive value.")
/// ```

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.range()), 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 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

@@ -2,6 +2,7 @@ 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_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -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),

View File

@@ -84,7 +84,7 @@ pub(crate) fn unreliable_callable_check(
if id == "hasattr" {
if checker.semantic().is_builtin("callable") {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("callable({})", checker.locator().slice(obj.range())),
format!("callable({})", checker.locator().slice(obj)),
expr.range(),
)));
}

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,43 @@
---
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
|
= 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

View File

@@ -31,7 +31,7 @@ pub(crate) fn fix_unnecessary_generator_list(
expr: &Expr,
) -> Result<Edit> {
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
let module_text = locator.slice(expr.range());
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)?;
@@ -63,7 +63,7 @@ pub(crate) fn fix_unnecessary_generator_set(checker: &Checker, expr: &Expr) -> R
let stylist = checker.stylist();
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
let module_text = locator.slice(expr.range());
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)?;
@@ -97,7 +97,7 @@ pub(crate) fn fix_unnecessary_generator_dict(checker: &Checker, expr: &Expr) ->
let locator = checker.locator();
let stylist = checker.stylist();
let module_text = locator.slice(expr.range());
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)?;
@@ -141,7 +141,7 @@ pub(crate) fn fix_unnecessary_list_comprehension_set(
let stylist = checker.stylist();
// Expr(Call(ListComp)))) ->
// Expr(SetComp)))
let module_text = locator.slice(expr.range());
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)?;
@@ -176,7 +176,7 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict(
let locator = checker.locator();
let stylist = checker.stylist();
let module_text = locator.slice(expr.range());
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)?;
@@ -261,7 +261,7 @@ pub(crate) fn fix_unnecessary_literal_set(checker: &Checker, expr: &Expr) -> Res
let stylist = checker.stylist();
// Expr(Call(List|Tuple)))) -> Expr(Set)))
let module_text = locator.slice(expr.range());
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)?;
@@ -302,7 +302,7 @@ pub(crate) fn fix_unnecessary_literal_dict(checker: &Checker, expr: &Expr) -> Re
let stylist = checker.stylist();
// Expr(Call(List|Tuple)))) -> Expr(Dict)))
let module_text = locator.slice(expr.range());
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)?;
@@ -371,7 +371,7 @@ pub(crate) fn fix_unnecessary_collection_call(checker: &Checker, expr: &Expr) ->
let stylist = checker.stylist();
// Expr(Call("list" | "tuple" | "dict")))) -> Expr(List|Tuple|Dict)
let module_text = locator.slice(expr.range());
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call(&tree)?;
let name = match_name(&call.func)?;
@@ -522,7 +522,7 @@ pub(crate) fn fix_unnecessary_literal_within_tuple_call(
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
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)?;
@@ -572,7 +572,7 @@ pub(crate) fn fix_unnecessary_literal_within_list_call(
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
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)?;
@@ -625,7 +625,7 @@ pub(crate) fn fix_unnecessary_list_call(
expr: &Expr,
) -> Result<Edit> {
// Expr(Call(List|Tuple)))) -> Expr(List|Tuple)))
let module_text = locator.slice(expr.range());
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)?;
@@ -646,7 +646,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let outer_call = match_call_mut(&mut tree)?;
let inner_call = match &outer_call.args[..] {
@@ -758,7 +758,7 @@ pub(crate) fn fix_unnecessary_double_cast_or_process(
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let outer_call = match_call_mut(&mut tree)?;
@@ -789,7 +789,7 @@ pub(crate) fn fix_unnecessary_comprehension(
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
match &tree {
@@ -878,7 +878,7 @@ pub(crate) fn fix_unnecessary_map(
parent: Option<&Expr>,
object_type: ObjectType,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
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)?;
@@ -966,21 +966,21 @@ pub(crate) fn fix_unnecessary_map(
}));
}
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 elements = match func_body.body.as_ref() {
Expression::Tuple(tuple) => &tuple.elements,
Expression::List(list) => &list.elements,
_ => {
bail!("Expected tuple or list for dictionary comprehension")
}
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")
};
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");
};
tree = Expression::DictComp(Box::new(DictComp {
@@ -1021,7 +1021,7 @@ pub(crate) fn fix_unnecessary_literal_within_dict_call(
stylist: &Stylist,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
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)?;
@@ -1041,7 +1041,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
expr: &Expr,
) -> Result<Fix> {
// Expr(ListComp) -> Expr(GeneratorExp)
let module_text = locator.slice(expr.range());
let module_text = locator.slice(expr);
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;

View File

@@ -89,7 +89,7 @@ pub(crate) fn unnecessary_map(
ObjectType::Generator => {
// Exclude the parent if already matched by other arms.
if parent
.and_then(ruff_python_ast::Expr::as_call_expr)
.and_then(Expr::as_call_expr)
.and_then(|call| call.func.as_name_expr())
.is_some_and(|name| matches!(name.id.as_str(), "list" | "set" | "dict"))
{
@@ -122,7 +122,7 @@ pub(crate) fn unnecessary_map(
// Only flag, e.g., `list(map(lambda x: x + 1, iterable))`.
let [Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, .. },
arguments: Arguments { args, keywords, .. },
..
})] = args
else {
@@ -133,6 +133,10 @@ pub(crate) fn unnecessary_map(
return;
}
if !keywords.is_empty() {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("map", func, args)
else {
return;
@@ -163,13 +167,21 @@ pub(crate) fn unnecessary_map(
// Only flag, e.g., `dict(map(lambda v: (v, v ** 2), values))`.
let [Expr::Call(ast::ExprCall {
func,
arguments: Arguments { args, .. },
arguments: Arguments { args, keywords, .. },
..
})] = args
else {
return;
};
if args.len() != 2 {
return;
}
if !keywords.is_empty() {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("map", func, args)
else {
return;

View File

@@ -61,7 +61,7 @@ C417.py:5:1: C417 [*] Unnecessary `map` usage (rewrite using a `list` comprehens
5 |+[x * 2 for x in nums]
6 6 | set(map(lambda x: x % 2 == 0, nums))
7 7 | dict(map(lambda v: (v, v**2), nums))
8 8 | map(lambda: "const", nums)
8 8 | dict(map(lambda v: [v, v**2], nums))
C417.py:6:1: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehension)
|
@@ -70,7 +70,7 @@ C417.py:6:1: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehensi
6 | set(map(lambda x: x % 2 == 0, nums))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
7 | dict(map(lambda v: (v, v**2), nums))
8 | map(lambda: "const", nums)
8 | dict(map(lambda v: [v, v**2], nums))
|
= help: Replace `map` with a `set` comprehension
@@ -81,8 +81,8 @@ C417.py:6:1: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehensi
6 |-set(map(lambda x: x % 2 == 0, nums))
6 |+{x % 2 == 0 for x in nums}
7 7 | dict(map(lambda v: (v, v**2), nums))
8 8 | map(lambda: "const", nums)
9 9 | map(lambda _: 3.0, nums)
8 8 | dict(map(lambda v: [v, v**2], nums))
9 9 | map(lambda: "const", nums)
C417.py:7:1: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehension)
|
@@ -90,8 +90,8 @@ C417.py:7:1: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehens
6 | set(map(lambda x: x % 2 == 0, nums))
7 | dict(map(lambda v: (v, v**2), nums))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
8 | map(lambda: "const", nums)
9 | map(lambda _: 3.0, nums)
8 | dict(map(lambda v: [v, v**2], nums))
9 | map(lambda: "const", nums)
|
= help: Replace `map` with a `dict` comprehension
@@ -101,172 +101,193 @@ C417.py:7:1: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehens
6 6 | set(map(lambda x: x % 2 == 0, nums))
7 |-dict(map(lambda v: (v, v**2), nums))
7 |+{v: v**2 for v in nums}
8 8 | map(lambda: "const", nums)
9 9 | map(lambda _: 3.0, nums)
10 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
8 8 | dict(map(lambda v: [v, v**2], nums))
9 9 | map(lambda: "const", nums)
10 10 | map(lambda _: 3.0, nums)
C417.py:8:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
C417.py:8:1: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehension)
|
6 | set(map(lambda x: x % 2 == 0, nums))
7 | dict(map(lambda v: (v, v**2), nums))
8 | map(lambda: "const", nums)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
9 | map(lambda _: 3.0, nums)
10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
8 | dict(map(lambda v: [v, v**2], nums))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
9 | map(lambda: "const", nums)
10 | map(lambda _: 3.0, nums)
|
= help: Replace `map` with a generator expression
= help: Replace `map` with a `dict` comprehension
Suggested fix
5 5 | list(map(lambda x: x * 2, nums))
6 6 | set(map(lambda x: x % 2 == 0, nums))
7 7 | dict(map(lambda v: (v, v**2), nums))
8 |-map(lambda: "const", nums)
8 |+("const" for _ in nums)
9 9 | map(lambda _: 3.0, nums)
10 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 11 | all(map(lambda v: isinstance(v, dict), nums))
8 |-dict(map(lambda v: [v, v**2], nums))
8 |+{v: v**2 for v in nums}
9 9 | map(lambda: "const", nums)
10 10 | map(lambda _: 3.0, nums)
11 11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
C417.py:9:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
7 | dict(map(lambda v: (v, v**2), nums))
8 | map(lambda: "const", nums)
9 | map(lambda _: 3.0, nums)
| ^^^^^^^^^^^^^^^^^^^^^^^^ C417
10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 | all(map(lambda v: isinstance(v, dict), nums))
8 | dict(map(lambda v: [v, v**2], nums))
9 | map(lambda: "const", nums)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
10 | map(lambda _: 3.0, nums)
11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
|
= help: Replace `map` with a generator expression
Suggested fix
6 6 | set(map(lambda x: x % 2 == 0, nums))
7 7 | dict(map(lambda v: (v, v**2), nums))
8 8 | map(lambda: "const", nums)
9 |-map(lambda _: 3.0, nums)
9 |+(3.0 for _ in nums)
10 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 11 | all(map(lambda v: isinstance(v, dict), nums))
12 12 | filter(func, map(lambda v: v, nums))
8 8 | dict(map(lambda v: [v, v**2], nums))
9 |-map(lambda: "const", nums)
9 |+("const" for _ in nums)
10 10 | map(lambda _: 3.0, nums)
11 11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
12 12 | all(map(lambda v: isinstance(v, dict), nums))
C417.py:10:13: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
C417.py:10:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
8 | map(lambda: "const", nums)
9 | map(lambda _: 3.0, nums)
10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
11 | all(map(lambda v: isinstance(v, dict), nums))
12 | filter(func, map(lambda v: v, nums))
8 | dict(map(lambda v: [v, v**2], nums))
9 | map(lambda: "const", nums)
10 | map(lambda _: 3.0, nums)
| ^^^^^^^^^^^^^^^^^^^^^^^^ C417
11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
12 | all(map(lambda v: isinstance(v, dict), nums))
|
= help: Replace `map` with a generator expression
Suggested fix
7 7 | dict(map(lambda v: (v, v**2), nums))
8 8 | map(lambda: "const", nums)
9 9 | map(lambda _: 3.0, nums)
10 |-_ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
10 |+_ = "".join((x in nums and "1" or "0" for x in range(123)))
11 11 | all(map(lambda v: isinstance(v, dict), nums))
12 12 | filter(func, map(lambda v: v, nums))
13 13 |
8 8 | dict(map(lambda v: [v, v**2], nums))
9 9 | map(lambda: "const", nums)
10 |-map(lambda _: 3.0, nums)
10 |+(3.0 for _ in nums)
11 11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
12 12 | all(map(lambda v: isinstance(v, dict), nums))
13 13 | filter(func, map(lambda v: v, nums))
C417.py:11:5: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
C417.py:11:13: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
9 | map(lambda _: 3.0, nums)
10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 | all(map(lambda v: isinstance(v, dict), nums))
9 | map(lambda: "const", nums)
10 | map(lambda _: 3.0, nums)
11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
12 | all(map(lambda v: isinstance(v, dict), nums))
13 | filter(func, map(lambda v: v, nums))
|
= help: Replace `map` with a generator expression
Suggested fix
8 8 | dict(map(lambda v: [v, v**2], nums))
9 9 | map(lambda: "const", nums)
10 10 | map(lambda _: 3.0, nums)
11 |-_ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 |+_ = "".join((x in nums and "1" or "0" for x in range(123)))
12 12 | all(map(lambda v: isinstance(v, dict), nums))
13 13 | filter(func, map(lambda v: v, nums))
14 14 |
C417.py:12:5: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
10 | map(lambda _: 3.0, nums)
11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
12 | all(map(lambda v: isinstance(v, dict), nums))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
12 | filter(func, map(lambda v: v, nums))
13 | filter(func, map(lambda v: v, nums))
|
= help: Replace `map` with a generator expression
Suggested fix
8 8 | map(lambda: "const", nums)
9 9 | map(lambda _: 3.0, nums)
10 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 |-all(map(lambda v: isinstance(v, dict), nums))
11 |+all((isinstance(v, dict) for v in nums))
12 12 | filter(func, map(lambda v: v, nums))
13 13 |
14 14 | # When inside f-string, then the fix should be surrounded by whitespace
9 9 | map(lambda: "const", nums)
10 10 | map(lambda _: 3.0, nums)
11 11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
12 |-all(map(lambda v: isinstance(v, dict), nums))
12 |+all((isinstance(v, dict) for v in nums))
13 13 | filter(func, map(lambda v: v, nums))
14 14 |
15 15 | # When inside f-string, then the fix should be surrounded by whitespace
C417.py:12:14: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
C417.py:13:14: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 | all(map(lambda v: isinstance(v, dict), nums))
12 | filter(func, map(lambda v: v, nums))
11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
12 | all(map(lambda v: isinstance(v, dict), nums))
13 | filter(func, map(lambda v: v, nums))
| ^^^^^^^^^^^^^^^^^^^^^^ C417
13 |
14 | # When inside f-string, then the fix should be surrounded by whitespace
14 |
15 | # When inside f-string, then the fix should be surrounded by whitespace
|
= help: Replace `map` with a generator expression
Suggested fix
9 9 | map(lambda _: 3.0, nums)
10 10 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
11 11 | all(map(lambda v: isinstance(v, dict), nums))
12 |-filter(func, map(lambda v: v, nums))
12 |+filter(func, (v for v in nums))
13 13 |
14 14 | # When inside f-string, then the fix should be surrounded by whitespace
15 15 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
10 10 | map(lambda _: 3.0, nums)
11 11 | _ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
12 12 | all(map(lambda v: isinstance(v, dict), nums))
13 |-filter(func, map(lambda v: v, nums))
13 |+filter(func, (v for v in nums))
14 14 |
15 15 | # When inside f-string, then the fix should be surrounded by whitespace
16 16 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
C417.py:15:8: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehension)
C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehension)
|
14 | # When inside f-string, then the fix should be surrounded by whitespace
15 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
15 | # When inside f-string, then the fix should be surrounded by whitespace
16 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
17 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
|
= help: Replace `map` with a `set` comprehension
Suggested fix
12 12 | filter(func, map(lambda v: v, nums))
13 13 |
14 14 | # When inside f-string, then the fix should be surrounded by whitespace
15 |-_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
15 |+_ = f"{ {x % 2 == 0 for x in nums} }"
16 16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
17 17 |
18 18 | # False negatives.
13 13 | filter(func, map(lambda v: v, nums))
14 14 |
15 15 | # When inside f-string, then the fix should be surrounded by whitespace
16 |-_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
16 |+_ = f"{ {x % 2 == 0 for x in nums} }"
17 17 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
18 18 |
19 19 | # False negatives.
C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehension)
C417.py:17:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehension)
|
14 | # When inside f-string, then the fix should be surrounded by whitespace
15 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
15 | # When inside f-string, then the fix should be surrounded by whitespace
16 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
17 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
17 |
18 | # False negatives.
18 |
19 | # False negatives.
|
= help: Replace `map` with a `dict` comprehension
Suggested fix
13 13 |
14 14 | # When inside f-string, then the fix should be surrounded by whitespace
15 15 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
16 |-_ = f"{dict(map(lambda v: (v, v**2), nums))}"
16 |+_ = f"{ {v: v**2 for v in nums} }"
17 17 |
18 18 | # False negatives.
19 19 | map(lambda x=2, y=1: x + y, nums, nums)
14 14 |
15 15 | # When inside f-string, then the fix should be surrounded by whitespace
16 16 | _ = f"{set(map(lambda x: x % 2 == 0, nums))}"
17 |-_ = f"{dict(map(lambda v: (v, v**2), nums))}"
17 |+_ = f"{ {v: v**2 for v in nums} }"
18 18 |
19 19 | # False negatives.
20 20 | map(lambda x=2, y=1: x + y, nums, nums)
C417.py:34:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
C417.py:35:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
33 | # Error: the `x` is overridden by the inner lambda.
34 | map(lambda x: lambda x: x, range(4))
34 | # Error: the `x` is overridden by the inner lambda.
35 | map(lambda x: lambda x: x, range(4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
35 |
36 | # Ok because of the default parameters, and variadic arguments.
36 |
37 | # Ok because of the default parameters, and variadic arguments.
|
= help: Replace `map` with a generator expression
Suggested fix
31 31 | map(lambda x: lambda: x, range(4))
32 32 |
33 33 | # Error: the `x` is overridden by the inner lambda.
34 |-map(lambda x: lambda x: x, range(4))
34 |+(lambda x: x for x in range(4))
35 35 |
36 36 | # Ok because of the default parameters, and variadic arguments.
37 37 | map(lambda x=1: x, nums)
32 32 | map(lambda x: lambda: x, range(4))
33 33 |
34 34 | # Error: the `x` is overridden by the inner lambda.
35 |-map(lambda x: lambda x: x, range(4))
35 |+(lambda x: x for x in range(4))
36 36 |
37 37 | # Ok because of the default parameters, and variadic arguments.
38 38 | map(lambda x=1: x, nums)

View File

@@ -149,6 +149,17 @@ import os
# Content Content Content Content Content Content Content Content Content Content
# Copyright 2023
"#
.trim(),
&settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
assert_messages!(diagnostics);
}
#[test]
fn char_boundary() {
let diagnostics = test_snippet(
r#"কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
"#
.trim(),
&settings::Settings::for_rules(vec![Rule::MissingCopyrightNotice]),

View File

@@ -1,8 +1,7 @@
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_source_file::Locator;
use ruff_text_size::{TextRange, TextSize};
use crate::settings::Settings;
@@ -33,11 +32,7 @@ pub(crate) fn missing_copyright_notice(
}
// Only search the first 1024 bytes in the file.
let contents = if locator.len() < 1024 {
locator.contents()
} else {
locator.up_to(TextSize::from(1024))
};
let contents = locator.up_to(locator.floor_char_boundary(TextSize::new(1024)));
// Locate the copyright notice.
if let Some(match_) = settings.flake8_copyright.notice_rgx.find(contents) {

View File

@@ -0,0 +1,10 @@
---
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---
<filename>:1:1: CPY001 Missing copyright notice at top of file
|
1 | কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
| CPY001
|

View File

@@ -55,7 +55,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
// Replace the parent with its non-duplicate child.
let child = if expr == left.as_ref() { right } else { left };
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.locator().slice(child.range()).to_string(),
checker.locator().slice(child.as_ref()).to_string(),
parent.range(),
)));
}

View File

@@ -1,7 +1,7 @@
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -23,18 +23,35 @@ impl AlwaysAutofixableViolation for NonEmptyStubBody {
/// PYI010
pub(crate) fn non_empty_stub_body(checker: &mut Checker, body: &[Stmt]) {
if let [Stmt::Expr(ast::StmtExpr { value, range: _ })] = body {
// Ignore multi-statement bodies (covered by PYI048).
let [stmt] = body else {
return;
};
// Ignore `pass` statements (covered by PYI009).
if stmt.is_pass_stmt() {
return;
}
// Ignore docstrings (covered by PYI021).
if is_docstring_stmt(stmt) {
return;
}
// Ignore `...` (the desired case).
if let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt {
if let Expr::Constant(ast::ExprConstant { value, .. }) = value.as_ref() {
if matches!(value, Constant::Ellipsis | Constant::Str(_)) {
if value.is_ellipsis() {
return;
}
}
}
let mut diagnostic = Diagnostic::new(NonEmptyStubBody, body[0].range());
let mut diagnostic = Diagnostic::new(NonEmptyStubBody, stmt.range());
if checker.patch(Rule::NonEmptyStubBody) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
format!("..."),
body[0].range(),
stmt.range(),
)));
};
checker.diagnostics.push(diagnostic);

View File

@@ -22,17 +22,16 @@ impl AlwaysAutofixableViolation for PassStatementStubBody {
/// PYI009
pub(crate) fn pass_statement_stub_body(checker: &mut Checker, body: &[Stmt]) {
let [stmt] = body else {
let [Stmt::Pass(pass)] = body else {
return;
};
if stmt.is_pass_stmt() {
let mut diagnostic = Diagnostic::new(PassStatementStubBody, stmt.range());
if checker.patch(Rule::PassStatementStubBody) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("..."),
stmt.range(),
)));
};
checker.diagnostics.push(diagnostic);
}
let mut diagnostic = Diagnostic::new(PassStatementStubBody, pass.range());
if checker.patch(Rule::PassStatementStubBody) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("..."),
pass.range(),
)));
};
checker.diagnostics.push(diagnostic);
}

View File

@@ -8,6 +8,7 @@ use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::{checkers::ast::Checker, rules::flake8_pyi::helpers::traverse_union};
/// ## What it does
@@ -38,7 +39,7 @@ use crate::{checkers::ast::Checker, rules::flake8_pyi::helpers::traverse_union};
/// ```
#[violation]
pub struct RedundantLiteralUnion {
literal: String,
literal: SourceCodeSnippet,
builtin_type: ExprType,
}
@@ -49,7 +50,11 @@ impl Violation for RedundantLiteralUnion {
literal,
builtin_type,
} = self;
format!("`Literal[{literal}]` is redundant in a union with `{builtin_type}`",)
if let Some(literal) = literal.full_display() {
format!("`Literal[{literal}]` is redundant in a union with `{builtin_type}`")
} else {
format!("`Literal` is redundant in a union with `{builtin_type}`")
}
}
}
@@ -88,7 +93,7 @@ pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr
if builtin_types_in_union.contains(&constant_type) {
checker.diagnostics.push(Diagnostic::new(
RedundantLiteralUnion {
literal: checker.locator().slice(literal_expr.range()).to_string(),
literal: SourceCodeSnippet::from_str(checker.locator().slice(literal_expr)),
builtin_type: constant_type,
},
literal_expr.range(),

View File

@@ -249,7 +249,7 @@ fn is_valid_default_value_with_annotation(
..
}) = left.as_ref()
{
return locator.slice(left.range()).len() <= 10;
return locator.slice(left.as_ref()).len() <= 10;
} else if let Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::USub,
operand,
@@ -262,7 +262,7 @@ fn is_valid_default_value_with_annotation(
..
}) = operand.as_ref()
{
return locator.slice(operand.range()).len() <= 10;
return locator.slice(operand.as_ref()).len() <= 10;
}
}
}

View File

@@ -1,9 +1,7 @@
use ruff_python_ast::Stmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::Stmt;
use crate::checkers::ast::Checker;
@@ -17,21 +15,12 @@ impl Violation for StubBodyMultipleStatements {
}
}
/// PYI010
/// PYI048
pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt, body: &[Stmt]) {
// If the function body consists of exactly one statement, abort.
if body.len() == 1 {
return;
if body.len() > 1 {
checker.diagnostics.push(Diagnostic::new(
StubBodyMultipleStatements,
stmt.identifier(),
));
}
// If the function body consists of exactly two statements, and the first is a
// docstring, abort (this is covered by PYI021).
if body.len() == 2 && is_docstring_stmt(&body[0]) {
return;
}
checker.diagnostics.push(Diagnostic::new(
StubBodyMultipleStatements,
stmt.identifier(),
));
}

View File

@@ -63,7 +63,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
UnnecessaryLiteralUnion {
members: literal_exprs
.into_iter()
.map(|literal_expr| checker.locator().slice(literal_expr.range()).to_string())
.map(|expr| checker.locator().slice(expr.as_ref()).to_string())
.collect(),
},
expr.range(),

View File

@@ -78,7 +78,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
UnnecessaryTypeUnion {
members: type_exprs
.into_iter()
.map(|type_expr| checker.locator().slice(type_expr.range()).to_string())
.map(|type_expr| checker.locator().slice(type_expr.as_ref()).to_string())
.collect(),
is_pep604_union,
},

View File

@@ -11,8 +11,8 @@ PYI010.pyi:6:5: PYI010 [*] Function body must contain only `...`
|
= help: Replace function body with `...`
Fix
3 3 | """foo""" # OK, strings are handled by another rule
Suggested fix
3 3 | """foo""" # OK, docstrings are handled by another rule
4 4 |
5 5 | def buzz():
6 |- print("buzz") # ERROR PYI010
@@ -31,7 +31,7 @@ PYI010.pyi:9:5: PYI010 [*] Function body must contain only `...`
|
= help: Replace function body with `...`
Fix
Suggested fix
6 6 | print("buzz") # ERROR PYI010
7 7 |
8 8 | def foo2():
@@ -46,14 +46,19 @@ PYI010.pyi:12:5: PYI010 [*] Function body must contain only `...`
11 | def bizz():
12 | x = 123 # ERROR PYI010
| ^^^^^^^ PYI010
13 |
14 | def foo3():
|
= help: Replace function body with `...`
Fix
Suggested fix
9 9 | 123 # ERROR PYI010
10 10 |
11 11 | def bizz():
12 |- x = 123 # ERROR PYI010
12 |+ ... # ERROR PYI010
13 13 |
14 14 | def foo3():
15 15 | pass # OK, pass is handled by another rule

View File

@@ -1,17 +1,31 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI048.pyi:11:5: PYI048 Function body must contain exactly one statement
PYI048.pyi:8:5: PYI048 Function body must contain exactly one statement
|
11 | def foo(): # ERROR PYI048
6 | """oof""" # OK
7 |
8 | def oof(): # ERROR PYI048
| ^^^ PYI048
12 | """foo"""
13 | print("foo")
9 | """oof"""
10 | print("foo")
|
PYI048.pyi:12:5: PYI048 Function body must contain exactly one statement
|
10 | print("foo")
11 |
12 | def foo(): # ERROR PYI048
| ^^^ PYI048
13 | """foo"""
14 | print("foo")
|
PYI048.pyi:17:5: PYI048 Function body must contain exactly one statement
|
17 | def buzz(): # ERROR PYI048
15 | print("foo")
16 |
17 | def buzz(): # ERROR PYI048
| ^^^^ PYI048
18 | print("fizz")
19 | print("buzz")

View File

@@ -411,7 +411,7 @@ fn to_pytest_raises_args<'a>(
"assertRaises" | "failUnlessRaises" => {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
// Ex) `assertRaises(Exception)`
([arg], []) => Cow::Borrowed(checker.locator().slice(arg.range())),
([arg], []) => Cow::Borrowed(checker.locator().slice(arg)),
// Ex) `assertRaises(expected_exception=Exception)`
([], [kwarg])
if kwarg
@@ -429,8 +429,8 @@ fn to_pytest_raises_args<'a>(
// Ex) `assertRaisesRegex(Exception, regex)`
([arg1, arg2], []) => Cow::Owned(format!(
"{}, match={}",
checker.locator().slice(arg1.range()),
checker.locator().slice(arg2.range())
checker.locator().slice(arg1),
checker.locator().slice(arg2)
)),
// Ex) `assertRaisesRegex(Exception, expected_regex=regex)`
([arg], [kwarg])
@@ -441,7 +441,7 @@ fn to_pytest_raises_args<'a>(
{
Cow::Owned(format!(
"{}, match={}",
checker.locator().slice(arg.range()),
checker.locator().slice(arg),
checker.locator().slice(kwarg.value.range())
))
}

View File

@@ -12,6 +12,7 @@ use ruff_python_ast::stmt_if::elif_else_range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::whitespace::indentation;
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::is_python_whitespace;
use crate::autofix::edits;
use crate::checkers::ast::Checker;
@@ -537,7 +538,7 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) {
edits::delete_stmt(stmt, None, checker.locator(), checker.indexer());
// Replace the `x = 1` statement with `return 1`.
let content = checker.locator().slice(assign.range());
let content = checker.locator().slice(assign);
let equals_index = content
.find('=')
.ok_or(anyhow::anyhow!("expected '=' in assignment statement"))?;
@@ -549,11 +550,11 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack) {
if content[after_equals..]
.chars()
.next()
.is_some_and(char::is_alphabetic)
.is_some_and(is_python_whitespace)
{
"return ".to_string()
} else {
"return".to_string()
} else {
"return ".to_string()
},
// Replace from the start of the assignment statement to the end of the equals
// sign.

View File

@@ -197,5 +197,25 @@ RET504.py:359:12: RET504 [*] Unnecessary assignment to `a` before `return` state
358 |- a = 1 # Comment
359 |- return a
358 |+ return 1 # Comment
360 359 |
361 360 |
362 361 | # Regression test for: https://github.com/astral-sh/ruff/issues/7098
RET504.py:365:12: RET504 [*] Unnecessary assignment to `D` before `return` statement
|
363 | def mavko_debari(P_kbar):
364 | D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
365 | return D
| ^ RET504
|
= help: Remove unnecessary assignment
Suggested fix
361 361 |
362 362 | # Regression test for: https://github.com/astral-sh/ruff/issues/7098
363 363 | def mavko_debari(P_kbar):
364 |- D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
365 |- return D
364 |+ return 0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2

View File

@@ -11,6 +11,8 @@ use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::helpers::{contains_effect, Truthiness};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_codegen::Generator;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -692,12 +694,12 @@ pub(crate) fn expr_or_not_expr(checker: &mut Checker, expr: &Expr) {
}
}
pub(crate) fn get_short_circuit_edit(
fn get_short_circuit_edit(
expr: &Expr,
range: TextRange,
truthiness: Truthiness,
in_boolean_test: bool,
checker: &Checker,
generator: Generator,
) -> Edit {
let content = if in_boolean_test {
match truthiness {
@@ -708,9 +710,17 @@ pub(crate) fn get_short_circuit_edit(
}
}
} else {
checker.generator().expr(expr)
generator.expr(expr)
};
Edit::range_replacement(content, range)
Edit::range_replacement(
if matches!(expr, Expr::Tuple(ast::ExprTuple { elts, ctx: _, range: _}) if !elts.is_empty())
{
format!("({content})")
} else {
content
},
range,
)
}
fn is_short_circuit(
@@ -734,7 +744,7 @@ fn is_short_circuit(
BoolOp::Or => Truthiness::Truthy,
};
let mut location = expr.start();
let mut furthest = expr;
let mut edit = None;
let mut remove = None;
@@ -749,7 +759,7 @@ fn is_short_circuit(
&& (!checker.semantic().in_boolean_test()
|| contains_effect(value, |id| checker.semantic().is_builtin(id)))
{
location = next_value.start();
furthest = next_value;
continue;
}
@@ -758,17 +768,19 @@ fn is_short_circuit(
// short-circuit expression is the first expression in the list; otherwise, we'll see it
// as `next_value` before we see it as `value`.
if value_truthiness == short_circuit_truthiness {
remove = Some(if location == value.start() {
ContentAround::After
} else {
ContentAround::Both
});
remove = Some(ContentAround::After);
edit = Some(get_short_circuit_edit(
value,
TextRange::new(location, expr.end()),
TextRange::new(
parenthesized_range(furthest.into(), expr.into(), checker.locator().contents())
.unwrap_or(furthest.range())
.start(),
expr.end(),
),
short_circuit_truthiness,
checker.semantic().in_boolean_test(),
checker,
checker.generator(),
));
break;
}
@@ -776,17 +788,22 @@ fn is_short_circuit(
// If the next expression is a constant, and it matches the short-circuit value, then
// we can return the location of the expression.
if next_value_truthiness == short_circuit_truthiness {
remove = Some(if index == values.len() - 2 {
remove = Some(if index + 1 == values.len() - 1 {
ContentAround::Before
} else {
ContentAround::Both
});
edit = Some(get_short_circuit_edit(
next_value,
TextRange::new(location, expr.end()),
TextRange::new(
parenthesized_range(furthest.into(), expr.into(), checker.locator().contents())
.unwrap_or(furthest.range())
.start(),
expr.end(),
),
short_circuit_truthiness,
checker.semantic().in_boolean_test(),
checker,
checker.generator(),
));
break;
}

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::{self as ast, Arguments, Constant, Expr};
use ruff_text_size::{Ranged, TextRange};
use crate::autofix::snippet::SourceCodeSnippet;
use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_none;
@@ -35,8 +36,8 @@ use crate::registry::AsRule;
/// - [Python documentation: `os.environ`](https://docs.python.org/3/library/os.html#os.environ)
#[violation]
pub struct UncapitalizedEnvironmentVariables {
expected: String,
original: String,
expected: SourceCodeSnippet,
actual: SourceCodeSnippet,
}
impl Violation for UncapitalizedEnvironmentVariables {
@@ -44,13 +45,21 @@ impl Violation for UncapitalizedEnvironmentVariables {
#[derive_message_formats]
fn message(&self) -> String {
let UncapitalizedEnvironmentVariables { expected, original } = self;
format!("Use capitalized environment variable `{expected}` instead of `{original}`")
let UncapitalizedEnvironmentVariables { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
format!("Use capitalized environment variable `{expected}` instead of `{actual}`")
} else {
format!("Use capitalized environment variable")
}
}
fn autofix_title(&self) -> Option<String> {
let UncapitalizedEnvironmentVariables { expected, original } = self;
Some(format!("Replace `{original}` with `{expected}`"))
let UncapitalizedEnvironmentVariables { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
Some(format!("Replace `{actual}` with `{expected}`"))
} else {
Some("Capitalize environment variable".to_string())
}
}
}
@@ -77,20 +86,28 @@ impl Violation for UncapitalizedEnvironmentVariables {
/// - [Python documentation: `dict.get`](https://docs.python.org/3/library/stdtypes.html#dict.get)
#[violation]
pub struct DictGetWithNoneDefault {
expected: String,
original: String,
expected: SourceCodeSnippet,
actual: SourceCodeSnippet,
}
impl AlwaysAutofixableViolation for DictGetWithNoneDefault {
#[derive_message_formats]
fn message(&self) -> String {
let DictGetWithNoneDefault { expected, original } = self;
format!("Use `{expected}` instead of `{original}`")
let DictGetWithNoneDefault { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
format!("Use `{expected}` instead of `{actual}`")
} else {
format!("Use `dict.get()` without default value")
}
}
fn autofix_title(&self) -> String {
let DictGetWithNoneDefault { expected, original } = self;
format!("Replace `{original}` with `{expected}`")
let DictGetWithNoneDefault { expected, actual } = self;
if let (Some(expected), Some(actual)) = (expected.full_display(), actual.full_display()) {
format!("Replace `{actual}` with `{expected}`")
} else {
"Remove default value".to_string()
}
}
}
@@ -141,8 +158,8 @@ pub(crate) fn use_capital_environment_variables(checker: &mut Checker, expr: &Ex
checker.diagnostics.push(Diagnostic::new(
UncapitalizedEnvironmentVariables {
expected: capital_env_var,
original: env_var.clone(),
expected: SourceCodeSnippet::new(capital_env_var),
actual: SourceCodeSnippet::new(env_var.clone()),
},
arg.range(),
));
@@ -181,8 +198,8 @@ fn check_os_environ_subscript(checker: &mut Checker, expr: &Expr) {
let mut diagnostic = Diagnostic::new(
UncapitalizedEnvironmentVariables {
expected: capital_env_var.clone(),
original: env_var.clone(),
expected: SourceCodeSnippet::new(capital_env_var.clone()),
actual: SourceCodeSnippet::new(env_var.clone()),
},
slice.range(),
);
@@ -238,15 +255,15 @@ pub(crate) fn dict_get_with_none_default(checker: &mut Checker, expr: &Expr) {
let expected = format!(
"{}({})",
checker.locator().slice(func.range()),
checker.locator().slice(key.range())
checker.locator().slice(func.as_ref()),
checker.locator().slice(key)
);
let original = checker.locator().slice(expr.range()).to_string();
let actual = checker.locator().slice(expr);
let mut diagnostic = Diagnostic::new(
DictGetWithNoneDefault {
expected: expected.clone(),
original,
expected: SourceCodeSnippet::new(expected.clone()),
actual: SourceCodeSnippet::from_str(actual),
},
expr.range(),
);

View File

@@ -4,6 +4,7 @@ use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::parenthesize::parenthesized_range;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -30,7 +31,7 @@ use crate::registry::AsRule;
/// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing)
#[violation]
pub struct IfExprWithTrueFalse {
expr: String,
is_compare: bool,
}
impl Violation for IfExprWithTrueFalse {
@@ -38,13 +39,21 @@ impl Violation for IfExprWithTrueFalse {
#[derive_message_formats]
fn message(&self) -> String {
let IfExprWithTrueFalse { expr } = self;
format!("Use `bool({expr})` instead of `True if {expr} else False`")
let IfExprWithTrueFalse { is_compare } = self;
if *is_compare {
format!("Remove unnecessary `True if ... else False`")
} else {
format!("Use `bool(...)` instead of `True if ... else False`")
}
}
fn autofix_title(&self) -> Option<String> {
let IfExprWithTrueFalse { expr } = self;
Some(format!("Replace with `not {expr}"))
let IfExprWithTrueFalse { is_compare } = self;
if *is_compare {
Some(format!("Remove unnecessary `True if ... else False`"))
} else {
Some(format!("Replace with `bool(...)"))
}
}
}
@@ -70,20 +79,16 @@ impl Violation for IfExprWithTrueFalse {
/// ## References
/// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing)
#[violation]
pub struct IfExprWithFalseTrue {
expr: String,
}
pub struct IfExprWithFalseTrue;
impl AlwaysAutofixableViolation for IfExprWithFalseTrue {
#[derive_message_formats]
fn message(&self) -> String {
let IfExprWithFalseTrue { expr } = self;
format!("Use `not {expr}` instead of `False if {expr} else True`")
format!("Use `not ...` instead of `False if ... else True`")
}
fn autofix_title(&self) -> String {
let IfExprWithFalseTrue { expr } = self;
format!("Replace with `bool({expr})")
format!("Replace with `not ...`")
}
}
@@ -135,7 +140,7 @@ impl AlwaysAutofixableViolation for IfExprWithTwistedArms {
}
/// SIM210
pub(crate) fn explicit_true_false_in_ifexpr(
pub(crate) fn if_expr_with_true_false(
checker: &mut Checker,
expr: &Expr,
test: &Expr,
@@ -148,33 +153,43 @@ pub(crate) fn explicit_true_false_in_ifexpr(
let mut diagnostic = Diagnostic::new(
IfExprWithTrueFalse {
expr: checker.generator().expr(test),
is_compare: test.is_compare_expr(),
},
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
if test.is_compare_expr() {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.locator().slice(test.range()).to_string(),
checker
.locator()
.slice(
parenthesized_range(test.into(), expr.into(), checker.locator().contents())
.unwrap_or(test.range()),
)
.to_string(),
expr.range(),
)));
} else if checker.semantic().is_builtin("bool") {
let node = ast::ExprName {
id: "bool".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
};
let node1 = ast::ExprCall {
func: Box::new(node.into()),
arguments: Arguments {
args: vec![test.clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
};
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().expr(&node1.into()),
checker.generator().expr(
&ast::ExprCall {
func: Box::new(
ast::ExprName {
id: "bool".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
}
.into(),
),
arguments: Arguments {
args: vec![test.clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
}
.into(),
),
expr.range(),
)));
};
@@ -183,7 +198,7 @@ pub(crate) fn explicit_true_false_in_ifexpr(
}
/// SIM211
pub(crate) fn explicit_false_true_in_ifexpr(
pub(crate) fn if_expr_with_false_true(
checker: &mut Checker,
expr: &Expr,
test: &Expr,
@@ -194,21 +209,17 @@ pub(crate) fn explicit_false_true_in_ifexpr(
return;
}
let mut diagnostic = Diagnostic::new(
IfExprWithFalseTrue {
expr: checker.generator().expr(test),
},
expr.range(),
);
let mut diagnostic = Diagnostic::new(IfExprWithFalseTrue, expr.range());
if checker.patch(diagnostic.kind.rule()) {
let node = test.clone();
let node1 = ast::ExprUnaryOp {
op: UnaryOp::Not,
operand: Box::new(node),
range: TextRange::default(),
};
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().expr(&node1.into()),
checker.generator().expr(
&ast::ExprUnaryOp {
op: UnaryOp::Not,
operand: Box::new(test.clone()),
range: TextRange::default(),
}
.into(),
),
expr.range(),
)));
}

View File

@@ -273,7 +273,7 @@ pub(crate) fn double_negation(checker: &mut Checker, expr: &Expr, op: UnaryOp, o
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().in_boolean_test() {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.locator().slice(operand.range()).to_string(),
checker.locator().slice(operand.as_ref()).to_string(),
expr.range(),
)));
} else if checker.semantic().is_builtin("bool") {

View File

@@ -1,14 +1,14 @@
---
source: crates/ruff/src/rules/flake8_simplify/mod.rs
---
SIM210.py:1:5: SIM210 [*] Use `bool(b)` instead of `True if b else False`
SIM210.py:1:5: SIM210 [*] Use `bool(...)` instead of `True if ... else False`
|
1 | a = True if b else False # SIM210
| ^^^^^^^^^^^^^^^^^^^^ SIM210
2 |
3 | a = True if b != c else False # SIM210
|
= help: Replace with `not b
= help: Replace with `bool(...)
Suggested fix
1 |-a = True if b else False # SIM210
@@ -17,7 +17,7 @@ SIM210.py:1:5: SIM210 [*] Use `bool(b)` instead of `True if b else False`
3 3 | a = True if b != c else False # SIM210
4 4 |
SIM210.py:3:5: SIM210 [*] Use `bool(b != c)` instead of `True if b != c else False`
SIM210.py:3:5: SIM210 [*] Remove unnecessary `True if ... else False`
|
1 | a = True if b else False # SIM210
2 |
@@ -26,7 +26,7 @@ SIM210.py:3:5: SIM210 [*] Use `bool(b != c)` instead of `True if b != c else Fal
4 |
5 | a = True if b + c else False # SIM210
|
= help: Replace with `not b != c
= help: Remove unnecessary `True if ... else False`
Suggested fix
1 1 | a = True if b else False # SIM210
@@ -37,7 +37,7 @@ SIM210.py:3:5: SIM210 [*] Use `bool(b != c)` instead of `True if b != c else Fal
5 5 | a = True if b + c else False # SIM210
6 6 |
SIM210.py:5:5: SIM210 [*] Use `bool(b + c)` instead of `True if b + c else False`
SIM210.py:5:5: SIM210 [*] Use `bool(...)` instead of `True if ... else False`
|
3 | a = True if b != c else False # SIM210
4 |
@@ -46,7 +46,7 @@ SIM210.py:5:5: SIM210 [*] Use `bool(b + c)` instead of `True if b + c else False
6 |
7 | a = False if b else True # OK
|
= help: Replace with `not b + c
= help: Replace with `bool(...)
Suggested fix
2 2 |
@@ -58,13 +58,32 @@ SIM210.py:5:5: SIM210 [*] Use `bool(b + c)` instead of `True if b + c else False
7 7 | a = False if b else True # OK
8 8 |
SIM210.py:15:9: SIM210 Use `bool(b)` instead of `True if b else False`
SIM210.py:15:9: SIM210 Use `bool(...)` instead of `True if ... else False`
|
13 | return False
14 |
15 | a = True if b else False
| ^^^^^^^^^^^^^^^^^^^^ SIM210
|
= help: Replace with `not b
= help: Replace with `bool(...)
SIM210.py:19:11: SIM210 [*] Remove unnecessary `True if ... else False`
|
18 | # Regression test for: https://github.com/astral-sh/ruff/issues/7076
19 | samesld = True if (psl.privatesuffix(urlparse(response.url).netloc) ==
| ___________^
20 | | psl.privatesuffix(src.netloc)) else False
| |____________________________________________________________________________^ SIM210
|
= help: Remove unnecessary `True if ... else False`
Suggested fix
16 16 |
17 17 |
18 18 | # Regression test for: https://github.com/astral-sh/ruff/issues/7076
19 |-samesld = True if (psl.privatesuffix(urlparse(response.url).netloc) ==
20 |- psl.privatesuffix(src.netloc)) else False
19 |+samesld = (psl.privatesuffix(urlparse(response.url).netloc) ==
20 |+ psl.privatesuffix(src.netloc))

View File

@@ -1,14 +1,14 @@
---
source: crates/ruff/src/rules/flake8_simplify/mod.rs
---
SIM211.py:1:5: SIM211 [*] Use `not b` instead of `False if b else True`
SIM211.py:1:5: SIM211 [*] Use `not ...` instead of `False if ... else True`
|
1 | a = False if b else True # SIM211
| ^^^^^^^^^^^^^^^^^^^^ SIM211
2 |
3 | a = False if b != c else True # SIM211
|
= help: Replace with `bool(b)
= help: Replace with `not ...`
Suggested fix
1 |-a = False if b else True # SIM211
@@ -17,7 +17,7 @@ SIM211.py:1:5: SIM211 [*] Use `not b` instead of `False if b else True`
3 3 | a = False if b != c else True # SIM211
4 4 |
SIM211.py:3:5: SIM211 [*] Use `not b != c` instead of `False if b != c else True`
SIM211.py:3:5: SIM211 [*] Use `not ...` instead of `False if ... else True`
|
1 | a = False if b else True # SIM211
2 |
@@ -26,7 +26,7 @@ SIM211.py:3:5: SIM211 [*] Use `not b != c` instead of `False if b != c else True
4 |
5 | a = False if b + c else True # SIM211
|
= help: Replace with `bool(b != c)
= help: Replace with `not ...`
Suggested fix
1 1 | a = False if b else True # SIM211
@@ -37,7 +37,7 @@ SIM211.py:3:5: SIM211 [*] Use `not b != c` instead of `False if b != c else True
5 5 | a = False if b + c else True # SIM211
6 6 |
SIM211.py:5:5: SIM211 [*] Use `not b + c` instead of `False if b + c else True`
SIM211.py:5:5: SIM211 [*] Use `not ...` instead of `False if ... else True`
|
3 | a = False if b != c else True # SIM211
4 |
@@ -46,7 +46,7 @@ SIM211.py:5:5: SIM211 [*] Use `not b + c` instead of `False if b + c else True`
6 |
7 | a = True if b else False # OK
|
= help: Replace with `bool(b + c)
= help: Replace with `not ...`
Suggested fix
2 2 |

View File

@@ -534,7 +534,7 @@ SIM222.py:85:6: SIM222 [*] Use `True` instead of `... or True`
87 87 | a or (1,) or True or (2,) # SIM222
88 88 |
SIM222.py:87:6: SIM222 [*] Use `1,` instead of `1, or ...`
SIM222.py:87:6: SIM222 [*] Use `(1,)` instead of `(1,) or ...`
|
85 | a or tuple(()) or True # SIM222
86 |
@@ -543,14 +543,14 @@ SIM222.py:87:6: SIM222 [*] Use `1,` instead of `1, or ...`
88 |
89 | a or tuple((1,)) or True or tuple((2,)) # SIM222
|
= help: Replace with `1,`
= help: Replace with `(1,)`
Suggested fix
84 84 |
85 85 | a or tuple(()) or True # SIM222
86 86 |
87 |-a or (1,) or True or (2,) # SIM222
87 |+a or 1, # SIM222
87 |+a or (1,) # SIM222
88 88 |
89 89 | a or tuple((1,)) or True or tuple((2,)) # SIM222
90 90 |
@@ -1003,5 +1003,42 @@ SIM222.py:153:11: SIM222 [*] Use `[1]` instead of `[1] or ...`
153 |-if f(a or [1] or True or [2]): # SIM222
153 |+if f(a or [1]): # SIM222
154 154 | pass
155 155 |
156 156 | # Regression test for: https://github.com/astral-sh/ruff/issues/7099
SIM222.py:157:30: SIM222 [*] Use `(int, int, int)` instead of `(int, int, int) or ...`
|
156 | # Regression test for: https://github.com/astral-sh/ruff/issues/7099
157 | def secondToTime(s0: int) -> (int, int, int) or str:
| ^^^^^^^^^^^^^^^^^^^^^^ SIM222
158 | m, s = divmod(s0, 60)
|
= help: Replace with `(int, int, int)`
Suggested fix
154 154 | pass
155 155 |
156 156 | # Regression test for: https://github.com/astral-sh/ruff/issues/7099
157 |-def secondToTime(s0: int) -> (int, int, int) or str:
157 |+def secondToTime(s0: int) -> (int, int, int):
158 158 | m, s = divmod(s0, 60)
159 159 |
160 160 |
SIM222.py:161:31: SIM222 [*] Use `(int, int, int)` instead of `(int, int, int) or ...`
|
161 | def secondToTime(s0: int) -> ((int, int, int) or str):
| ^^^^^^^^^^^^^^^^^^^^^^ SIM222
162 | m, s = divmod(s0, 60)
|
= help: Replace with `(int, int, int)`
Suggested fix
158 158 | m, s = divmod(s0, 60)
159 159 |
160 160 |
161 |-def secondToTime(s0: int) -> ((int, int, int) or str):
161 |+def secondToTime(s0: int) -> ((int, int, int)):
162 162 | m, s = divmod(s0, 60)

View File

@@ -1,13 +1,13 @@
use ruff_python_ast::{self as ast, ElifElseClause, ExceptHandler, MatchCase, Stmt};
use ruff_text_size::{Ranged, TextRange, TextSize};
use std::iter::Peekable;
use std::slice;
use ruff_notebook::Notebook;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, ElifElseClause, ExceptHandler, MatchCase, Stmt};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::directives::IsortDirectives;
use crate::jupyter::Notebook;
use crate::rules::isort::helpers;
use crate::source_kind::SourceKind;
@@ -43,7 +43,7 @@ impl<'a> BlockBuilder<'a> {
locator: &'a Locator<'a>,
directives: &'a IsortDirectives,
is_stub: bool,
source_kind: Option<&'a SourceKind>,
source_kind: &'a SourceKind,
) -> Self {
Self {
locator,
@@ -53,7 +53,7 @@ impl<'a> BlockBuilder<'a> {
exclusions: &directives.exclusions,
nested: false,
cell_offsets: source_kind
.and_then(SourceKind::notebook)
.as_ipy_notebook()
.map(Notebook::cell_offsets)
.map(|offsets| offsets.iter().peekable()),
}

View File

@@ -13,7 +13,7 @@ pub(super) fn trailing_comma(
locator: &Locator,
source_type: PySourceType,
) -> TrailingComma {
let contents = locator.slice(stmt.range());
let contents = locator.slice(stmt);
let mut count = 0u32;
let mut trailing_comma = TrailingComma::Absent;
for (tok, _) in lexer::lex_starts_at(contents, source_type.as_mode(), stmt.start()).flatten() {

View File

@@ -52,5 +52,6 @@ pub mod pyflakes;
pub mod pygrep_hooks;
pub mod pylint;
pub mod pyupgrade;
pub mod refurb;
pub mod ruff;
pub mod tryceratops;

View File

@@ -1,10 +1,10 @@
use ruff_python_ast::{self as ast, Expr};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::registry::AsRule;
/// ## What it does
@@ -77,21 +77,15 @@ pub(crate) fn deprecated_function(checker: &mut Checker, expr: &Expr) {
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
match expr {
Expr::Name(_) => {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
replacement.to_string(),
expr.range(),
)));
}
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
replacement.to_string(),
attr.range(),
)));
}
_ => {}
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from("numpy", replacement),
expr.start(),
checker.semantic(),
)?;
let replacement_edit = Edit::range_replacement(binding, expr.range());
Ok(Fix::suggested_edits(import_edit, [replacement_edit]))
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,201 +1,225 @@
---
source: crates/ruff/src/rules/numpy/mod.rs
---
NPY003.py:3:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead
NPY003.py:4:5: NPY003 [*] `np.round_` is deprecated; use `np.round` instead
|
1 | import numpy as np
2 |
3 | np.round_(np.random.rand(5, 5), 2)
| ^^^^^^^^^ NPY003
4 | np.product(np.random.rand(5, 5))
5 | np.cumproduct(np.random.rand(5, 5))
2 | import numpy as np
3 |
4 | np.round_(np.random.rand(5, 5), 2)
| ^^^^^^^^^ NPY003
5 | np.product(np.random.rand(5, 5))
6 | np.cumproduct(np.random.rand(5, 5))
|
= help: Replace with `np.round`
Suggested fix
1 1 | import numpy as np
2 2 |
3 |-np.round_(np.random.rand(5, 5), 2)
3 |+np.round(np.random.rand(5, 5), 2)
4 4 | np.product(np.random.rand(5, 5))
5 5 | np.cumproduct(np.random.rand(5, 5))
6 6 | np.sometrue(np.random.rand(5, 5))
1 1 | def func():
2 2 | import numpy as np
3 3 |
4 |- np.round_(np.random.rand(5, 5), 2)
4 |+ np.round(np.random.rand(5, 5), 2)
5 5 | np.product(np.random.rand(5, 5))
6 6 | np.cumproduct(np.random.rand(5, 5))
7 7 | np.sometrue(np.random.rand(5, 5))
NPY003.py:4:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead
NPY003.py:5:5: NPY003 [*] `np.product` is deprecated; use `np.prod` instead
|
3 | np.round_(np.random.rand(5, 5), 2)
4 | np.product(np.random.rand(5, 5))
| ^^^^^^^^^^ NPY003
5 | np.cumproduct(np.random.rand(5, 5))
6 | np.sometrue(np.random.rand(5, 5))
4 | np.round_(np.random.rand(5, 5), 2)
5 | np.product(np.random.rand(5, 5))
| ^^^^^^^^^^ NPY003
6 | np.cumproduct(np.random.rand(5, 5))
7 | np.sometrue(np.random.rand(5, 5))
|
= help: Replace with `np.prod`
Suggested fix
1 1 | import numpy as np
2 2 |
3 3 | np.round_(np.random.rand(5, 5), 2)
4 |-np.product(np.random.rand(5, 5))
4 |+np.prod(np.random.rand(5, 5))
5 5 | np.cumproduct(np.random.rand(5, 5))
6 6 | np.sometrue(np.random.rand(5, 5))
7 7 | np.alltrue(np.random.rand(5, 5))
2 2 | import numpy as np
3 3 |
4 4 | np.round_(np.random.rand(5, 5), 2)
5 |- np.product(np.random.rand(5, 5))
5 |+ np.prod(np.random.rand(5, 5))
6 6 | np.cumproduct(np.random.rand(5, 5))
7 7 | np.sometrue(np.random.rand(5, 5))
8 8 | np.alltrue(np.random.rand(5, 5))
NPY003.py:5:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead
NPY003.py:6:5: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead
|
3 | np.round_(np.random.rand(5, 5), 2)
4 | np.product(np.random.rand(5, 5))
5 | np.cumproduct(np.random.rand(5, 5))
| ^^^^^^^^^^^^^ NPY003
6 | np.sometrue(np.random.rand(5, 5))
7 | np.alltrue(np.random.rand(5, 5))
4 | np.round_(np.random.rand(5, 5), 2)
5 | np.product(np.random.rand(5, 5))
6 | np.cumproduct(np.random.rand(5, 5))
| ^^^^^^^^^^^^^ NPY003
7 | np.sometrue(np.random.rand(5, 5))
8 | np.alltrue(np.random.rand(5, 5))
|
= help: Replace with `np.cumprod`
Suggested fix
2 2 |
3 3 | np.round_(np.random.rand(5, 5), 2)
4 4 | np.product(np.random.rand(5, 5))
5 |-np.cumproduct(np.random.rand(5, 5))
5 |+np.cumprod(np.random.rand(5, 5))
6 6 | np.sometrue(np.random.rand(5, 5))
7 7 | np.alltrue(np.random.rand(5, 5))
8 8 |
3 3 |
4 4 | np.round_(np.random.rand(5, 5), 2)
5 5 | np.product(np.random.rand(5, 5))
6 |- np.cumproduct(np.random.rand(5, 5))
6 |+ np.cumprod(np.random.rand(5, 5))
7 7 | np.sometrue(np.random.rand(5, 5))
8 8 | np.alltrue(np.random.rand(5, 5))
9 9 |
NPY003.py:6:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead
NPY003.py:7:5: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead
|
4 | np.product(np.random.rand(5, 5))
5 | np.cumproduct(np.random.rand(5, 5))
6 | np.sometrue(np.random.rand(5, 5))
| ^^^^^^^^^^^ NPY003
7 | np.alltrue(np.random.rand(5, 5))
5 | np.product(np.random.rand(5, 5))
6 | np.cumproduct(np.random.rand(5, 5))
7 | np.sometrue(np.random.rand(5, 5))
| ^^^^^^^^^^^ NPY003
8 | np.alltrue(np.random.rand(5, 5))
|
= help: Replace with `np.any`
Suggested fix
3 3 | np.round_(np.random.rand(5, 5), 2)
4 4 | np.product(np.random.rand(5, 5))
5 5 | np.cumproduct(np.random.rand(5, 5))
6 |-np.sometrue(np.random.rand(5, 5))
6 |+np.any(np.random.rand(5, 5))
7 7 | np.alltrue(np.random.rand(5, 5))
8 8 |
9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue
4 4 | np.round_(np.random.rand(5, 5), 2)
5 5 | np.product(np.random.rand(5, 5))
6 6 | np.cumproduct(np.random.rand(5, 5))
7 |- np.sometrue(np.random.rand(5, 5))
7 |+ np.any(np.random.rand(5, 5))
8 8 | np.alltrue(np.random.rand(5, 5))
9 9 |
10 10 |
NPY003.py:7:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead
NPY003.py:8:5: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead
|
5 | np.cumproduct(np.random.rand(5, 5))
6 | np.sometrue(np.random.rand(5, 5))
7 | np.alltrue(np.random.rand(5, 5))
| ^^^^^^^^^^ NPY003
8 |
9 | from numpy import round_, product, cumproduct, sometrue, alltrue
6 | np.cumproduct(np.random.rand(5, 5))
7 | np.sometrue(np.random.rand(5, 5))
8 | np.alltrue(np.random.rand(5, 5))
| ^^^^^^^^^^ NPY003
|
= help: Replace with `np.all`
Suggested fix
4 4 | np.product(np.random.rand(5, 5))
5 5 | np.cumproduct(np.random.rand(5, 5))
6 6 | np.sometrue(np.random.rand(5, 5))
7 |-np.alltrue(np.random.rand(5, 5))
7 |+np.all(np.random.rand(5, 5))
8 8 |
9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue
5 5 | np.product(np.random.rand(5, 5))
6 6 | np.cumproduct(np.random.rand(5, 5))
7 7 | np.sometrue(np.random.rand(5, 5))
8 |- np.alltrue(np.random.rand(5, 5))
8 |+ np.all(np.random.rand(5, 5))
9 9 |
10 10 |
11 11 | def func():
NPY003.py:11:1: NPY003 [*] `np.round_` is deprecated; use `np.round` instead
NPY003.py:14:5: NPY003 [*] `np.round_` is deprecated; use `np.round` instead
|
9 | from numpy import round_, product, cumproduct, sometrue, alltrue
10 |
11 | round_(np.random.rand(5, 5), 2)
| ^^^^^^ NPY003
12 | product(np.random.rand(5, 5))
13 | cumproduct(np.random.rand(5, 5))
12 | from numpy import round_, product, cumproduct, sometrue, alltrue
13 |
14 | round_(np.random.rand(5, 5), 2)
| ^^^^^^ NPY003
15 | product(np.random.rand(5, 5))
16 | cumproduct(np.random.rand(5, 5))
|
= help: Replace with `np.round`
Suggested fix
8 8 |
9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue
10 10 |
11 |-round_(np.random.rand(5, 5), 2)
11 |+round(np.random.rand(5, 5), 2)
12 12 | product(np.random.rand(5, 5))
13 13 | cumproduct(np.random.rand(5, 5))
14 14 | sometrue(np.random.rand(5, 5))
1 |+from numpy import round
1 2 | def func():
2 3 | import numpy as np
3 4 |
--------------------------------------------------------------------------------
11 12 | def func():
12 13 | from numpy import round_, product, cumproduct, sometrue, alltrue
13 14 |
14 |- round_(np.random.rand(5, 5), 2)
15 |+ round(np.random.rand(5, 5), 2)
15 16 | product(np.random.rand(5, 5))
16 17 | cumproduct(np.random.rand(5, 5))
17 18 | sometrue(np.random.rand(5, 5))
NPY003.py:12:1: NPY003 [*] `np.product` is deprecated; use `np.prod` instead
NPY003.py:15:5: NPY003 [*] `np.product` is deprecated; use `np.prod` instead
|
11 | round_(np.random.rand(5, 5), 2)
12 | product(np.random.rand(5, 5))
| ^^^^^^^ NPY003
13 | cumproduct(np.random.rand(5, 5))
14 | sometrue(np.random.rand(5, 5))
14 | round_(np.random.rand(5, 5), 2)
15 | product(np.random.rand(5, 5))
| ^^^^^^^ NPY003
16 | cumproduct(np.random.rand(5, 5))
17 | sometrue(np.random.rand(5, 5))
|
= help: Replace with `np.prod`
Suggested fix
9 9 | from numpy import round_, product, cumproduct, sometrue, alltrue
10 10 |
11 11 | round_(np.random.rand(5, 5), 2)
12 |-product(np.random.rand(5, 5))
12 |+prod(np.random.rand(5, 5))
13 13 | cumproduct(np.random.rand(5, 5))
14 14 | sometrue(np.random.rand(5, 5))
15 15 | alltrue(np.random.rand(5, 5))
1 |+from numpy import prod
1 2 | def func():
2 3 | import numpy as np
3 4 |
--------------------------------------------------------------------------------
12 13 | from numpy import round_, product, cumproduct, sometrue, alltrue
13 14 |
14 15 | round_(np.random.rand(5, 5), 2)
15 |- product(np.random.rand(5, 5))
16 |+ prod(np.random.rand(5, 5))
16 17 | cumproduct(np.random.rand(5, 5))
17 18 | sometrue(np.random.rand(5, 5))
18 19 | alltrue(np.random.rand(5, 5))
NPY003.py:13:1: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead
NPY003.py:16:5: NPY003 [*] `np.cumproduct` is deprecated; use `np.cumprod` instead
|
11 | round_(np.random.rand(5, 5), 2)
12 | product(np.random.rand(5, 5))
13 | cumproduct(np.random.rand(5, 5))
| ^^^^^^^^^^ NPY003
14 | sometrue(np.random.rand(5, 5))
15 | alltrue(np.random.rand(5, 5))
14 | round_(np.random.rand(5, 5), 2)
15 | product(np.random.rand(5, 5))
16 | cumproduct(np.random.rand(5, 5))
| ^^^^^^^^^^ NPY003
17 | sometrue(np.random.rand(5, 5))
18 | alltrue(np.random.rand(5, 5))
|
= help: Replace with `np.cumprod`
Suggested fix
10 10 |
11 11 | round_(np.random.rand(5, 5), 2)
12 12 | product(np.random.rand(5, 5))
13 |-cumproduct(np.random.rand(5, 5))
13 |+cumprod(np.random.rand(5, 5))
14 14 | sometrue(np.random.rand(5, 5))
15 15 | alltrue(np.random.rand(5, 5))
1 |+from numpy import cumprod
1 2 | def func():
2 3 | import numpy as np
3 4 |
--------------------------------------------------------------------------------
13 14 |
14 15 | round_(np.random.rand(5, 5), 2)
15 16 | product(np.random.rand(5, 5))
16 |- cumproduct(np.random.rand(5, 5))
17 |+ cumprod(np.random.rand(5, 5))
17 18 | sometrue(np.random.rand(5, 5))
18 19 | alltrue(np.random.rand(5, 5))
NPY003.py:14:1: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead
NPY003.py:17:5: NPY003 [*] `np.sometrue` is deprecated; use `np.any` instead
|
12 | product(np.random.rand(5, 5))
13 | cumproduct(np.random.rand(5, 5))
14 | sometrue(np.random.rand(5, 5))
| ^^^^^^^^ NPY003
15 | alltrue(np.random.rand(5, 5))
15 | product(np.random.rand(5, 5))
16 | cumproduct(np.random.rand(5, 5))
17 | sometrue(np.random.rand(5, 5))
| ^^^^^^^^ NPY003
18 | alltrue(np.random.rand(5, 5))
|
= help: Replace with `np.any`
Suggested fix
11 11 | round_(np.random.rand(5, 5), 2)
12 12 | product(np.random.rand(5, 5))
13 13 | cumproduct(np.random.rand(5, 5))
14 |-sometrue(np.random.rand(5, 5))
14 |+any(np.random.rand(5, 5))
15 15 | alltrue(np.random.rand(5, 5))
1 |+from numpy import any
1 2 | def func():
2 3 | import numpy as np
3 4 |
--------------------------------------------------------------------------------
14 15 | round_(np.random.rand(5, 5), 2)
15 16 | product(np.random.rand(5, 5))
16 17 | cumproduct(np.random.rand(5, 5))
17 |- sometrue(np.random.rand(5, 5))
18 |+ any(np.random.rand(5, 5))
18 19 | alltrue(np.random.rand(5, 5))
NPY003.py:15:1: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead
NPY003.py:18:5: NPY003 [*] `np.alltrue` is deprecated; use `np.all` instead
|
13 | cumproduct(np.random.rand(5, 5))
14 | sometrue(np.random.rand(5, 5))
15 | alltrue(np.random.rand(5, 5))
| ^^^^^^^ NPY003
16 | cumproduct(np.random.rand(5, 5))
17 | sometrue(np.random.rand(5, 5))
18 | alltrue(np.random.rand(5, 5))
| ^^^^^^^ NPY003
|
= help: Replace with `np.all`
Suggested fix
12 12 | product(np.random.rand(5, 5))
13 13 | cumproduct(np.random.rand(5, 5))
14 14 | sometrue(np.random.rand(5, 5))
15 |-alltrue(np.random.rand(5, 5))
15 |+all(np.random.rand(5, 5))
1 |+from numpy import all
1 2 | def func():
2 3 | import numpy as np
3 4 |
--------------------------------------------------------------------------------
15 16 | product(np.random.rand(5, 5))
16 17 | cumproduct(np.random.rand(5, 5))
17 18 | sometrue(np.random.rand(5, 5))
18 |- alltrue(np.random.rand(5, 5))
19 |+ all(np.random.rand(5, 5))

View File

@@ -1,7 +1,8 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{self as ast, Keyword};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Keyword, Stmt};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
@@ -75,13 +76,17 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
// the star argument _doesn't_ contain an override).
// 2. The call is part of a larger expression (we're converting an expression to a
// statement, and expressions can't contain statements).
let statement = checker.semantic().current_statement();
if !seen_star
&& checker.semantic().current_statement().is_expr_stmt()
&& checker.semantic().current_expression_parent().is_none()
&& statement.is_expr_stmt()
{
if let Some(fix) =
convert_inplace_argument_to_assignment(call, keyword, checker.locator())
{
if let Some(fix) = convert_inplace_argument_to_assignment(
call,
keyword,
statement,
checker.locator(),
) {
diagnostic.set_fix(fix);
}
}
@@ -101,13 +106,16 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
fn convert_inplace_argument_to_assignment(
call: &ast::ExprCall,
keyword: &Keyword,
statement: &Stmt,
locator: &Locator,
) -> Option<Fix> {
// Add the assignment.
let attr = call.func.as_attribute_expr()?;
let insert_assignment = Edit::insertion(
format!("{name} = ", name = locator.slice(attr.value.range())),
call.start(),
parenthesized_range(call.into(), statement.into(), locator.contents())
.unwrap_or(call.range())
.start(),
);
// Remove the `inplace` argument.

View File

@@ -158,4 +158,20 @@ PD002.py:28:38: PD002 `inplace=True` should be avoided; it has inconsistent beha
|
= help: Assign to variable; remove `inplace` arg
PD002.py:33:24: PD002 [*] `inplace=True` should be avoided; it has inconsistent behavior
|
31 | torch.m.ReLU(inplace=True) # safe because this isn't a pandas call
32 |
33 | (x.drop(["a"], axis=1, inplace=True))
| ^^^^^^^^^^^^ PD002
|
= help: Assign to variable; remove `inplace` arg
Suggested fix
30 30 |
31 31 | torch.m.ReLU(inplace=True) # safe because this isn't a pandas call
32 32 |
33 |-(x.drop(["a"], axis=1, inplace=True))
33 |+x = (x.drop(["a"], axis=1))

View File

@@ -22,6 +22,7 @@ pub(super) fn is_acronym(name: &str, asname: &str) -> bool {
name.chars().filter(|c| c.is_uppercase()).join("") == asname
}
/// Returns `true` if the statement is an assignment to a named tuple.
pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool {
let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else {
return false;
@@ -30,13 +31,12 @@ pub(super) fn is_named_tuple_assignment(stmt: &Stmt, semantic: &SemanticModel) -
return false;
};
semantic.resolve_call_path(func).is_some_and(|call_path| {
matches!(
call_path.as_slice(),
["collections", "namedtuple"] | ["typing", "NamedTuple"]
)
matches!(call_path.as_slice(), ["collections", "namedtuple"])
|| semantic.match_typing_call_path(&call_path, "NamedTuple")
})
}
/// Returns `true` if the statement is an assignment to a `TypedDict`.
pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool {
let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else {
return false;
@@ -44,11 +44,10 @@ pub(super) fn is_typed_dict_assignment(stmt: &Stmt, semantic: &SemanticModel) ->
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
return false;
};
semantic
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["typing", "TypedDict"]))
semantic.match_typing_expr(func, "TypedDict")
}
/// Returns `true` if the statement is an assignment to a `TypeVar` or `NewType`.
pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool {
let Stmt::Assign(ast::StmtAssign { value, .. }) = stmt else {
return false;
@@ -56,9 +55,18 @@ pub(super) fn is_type_var_assignment(stmt: &Stmt, semantic: &SemanticModel) -> b
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
return false;
};
semantic
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["typing", "TypeVar" | "NewType"]))
semantic.resolve_call_path(func).is_some_and(|call_path| {
semantic.match_typing_call_path(&call_path, "TypeVar")
|| semantic.match_typing_call_path(&call_path, "NewType")
})
}
/// Returns `true` if the statement is an assignment to a `TypeAlias`.
pub(super) fn is_type_alias_assignment(stmt: &Stmt, semantic: &SemanticModel) -> bool {
let Stmt::AnnAssign(ast::StmtAnnAssign { annotation, .. }) = stmt else {
return false;
};
semantic.match_typing_expr(annotation, "TypeAlias")
}
pub(super) fn is_typed_dict_class(arguments: Option<&Arguments>, semantic: &SemanticModel) -> bool {

View File

@@ -3,6 +3,7 @@ use ruff_python_ast::Stmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Scope, ScopeKind};
use crate::settings::types::IdentifierPattern;
@@ -51,7 +52,7 @@ pub(crate) fn dunder_function_name(
if matches!(scope.kind, ScopeKind::Class(_)) {
return None;
}
if !(name.starts_with("__") && name.ends_with("__")) {
if !visibility::is_magic(name) {
return None;
}
// Allowed under PEP 562 (https://peps.python.org/pep-0562/).

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