Compare commits

...

64 Commits

Author SHA1 Message Date
Charlie Marsh
8fbec8e6a2 Update docs to match updated logo and color palette 2023-06-21 20:48:29 -04:00
Charlie Marsh
ac146e11f0 Allow typing.Final for mutable-class-default annotations (RUF012) (#5274)
## Summary

See: https://github.com/astral-sh/ruff/issues/5243.
2023-06-22 00:24:53 +00:00
Charlie Marsh
1229600e1d Ignore Pydantic classes when evaluating mutable-class-default (RUF012) (#5273)
Closes https://github.com/astral-sh/ruff/issues/5272.
2023-06-21 23:59:44 +00:00
Micha Reiser
ccf34aae8c Format Attribute Expression (#5259) 2023-06-21 21:33:53 +00:00
Tom Kuson
341b12d918 Complete documentation for Ruff-specific rules (#5262)
## Summary

Completes the documentation for the Ruff-specific ruleset. Related to
#2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-21 21:30:44 +00:00
Micha Reiser
3d7411bfaf Use trait for labels instead of TypeId (#5270) 2023-06-21 22:26:09 +01:00
David Szotten
1eccbbb60e Format StmtFor (#5163)
<!--
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

format StmtFor

still trying to learn how to help out with the formatter. trying
something slightly more advanced than [break](#5158)

mostly copied form StmtWhile

## Test Plan

snapshots
2023-06-21 23:00:31 +02:00
Charlie Marsh
e71f044f0d Avoid including nursery rules in linter-level selectors (#5268)
## Summary

Ensures that `--select PL` and `--select PLC` don't include `PLC1901`.
Previously, `--select PL` _did_, because it's a "linter-level selector"
(`--select PLC` is viewed as selecting the `C` prefix from `PL`), and we
were missing this filtering path.
2023-06-21 20:11:40 +00:00
James Berry
f194572be8 Remove visit_arg_with_default (#5265)
## Summary

This is a follow up to #5221. Turns out it was easy to restructure the
visitor to get the right order, I'm just dumb 🤷‍♂️ I've
removed `visit_arg_with_default` entirely from the `Visitor`, although
it still exists as part of `preorder::Visitor`.
2023-06-21 16:00:24 -04:00
Charlie Marsh
62e2c46f98 Move compare-to-empty-string to nursery (#5264)
## Summary

This rule has too many false positives. It has parity with the Pylint
version, but the Pylint version is part of an
[extension](https://pylint.readthedocs.io/en/stable/user_guide/messages/convention/compare-to-empty-string.html),
and so requires explicit opt-in.

I'm moving this rule to the nursery to require explicit opt-in, as with
Pylint.

Closes #4282.
2023-06-21 19:47:02 +00:00
konstin
9419d3f9c8 Special ExprTuple formatting option for for-loops (#5175)
## Motivation

While black keeps parentheses nearly everywhere, the notable exception
is in the body of for loops:
```python
for (a, b) in x:
    pass
```
becomes
```python
for a, b in x:
    pass
```

This currently blocks #5163, which this PR should unblock.

## Solution

This changes the `ExprTuple` formatting option to include one additional
option that removes the parentheses when not using magic trailing comma
and not breaking. It is supposed to be used through
```rust
#[derive(Debug)]
struct ExprTupleWithoutParentheses<'a>(&'a Expr);

impl Format<PyFormatContext<'_>> for ExprTupleWithoutParentheses<'_> {
    fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
        match self.0 {
            Expr::Tuple(expr_tuple) => expr_tuple
                .format()
                .with_options(TupleParentheses::StripInsideForLoop)
                .fmt(f),
            other => other.format().with_options(Parenthesize::IfBreaks).fmt(f),
        }
    }
}
```


## Testing

The for loop formatting isn't merged due to missing this (and i didn't
want to create more git weirdness across two people), but I've confirmed
that when applying this to while loops instead of for loops, then
```rust
        write!(
            f,
            [
                text("while"),
                space(),
                ExprTupleWithoutParentheses(test.as_ref()),
                text(":"),
                trailing_comments(trailing_condition_comments),
                block_indent(&body.format())
            ]
        )?;
```
makes
```python
while (a, b):
    pass

while (
    ajssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssa,
    b,
):
    pass

while (a,b,):
    pass
```
formatted as
```python
while a, b:
    pass

while (
    ajssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssa,
    b,
):
    pass

while (
    a,
    b,
):
    pass
```
2023-06-21 21:17:47 +02:00
James Berry
9b5fb8f38f Fix AST visitor traversal order (#5221)
## Summary

According to the AST visitor documentation, the AST visitor "visits all
nodes in the AST recursively in evaluation-order". However, the current
traversal fails to meet this specification in a few places.

### Function traversal

```python
order = []
@(order.append("decorator") or (lambda x: x))
def f(
    posonly: order.append("posonly annotation") = order.append("posonly default"),
    /,
    arg: order.append("arg annotation") = order.append("arg default"),
    *args: order.append("vararg annotation"),
    kwarg: order.append("kwarg annotation") = order.append("kwarg default"),
    **kwargs: order.append("kwarg annotation")
) -> order.append("return annotation"):
    pass
print(order)
```

Executing the above snippet using CPython 3.10.6 prints the following
result (formatted for readability):
```python
[
    'decorator',
    'posonly default',
    'arg default',
    'kwarg default',
    'arg annotation',
    'posonly annotation',
    'vararg annotation',
    'kwarg annotation',
    'kwarg annotation',
    'return annotation',
]
```

Here we can see that decorators are evaluated first, followed by
argument defaults, and annotations are last. The current traversal of a
function's AST does not align with this order.

### Annotated assignment traversal
```python
order = []
x: order.append("annotation") = order.append("expression")
print(order)
```

Executing the above snippet using CPython 3.10.6 prints the following
result:
```python
['expression', 'annotation']
```

Here we can see that an annotated assignments annotation gets evaluated
after the assignment's expression. The current traversal of an annotated
assignment's AST does not align with this order.

## Why?

I'm slowly working on #3946 and porting over some of the logic and tests
from ssort. ssort is very sensitive to AST traversal order, so ensuring
the utmost correctness here is important.

## Test Plan

There doesn't seem to be existing tests for the AST visitor, so I didn't
bother adding tests for these very subtle changes. However, this
behavior will be captured in the tests for the PR which addresses #3946.
2023-06-21 14:40:58 -04:00
konstin
d7c7484618 Format function argument separator comments (#5211)
## Summary

This is a complete rewrite of the handling of `/` and `*` comment
handling in function signatures. The key problem is that slash and star
don't have a note. We now parse out the positions of slash and star and
their respective preceding and following note. I've left code comments
for each possible case of function signature structure and comment
placement

## Test Plan

I extended the function statement fixtures with cases that i found. If
you have more weird edge cases your input would be appreciated.
2023-06-21 17:56:47 +00:00
konstin
bc63cc9b3c Fix remaining CPython formatter errors except for function argument separator comments (#5210)
## Summary

This fixes two problems discovered when trying to format the cpython
repo with `cargo run --bin ruff_dev -- check-formatter-stability
projects/cpython`:

The first is to ignore try/except trailing comments for now since they
lead to unstable formatting on the dummy.

The second is to avoid dropping trailing if comments through placement:
This changes the placement to keep a comment trailing an if-elif or
if-elif-else to keep the comment a trailing comment on the entire if.
Previously the last comment would have been lost.
```python
if "first if":
    pass
elif "first elif":
    pass
```

The last remaining problem in cpython so far is function signature
argument separator comment placement which is its own PR on top of this.

## Test Plan

I added test fixtures of minimized examples with links back to the
original cpython location
2023-06-21 19:45:53 +02:00
Charlie Marsh
bf1a94ee54 Initialize caches for packages and standalone files (#5237)
## Summary

While fixing https://github.com/astral-sh/ruff/pull/5233, I noticed that
in FastAPI, 343 out of 823 files weren't hitting the cache. It turns out
these are standalone files in the documentation that lack a "package
root". Later, when looking up the cache entries, we fallback to the
package directory.

This PR ensures that we initialize the cache for both kinds of files:
those that are in a package, and those that aren't.

The total size of the FastAPI cache for me is now 388K. I also suspect
that this approach is much faster than as initially written, since
before, we were probably initializing one cache per _directory_.

## Test Plan

Ran `cargo run -p ruff_cli -- check ../fastapi --verbose`; verified
that, on second execution, there were no "Checking" entries in the logs.
2023-06-21 17:29:09 +00:00
Dhruv Manilawala
c792c10eaa Add support for nested quoted annotations in RUF013 (#5254)
## Summary

This is a follow up on #5235 to add support for nested quoted
annotations for RUF013.

## Test Plan

`cargo test`
2023-06-21 17:25:27 +00:00
Evan Rittenhouse
f9ffb3d50d Add Applicability to pylint (#5251) 2023-06-21 17:22:01 +00:00
Evan Rittenhouse
2b76d88bd3 Add Applicability to pandas_vet (#5252) 2023-06-21 17:12:47 +00:00
Evan Rittenhouse
41ef17b007 Add Applicability to pyflakes (#5253) 2023-06-21 17:04:55 +00:00
Charlie Marsh
0aa21277c6 Improve documentation for overlong-line rules (#5260)
Closes https://github.com/astral-sh/ruff/issues/5248.
2023-06-21 17:02:20 +00:00
Charlie Marsh
ecf61d49fa Restore existing bindings when unbinding caught exceptions (#5256)
## Summary

In the latest release, we made some improvements to the semantic model,
but our modifications to exception-unbinding are causing some
false-positives. For example:

```py
try:
    v = 3
except ImportError as v:
    print(v)
else:
    print(v)
```

In the latest release, we started unbinding `v` after the `except`
handler. (We used to restore the existing binding, the `v = 3`, but this
was quite complicated.) Because we don't have full branch analysis, we
can't then know that `v` is still bound in the `else` branch.

The solution here modifies `resolve_read` to skip-lookup when hitting
unbound exceptions. So when store the "unbind" for `except ImportError
as v`, we save the binding that it shadowed `v = 3`, and skip to that.

Closes #5249.

Closes #5250.
2023-06-21 12:53:58 -04:00
Charlie Marsh
d99b3bf661 Add some projects to the ecosystem CI check (#5258) 2023-06-21 12:42:58 -04:00
Micha Reiser
e47aa468d5 Format Identifier (#5255) 2023-06-21 17:35:37 +02:00
konstin
6155fd647d Format Slice Expressions (#5047)
This formats slice expressions and subscript expressions.

Spaces around the colons follows the same rules as black
(https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices):
```python
e00 = "e"[:]
e01 = "e"[:1]
e02 = "e"[: a()]
e10 = "e"[1:]
e11 = "e"[1:1]
e12 = "e"[1 : a()]
e20 = "e"[a() :]
e21 = "e"[a() : 1]
e22 = "e"[a() : a()]
e200 = "e"[a() : :]
e201 = "e"[a() :: 1]
e202 = "e"[a() :: a()]
e210 = "e"[a() : 1 :]
```

Comment placement is different due to our very different infrastructure.
If we have explicit bounds (e.g. `x[1:2]`) all comments get assigned as
leading or trailing to the bound expression. If a bound is missing
`[:]`, comments get marked as dangling and placed in the same section as
they were originally in:
```python
x = "x"[ # a
      # b
    :  # c
      # d
]
```
to
```python
x = "x"[
    # a
    # b
    :
    # c
    # d
]
```
Except for the potential trailing end-of-line comments, all comments get
formatted on their own line. This can be improved by keeping end-of-line
comments after the opening bracket or after a colon as such but the
changes were already complex enough.

I added tests for comment placement and spaces.
2023-06-21 15:09:39 +00:00
Charlie Marsh
4634560c80 Ensure release tagging has access to repo clone (#5240)
## Summary

The [release
failed](https://github.com/astral-sh/ruff/actions/runs/5329733171/jobs/9656004063),
but late enough that I was able to do the remaining steps manually. The
issue here is that the tagging step requires that we clone the repo. I
split the upload (to PyPI), tagging (in Git), and publishing (to GitHub
Releases) phases into their own steps, since they need different
resources + permissions anyway.
2023-06-21 10:24:33 -04:00
Charlie Marsh
10885d09a1 Add support for top-level quoted annotations in RUF013 (#5235)
## Summary

This PR adds support for autofixing annotations like:

```python
def f(x: "int" = None):
    ...
```

However, we don't yet support nested quotes, like:

```python
def f(x: Union["int", "str"] = None):
    ...
```

Closes #5231.
2023-06-21 10:23:37 -04:00
konstin
44156f6962 Improve debuggability of place_comment (#5209)
## Summary

I found it hard to figure out which function decides placement for a
specific comment. An explicit loop makes this easier to debug

## Test Plan

There should be no functional changes, no changes to the formatting of
the fixtures.
2023-06-21 09:52:13 +00:00
konstin
f551c9aad2 Unify benchmarking and profiling docs (#5145)
This moves all docs about benchmarking and profiling into
CONTRIBUTING.md by moving the readme of `ruff_benchmark` and adding more
information on profiling.

We need to somehow consolidate that documentation, but i'm not convinced
that this is the best way (i tried subpages in mkdocs, but that didn't
seem good either), so i'm happy to take suggestions.
2023-06-21 09:39:56 +00:00
Micha Reiser
653dbb6d17 Format BoolOp (#4986) 2023-06-21 09:27:57 +00:00
konstin
db301c14bd Consistently name comment own line/end-of-line line_position() (#5215)
## Summary

Previously, `DecoratedComment` used `text_position()` and
`SourceComment` used `position()`. This PR unifies this to
`line_position` everywhere.

## Test Plan

This is a rename refactoring.
2023-06-21 11:04:56 +02:00
Micha Reiser
1336ca601b Format UnaryExpr
<!--
Thank you for contributing to Ruff! To help us out with reviewing, please consider the following:

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

## Summary

This PR adds basic formatting for unary expressions.

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

## Test Plan

I added a new `unary.py` with custom test cases
2023-06-21 10:09:47 +02:00
Micha Reiser
3973836420 Correctly handle left/right breaking of binary expression
<!--
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
Black supports for layouts when it comes to breaking binary expressions:

```rust
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BinaryLayout {
    /// Put each operand on their own line if either side expands
    Default,

    /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit.
    ///
    ///```python
    /// [
    ///     a,
    ///     b
    /// ] & c
    ///```
    ExpandLeft,

    /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit.
    ///
    /// ```python
    /// a & [
    ///     b,
    ///     c
    /// ]
    /// ```
    ExpandRight,

    /// Both the left and right side can be expanded. Try in the following order:
    /// * expand the right side
    /// * expand the left side
    /// * expand both sides
    ///
    /// to make the expression fit
    ///
    /// ```python
    /// [
    ///     a,
    ///     b
    /// ] & [
    ///     c,
    ///     d
    /// ]
    /// ```
    ExpandRightThenLeft,
}
```

Our current implementation only handles `ExpandRight` and `Default` correctly. This PR adds support for `ExpandRightThenLeft` and `ExpandLeft`. 

## Test Plan

I added tests that play through all 4 binary expression layouts.
2023-06-21 09:40:05 +02:00
Charlie Marsh
a332f078db Checkout repo to support release tag validation (#5238)
## Summary

The
[release](https://github.com/astral-sh/ruff/actions/runs/5329340068/jobs/9655224008)
failed due to an inability to find `pyproject.toml`. This PR moves that
validation into its own step (so we can fail fast) and ensures we clone
the repo.
2023-06-21 03:16:35 +00:00
Charlie Marsh
e0339b538b Bump version to 0.0.274 (#5230) 2023-06-20 22:12:32 -04:00
Charlie Marsh
07b6b7401f Move copyright rules to flake8_copyright module (#5236)
## Summary

I initially wanted this category to be more general and decoupled from
the plugin, but I got some feedback that the titling felt inconsistent
with others.
2023-06-21 01:56:40 +00:00
Charlie Marsh
1db7d9e759 Avoid erroneous RUF013 violations for quoted annotations (#5234)
## Summary

Temporary fix for #5231: if we can't flag and fix these properly, just
disabling them for now.

\cc @dhruvmanila 

## Test Plan

`cargo test`
2023-06-21 01:29:12 +00:00
Charlie Marsh
621e9ace88 Use package roots rather than package members for cache initialization (#5233)
## Summary

This is a proper fix for the issue patched-over in
https://github.com/astral-sh/ruff/pull/5229, thanks to an extremely
helpful repro from @tlambert03 in that thread. It looks like we were
using the keys of `package_roots` rather than the values to initialize
the cache -- but it's a map from package to package root.

## Test Plan

Reverted #5229, then ran through the plan that @tlambert03 included in
https://github.com/astral-sh/ruff/pull/5229#issuecomment-1599723226.
Verified the panic before but not after this change.
2023-06-20 21:21:45 -04:00
Charlie Marsh
f9f77cf617 Revert change to RUF010 to remove unnecessary str calls (#5232)
## Summary

This PR reverts #4971 (aba073a791). It
turns out that `f"{str(x)}"` and `f"{x}"` are often but not exactly
equivalent, and performing that conversion automatically can lead to
subtle bugs, See the discussion in
https://github.com/astral-sh/ruff/issues/4958.
2023-06-20 21:15:17 -04:00
Charlie Marsh
1a2bd984f2 Avoid .unwrap() on cache access (#5229)
## Summary

I haven't been able to determine why / when this is happening, but in
some cases, users are reporting that this `unwrap()` is causing a panic.
It's fine to just return `None` here and fallback to "No cache",
certainly better than panicking (while we figure out the edge case).

Closes #5225.

Closes #5228.
2023-06-20 19:01:21 -04:00
Tom Kuson
4717d0779f Complete flake8-debugger documentation (#5223)
## Summary

Completes the documentation for the `flake8-debugger` ruleset. Related
to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-20 21:04:32 +00:00
Florian Stasse
07409ce201 Fixed typo in numpy deprecated type alias rule documentation (#5224)
## Summary

It is a very simple typo fix in the "numy deprecated type alias"
documentation.
2023-06-20 16:51:51 -04:00
Addison Crump
2c0ec97782 Use cpython with fuzzer corpus (#5183)
Following #5055, add cpython as a member of the fuzzer corpus
unconditionally.
2023-06-20 16:51:06 -04:00
Micha Reiser
e520a3a721 Fix ArgWithDefault comments handling (#5204) 2023-06-20 20:48:07 +00:00
Charlie Marsh
fde5dbc9aa Bump version to 0.0.273 (#5218) 2023-06-20 14:37:28 -04:00
konstin
b4bd5a5acb Make the release workflow more resilient (#4728)
## Summary

Currently, it is possible to create a tag and then have the release
fail, which is a problem since we can't edit the tag
(https://github.com/charliermarsh/ruff/issues/4468). This change the
release process so that the tag is created inside the release workflow.
This leaves as a failure mode that we have published to pypi but then
creating the tag or GitHub release doesn't work, but in this case we can
restart and the pypi upload is just skipped because we use the skip
existing option.

The release workflow is started by a workflow dispatch with the tag
instead of creating the tag yourself. You can start the release workflow
without a tag to do a dry run which does not publish an artifacts. You
can optionally add a git sha to the workflow run and it will verify that
the release runs on the mentioned commit.

This also adds docs on how to release and a small style improvement for
the maturin integration.

## Test Plan

Testing is hard since we can't do real releases, i've tested a minimized
workflow in a separate dummy repository.
2023-06-20 18:33:09 +00:00
konstin
acb23dce3c Fix subprocess.run on Windows Python 3.7 (#5220)
## Summary

From the [subprocess
docs](https://docs.python.org/3/library/subprocess.html#subprocess.Popen):

> Changed in version 3.6: args parameter accepts a path-like object if
shell is False and a sequence containing path-like objects on POSIX.
>
> Changed in version 3.8: args parameter accepts a path-like object if
shell is False and a sequence containing bytes and path-like objects on
Windows.

We want to support python 3.7 on windows, so we need to convert the
`Path` into a `str`
2023-06-20 13:53:32 -04:00
Charlie Marsh
30734f06fd Support parenthesized expressions when splitting compound assertions (#5219)
## Summary

I'm looking into the Black stability tests, and here's one failing case.

We split `assert a and (b and c)` into:

```python
assert a
assert (b and c)
```

We fail to split `assert (b and c)` due to the parentheses. But Black
then removes then, and when running Ruff again, we get:

```python
assert a
assert b
assert c
```

This PR just enables us to fix to this in one pass.
2023-06-20 13:47:01 -04:00
Charlie Marsh
4547002eb7 Remove defaults from fixtures/pyproject.toml (#5217)
## Summary

These should be encoded in the tests themselves, rather than here. In
fact, I think they're all unused?
2023-06-20 13:16:00 -04:00
Charlie Marsh
310abc769d Move StarImport to its own module (#5186) 2023-06-20 13:12:46 -04:00
Micha Reiser
b369288833 Accept any Into<AnyNodeRef> as Comments arguments (#5205) 2023-06-20 16:49:21 +00:00
Dhruv Manilawala
6f7d3cc798 Add option (-o/--output-file) to write output to a file (#4950)
## Summary

A new CLI option (`-o`/`--output-file`) to write output to a file
instead of stdout.

Major change is to remove the lock acquired on stdout. The argument is
that the output is buffered and thus the lock is acquired only when
writing a block (8kb). As per the benchmark below there is a slight
performance penalty.

Reference:
https://rustmagazine.org/issue-3/javascript-compiler/#printing-is-slow

## Benchmarks

_Output is truncated to only contain useful information:_

Command: `check --isolated --no-cache --select=ALL --show-source
./test-repos/cpython"`

Latest HEAD (361d45f2b2) with and without
the manual lock on stdout:

```console
Benchmark 1: With lock
  Time (mean ± σ):      5.687 s ±  0.075 s    [User: 17.110 s, System: 0.486 s]
  Range (min … max):    5.615 s …  5.860 s    10 runs

Benchmark 2: Without lock
  Time (mean ± σ):      5.719 s ±  0.064 s    [User: 17.095 s, System: 0.491 s]
  Range (min … max):    5.640 s …  5.865 s    10 runs

Summary
  (1) ran 1.01 ± 0.02 times faster than (2)
```

This PR:

```console
Benchmark 1: This PR
  Time (mean ± σ):      5.855 s ±  0.058 s    [User: 17.197 s, System: 0.491 s]
  Range (min … max):    5.786 s …  5.987 s    10 runs
 
Benchmark 2: Latest HEAD with lock
  Time (mean ± σ):      5.645 s ±  0.033 s    [User: 16.922 s, System: 0.495 s]
  Range (min … max):    5.600 s …  5.712 s    10 runs
 
Summary
  (2) ran 1.04 ± 0.01 times faster than (1)
```

## Test Plan

Run all of the commands which gives output with and without the
`--output-file=ruff.out` option:
* `--show-settings`
* `--show-files`
* `--show-fixes`
* `--diff`
* `--select=ALL`
* `--select=All --show-source`
* `--watch` (only stdout allowed)

resolves: #4754
2023-06-20 22:16:49 +05:30
Micha Reiser
d9e59b21cd Add BestFittingMode (#5184)
## Summary
Black supports for layouts when it comes to breaking binary expressions:

```rust
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BinaryLayout {
    /// Put each operand on their own line if either side expands
    Default,

    /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit.
    ///
    ///```python
    /// [
    ///     a,
    ///     b
    /// ] & c
    ///```
    ExpandLeft,

    /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit.
    ///
    /// ```python
    /// a & [
    ///     b,
    ///     c
    /// ]
    /// ```
    ExpandRight,

    /// Both the left and right side can be expanded. Try in the following order:
    /// * expand the right side
    /// * expand the left side
    /// * expand both sides
    ///
    /// to make the expression fit
    ///
    /// ```python
    /// [
    ///     a,
    ///     b
    /// ] & [
    ///     c,
    ///     d
    /// ]
    /// ```
    ExpandRightThenLeft,
}
```

Our current implementation only handles `ExpandRight` and `Default` correctly. `ExpandLeft` turns out to be surprisingly hard. This PR adds a new `BestFittingMode` parameter to `BestFitting` to support `ExpandLeft`.

There are 3 variants that `ExpandLeft` must support:

**Variant 1**: Everything fits on the line (easy)

```python
[a, b] + c
```

**Variant 2**: Left breaks, but right fits on the line. Doesn't need parentheses

```python
[
	a,
	b
] + c
```

**Variant 3**: The left breaks, but there's still not enough space for the right hand side. Parenthesize the whole expression:

```python
(
	[
		a, 
		b
	]
	+ ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
)
```

Solving Variant 1 and 2 on their own is straightforward The printer gives us this behavior by nesting right inside of the group of left:

```
group(&format_args![
	if_group_breaks(&text("(")),
	soft_block_indent(&group(&format_args![
		left, 
		soft_line_break_or_space(), 
		op, 
		space(), 
		group(&right)
	])),
	if_group_breaks(&text(")"))
])
```

The fundamental problem is that the outer group, which adds the parentheses, always breaks if the left side breaks. That means, we end up with

```python
(
	[
		a,
		b
	] + c
)
```

which is not what we want (we only want parentheses if the right side doesn't fit). 

Okay, so nesting groups don't work because of the outer parentheses. Sequencing groups doesn't work because it results in a right-to-left breaking which is the opposite of what we want. 

Could we use best fitting? Almost! 

```
best_fitting![
	// All flat
	format_args![left, space(), op, space(), right],
	// Break left
	format_args!(group(&left).should_expand(true), space(), op, space(), right],
	// Break all
	format_args![
		text("("), 
		block_indent!(&format_args![
			left, 
			hard_line_break(), 
			op,
			space()
			right
		])
	]
]
```

I hope I managed to write this up correctly. The problem is that the printer never reaches the 3rd variant because the second variant always fits:

* The `group(&left).should_expand(true)` changes the group so that all `soft_line_breaks` are turned into hard line breaks. This is necessary because we want to test if the content fits if we break after the `[`. 
* Now, the whole idea of `best_fitting` is that you can pretend that some content fits on the line when it actually does not. The way this works is that the printer **only** tests if all the content of the variant **up to** the first line break fits on the line (we insert that line break by using `should_expand(true))`. The printer doesn't care whether the rest `a\n, b\n ] + c` all fits on (multiple?) lines. 

Why does breaking right work but not breaking the left? The difference is that we can make the decision whether to parenthesis the expression based on the left expression. We can't do this for breaking left because the decision whether to insert parentheses or not would depend on a lookahead: will the right side break. We simply don't know this yet when printing the parentheses (it would work for the right parentheses but not for the left and indent).

What we kind of want here is to tell the printer: Look, what comes here may or may not fit on a single line but we don't care. Simply test that what comes **after** fits on a line. 

This PR adds a new `BestFittingMode` that has a new `AllLines` option that gives us the desired behavior of testing all content and not just up to the first line break. 

## Test Plan

I added a new example to  `BestFitting::with_mode`
2023-06-20 18:16:01 +02:00
Tom Kuson
6929fcc55f Complete flake8-bugbear documentation (#5178)
## Summary

Completes the documentation for the `flake8-bugbear` ruleset. Related to
#2646.

## Test Plan

`python scripts/check_docs_formatted.py`

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-06-20 12:10:58 -04:00
Charlie Marsh
7bc33a8d5f Remove identifier lexing in favor of parser ranges (#5195)
## Summary

Now that all identifiers include ranges (#5194), we can remove a ton of
this "custom lexing" code that we have to sketchily extract identifier
ranges from source.

## Test Plan

`cargo test`
2023-06-20 12:07:29 -04:00
Charlie Marsh
6331598511 Upgrade RustPython to access ranged names (#5194)
## Summary

In https://github.com/astral-sh/RustPython-Parser/pull/8, we modified
RustPython to include ranges for any identifiers that aren't
`Expr::Name` (which already has an identifier).

For example, the `e` in `except ValueError as e` was previously
un-ranged. To extract its range, we had to do some lexing of our own.
This change should improve performance and let us remove a bunch of
code.

## Test Plan

`cargo test`
2023-06-20 15:43:38 +00:00
Thomas de Zeeuw
17f1ecd56e Open cache files in parallel (#5120)
## Summary

Open cache files in parallel (again), brings the performance back to be roughly equal to the old implementation.

## Test Plan

Existing tests should keep working.
2023-06-20 17:43:09 +02:00
Dhruv Manilawala
062b6e5c2b Handle trailing newline in Jupyter notebook JSON string (#5202)
## Summary

Handle trailing newline in Jupyter Notebook JSON string similar to how
`black`
does it.

## Test Plan

Add test cases when the JSON string for notebook ends with and without a
newline.

resolves: #5190
2023-06-20 10:19:11 +00:00
David Szotten
773e79b481 basic formatting for ExprDict (#5167) 2023-06-20 09:25:08 +00:00
Logan Hunt
dfb04e679e Small binary size optimization (#5203) 2023-06-20 08:47:01 +02:00
konstin
5c5d2815af Document gitignore (#5191)
This docs-only change adds explanations to all custom .gitignore entries
2023-06-20 08:07:30 +02:00
Charlie Marsh
4cc3cdba16 Use some more wildcard imports in rules (#5201) 2023-06-20 03:21:08 +00:00
Charlie Marsh
a797e05602 Use a consistent argument ordering for Indexer (#5200) 2023-06-20 02:59:51 +00:00
Evan Rittenhouse
62aa77df31 Fix corner case involving terminal backslash after fixing W293 (#5172)
## Summary

Fixes #4404. 

Consider this file:
```python
if True:
    x = 1; \
<space><space><space>
```

The current implementation of W293 removes the 3 spaces on line 2. This
fix changes the file to:
```python
if True:
    x = 1; \
```
A file can't end in a `\`, according to Python's [lexical
analysis](https://docs.python.org/3/reference/lexical_analysis.html), so
subsequent iterations of the autofixer fail (the AST-based ones
specifically, since they depend on a valid syntax tree and get
re-parsed).

This patch examines the line before the line checked in `W293`. If its
first non-whitespace character is a `\`, the patch will extend the
diagnostic's fix range to all whitespace up until the previous line's
*second* non-whitespace character; that is, it deletes all spaces and
potential `\`s up until the next non-whitespace character on the
previous line.

## Test Plan
Ran `cargo run -p ruff_cli -- ~/Downloads/aa.py --fix --select W293,D100
--no-cache` against the above file. This resulted in:
```
/Users/evan/Downloads/aa.py:1:1: D100 Missing docstring in public module
Found 2 errors (1 fixed, 1 remaining).
```
The file's contents, after the fix:
```python
if True:
    x = 1;<space>
```
The `\` was removed, leaving the terminal space. The space should be
handled by `Rule::TrailingWhitespace`, not `BlankLineWithWhitespace`.
2023-06-20 02:57:24 +00:00
Charlie Marsh
64bd955c58 Remove continuations before trailing semicolons (#5199)
## Summary

Closes #4828.
2023-06-20 02:22:32 +00:00
323 changed files with 9064 additions and 3571 deletions

View File

@@ -76,6 +76,9 @@ jobs:
cargo insta test --all --all-features
git diff --exit-code
- run: cargo test --package ruff_cli --test black_compatibility_test -- --ignored
# Skipped as it's currently broken. The resource were moved from the
# ruff_cli to ruff crate, but this test was not updated.
if: false
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:
@@ -217,11 +220,10 @@ jobs:
- name: "Build wheels"
uses: PyO3/maturin-action@v1
with:
manylinux: auto
args: --out dist
- name: "Test wheel"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
pip install --force-reinstall --find-links dist ${{ env.PACKAGE_NAME }}
ruff --help
python -m ruff --help
- name: "Remove wheels from cache"

View File

@@ -2,8 +2,17 @@ name: "[ruff] Release"
on:
workflow_dispatch:
release:
types: [ published ]
inputs:
tag:
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
type: string
sha:
description: "Optionally, the full sha of the commit to be released"
type: string
push:
paths:
# When we change pyproject.toml, we want to ensure that the maturin builds still work
- pyproject.toml
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -383,8 +392,39 @@ jobs:
*.tar.gz
*.sha256
release:
name: Release
validate-tag:
name: Validate tag
runs-on: ubuntu-latest
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
steps:
- uses: actions/checkout@v3
- name: Check tag consistency
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
- name: Check SHA consistency
if: ${{ inputs.sha }}
run: |
git_sha=$(git rev-parse HEAD)
if [ "${{ inputs.sha }}" != "${git_sha}" ]; then
echo "The specified sha does not match the git checkout" >&2
echo "${{ inputs.sha }}" >&2
echo "${git_sha}" >&2
exit 1
else
echo "Releasing ${git_sha}"
fi
upload-release:
name: Upload to PyPI
runs-on: ubuntu-latest
needs:
- macos-universal
@@ -394,25 +434,56 @@ jobs:
- linux-cross
- musllinux
- musllinux-cross
if: "startsWith(github.ref, 'refs/tags/')"
- validate-tag
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
environment:
name: release
permissions:
# For pypi trusted publishing
id-token: write
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
path: wheels
- name: "Publish to PyPi"
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
skip-existing: true
packages-dir: wheels
verbose: true
tag-release:
name: Tag release
runs-on: ubuntu-latest
needs: upload-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For git tag
contents: write
steps:
- uses: actions/checkout@v3
- name: git tag
run: |
git config user.email "hey@astral.sh"
git config user.name "Ruff Release CI"
git tag -m "v${{ inputs.tag }}" "v${{ inputs.tag }}"
# If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip
# existing), so we make a non-destructive exit here
git push --tags
publish-release:
name: Publish to GitHub
runs-on: ubuntu-latest
needs: tag-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v3
with:
name: binaries
@@ -420,7 +491,9 @@ jobs:
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v1
with:
draft: false
files: binaries/*
tag_name: v${{ inputs.tag }}
# After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers

12
.gitignore vendored
View File

@@ -1,15 +1,25 @@
# Benchmarking cpython (CONTRIBUTING.md)
crates/ruff/resources/test/cpython
# generate_mkdocs.py
mkdocs.yml
.overrides
# check_ecosystem.py
ruff-old
github_search*.jsonl
# update_schemastore.py
schemastore
# `maturin develop` and ecosystem_all_check.sh
.venv*
# Formatter debugging (crates/ruff_python_formatter/README.md)
scratch.py
# Created by `perf` (CONTRIBUTING.md)
perf.data
perf.data.old
# Created by `flamegraph` (CONTRIBUTING.md)
flamegraph.svg
# Additional target directories that don't invalidate the main compile cache when changing linker settings
# Additional target directories that don't invalidate the main compile cache when changing linker settings,
# e.g. `CARGO_TARGET_DIR=target-maturin maturin build --release --strip` or
# `CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib`
/target*
###

View File

@@ -4,6 +4,7 @@ exclude: |
(?x)^(
crates/ruff/resources/.*|
crates/ruff/src/rules/.*/snapshots/.*|
crates/ruff_cli/resources/.*|
crates/ruff_python_formatter/resources/.*|
crates/ruff_python_formatter/src/snapshots/.*
)$

View File

@@ -12,7 +12,7 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
- [MkDocs](#mkdocs)
- [Release Process](#release-process)
- [Benchmarks](#benchmarks)
- [Benchmarks](#benchmarking-and-profiling)
## The Basics
@@ -271,6 +271,28 @@ them to [PyPI](https://pypi.org/project/ruff/).
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
### Creating a new release
1. Update the version with `rg 0.0.269 --files-with-matches | xargs sed -i 's/0.0.269/0.0.270/g'`
1. Update `BREAKING_CHANGES.md`
1. Create a PR with the version and `BREAKING_CHANGES.md` updated
1. Merge the PR
1. Run the release workflow with the version number (without starting `v`) as input. Make sure
main has your merged PR as last commit
1. The release workflow will do the following:
1. Build all the assets. If this fails (even though we tested in step 4), we havent tagged or
uploaded anything, you can restart after pushing a fix
1. Upload to pypi
1. Create and push the git tag (from pyproject.toml). We create the git tag only here
because we can't change it ([#4468](https://github.com/charliermarsh/ruff/issues/4468)), so
we want to make sure everything up to and including publishing to pypi worked.
1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail without causing fallout, it is possible (if
inconvenient) to trigger the downstream jobs manually
1. Create release notes in GitHub UI and promote from draft to proper release(<https://github.com/charliermarsh/ruff/releases/new>)
1. If needed, [update the schemastore](https://github.com/charliermarsh/ruff/blob/main/scripts/update_schemastore.py)
1. If needed, update ruff-lsp and ruff-vscode
## Ecosystem CI
GitHub Actions will run your changes against a number of real-world projects from GitHub and
@@ -285,7 +307,15 @@ downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-ag
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/astral-sh/ruff/blob/main/scripts/Dockerfile.ecosystem).
Note that this check will take a while to run.
## Benchmarks
## Benchmarking and Profiling
We have several ways of benchmarking and profiling Ruff:
- Our main performance benchmark comparing Ruff with other tools on the CPython codebase
- Microbenchmarks which the linter or the formatter on individual files. There run on pull requests.
- Profiling the linter on either the microbenchmarks or entire projects
### CPython Benchmark
First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase,
which makes it a good target for benchmarking.
@@ -364,9 +394,9 @@ Summary
159.43 ± 2.48 times faster than 'pycodestyle crates/ruff/resources/test/cpython'
```
You can run `poetry install` from `./scripts` to create a working environment for the above. All
reported benchmarks were computed using the versions specified by `./scripts/pyproject.toml`
on Python 3.11.
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
above. All reported benchmarks were computed using the versions specified by
`./scripts/benchmarks/pyproject.toml` on Python 3.11.
To benchmark Pylint, remove the following files from the CPython repository:
@@ -407,3 +437,116 @@ Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus
Time (mean ± σ): 30.119 s ± 0.195 s [User: 28.638 s, System: 0.390 s]
Range (min … max): 29.813 s … 30.356 s 10 runs
```
## Microbenchmarks
The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files.
You can run the benchmarks with
```shell
cargo benchmark
```
### Benchmark driven Development
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
`--save-baseline=<name>` to store an initial baseline benchmark (e.g. on `main`) and then use
`--benchmark=<name>` to compare against that benchmark. Criterion will print a message telling you
if the benchmark improved/regressed compared to that baseline.
```shell
# Run once on your "baseline" code
cargo benchmark --save-baseline=main
# Then iterate with
cargo benchmark --baseline=main
```
### PR Summary
You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings.
This is useful to illustrate the improvements of a PR.
```shell
# On main
cargo benchmark --save-baseline=main
# After applying your changes
cargo benchmark --save-baseline=pr
critcmp main pr
```
You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comparison.
```bash
cargo install critcmp
```
### Tips
- Use `cargo benchmark <filter>` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic`
to only run the pydantic tests.
- Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance)
- Use `cargo benchmark --quick` to get faster results (more prone to noise)
## Profiling Projects
You can either use the microbenchmarks from above or a project directory for benchmarking. There
are a lot of profiling tools out there,
[The Rust Performance Book](https://nnethercote.github.io/perf-book/profiling.html) lists some
examples.
### Linux
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
```shell
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
```
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to
gather enough samples for a good flamegraph (change the 999, the sample rate, and the 30, the number
of checks, to your liking)
```shell
cargo build --bin ruff_dev --profile=release-debug
perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
```
Then convert the recorded profile
```shell
perf script -F +pid > /tmp/test.perf
```
You can now view the converted file with [firefox profiler](https://profiler.firefox.com/), with a
more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling)
An alternative is to convert the perf data to `flamegraph.svg` using
[flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`):
```shell
flamegraph --perfdata perf.data
```
### Mac
Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments):
```shell
cargo install cargo-instruments
```
Then run the profiler with
```shell
cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1
```
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`
for profiling the allocations.
- You may want to pass an additional filter to run a single test file
Otherwise, follow the instructions from the linux section.

18
Cargo.lock generated
View File

@@ -733,7 +733,7 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.272"
version = "0.0.274"
dependencies = [
"anyhow",
"clap",
@@ -1793,7 +1793,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.272"
version = "0.0.274"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1889,7 +1889,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.272"
version = "0.0.274"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2105,7 +2105,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"schemars",
"serde",
@@ -2183,7 +2183,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"is-macro",
"num-bigint",
@@ -2194,7 +2194,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"bitflags 2.3.1",
"itertools",
@@ -2206,7 +2206,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2218,7 +2218,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"anyhow",
"is-macro",
@@ -2241,7 +2241,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=8d74eee75031b68d2204219963fae54a3f31a394#8d74eee75031b68d2204219963fae54a3f31a394"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"is-macro",
"memchr",

View File

@@ -24,7 +24,6 @@ ignore = { version = "0.4.20" }
insta = { version = "1.28.0" }
is-macro = { version = "0.2.2" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
log = { version = "0.4.17" }
memchr = "2.5.0"
nohash-hasher = { version = "0.2.0" }
@@ -36,11 +35,6 @@ proc-macro2 = { version = "1.0.51" }
quote = { version = "1.0.23" }
regex = { version = "1.7.1" }
rustc-hash = { version = "1.1.0" }
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394", default-features = false, features = ["num-bigint"] }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "8d74eee75031b68d2204219963fae54a3f31a394" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93" }
@@ -53,8 +47,22 @@ syn = { version = "2.0.15" }
test-case = { version = "3.0.0" }
toml = { version = "0.7.2" }
# v0.0.1
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
# v0.0.3
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" }
# v0.0.3
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]}
# v0.0.3
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false, features = ["num-bigint"] }
# v0.0.3
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca", default-features = false }
# v0.0.3
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "08ebbe40d7776cac6e3ba66277d435056f2b8dca" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] }
[profile.release]
lto = "fat"
codegen-units = 1
[profile.dev.package.insta]
opt-level = 3

View File

@@ -14,9 +14,9 @@ An extremely fast Python linter, written in Rust.
<p align="center">
<picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/212613422-7faaf278-706b-4294-ad92-236ffcab3430.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/232603514-c95e9b0f-6b31-43de-9a80-9e844173fd6a.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/232603516-4fb4892d-585c-4b20-b810-3db9161831e4.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/232603516-4fb4892d-585c-4b20-b810-3db9161831e4.svg">
</picture>
</p>
@@ -139,7 +139,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.272
rev: v0.0.274
hooks:
- id: ruff
```

View File

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

View File

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

View File

@@ -21,6 +21,13 @@ def test_error():
assert something and something_else == """error
message
"""
assert (
something
and something_else
== """error
message
"""
)
# recursive case
assert not (a or not (b or c))
@@ -31,14 +38,6 @@ def test_error():
assert not (something or something_else and something_third), "with message"
# detected, but no autofix for mixed conditions (e.g. `a or b and c`)
assert not (something or something_else and something_third)
# detected, but no autofix for parenthesized conditions
assert (
something
and something_else
== """error
message
"""
)
assert something # OK

View File

@@ -34,4 +34,4 @@
},
"nbformat": 4,
"nbformat_minor": 5
}
}

View File

@@ -0,0 +1,38 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "4cec6161-f594-446c-ab65-37395bbb3127",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"_ = math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -63,3 +63,6 @@ class Foo:
#: E702:2:4
while 1:
1;...
#: E703:2:1
0\
;

View File

@@ -1,53 +1,8 @@
[tool.ruff]
allowed-confusables = ["", "ρ", ""]
line-length = 88
extend-exclude = [
"excluded_file.py",
"migrations",
"with_excluded_file/other_excluded_file.py",
]
external = ["V101"]
per-file-ignores = { "__init__.py" = ["F401"] }
[tool.ruff.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
[tool.ruff.flake8-builtins]
builtins-ignorelist = ["id", "dir"]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
multiline-quotes = "double"
docstring-quotes = "double"
avoid-escape = true
[tool.ruff.mccabe]
max-complexity = 10
[tool.ruff.pep8-naming]
classmethod-decorators = ["pydantic.validator"]
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "parents"
[tool.ruff.flake8-tidy-imports.banned-api]
"cgi".msg = "The cgi module is deprecated."
"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
[tool.ruff.flake8-errmsg]
max-string-length = 20
[tool.ruff.flake8-import-conventions.aliases]
pandas = "pd"
[tool.ruff.flake8-import-conventions.extend-aliases]
"dask.dataframe" = "dd"
[tool.ruff.flake8-pytest-style]
fixture-parentheses = false
parametrize-names-type = "csv"
parametrize-values-type = "tuple"
parametrize-values-row-type = "list"
raises-require-match-for = ["Exception", "TypeError", "KeyError"]
raises-extend-require-match-for = ["requests.RequestException"]
mark-parentheses = false

View File

@@ -34,12 +34,3 @@ f"{ascii(bla)}" # OK
" intermediary content "
f" that flows {repr(obj)} of type {type(obj)}.{additional_message}" # RUF010
)
f"{str(bla)}" # RUF010
f"{str(bla):20}" # RUF010
f"{bla!s}" # RUF010
f"{bla!s:20}" # OK

View File

@@ -1,5 +1,5 @@
import typing
from typing import ClassVar, Sequence
from typing import ClassVar, Sequence, Final
KNOWINGLY_MUTABLE_DEFAULT = []
@@ -10,6 +10,7 @@ class A:
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: typing.ClassVar[list[int]] = []
final_variable: typing.Final[list[int]] = []
class B:
@@ -18,6 +19,7 @@ class B:
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
from dataclasses import dataclass, field
@@ -31,3 +33,17 @@ class C:
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
from pydantic import BaseModel
class D(BaseModel):
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []

View File

@@ -18,19 +18,19 @@ def f(arg: object = None):
pass
def f(arg: int = None): # RUF011
def f(arg: int = None): # RUF013
pass
def f(arg: str = None): # RUF011
def f(arg: str = None): # RUF013
pass
def f(arg: typing.List[str] = None): # RUF011
def f(arg: typing.List[str] = None): # RUF013
pass
def f(arg: Tuple[str] = None): # RUF011
def f(arg: Tuple[str] = None): # RUF013
pass
@@ -64,15 +64,15 @@ def f(arg: Union[int, str, Any] = None):
pass
def f(arg: Union = None): # RUF011
def f(arg: Union = None): # RUF013
pass
def f(arg: Union[int, str] = None): # RUF011
def f(arg: Union[int, str] = None): # RUF013
pass
def f(arg: typing.Union[int, str] = None): # RUF011
def f(arg: typing.Union[int, str] = None): # RUF013
pass
@@ -91,11 +91,11 @@ def f(arg: int | float | str | None = None):
pass
def f(arg: int | float = None): # RUF011
def f(arg: int | float = None): # RUF013
pass
def f(arg: int | float | str | bytes = None): # RUF011
def f(arg: int | float | str | bytes = None): # RUF013
pass
@@ -110,11 +110,11 @@ def f(arg: Literal[1, 2, None, 3] = None):
pass
def f(arg: Literal[1, "foo"] = None): # RUF011
def f(arg: Literal[1, "foo"] = None): # RUF013
pass
def f(arg: typing.Literal[1, "foo", True] = None): # RUF011
def f(arg: typing.Literal[1, "foo", True] = None): # RUF013
pass
@@ -133,11 +133,11 @@ def f(arg: Annotated[Any, ...] = None):
pass
def f(arg: Annotated[int, ...] = None): # RUF011
def f(arg: Annotated[int, ...] = None): # RUF013
pass
def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011
def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013
pass
@@ -153,9 +153,9 @@ def f(
def f(
arg1: int = None, # RUF011
arg2: Union[int, float] = None, # RUF011
arg3: Literal[1, 2, 3] = None, # RUF011
arg1: int = None, # RUF013
arg2: Union[int, float] = None, # RUF013
arg3: Literal[1, 2, 3] = None, # RUF013
):
pass
@@ -183,5 +183,41 @@ def f(arg: Union[Annotated[int, ...], Annotated[Optional[float], ...]] = None):
pass
def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011
def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013
pass
# Quoted
def f(arg: "int" = None): # RUF013
pass
def f(arg: "str" = None): # RUF013
pass
def f(arg: "st" "r" = None): # RUF013
pass
def f(arg: "Optional[int]" = None):
pass
def f(arg: Union["int", "str"] = None): # RUF013
pass
def f(arg: Union["int", "None"] = None):
pass
def f(arg: Union["No" "ne", "int"] = None):
pass
# Avoid flagging when there's a parse error in the forward reference
def f(arg: Union["<>", "int"] = None):
pass

View File

@@ -60,8 +60,8 @@ pub(crate) fn remove_unused_imports<'a>(
stmt: &Stmt,
parent: Option<&Stmt>,
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
indexer: &Indexer,
) -> Result<Edit> {
match codemods::remove_imports(unused_imports, stmt, locator, stylist)? {
None => Ok(delete_stmt(stmt, parent, locator, indexer)),

View File

@@ -366,9 +366,7 @@ where
}
if self.enabled(Rule::AmbiguousFunctionName) {
if let Some(diagnostic) =
pycodestyle::rules::ambiguous_function_name(name, || {
stmt.identifier(self.locator)
})
pycodestyle::rules::ambiguous_function_name(name, || stmt.identifier())
{
self.diagnostics.push(diagnostic);
}
@@ -383,7 +381,6 @@ where
decorator_list,
&self.settings.pep8_naming.ignore_names,
&self.semantic,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
@@ -452,7 +449,6 @@ where
stmt,
name,
&self.settings.pep8_naming.ignore_names,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
@@ -503,7 +499,6 @@ where
name,
body,
self.settings.mccabe.max_complexity,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
@@ -523,7 +518,6 @@ where
stmt,
body,
self.settings.pylint.max_returns,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
@@ -533,7 +527,6 @@ where
stmt,
body,
self.settings.pylint.max_branches,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
@@ -543,7 +536,6 @@ where
stmt,
body,
self.settings.pylint.max_statements,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
@@ -605,7 +597,6 @@ where
name,
decorator_list,
args,
self.locator,
);
}
if self.enabled(Rule::FStringDocstring) {
@@ -693,9 +684,9 @@ where
pyupgrade::rules::unnecessary_class_parentheses(self, class_def, stmt);
}
if self.enabled(Rule::AmbiguousClassName) {
if let Some(diagnostic) = pycodestyle::rules::ambiguous_class_name(name, || {
stmt.identifier(self.locator)
}) {
if let Some(diagnostic) =
pycodestyle::rules::ambiguous_class_name(name, || stmt.identifier())
{
self.diagnostics.push(diagnostic);
}
}
@@ -704,7 +695,6 @@ where
stmt,
name,
&self.settings.pep8_naming.ignore_names,
self.locator,
) {
self.diagnostics.push(diagnostic);
}
@@ -714,7 +704,6 @@ where
stmt,
bases,
name,
self.locator,
&self.settings.pep8_naming.ignore_names,
) {
self.diagnostics.push(diagnostic);
@@ -813,7 +802,7 @@ where
let qualified_name = &alias.name;
self.add_binding(
name,
alias.identifier(self.locator),
alias.identifier(),
BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }),
BindingFlags::EXTERNAL,
);
@@ -825,7 +814,7 @@ where
if alias
.asname
.as_ref()
.map_or(false, |asname| asname == &alias.name)
.map_or(false, |asname| asname.as_str() == alias.name.as_str())
{
flags |= BindingFlags::EXPLICIT_EXPORT;
}
@@ -834,7 +823,7 @@ where
let qualified_name = &alias.name;
self.add_binding(
name,
alias.identifier(self.locator),
alias.identifier(),
BindingKind::Import(Import { qualified_name }),
flags,
);
@@ -1052,7 +1041,7 @@ where
self.add_binding(
name,
alias.identifier(self.locator),
alias.identifier(),
BindingKind::FutureImport,
BindingFlags::empty(),
);
@@ -1110,7 +1099,7 @@ where
if alias
.asname
.as_ref()
.map_or(false, |asname| asname == &alias.name)
.map_or(false, |asname| asname.as_str() == alias.name.as_str())
{
flags |= BindingFlags::EXPLICIT_EXPORT;
}
@@ -1123,7 +1112,7 @@ where
helpers::format_import_from_member(level, module, &alias.name);
self.add_binding(
name,
alias.identifier(self.locator),
alias.identifier(),
BindingKind::FromImport(FromImport { qualified_name }),
flags,
);
@@ -1804,7 +1793,7 @@ where
self.add_binding(
name,
stmt.identifier(self.locator),
stmt.identifier(),
BindingKind::FunctionDefinition,
BindingFlags::empty(),
);
@@ -2029,7 +2018,7 @@ where
self.semantic.pop_definition();
self.add_binding(
name,
stmt.identifier(self.locator),
stmt.identifier(),
BindingKind::ClassDefinition,
BindingFlags::empty(),
);
@@ -3846,7 +3835,7 @@ where
}
match name {
Some(name) => {
let range = except_handler.try_identifier(self.locator).unwrap();
let range = except_handler.try_identifier().unwrap();
if self.enabled(Rule::AmbiguousVariableName) {
if let Some(diagnostic) =
@@ -3863,6 +3852,9 @@ where
);
}
// Store the existing binding, if any.
let existing_id = self.semantic.lookup(name);
// Add the bound exception name to the scope.
let binding_id = self.add_binding(
name,
@@ -3873,14 +3865,6 @@ where
walk_except_handler(self, except_handler);
// Remove it from the scope immediately after.
self.add_binding(
name,
range,
BindingKind::UnboundException,
BindingFlags::empty(),
);
// If the exception name wasn't used in the scope, emit a diagnostic.
if !self.semantic.is_used(binding_id) {
if self.enabled(Rule::UnusedVariable) {
@@ -3900,6 +3884,13 @@ where
self.diagnostics.push(diagnostic);
}
}
self.add_binding(
name,
range,
BindingKind::UnboundException(existing_id),
BindingFlags::empty(),
);
}
None => walk_except_handler(self, except_handler),
}
@@ -3961,7 +3952,7 @@ where
// upstream.
self.add_binding(
&arg.arg,
arg.identifier(self.locator),
arg.identifier(),
BindingKind::Argument,
BindingFlags::empty(),
);
@@ -4001,7 +3992,7 @@ where
{
self.add_binding(
name,
pattern.try_identifier(self.locator).unwrap(),
pattern.try_identifier().unwrap(),
BindingKind::Assignment,
BindingFlags::empty(),
);
@@ -4247,7 +4238,7 @@ impl<'a> Checker<'a> {
let shadowed = &self.semantic.bindings[shadowed_id];
if !matches!(
shadowed.kind,
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException,
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException(_),
) {
let references = shadowed.references.clone();
let is_global = shadowed.is_global();

View File

@@ -1,14 +1,14 @@
//! Lint rules based on checking physical lines.
use std::path::Path;
use ruff_text_size::TextSize;
use std::path::Path;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_whitespace::UniversalNewlines;
use crate::registry::Rule;
use crate::rules::copyright::rules::missing_copyright_notice;
use crate::rules::flake8_copyright::rules::missing_copyright_notice;
use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective};
use crate::rules::flake8_executable::rules::{
shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace,
@@ -146,7 +146,7 @@ pub(crate) fn check_physical_lines(
}
if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace {
if let Some(diagnostic) = trailing_whitespace(&line, settings) {
if let Some(diagnostic) = trailing_whitespace(&line, locator, indexer, settings) {
diagnostics.push(diagnostic);
}
}
@@ -185,9 +185,10 @@ pub(crate) fn check_physical_lines(
#[cfg(test)]
mod tests {
use std::path::Path;
use rustpython_parser::lexer::lex;
use rustpython_parser::Mode;
use std::path::Path;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};

View File

@@ -109,7 +109,7 @@ pub(crate) fn check_tokens(
// ERA001
if enforce_commented_out_code {
diagnostics.extend(eradicate::rules::commented_out_code(
indexer, locator, settings,
locator, indexer, settings,
));
}
@@ -141,7 +141,7 @@ pub(crate) fn check_tokens(
// E701, E702, E703
if enforce_compound_statements {
diagnostics.extend(
pycodestyle::rules::compound_statements(tokens, settings)
pycodestyle::rules::compound_statements(tokens, locator, indexer, settings)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
@@ -187,7 +187,7 @@ pub(crate) fn check_tokens(
// PYI033
if enforce_type_comment_in_stub && is_stub {
diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(indexer, locator));
diagnostics.extend(flake8_pyi::rules::type_comment_in_stub(locator, indexer));
}
// TD001, TD002, TD003, TD004, TD005, TD006, TD007
@@ -204,7 +204,7 @@ pub(crate) fn check_tokens(
.collect();
diagnostics.extend(
flake8_todos::rules::todos(&todo_comments, indexer, locator, settings)
flake8_todos::rules::todos(&todo_comments, locator, indexer, settings)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);

View File

@@ -157,7 +157,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// pylint
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
(Pylint, "C1901") => (RuleGroup::Unspecified, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
(Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit),
@@ -375,7 +375,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault),
// copyright
(Copyright, "001") => (RuleGroup::Nursery, rules::copyright::rules::MissingCopyrightNotice),
(Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice),
// pyupgrade
(Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType),

View File

@@ -1,6 +1,6 @@
use std::cmp::Ordering;
use std::fs::File;
use std::io::{BufReader, BufWriter, Write};
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::iter;
use std::path::Path;
@@ -34,9 +34,9 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
})?;
let code = notebook.content().to_string();
notebook.update_cell_content(&code);
let mut buffer = BufWriter::new(Vec::new());
notebook.write_inner(&mut buffer)?;
Ok(String::from_utf8(buffer.into_inner()?)?)
let mut writer = Vec::new();
notebook.write_inner(&mut writer)?;
Ok(String::from_utf8(writer)?)
}
/// Return `true` if the [`Path`] appears to be that of a jupyter notebook file (`.ipynb`).
@@ -113,13 +113,17 @@ pub struct Notebook {
cell_offsets: Vec<TextSize>,
/// The cell index of all valid code cells in the notebook.
valid_code_cells: Vec<u32>,
/// Flag to indicate if the JSON string of the notebook has a trailing newline.
trailing_newline: bool,
}
impl Notebook {
/// Read the Jupyter Notebook from the given [`Path`].
///
/// See also the black implementation
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
pub fn read(path: &Path) -> Result<Self, Box<Diagnostic>> {
let reader = BufReader::new(File::open(path).map_err(|err| {
let mut reader = BufReader::new(File::open(path).map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
@@ -127,6 +131,18 @@ impl Notebook {
TextRange::default(),
)
})?);
let trailing_newline = reader.seek(SeekFrom::End(-1)).is_ok_and(|_| {
let mut buf = [0; 1];
reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n')
});
reader.rewind().map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
},
TextRange::default(),
)
})?;
let raw_notebook: RawNotebook = match serde_json::from_reader(reader) {
Ok(notebook) => notebook,
Err(err) => {
@@ -240,6 +256,7 @@ impl Notebook {
content: contents.join("\n") + "\n",
cell_offsets,
valid_code_cells,
trailing_newline,
})
}
@@ -411,8 +428,11 @@ impl Notebook {
fn write_inner(&self, writer: &mut impl Write) -> anyhow::Result<()> {
// https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#LL1041
let formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
let mut ser = serde_json::Serializer::with_formatter(writer, formatter);
SortAlphabetically(&self.raw).serialize(&mut ser)?;
let mut serializer = serde_json::Serializer::with_formatter(writer, formatter);
SortAlphabetically(&self.raw).serialize(&mut serializer)?;
if self.trailing_newline {
writeln!(serializer.into_inner())?;
}
Ok(())
}
@@ -426,7 +446,6 @@ impl Notebook {
#[cfg(test)]
mod test {
use std::io::BufWriter;
use std::path::Path;
use anyhow::Result;
@@ -438,7 +457,7 @@ mod test {
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
use crate::test::{test_notebook_path, test_resource_path};
use crate::test::{read_jupyter_notebook, test_notebook_path, test_resource_path};
use crate::{assert_messages, settings};
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
@@ -450,15 +469,13 @@ mod test {
#[test]
fn test_valid() {
let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb");
assert!(Notebook::read(path).is_ok());
assert!(read_jupyter_notebook(Path::new("valid.ipynb")).is_ok());
}
#[test]
fn test_r() {
// We can load this, it will be filtered out later
let path = Path::new("resources/test/fixtures/jupyter/R.ipynb");
assert!(Notebook::read(path).is_ok());
assert!(read_jupyter_notebook(Path::new("R.ipynb")).is_ok());
}
#[test]
@@ -506,9 +523,8 @@ mod test {
}
#[test]
fn test_concat_notebook() {
let path = Path::new("resources/test/fixtures/jupyter/valid.ipynb");
let notebook = Notebook::read(path).unwrap();
fn test_concat_notebook() -> Result<()> {
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;
assert_eq!(
notebook.content,
r#"def unused_variable():
@@ -546,6 +562,7 @@ print("after empty cells")
198.into()
]
);
Ok(())
}
#[test]
@@ -568,12 +585,26 @@ print("after empty cells")
Path::new("after_fix.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
let mut writer = BufWriter::new(Vec::new());
let mut writer = Vec::new();
source_kind.expect_jupyter().write_inner(&mut writer)?;
let actual = String::from_utf8(writer.into_inner()?)?;
let actual = String::from_utf8(writer)?;
let expected =
std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?;
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 = read_jupyter_notebook(path)?;
assert_eq!(notebook.trailing_newline, trailing_newline);
let mut writer = Vec::new();
notebook.write_inner(&mut writer)?;
let string = String::from_utf8(writer)?;
assert_eq!(string.ends_with('\n'), trailing_newline);
Ok(())
}
}

View File

@@ -78,7 +78,7 @@ fn detect_package_root_with_cache<'a>(
current
}
/// Return a mapping from Python file to its package root.
/// Return a mapping from Python package to its package root.
pub fn detect_package_roots<'a>(
files: &[&'a Path],
resolver: &'a Resolver,

View File

@@ -251,7 +251,7 @@ impl Renamer {
| BindingKind::ClassDefinition
| BindingKind::FunctionDefinition
| BindingKind::Deletion
| BindingKind::UnboundException => {
| BindingKind::UnboundException(_) => {
Some(Edit::range_replacement(target.to_string(), binding.range))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,8 +48,8 @@ fn is_standalone_comment(line: &str) -> bool {
/// ERA001
pub(crate) fn commented_out_code(
indexer: &Indexer,
locator: &Locator,
indexer: &Indexer,
settings: &Settings,
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];

View File

@@ -653,7 +653,7 @@ pub(crate) fn definition(
MissingReturnTypeClassMethod {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
} else if is_method
@@ -664,7 +664,7 @@ pub(crate) fn definition(
MissingReturnTypeStaticMethod {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
} else if is_method && visibility::is_init(name) {
@@ -676,7 +676,7 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
@@ -693,7 +693,7 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
@@ -713,7 +713,7 @@ pub(crate) fn definition(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
}
@@ -723,7 +723,7 @@ pub(crate) fn definition(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
}

View File

@@ -9,6 +9,40 @@ use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
/// ## What it does
/// Checks for abstract classes without abstract methods.
///
/// ## Why is this bad?
/// Abstract base classes are used to define interfaces. If they have no abstract
/// methods, they are not useful.
///
/// If the class is not meant to be used as an interface, it should not be an
/// abstract base class. Remove the `ABC` base class from the class definition,
/// or add an abstract method to the class.
///
/// ## Example
/// ```python
/// from abc import ABC
///
///
/// class Foo(ABC):
/// def method(self):
/// bar()
/// ```
///
/// Use instead:
/// ```python
/// from abc import ABC, abstractmethod
///
///
/// class Foo(ABC):
/// @abstractmethod
/// def method(self):
/// bar()
/// ```
///
/// ## References
/// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html)
#[violation]
pub struct AbstractBaseClassWithoutAbstractMethod {
name: String,
@@ -21,6 +55,40 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod {
format!("`{name}` is an abstract base class, but it has no abstract methods")
}
}
/// ## What it does
/// Checks for empty methods in abstract base classes without an abstract
/// decorator.
///
/// ## Why is this bad?
/// Empty methods in abstract base classes without an abstract decorator are
/// indicative of unfinished code or a mistake.
///
/// Instead, add an abstract method decorated to indicate that it is abstract,
/// or implement the method.
///
/// ## Example
/// ```python
/// from abc import ABC
///
///
/// class Foo(ABC):
/// def method(self):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from abc import ABC, abstractmethod
///
///
/// class Foo(ABC):
/// @abstractmethod
/// def method(self):
/// ...
/// ```
///
/// ## References
/// - [Python documentation: abc](https://docs.python.org/3/library/abc.html)
#[violation]
pub struct EmptyMethodWithoutAbstractDecorator {
name: String,
@@ -134,7 +202,7 @@ pub(crate) fn abstract_base_class(
AbstractBaseClassWithoutAbstractMethod {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
}

View File

@@ -8,6 +8,28 @@ use ruff_python_ast::helpers::is_const_false;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `assert False`.
///
/// ## Why is this bad?
/// Python removes `assert` statements when running in optimized mode
/// (`python -O`), making `assert False` an unreliable means of
/// raising an `AssertionError`.
///
/// Instead, raise an `AssertionError` directly.
///
/// ## Example
/// ```python
/// assert False
/// ```
///
/// Use instead:
/// ```python
/// raise AssertionError
/// ```
///
/// ## References
/// - [Python documentation: `assert`](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)
#[violation]
pub struct AssertFalse;

View File

@@ -5,6 +5,39 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for assignments to `os.environ`.
///
/// ## Why is this bad?
/// In Python, `os.environ` is a mapping that represents the environment of the
/// current process.
///
/// However, reassigning to `os.environ` does not clear the environment. Instead,
/// it merely updates the `os.environ` for the current process. This can lead to
/// unexpected behavior, especially when running the program in a subprocess.
///
/// Instead, use `os.environ.clear()` to clear the environment, or use the
/// `env` argument of `subprocess.Popen` to pass a custom environment to
/// a subprocess.
///
/// ## Example
/// ```python
/// import os
///
/// os.environ = {"foo": "bar"}
/// ```
///
/// Use instead:
/// ```python
/// import os
///
/// os.environ.clear()
/// os.environ["foo"] = "bar"
/// ```
///
/// ## References
/// - [Python documentation: `os.environ`](https://docs.python.org/3/library/os.html#os.environ)
/// - [Python documentation: `subprocess.Popen`](https://docs.python.org/3/library/subprocess.html#subprocess.Popen)
#[violation]
pub struct AssignmentToOsEnviron;

View File

@@ -6,6 +6,57 @@ use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the `functools.lru_cache` and `functools.cache`
/// decorators on methods.
///
/// ## Why is this bad?
/// Using the `functools.lru_cache` and `functools.cache` decorators on methods
/// can lead to memory leaks, as the global cache will retain a reference to
/// the instance, preventing it from being garbage collected.
///
/// Instead, refactor the method to depend only on its arguments and not on the
/// instance of the class, or use the `@lru_cache` decorator on a function
/// outside of the class.
///
/// ## Example
/// ```python
/// from functools import lru_cache
///
///
/// def square(x: int) -> int:
/// return x * x
///
///
/// class Number:
/// value: int
///
/// @lru_cache
/// def squared(self):
/// return square(self.value)
/// ```
///
/// Use instead:
/// ```python
/// from functools import lru_cache
///
///
/// @lru_cache
/// def square(x: int) -> int:
/// return x * x
///
///
/// class Number:
/// value: int
///
/// def squared(self):
/// return square(self.value)
/// ```
///
/// ## References
/// - [Python documentation: `functools.lru_cache`](https://docs.python.org/3/library/functools.html#functools.lru_cache)
/// - [Python documentation: `functools.cache`](https://docs.python.org/3/library/functools.html#functools.cache)
/// - [don't lru_cache methods!](https://www.youtube.com/watch?v=sVjtp6tGo0g)
#[violation]
pub struct CachedInstanceMethod;

View File

@@ -12,6 +12,33 @@ use ruff_python_ast::call_path::CallPath;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
/// ## What it does
/// Checks for `try-except` blocks with duplicate exception handlers.
///
/// ## Why is this bad?
/// Duplicate exception handlers are redundant, as the first handler will catch
/// the exception, making the second handler unreachable.
///
/// ## Example
/// ```python
/// try:
/// ...
/// except ValueError:
/// ...
/// except ValueError:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// try:
/// ...
/// except ValueError:
/// ...
/// ```
///
/// ## References
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
#[violation]
pub struct DuplicateTryBlockException {
name: String,
@@ -24,6 +51,36 @@ impl Violation for DuplicateTryBlockException {
format!("try-except block with duplicate exception `{name}`")
}
}
/// ## What it does
/// Checks for exception handlers that catch duplicate exceptions.
///
/// ## Why is this bad?
/// Including the same exception multiple times in the same handler is redundant,
/// as the first exception will catch the exception, making the second exception
/// unreachable. The same applies to exception hierarchies, as a handler for a
/// parent exception (like `Exception`) will also catch child exceptions (like
/// `ValueError`).
///
/// ## Example
/// ```python
/// try:
/// ...
/// except (Exception, ValueError): # `Exception` includes `ValueError`.
/// ...
/// ```
///
/// Use instead:
/// ```python
/// try:
/// ...
/// except Exception:
/// ...
/// ```
///
/// ## References
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)
#[violation]
pub struct DuplicateHandlerException {
pub names: Vec<String>,

View File

@@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for exception handlers that catch an empty tuple.
///
/// ## Why is this bad?
/// An exception handler that catches an empty tuple will not catch anything,
/// and is indicative of a mistake. Instead, add exceptions to the `except`
/// clause.
///
/// ## Example
/// ```python
/// try:
/// 1 / 0
/// except ():
/// ...
/// ```
///
/// Use instead:
/// ```python
/// try:
/// 1 / 0
/// except ZeroDivisionError:
/// ...
/// ```
///
/// ## References
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
#[violation]
pub struct ExceptWithEmptyTuple;

View File

@@ -7,6 +7,32 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for exception handlers that catch non-exception classes.
///
/// ## Why is this bad?
/// Catching classes that do not inherit from `BaseException` will raise a
/// `TypeError`.
///
/// ## Example
/// ```python
/// try:
/// 1 / 0
/// except 1:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// try:
/// 1 / 0
/// except ZeroDivisionError:
/// ...
/// ```
///
/// ## References
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
/// - [Python documentation: Built-in Exceptions](https://docs.python.org/3/library/exceptions.html#built-in-exceptions)
#[violation]
pub struct ExceptWithNonExceptionClasses;

View File

@@ -6,6 +6,30 @@ use ruff_python_ast::identifier::Identifier;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for docstrings that are written via f-strings.
///
/// ## Why is this bad?
/// Python will interpret the f-string as a joined string, rather than as a
/// docstring. As such, the "docstring" will not be accessible via the
/// `__doc__` attribute, nor will it be picked up by any automated
/// documentation tooling.
///
/// ## Example
/// ```python
/// def foo():
/// f"""Not a docstring."""
/// ```
///
/// Use instead:
/// ```python
/// def foo():
/// """A docstring."""
/// ```
///
/// ## References
/// - [PEP 257](https://peps.python.org/pep-0257/)
/// - [Python documentation: Formatted string literals](https://docs.python.org/3/reference/lexical_analysis.html#f-strings)
#[violation]
pub struct FStringDocstring;
@@ -29,8 +53,7 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
let Expr::JoinedStr ( _) = value.as_ref() else {
return;
};
checker.diagnostics.push(Diagnostic::new(
FStringDocstring,
stmt.identifier(checker.locator),
));
checker
.diagnostics
.push(Diagnostic::new(FStringDocstring, stmt.identifier()));
}

View File

@@ -10,6 +10,39 @@ use ruff_python_ast::visitor::Visitor;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for function definitions that use a loop variable.
///
/// ## Why is this bad?
/// The loop variable is not bound in the function definition, so it will always
/// have the value it had in the last iteration when the function is called.
///
/// Instead, consider using a default argument to bind the loop variable at
/// function definition time. Or, use `functools.partial`.
///
/// ## Example
/// ```python
/// adders = [lambda x: x + i for i in range(3)]
/// values = [adder(1) for adder in adders] # [3, 3, 3]
/// ```
///
/// Use instead:
/// ```python
/// adders = [lambda x, i=i: x + i for i in range(3)]
/// values = [adder(1) for adder in adders] # [1, 2, 3]
/// ```
///
/// Or:
/// ```python
/// from functools import partial
///
/// adders = [partial(lambda x, i: x + i, i) for i in range(3)]
/// values = [adder(1) for adder in adders] # [1, 2, 3]
/// ```
///
/// ## References
/// - [The Hitchhiker's Guide to Python: Late Binding Closures](https://docs.python-guide.org/writing/gotchas/#late-binding-closures)
/// - [Python documentation: functools.partial](https://docs.python.org/3/library/functools.html#functools.partial)
#[violation]
pub struct FunctionUsesLoopVariable {
name: String,

View File

@@ -1,5 +1,5 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged};
use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Identifier, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -8,6 +8,29 @@ use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `getattr` that take a constant attribute value as an
/// argument (e.g., `getattr(obj, "foo")`).
///
/// ## Why is this bad?
/// `getattr` is used to access attributes dynamically. If the attribute is
/// defined as a constant, it is no safer than a typical property access. When
/// possible, prefer property access over `getattr` calls, as the former is
/// more concise and idiomatic.
///
///
/// ## Example
/// ```python
/// getattr(obj, "foo")
/// ```
///
/// Use instead:
/// ```python
/// obj.foo
/// ```
///
/// ## References
/// - [Python documentation: `getattr`](https://docs.python.org/3/library/functions.html#getattr)
#[violation]
pub struct GetAttrWithConstant;
@@ -27,7 +50,7 @@ impl AlwaysAutofixableViolation for GetAttrWithConstant {
fn attribute(value: &Expr, attr: &str) -> Expr {
ast::ExprAttribute {
value: Box::new(value.clone()),
attr: attr.into(),
attr: Identifier::new(attr.to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
}

View File

@@ -5,6 +5,40 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `break`, `continue`, and `return` statements in `finally`
/// blocks.
///
/// ## Why is this bad?
/// The use of `break`, `continue`, and `return` statements in `finally` blocks
/// can cause exceptions to be silenced.
///
/// `finally` blocks execute regardless of whether an exception is raised. If a
/// `break`, `continue`, or `return` statement is reached in a `finally` block,
/// any exception raised in the `try` or `except` blocks will be silenced.
///
/// ## Example
/// ```python
/// def speed(distance, time):
/// try:
/// return distance / time
/// except ZeroDivisionError:
/// raise ValueError("Time cannot be zero")
/// finally:
/// return 299792458 # `ValueError` is silenced
/// ```
///
/// Use instead:
/// ```python
/// def speed(distance, time):
/// try:
/// return distance / time
/// except ZeroDivisionError:
/// raise ValueError("Time cannot be zero")
/// ```
///
/// ## References
/// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement)
#[violation]
pub struct JumpStatementInFinally {
name: String,

View File

@@ -8,6 +8,33 @@ use ruff_python_ast::visitor::Visitor;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for loop control variables that override the loop iterable.
///
/// ## Why is this bad?
/// Loop control variables should not override the loop iterable, as this can
/// lead to confusing behavior.
///
/// Instead, use a distinct variable name for any loop control variables.
///
/// ## Example
/// ```python
/// items = [1, 2, 3]
///
/// for items in items:
/// print(items)
/// ```
///
/// Use instead:
/// ```python
/// items = [1, 2, 3]
///
/// for item in items:
/// print(item)
/// ```
///
/// ## References
/// - [Python documentation: The `for` statement](https://docs.python.org/3/reference/compound_stmts.html#the-for-statement)
#[violation]
pub struct LoopVariableOverridesIterator {
name: String,

View File

@@ -6,6 +6,46 @@ use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of mutable objects as function argument defaults.
///
/// ## Why is this bad?
/// Function defaults are evaluated once, when the function is defined.
///
/// The same mutable object is then shared across all calls to the function.
/// If the object is modified, those modifications will persist across calls,
/// which can lead to unexpected behavior.
///
/// Instead, prefer to use immutable data structures, or take `None` as a
/// default, and initialize a new mutable object inside the function body
/// for each call.
///
/// ## Example
/// ```python
/// def add_to_list(item, some_list=[]):
/// some_list.append(item)
/// return some_list
///
///
/// l1 = add_to_list(0) # [0]
/// l2 = add_to_list(1) # [0, 1]
/// ```
///
/// Use instead:
/// ```python
/// def add_to_list(item, some_list=None):
/// if some_list is None:
/// some_list = []
/// some_list.append(item)
/// return some_list
///
///
/// l1 = add_to_list(0) # [0]
/// l2 = add_to_list(1) # [1]
/// ```
///
/// ## References
/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
#[violation]
pub struct MutableArgumentDefault;

View File

@@ -5,6 +5,26 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `raise` statements that raise a literal value.
///
/// ## Why is this bad?
/// `raise` must be followed by an exception instance or an exception class,
/// and exceptions must be instances of `BaseException` or a subclass thereof.
/// Raising a literal will raise a `TypeError` at runtime.
///
/// ## Example
/// ```python
/// raise "foo"
/// ```
///
/// Use instead:
/// ```python
/// raise Exception("foo")
/// ```
///
/// ## References
/// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement)
#[violation]
pub struct RaiseLiteral;

View File

@@ -8,6 +8,44 @@ use ruff_python_stdlib::str::is_cased_lowercase;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `raise` statements in exception handlers that lack a `from`
/// clause.
///
/// ## Why is this bad?
/// In Python, `raise` can be used with or without an exception from which the
/// current exception is derived. This is known as exception chaining. When
/// printing the stack trace, chained exceptions are displayed in such a way
/// so as make it easier to trace the exception back to its root cause.
///
/// When raising an exception from within an `except` clause, always include a
/// `from` clause to facilitate exception chaining. If the exception is not
/// chained, it will be difficult to trace the exception back to its root cause.
///
/// ## Example
/// ```python
/// try:
/// ...
/// except FileNotFoundError:
/// if ...:
/// raise RuntimeError("...")
/// else:
/// raise UserWarning("...")
/// ```
///
/// Use instead:
/// ```python
/// try:
/// ...
/// except FileNotFoundError as exc:
/// if ...:
/// raise RuntimeError("...") from None
/// else:
/// raise UserWarning("...") from exc
/// ```
///
/// ## References
/// - [Python documentation: `raise` statement](https://docs.python.org/3/reference/simple_stmts.html#the-raise-statement)
#[violation]
pub struct RaiseWithoutFromInsideExcept;

View File

@@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for single-element tuples in exception handlers (e.g.,
/// `except (ValueError,):`).
///
/// ## Why is this bad?
/// A tuple with a single element can be more concisely and idiomatically
/// expressed as a single value.
///
/// ## Example
/// ```python
/// try:
/// ...
/// except (ValueError,):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// try:
/// ...
/// except ValueError:
/// ...
/// ```
///
/// ## References
/// - [Python documentation: `except` clause](https://docs.python.org/3/reference/compound_stmts.html#except-clause)
#[violation]
pub struct RedundantTupleInExceptionHandler {
name: String,

View File

@@ -1,5 +1,5 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Ranged, Stmt};
use rustpython_parser::ast::{self, Constant, Expr, ExprContext, Identifier, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -9,6 +9,28 @@ use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of `setattr` that take a constant attribute value as an
/// argument (e.g., `setattr(obj, "foo", 42)`).
///
/// ## Why is this bad?
/// `setattr` is used to set attributes dynamically. If the attribute is
/// defined as a constant, it is no safer than a typical property access. When
/// possible, prefer property access over `setattr` calls, as the former is
/// more concise and idiomatic.
///
/// ## Example
/// ```python
/// setattr(obj, "foo", 42)
/// ```
///
/// Use instead:
/// ```python
/// obj.foo = 42
/// ```
///
/// ## References
/// - [Python documentation: `setattr`](https://docs.python.org/3/library/functions.html#setattr)
#[violation]
pub struct SetAttrWithConstant;
@@ -30,7 +52,7 @@ fn assignment(obj: &Expr, name: &str, value: &Expr, generator: Generator) -> Str
let stmt = Stmt::Assign(ast::StmtAssign {
targets: vec![Expr::Attribute(ast::ExprAttribute {
value: Box::new(obj.clone()),
attr: name.into(),
attr: Identifier::new(name.to_string(), TextRange::default()),
ctx: ExprContext::Store,
range: TextRange::default(),
})],

View File

@@ -1,12 +1,3 @@
//! Checks for `f(x=0, *(1, 2))`.
//!
//! ## Why is this bad?
//!
//! Star-arg unpacking after a keyword argument is strongly discouraged. It only
//! works when the keyword parameter is declared after all parameters supplied
//! by the unpacked sequence, and this change of ordering can surprise and
//! mislead readers.
use rustpython_parser::ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
@@ -14,6 +5,45 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for function calls that use star-argument unpacking after providing a
/// keyword argument
///
/// ## Why is this bad?
/// In Python, you can use star-argument unpacking to pass a list or tuple of
/// arguments to a function.
///
/// Providing a star-argument after a keyword argument can lead to confusing
/// behavior, and is only supported for backwards compatibility.
///
/// ## Example
/// ```python
/// def foo(x, y, z):
/// return x, y, z
///
///
/// foo(1, 2, 3) # (1, 2, 3)
/// foo(1, *[2, 3]) # (1, 2, 3)
/// # foo(x=1, *[2, 3]) # TypeError
/// # foo(y=2, *[1, 3]) # TypeError
/// foo(z=3, *[1, 2]) # (1, 2, 3) # No error, but confusing!
/// ```
///
/// Use instead:
/// ```python
/// def foo(x, y, z):
/// return x, y, z
///
///
/// foo(1, 2, 3) # (1, 2, 3)
/// foo(x=1, y=2, z=3) # (1, 2, 3)
/// foo(*[1, 2, 3]) # (1, 2, 3)
/// foo(*[1, 2], 3) # (1, 2, 3)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [Disallow iterable argument unpacking after a keyword argument?](https://github.com/python/cpython/issues/82741)
#[violation]
pub struct StarArgUnpackingAfterKeywordArg;

View File

@@ -6,6 +6,32 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of multi-character strings in `.strip()`, `.lstrip()`, and
/// `.rstrip()` calls.
///
/// ## Why is this bad?
/// All characters in the call to `.strip()`, `.lstrip()`, or `.rstrip()` are
/// removed from the leading and trailing ends of the string. If the string
/// contains multiple characters, the reader may be misled into thinking that
/// a prefix or suffix is being removed, rather than a set of characters.
///
/// In Python 3.9 and later, you can use `str#removeprefix` and
/// `str#removesuffix` to remove an exact prefix or suffix from a string,
/// respectively, which should be preferred when possible.
///
/// ## Example
/// ```python
/// "abcba".strip("ab") # "c"
/// ```
///
/// Use instead:
/// ```python
/// "abcba".removeprefix("ab").removesuffix("ba") # "c"
/// ```
///
/// ## References
/// - [Python documentation: `str.strip`](https://docs.python.org/3/library/stdtypes.html#str.strip)
#[violation]
pub struct StripWithMultiCharacters;

View File

@@ -1,22 +1,3 @@
//! Checks for `++n`.
//!
//! ## Why is this bad?
//!
//! Python does not support the unary prefix increment. Writing `++n` is
//! equivalent to `+(+(n))`, which equals `n`.
//!
//! ## Example
//!
//! ```python
//! ++n;
//! ```
//!
//! Use instead:
//!
//! ```python
//! n += 1
//! ```
use rustpython_parser::ast::{self, Expr, Ranged, UnaryOp};
use ruff_diagnostics::{Diagnostic, Violation};
@@ -24,6 +5,26 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of the unary prefix increment operator (e.g., `++n`).
///
/// ## Why is this bad?
/// Python does not support the unary prefix increment operator. Writing `++n`
/// is equivalent to `+(+(n))`, which is equivalent to `n`.
///
/// ## Example
/// ```python
/// ++n
/// ```
///
/// Use instead:
/// ```python
/// n += 1
/// ```
///
/// ## References
/// - [Python documentation: Unary arithmetic and bitwise operations](https://docs.python.org/3/reference/expressions.html#unary-arithmetic-and-bitwise-operations)
/// - [Python documentation: Augmented assignment statements](https://docs.python.org/3/reference/simple_stmts.html#augmented-assignment-statements)
#[violation]
pub struct UnaryPrefixIncrement;

View File

@@ -5,6 +5,32 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `hasattr` to test if an object is callable (e.g.,
/// `hasattr(obj, "__call__")`).
///
/// ## Why is this bad?
/// Using `hasattr` is an unreliable mechanism for testing if an object is
/// callable. If `obj` implements a custom `__getattr__`, or if its `__call__`
/// is itself not callable, you may get misleading results.
///
/// Instead, use `callable(obj)` to test if `obj` is callable.
///
/// ## Example
/// ```python
/// hasattr(obj, "__call__")
/// ```
///
/// Use instead:
/// ```python
/// callable(obj)
/// ```
///
/// ## References
/// - [Python documentation: `callable`](https://docs.python.org/3/library/functions.html#callable)
/// - [Python documentation: `hasattr`](https://docs.python.org/3/library/functions.html#hasattr)
/// - [Python documentation: `__getattr__`](https://docs.python.org/3/reference/datamodel.html#object.__getattr__)
/// - [Python documentation: `__call__`](https://docs.python.org/3/reference/datamodel.html#object.__call__)
#[violation]
pub struct UnreliableCallableCheck;

View File

@@ -1,23 +1,3 @@
//! Checks for unused loop variables.
//!
//! ## Why is this bad?
//!
//! Unused variables may signal a mistake or unfinished code.
//!
//! ## Example
//!
//! ```python
//! for x in range(10):
//! method()
//! ```
//!
//! Prefix the variable with an underscore:
//!
//! ```python
//! for _x in range(10):
//! method()
//! ```
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{self, Expr, Ranged, Stmt};
@@ -35,6 +15,31 @@ enum Certainty {
Uncertain,
}
/// ## What it does
/// Checks for unused variables in loops (e.g., `for` and `while` statements).
///
/// ## Why is this bad?
/// Defining a variable in a loop statement that is never used can confuse
/// readers.
///
/// If the variable is intended to be unused (e.g., to facilitate
/// destructuring of a tuple or other object), prefix it with an underscore
/// to indicate the intent. Otherwise, remove the variable entirely.
///
/// ## Example
/// ```python
/// for i, j in foo:
/// bar(i)
/// ```
///
/// Use instead:
/// ```python
/// for i, _j in foo:
/// bar(i)
/// ```
///
/// ## References
/// - [PEP 8: Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions)
#[violation]
pub struct UnusedLoopControlVariable {
/// The name of the loop control variable.

View File

@@ -5,6 +5,26 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for useless comparisons.
///
/// ## Why is this bad?
/// Useless comparisons have no effect on the program, and are often included
/// by mistake. If the comparison is intended to enforce an invariant, prepend
/// the comparison with an `assert`. Otherwise, remove it entirely.
///
/// ## Example
/// ```python
/// foo == bar
/// ```
///
/// Use instead:
/// ```python
/// assert foo == bar, "`foo` and `bar` should be equal."
/// ```
///
/// ## References
/// - [Python documentation: `assert` statement](https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement)
#[violation]
pub struct UselessComparison;

View File

@@ -5,6 +5,36 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `contextlib.suppress` without arguments.
///
/// ## Why is this bad?
/// `contextlib.suppress` is a context manager that suppresses exceptions. It takes,
/// as arguments, the exceptions to suppress within the enclosed block. If no
/// exceptions are specified, then the context manager won't suppress any
/// exceptions, and is thus redundant.
///
/// Consider adding exceptions to the `contextlib.suppress` call, or removing the
/// context manager entirely.
///
/// ## Example
/// ```python
/// import contextlib
///
/// with contextlib.suppress():
/// foo()
/// ```
///
/// Use instead:
/// ```python
/// import contextlib
///
/// with contextlib.suppress(Exception):
/// foo()
/// ```
///
/// ## References
/// - [Python documentation: contextlib.suppress](https://docs.python.org/3/library/contextlib.html#contextlib.suppress)
#[violation]
pub struct UselessContextlibSuppress;

View File

@@ -6,12 +6,23 @@ use ruff_python_ast::helpers::contains_effect;
use crate::checkers::ast::Checker;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum Kind {
Expression,
Attribute,
}
/// ## What it does
/// Checks for useless expressions.
///
/// ## Why is this bad?
/// Useless expressions have no effect on the program, and are often included
/// by mistake. Assign a useless expression to a variable, or remove it
/// entirely.
///
/// ## Example
/// ```python
/// 1 + 1
/// ```
///
/// Use instead:
/// ```python
/// foo = 1 + 1
/// ```
#[violation]
pub struct UselessExpression {
kind: Kind,
@@ -74,3 +85,9 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
value.range(),
));
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum Kind {
Expression,
Attribute,
}

View File

@@ -7,6 +7,29 @@ use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `zip` calls without an explicit `strict` parameter.
///
/// ## Why is this bad?
/// By default, if the iterables passed to `zip` are of different lengths, the
/// resulting iterator will be silently truncated to the length of the shortest
/// iterable. This can lead to subtle bugs.
///
/// Use the `strict` parameter to raise a `ValueError` if the iterables are of
/// non-uniform length.
///
/// ## Example
/// ```python
/// zip(a, b)
/// ```
///
/// Use instead:
/// ```python
/// zip(a, b, strict=True)
/// ```
///
/// ## References
/// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip)
#[violation]
pub struct ZipWithoutExplicitStrict;

View File

@@ -1,8 +1,7 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{ExceptHandler, Expr, Ranged, Stmt};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::source_code::Locator;
use ruff_python_ast::identifier::{Identifier, TryIdentifier};
use ruff_python_stdlib::builtins::BUILTINS;
pub(super) fn shadows_builtin(name: &str, ignorelist: &[String]) -> bool {
@@ -16,12 +15,12 @@ pub(crate) enum AnyShadowing<'a> {
ExceptHandler(&'a ExceptHandler),
}
impl AnyShadowing<'_> {
pub(crate) fn range(self, locator: &Locator) -> TextRange {
impl Identifier for AnyShadowing<'_> {
fn identifier(&self) -> TextRange {
match self {
AnyShadowing::Expression(expr) => expr.range(),
AnyShadowing::Statement(stmt) => stmt.identifier(locator),
AnyShadowing::ExceptHandler(handler) => handler.range(),
AnyShadowing::Statement(stmt) => stmt.identifier(),
AnyShadowing::ExceptHandler(handler) => handler.try_identifier().unwrap(),
}
}
}

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use rustpython_parser::ast;
use crate::checkers::ast::Checker;
@@ -83,7 +84,7 @@ pub(crate) fn builtin_attribute_shadowing(
BuiltinAttributeShadowing {
name: name.to_string(),
},
shadowing.range(checker.locator),
shadowing.identifier(),
));
}
}

View File

@@ -1,6 +1,7 @@
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier::Identifier;
use crate::checkers::ast::Checker;
@@ -68,7 +69,7 @@ pub(crate) fn builtin_variable_shadowing(
BuiltinVariableShadowing {
name: name.to_string(),
},
shadowing.range(checker.locator),
shadowing.identifier(),
));
}
}

View File

@@ -122,15 +122,13 @@ A001.py:16:7: A001 Variable `slice` is shadowing a Python builtin
17 | pass
|
A001.py:21:1: A001 Variable `ValueError` is shadowing a Python builtin
A001.py:21:23: A001 Variable `ValueError` is shadowing a Python builtin
|
19 | try:
20 | ...
21 | / except ImportError as ValueError:
22 | | ...
| |_______^ A001
23 |
24 | for memoryview, *bytearray in []:
19 | try:
20 | ...
21 | except ImportError as ValueError:
| ^^^^^^^^^^ A001
22 | ...
|
A001.py:24:5: A001 Variable `memoryview` is shadowing a Python builtin

View File

@@ -102,15 +102,13 @@ A001.py:16:7: A001 Variable `slice` is shadowing a Python builtin
17 | pass
|
A001.py:21:1: A001 Variable `ValueError` is shadowing a Python builtin
A001.py:21:23: A001 Variable `ValueError` is shadowing a Python builtin
|
19 | try:
20 | ...
21 | / except ImportError as ValueError:
22 | | ...
| |_______^ A001
23 |
24 | for memoryview, *bytearray in []:
19 | try:
20 | ...
21 | except ImportError as ValueError:
| ^^^^^^^^^^ A001
22 | ...
|
A001.py:24:5: A001 Variable `memoryview` is shadowing a Python builtin

View File

@@ -75,7 +75,7 @@ import os
"#
.trim(),
&settings::Settings {
copyright: super::settings::Settings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
@@ -95,7 +95,7 @@ import os
"#
.trim(),
&settings::Settings {
copyright: super::settings::Settings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
@@ -113,7 +113,7 @@ import os
"#
.trim(),
&settings::Settings {
copyright: super::settings::Settings {
flake8_copyright: super::settings::Settings {
min_file_size: 256,
..super::settings::Settings::default()
},

View File

@@ -28,7 +28,7 @@ pub(crate) fn missing_copyright_notice(
settings: &Settings,
) -> Option<Diagnostic> {
// Ignore files that are too small to contain a copyright notice.
if locator.len() < settings.copyright.min_file_size {
if locator.len() < settings.flake8_copyright.min_file_size {
return None;
}
@@ -40,8 +40,8 @@ pub(crate) fn missing_copyright_notice(
};
// Locate the copyright notice.
if let Some(match_) = settings.copyright.notice_rgx.find(contents) {
match settings.copyright.author {
if let Some(match_) = settings.flake8_copyright.notice_rgx.find(contents) {
match settings.flake8_copyright.author {
Some(ref author) => {
// Ensure that it's immediately followed by the author.
if contents[match_.end()..].trim_start().starts_with(author) {

View File

@@ -1,4 +1,4 @@
//! Settings for the `copyright` plugin.
//! Settings for the `flake8-copyright` plugin.
use once_cell::sync::Lazy;
use regex::Regex;
@@ -12,7 +12,7 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions};
#[serde(
deny_unknown_fields,
rename_all = "kebab-case",
rename = "CopyrightOptions"
rename = "Flake8CopyrightOptions"
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,28 @@ use ruff_python_ast::call_path::{format_call_path, from_unqualified_name, CallPa
use crate::checkers::ast::Checker;
use crate::rules::flake8_debugger::types::DebuggerUsingType;
/// ## What it does
/// Checks for the presence of debugger calls and imports.
///
/// ## Why is this bad?
/// Debugger calls and imports should be used for debugging purposes only. The
/// presence of a debugger call or import in production code is likely a
/// mistake and may cause unintended behavior, such as exposing sensitive
/// information or causing the program to hang.
///
/// Instead, consider using a logging library to log information about the
/// program's state, and writing tests to verify that the program behaves
/// as expected.
///
/// ## Example
/// ```python
/// def foo():
/// breakpoint()
/// ```
///
/// ## References
/// - [Python documentation: `pdb` — The Python Debugger](https://docs.python.org/3/library/pdb.html)
/// - [Python documentation: `logging` — Logging facility for Python](https://docs.python.org/3/library/logging.html)
#[violation]
pub struct Debugger {
using_type: DebuggerUsingType,

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{self, Expr};
/// Returns true if the [`Expr`] is an internationalization function call.
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[String]) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = func {
functions_names.contains(id.as_ref())
functions_names.contains(id)
} else {
false
}

View File

@@ -1,6 +1,6 @@
pub(crate) use f_string_in_gettext_func_call::*;
pub(crate) use format_in_gettext_func_call::*;
pub(crate) use is_gettext_func_call::is_gettext_func_call;
pub(crate) use is_gettext_func_call::*;
pub(crate) use printf_in_gettext_func_call::*;
mod f_string_in_gettext_func_call;

View File

@@ -16,7 +16,7 @@ custom.py:4:8: ICN001 `dask.array` should be imported as `da`
|
3 | import altair # unconventional
4 | import dask.array # unconventional
| ^^^^ ICN001
| ^^^^^^^^^^ ICN001
5 | import dask.dataframe # unconventional
6 | import matplotlib.pyplot # unconventional
|
@@ -27,7 +27,7 @@ custom.py:5:8: ICN001 `dask.dataframe` should be imported as `dd`
3 | import altair # unconventional
4 | import dask.array # unconventional
5 | import dask.dataframe # unconventional
| ^^^^ ICN001
| ^^^^^^^^^^^^^^ ICN001
6 | import matplotlib.pyplot # unconventional
7 | import numpy # unconventional
|
@@ -38,7 +38,7 @@ custom.py:6:8: ICN001 `matplotlib.pyplot` should be imported as `plt`
4 | import dask.array # unconventional
5 | import dask.dataframe # unconventional
6 | import matplotlib.pyplot # unconventional
| ^^^^^^^^^^ ICN001
| ^^^^^^^^^^^^^^^^^ ICN001
7 | import numpy # unconventional
8 | import pandas # unconventional
|
@@ -115,7 +115,7 @@ custom.py:13:8: ICN001 `plotly.express` should be imported as `px`
11 | import holoviews # unconventional
12 | import panel # unconventional
13 | import plotly.express # unconventional
| ^^^^^^ ICN001
| ^^^^^^^^^^^^^^ ICN001
14 | import matplotlib # unconventional
15 | import polars # unconventional
|

View File

@@ -16,7 +16,7 @@ defaults.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt`
|
3 | import altair # unconventional
4 | import matplotlib.pyplot # unconventional
| ^^^^^^^^^^ ICN001
| ^^^^^^^^^^^^^^^^^ ICN001
5 | import numpy # unconventional
6 | import pandas # unconventional
|

View File

@@ -6,7 +6,7 @@ from_imports.py:3:8: ICN001 `xml.dom.minidom` should be imported as `md`
1 | # Test absolute imports
2 | # Violation cases
3 | import xml.dom.minidom
| ^^^ ICN001
| ^^^^^^^^^^^^^^^ ICN001
4 | import xml.dom.minidom as wrong
5 | from xml.dom import minidom as wrong
|

View File

@@ -16,7 +16,7 @@ override_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt`
|
3 | import altair # unconventional
4 | import matplotlib.pyplot # unconventional
| ^^^^^^^^^^ ICN001
| ^^^^^^^^^^^^^^^^^ ICN001
5 | import numpy # unconventional
6 | import pandas # unconventional
|

View File

@@ -16,7 +16,7 @@ remove_default.py:4:8: ICN001 `matplotlib.pyplot` should be imported as `plt`
|
3 | import altair # unconventional
4 | import matplotlib.pyplot # unconventional
| ^^^^^^^^^^ ICN001
| ^^^^^^^^^^^^^^^^^ ICN001
5 | import numpy # not checked
6 | import pandas # unconventional
|

View File

@@ -1,3 +1,3 @@
pub(crate) use logging_call::logging_call;
pub(crate) use logging_call::*;
mod logging_call;

View File

@@ -5,7 +5,7 @@ use itertools::Either::{Left, Right};
use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, BoolOp, Expr, ExprContext, Ranged};
use rustpython_parser::ast::{self, BoolOp, Expr, ExprContext, Identifier, Ranged};
use ruff_diagnostics::AlwaysAutofixableViolation;
use ruff_diagnostics::{Diagnostic, Edit, Fix};
@@ -140,7 +140,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
});
let node2 = Expr::Attribute(ast::ExprAttribute {
value: Box::new(node1),
attr: attr_name.into(),
attr: Identifier::new(attr_name.to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
});

View File

@@ -148,7 +148,7 @@ pub(crate) fn non_self_return_type(
class_name: class_def.name.to_string(),
method_name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
return;
@@ -162,7 +162,7 @@ pub(crate) fn non_self_return_type(
class_name: class_def.name.to_string(),
method_name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
return;
@@ -177,7 +177,7 @@ pub(crate) fn non_self_return_type(
class_name: class_def.name.to_string(),
method_name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
return;
@@ -193,7 +193,7 @@ pub(crate) fn non_self_return_type(
class_name: class_def.name.to_string(),
method_name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
}
@@ -206,7 +206,7 @@ pub(crate) fn non_self_return_type(
class_name: class_def.name.to_string(),
method_name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
}

View File

@@ -90,7 +90,7 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
StrOrReprDefinedInStub {
name: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
let stmt = checker.semantic().stmt();

View File

@@ -32,6 +32,6 @@ pub(crate) fn stub_body_multiple_statements(checker: &mut Checker, stmt: &Stmt,
checker.diagnostics.push(Diagnostic::new(
StubBodyMultipleStatements,
stmt.identifier(checker.locator),
stmt.identifier(),
));
}

View File

@@ -34,7 +34,7 @@ impl Violation for TypeCommentInStub {
}
/// PYI033
pub(crate) fn type_comment_in_stub(indexer: &Indexer, locator: &Locator) -> Vec<Diagnostic> {
pub(crate) fn type_comment_in_stub(locator: &Locator, indexer: &Indexer) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
for range in indexer.comment_ranges() {

View File

@@ -9,7 +9,6 @@ use libcst_native::{
};
use rustpython_parser::ast::{self, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp};
use crate::autofix::codemods::CodegenStylist;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::{has_comments_in, Truthiness};
@@ -17,6 +16,7 @@ use ruff_python_ast::source_code::{Locator, Stylist};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{visitor, whitespace};
use crate::autofix::codemods::CodegenStylist;
use crate::checkers::ast::Checker;
use crate::cst::matchers::match_indented_block;
use crate::cst::matchers::match_module;
@@ -315,6 +315,54 @@ fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> {
}))
}
/// Propagate parentheses from a parent to a child expression, if necessary.
///
/// For example, when splitting:
/// ```python
/// assert (a and b ==
/// """)
/// ```
///
/// The parentheses need to be propagated to the right-most expression:
/// ```python
/// assert a
/// assert (b ==
/// "")
/// ```
fn parenthesize<'a>(expression: Expression<'a>, parent: &Expression<'a>) -> Expression<'a> {
if matches!(
expression,
Expression::Comparison(_)
| Expression::UnaryOperation(_)
| Expression::BinaryOperation(_)
| Expression::BooleanOperation(_)
| Expression::Attribute(_)
| Expression::Tuple(_)
| Expression::Call(_)
| Expression::GeneratorExp(_)
| Expression::ListComp(_)
| Expression::SetComp(_)
| Expression::DictComp(_)
| Expression::List(_)
| Expression::Set(_)
| Expression::Dict(_)
| Expression::Subscript(_)
| Expression::StarredElement(_)
| Expression::IfExp(_)
| Expression::Lambda(_)
| Expression::Yield(_)
| Expression::Await(_)
| Expression::ConcatenatedString(_)
| Expression::FormattedString(_)
| Expression::NamedExpr(_)
) {
if let (Some(left), Some(right)) = (parent.lpar().first(), parent.rpar().first()) {
return expression.with_parens(left.clone(), right.clone());
}
}
expression
}
/// Replace composite condition `assert a == "hello" and b == "world"` with two statements
/// `assert a == "hello"` and `assert b == "world"`.
fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Result<Edit> {
@@ -363,10 +411,6 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) ->
bail!("Expected simple statement to be an assert")
};
if !(assert_statement.test.lpar().is_empty() && assert_statement.test.rpar().is_empty()) {
bail!("Unable to split parenthesized condition");
}
// Extract the individual conditions.
let mut conditions: Vec<Expression> = Vec::with_capacity(2);
match &assert_statement.test {
@@ -374,8 +418,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) ->
if matches!(op.operator, libcst_native::UnaryOp::Not { .. }) {
if let Expression::BooleanOperation(op) = &*op.expression {
if matches!(op.operator, BooleanOp::Or { .. }) {
conditions.push(negate(&op.left));
conditions.push(negate(&op.right));
conditions.push(parenthesize(negate(&op.left), &assert_statement.test));
conditions.push(parenthesize(negate(&op.right), &assert_statement.test));
} else {
bail!("Expected assert statement to be a composite condition");
}
@@ -386,8 +430,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) ->
}
Expression::BooleanOperation(op) => {
if matches!(op.operator, BooleanOp::And { .. }) {
conditions.push(*op.left.clone());
conditions.push(*op.right.clone());
conditions.push(parenthesize(*op.left.clone(), &assert_statement.test));
conditions.push(parenthesize(*op.right.clone(), &assert_statement.test));
} else {
bail!("Expected assert statement to be a composite condition");
}

View File

@@ -379,7 +379,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: &
PytestIncorrectFixtureNameUnderscore {
function: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
} else if checker.enabled(Rule::PytestMissingFixtureNameUnderscore)
&& !visitor.has_return_with_value
@@ -390,7 +390,7 @@ fn check_fixture_returns(checker: &mut Checker, stmt: &Stmt, name: &str, body: &
PytestMissingFixtureNameUnderscore {
function: name.to_string(),
},
stmt.identifier(checker.locator),
stmt.identifier(),
));
}

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{self, Expr, Identifier, Keyword, Ranged, Stmt, WithItem};
use rustpython_parser::ast::{self, Expr, Keyword, Ranged, Stmt, WithItem};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -74,9 +74,12 @@ pub(crate) fn raises_call(checker: &mut Checker, func: &Expr, args: &[Expr], key
}
if checker.enabled(Rule::PytestRaisesTooBroad) {
let match_keyword = keywords
.iter()
.find(|kw| kw.arg == Some(Identifier::new("match")));
let match_keyword = keywords.iter().find(|keyword| {
keyword
.arg
.as_ref()
.map_or(false, |arg| arg.as_str() == "match")
});
if let Some(exception) = args.first() {
if let Some(match_keyword) = match_keyword {

View File

@@ -3,7 +3,9 @@ use std::hash::BuildHasherDefault;
use anyhow::{anyhow, bail, Result};
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Keyword, Stmt, UnaryOp};
use rustpython_parser::ast::{
self, CmpOp, Constant, Expr, ExprContext, Identifier, Keyword, Stmt, UnaryOp,
};
/// An enum to represent the different types of assertions present in the
/// `unittest` module. Note: any variants that can't be replaced with plain
@@ -388,7 +390,7 @@ impl UnittestAssert {
};
let node1 = ast::ExprAttribute {
value: Box::new(node.into()),
attr: "search".into(),
attr: Identifier::new("search".to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
};

View File

@@ -163,8 +163,8 @@ PT018.py:21:5: PT018 [*] Assertion should be broken down into multiple parts
22 | | message
23 | | """
| |_______^ PT018
24 |
25 | # recursive case
24 | assert (
25 | something
|
= help: Break down assertion into multiple parts
@@ -177,131 +177,144 @@ PT018.py:21:5: PT018 [*] Assertion should be broken down into multiple parts
22 |+ assert something_else == """error
22 23 | message
23 24 | """
24 25 |
24 25 | assert (
PT018.py:26:5: PT018 [*] Assertion should be broken down into multiple parts
PT018.py:24:5: PT018 [*] Assertion should be broken down into multiple parts
|
25 | # recursive case
26 | assert not (a or not (b or c))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
27 | assert not (a or not (b and c))
|
= help: Break down assertion into multiple parts
Suggested fix
23 23 | """
24 24 |
25 25 | # recursive case
26 |- assert not (a or not (b or c))
26 |+ assert not a
27 |+ assert (b or c)
27 28 | assert not (a or not (b and c))
28 29 |
29 30 | # detected, but no autofix for messages
PT018.py:27:5: PT018 [*] Assertion should be broken down into multiple parts
|
25 | # recursive case
26 | assert not (a or not (b or c))
27 | assert not (a or not (b and c))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
28 |
29 | # detected, but no autofix for messages
|
= help: Break down assertion into multiple parts
Suggested fix
24 24 |
25 25 | # recursive case
26 26 | assert not (a or not (b or c))
27 |- assert not (a or not (b and c))
27 |+ assert not a
28 |+ assert (b and c)
28 29 |
29 30 | # detected, but no autofix for messages
30 31 | assert something and something_else, "error message"
PT018.py:30:5: PT018 Assertion should be broken down into multiple parts
|
29 | # detected, but no autofix for messages
30 | assert something and something_else, "error message"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
31 | assert not (something or something_else and something_third), "with message"
32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`)
|
= help: Break down assertion into multiple parts
PT018.py:31:5: PT018 Assertion should be broken down into multiple parts
|
29 | # detected, but no autofix for messages
30 | assert something and something_else, "error message"
31 | assert not (something or something_else and something_third), "with message"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`)
33 | assert not (something or something_else and something_third)
|
= help: Break down assertion into multiple parts
PT018.py:33:5: PT018 Assertion should be broken down into multiple parts
|
31 | assert not (something or something_else and something_third), "with message"
32 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`)
33 | assert not (something or something_else and something_third)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
34 | # detected, but no autofix for parenthesized conditions
35 | assert (
|
= help: Break down assertion into multiple parts
PT018.py:35:5: PT018 Assertion should be broken down into multiple parts
|
33 | assert not (something or something_else and something_third)
34 | # detected, but no autofix for parenthesized conditions
35 | assert (
22 | message
23 | """
24 | assert (
| _____^
36 | | something
37 | | and something_else
38 | | == """error
39 | | message
40 | | """
41 | | )
25 | | something
26 | | and something_else
27 | | == """error
28 | | message
29 | | """
30 | | )
| |_____^ PT018
31 |
32 | # recursive case
|
= help: Break down assertion into multiple parts
Suggested fix
21 21 | assert something and something_else == """error
22 22 | message
23 23 | """
24 |+ assert something
24 25 | assert (
25 |- something
26 |- and something_else
26 |+ something_else
27 27 | == """error
28 28 | message
29 29 | """
PT018.py:33:5: PT018 [*] Assertion should be broken down into multiple parts
|
32 | # recursive case
33 | assert not (a or not (b or c))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
34 | assert not (a or not (b and c))
|
= help: Break down assertion into multiple parts
Suggested fix
30 30 | )
31 31 |
32 32 | # recursive case
33 |- assert not (a or not (b or c))
33 |+ assert not a
34 |+ assert (b or c)
34 35 | assert not (a or not (b and c))
35 36 |
36 37 | # detected, but no autofix for messages
PT018.py:34:5: PT018 [*] Assertion should be broken down into multiple parts
|
32 | # recursive case
33 | assert not (a or not (b or c))
34 | assert not (a or not (b and c))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
35 |
36 | # detected, but no autofix for messages
|
= help: Break down assertion into multiple parts
Suggested fix
31 31 |
32 32 | # recursive case
33 33 | assert not (a or not (b or c))
34 |- assert not (a or not (b and c))
34 |+ assert not a
35 |+ assert (b and c)
35 36 |
36 37 | # detected, but no autofix for messages
37 38 | assert something and something_else, "error message"
PT018.py:37:5: PT018 Assertion should be broken down into multiple parts
|
36 | # detected, but no autofix for messages
37 | assert something and something_else, "error message"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
38 | assert not (something or something_else and something_third), "with message"
39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`)
|
= help: Break down assertion into multiple parts
PT018.py:38:5: PT018 Assertion should be broken down into multiple parts
|
36 | # detected, but no autofix for messages
37 | assert something and something_else, "error message"
38 | assert not (something or something_else and something_third), "with message"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`)
40 | assert not (something or something_else and something_third)
|
= help: Break down assertion into multiple parts
PT018.py:40:5: PT018 Assertion should be broken down into multiple parts
|
38 | assert not (something or something_else and something_third), "with message"
39 | # detected, but no autofix for mixed conditions (e.g. `a or b and c`)
40 | assert not (something or something_else and something_third)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
|
= help: Break down assertion into multiple parts
PT018.py:44:1: PT018 [*] Assertion should be broken down into multiple parts
|
43 | assert something # OK
44 | assert something and something_else # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
45 | assert something and something_else and something_third # Error
|
= help: Break down assertion into multiple parts
Suggested fix
41 41 |
42 42 |
43 43 | assert something # OK
44 |-assert something and something_else # Error
44 |+assert something
45 |+assert something_else
45 46 | assert something and something_else and something_third # Error
PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts
|
44 | assert something # OK
45 | assert something and something_else # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
46 | assert something and something_else and something_third # Error
|
= help: Break down assertion into multiple parts
Suggested fix
42 42 |
43 43 |
44 44 | assert something # OK
45 |-assert something and something_else # Error
45 |+assert something
46 |+assert something_else
46 47 | assert something and something_else and something_third # Error
PT018.py:46:1: PT018 [*] Assertion should be broken down into multiple parts
|
44 | assert something # OK
45 | assert something and something_else # Error
46 | assert something and something_else and something_third # Error
43 | assert something # OK
44 | assert something and something_else # Error
45 | assert something and something_else and something_third # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
|
= help: Break down assertion into multiple parts
Suggested fix
43 43 |
44 44 | assert something # OK
45 45 | assert something and something_else # Error
46 |-assert something and something_else and something_third # Error
46 |+assert something and something_else
47 |+assert something_third
42 42 |
43 43 | assert something # OK
44 44 | assert something and something_else # Error
45 |-assert something and something_else and something_third # Error
45 |+assert something and something_else
46 |+assert something_third

View File

@@ -1,7 +1,7 @@
use log::error;
use ruff_text_size::TextRange;
use rustc_hash::FxHashSet;
use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Ranged, Stmt};
use rustpython_parser::ast::{self, CmpOp, Constant, Expr, ExprContext, Identifier, Ranged, Stmt};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -941,7 +941,7 @@ pub(crate) fn use_dict_get_with_default(
let node1 = *test_key.clone();
let node2 = ast::ExprAttribute {
value: expected_subscript.clone(),
attr: "get".into(),
attr: Identifier::new("get".to_string(), TextRange::default()),
ctx: ExprContext::Load,
range: TextRange::default(),
};

View File

@@ -77,7 +77,7 @@ pub(crate) fn no_slots_in_namedtuple_subclass(
if !has_slots(&class.body) {
checker.diagnostics.push(Diagnostic::new(
NoSlotsInNamedtupleSubclass,
stmt.identifier(checker.locator),
stmt.identifier(),
));
}
}

View File

@@ -59,10 +59,9 @@ pub(crate) fn no_slots_in_str_subclass(checker: &mut Checker, stmt: &Stmt, class
})
}) {
if !has_slots(&class.body) {
checker.diagnostics.push(Diagnostic::new(
NoSlotsInStrSubclass,
stmt.identifier(checker.locator),
));
checker
.diagnostics
.push(Diagnostic::new(NoSlotsInStrSubclass, stmt.identifier()));
}
}
}

View File

@@ -63,10 +63,9 @@ pub(crate) fn no_slots_in_tuple_subclass(checker: &mut Checker, stmt: &Stmt, cla
})
}) {
if !has_slots(&class.body) {
checker.diagnostics.push(Diagnostic::new(
NoSlotsInTupleSubclass,
stmt.identifier(checker.locator),
));
checker
.diagnostics
.push(Diagnostic::new(NoSlotsInTupleSubclass, stmt.identifier()));
}
}
}

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