Compare commits

..

220 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
Charlie Marsh
8e06140d1d Remove continuations when deleting statements (#5198)
## Summary

This PR modifies our statement deletion logic to delete any preceding
continuation lines.

For example, given:

```py
x = 1; \
  import os
```

We'll now rewrite to:

```py
x = 1;
```

In addition, the logic can now handle multiple preceding continuations
(which is unlikely, but valid).
2023-06-19 22:04:28 -04:00
Charlie Marsh
015895bcae Move copyright rule to nursery (#5197)
## Summary

I want this to be explicitly opted-into.
2023-06-19 21:41:47 -04:00
Charlie Marsh
36e01ad6eb Upgrade RustPython (#5192)
## Summary

This PR upgrade RustPython to pull in the changes to `Arguments` (zip
defaults with their identifiers) and all the renames to `CmpOp` and
friends.
2023-06-19 21:09:53 +00:00
Charlie Marsh
ddfdc3bb01 Add rule documentation URL to JSON output (#5187)
## Summary

I want to include URLs to the rule documentation in the LSP (the LSP has
a native `code_description` field for this, which, if specified, causes
the source to be rendered as a link to the docs). This PR exposes the
URL to the documentation in the Ruff JSON output.
2023-06-19 21:09:15 +00:00
Dhruv Manilawala
48f4f2d63d Maintain consistency when deserializing to JSON (#5114)
## Summary

Maintain consistency while deserializing Jupyter notebook to JSON. The
following changes were made:

1. Use string array to store the source value as that's the default
(5781720423/nbformat/v4/nbjson.py (L56-L57))
2. Remove unused structs and enums
3. Reorder the keys in alphabetical order as that's the default.
(5781720423/nbformat/v4/nbjson.py (L51))

### Side effect

Removing the `preserve_order` feature means that the order of keys in
JSON output (`--format json`) will be in alphabetical order. This is
because the value is represented using `serde_json::Value` which
internally is a `BTreeMap`, thus sorting it as per the string key. For
posterity if this turns out to be not ideal, then we could define a
struct representing the JSON object and the order of struct fields will
determine the order in the JSON string.

## Test Plan

Add a test case to assert the raw JSON string.
2023-06-19 23:47:56 +05:30
Charlie Marsh
94abf7f088 Rename *Importation structs to *Import (#5185)
## Summary

I find "Importation" a bit awkward, it may not even be grammatically
correct here.
2023-06-19 12:09:10 -04:00
Thomas de Zeeuw
e3c12764f8 Only use a single cache file per Python package (#5117)
## Summary

This changes the caching design from one cache file per source file, to
one cache file per package. This greatly reduces the amount of cache
files that are opened and written, while maintaining roughly the same
(combined) size as bincode is very compact.

Below are some very much not scientific performance tests. It uses
projects/sources to check:

* small.py: single, 31 bytes Python file with 2 errors.
* test.py: single, 43k Python file with 8 errors.
* fastapi: FastAPI repo, 1134 files checked, 0 errors.

Source   | Before # files | After # files | Before size | After size
-------|-------|-------|-------|-------
small.py | 1              | 1             | 20 K        | 20 K
test.py  | 1              | 1             | 60 K        | 60 K
fastapi  | 1134           | 518           | 4.5 M       | 2.3 M

One question that might come up is why fastapi still has 518 cache files
and not 1? That is because this is using the existing package
resolution, which sees examples, docs, etc. as separate from the "main"
source code (in the fastapi directory in the repo). In this future it
might be worth consider switching to a one cache file per repo strategy.

This new design is not perfect and does have a number of known issues.
First, like the old design it doesn't remove the cache for a source file
that has been (re)moved until `ruff clean` is called.

Second, this currently uses a large mutex around the mutation of the
package cache (e.g. inserting result). This could be (or become) a
bottleneck. It's future work to test and improve this (if needed).

Third, currently the packages and opened and stored in a sequential
loop, this could be done parallel. This is also future work.


## Test Plan

Run `ruff check` (with caching enabled) twice on any Python source code
and it should produce the same results.
2023-06-19 17:46:13 +02:00
konstin
b8d378b0a3 Add a script that tests formatter stability on repositories (#5055)
## Summary

We want to ensure that once formatted content stays the same when
formatted again, which is known as formatter stability or formatter
idempotency, and that the formatter prints syntactically valid code. As
our test cases cover only a limited amount of code, this allows checking
entire repositories.

This adds a new subcommand to `ruff_dev` which can be invoked as `cargo
run --bin ruff_dev -- check-formatter-stability <repo>`. While initially
only intended to check stability, it has also found cases where the
formatter printed invalid syntax or panicked.

 ## Test Plan

Running this on cpython is already identifying bugs
(https://github.com/astral-sh/ruff/pull/5089)
2023-06-19 14:13:38 +00:00
konstin
0e028142f4 Explain dangling comments in the formatter (#5170)
This documentation change improves the section on dangling comments in
the formatter.

---------

Co-authored-by: David Szotten <davidszotten@gmail.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-06-19 14:24:45 +02:00
konstin
361d45f2b2 Add cargo dev repeat for profiling (#5144)
## Summary

This adds a new subcommand that can be used as

```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
flamegraph --perfdata perf.data
```

## Test Plan

This is a ruff internal script. I successfully used it to profile
cpython with the instructions above
2023-06-19 11:40:09 +02:00
Charlie Marsh
be11cae619 Fix allowed-ellipsis detection (#5174)
## Summary

We weren't resetting the `allow_ellipsis` flag properly, which
ultimately caused us to treat the semicolon as "unnecessary" rather than
"creating a multi-statement line".

Closes #5154.
2023-06-19 04:19:41 +00:00
Charlie Marsh
2b82caa163 Detect continuations at start-of-file (#5173)
## Summary

Given:

```python
\
import os
```

Deleting `import os` leaves a syntax error: a file can't end in a
continuation. We have code to handle this case, but it failed to pick up
continuations at the _very start_ of a file.

Closes #5156.
2023-06-19 00:09:02 -04:00
Charlie Marsh
a6cf31cc89 Move dead_scopes to deferred.scopes (#5171)
## Summary

This is more consistent with the rest of the `deferred` patterns.
2023-06-18 15:57:38 +00:00
Charlie Marsh
524a2045ba Enable autofix for unconventional imports rule (#5152)
## Summary

We can now automatically rewrite `import pandas` to `import pandas as
pd`, with minimal changes needed.
2023-06-18 15:56:42 +00:00
Charlie Marsh
a0b750f74b Move unconventional import rule to post-binding phase (#5151)
## Summary

This PR moves the "unconventional import alias" rule (which enforces,
e.g., that `pandas` is imported as `pd`) to the "dead scopes" phase,
after the main linter pass. This (1) avoids an allocation since we no
longer need to create the qualified name in the linter pass; and (2)
will allow us to autofix it, since we'll have access to all references.

## Test Plan

`cargo test` -- all changes are to ranges (which are improvements IMO).
2023-06-18 15:23:40 +00:00
Chris Pryer
195b36c429 Format continue statement (#5165)
<!--
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 `continue` statement.

## Test Plan

`continue` is used already in some tests, but if a new test is needed I
could add it.

---------

Co-authored-by: konstin <konstin@mailbox.org>
2023-06-18 11:25:59 +00:00
konstin
5c416e4d9b Pre commit without cargo and other pre-PR improvements (#5146)
This tackles three problems:
* pre-commit was slow because it ran cargo commands
* Improve the clarity on what you need to run to get your PR pass on CI
(and make those fast)
* You had to compile and run `cargo dev generate-all` separately, which
was slow

The first change is to remove all cargo commands except running ruff
itself from pre-commit. With `cargo run --bin ruff` already compiled it
takes about 7s on my machine. It would make sense to also use the ruff
pre-commit action here even if we're then lagging a release behind for
checking ruff on ruff.

The contributing guide is now clear about what you need to run:

```shell
cargo clippy --workspace --all-targets --all-features -- -D warnings  # Linting...
RUFF_UPDATE_SCHEMA=1 cargo test  # Testing and updating ruff.schema.json
pre-commit run --all-files  # rust and python formatting, markdown and python linting, etc.
```

Example timings from my machine:

`cargo clippy --workspace --all-targets --all-features -- -D warnings`:
23s
`RUFF_UPDATE_SCHEMA=1 cargo test`: 2min (recompiling), 1min (no code
changes, this is mainly doc tests)
`pre-commit run --all-files`: 7s

The exact numbers don't matter so much as the approximate experience (6s
is easier to just wait than 1min, esp if you need to fix and rerun). The
biggest remaining block seems to be doc tests, i'm surprised i didn't
find any solution to speeding them up (nextest simply doesn't run them
at all). Also note that the formatter has it's own tests which are much
faster since they avoid linking ruff (`cargo test
ruff_python_formatter`).

The third change is to enable `cargo test` to update the schema. Similar
to `INSTA_UPDATE=always`, i've added `RUFF_UPDATE_SCHEMA=1` (name open
to bikeshedding), so `RUFF_UPDATE_SCHEMA=1 cargo test` updates the
schema, while `cargo test` still fails as expected if the repo isn't
up-to-date.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2023-06-18 11:00:42 +00:00
konstin
763d38cafb Refactor top llvm-lines entry (#5147)
## Summary

This refactors the top entry in terms of llvm lines,
`RuleCodePrefix::iter()`. It's only used for generating the schema and
the clap completion so no effect on performance.

I've confirmed with
```
CARGO_TARGET_DIR=target-llvm-lines RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib | head -n 20
```
that this indeed remove the method from the list of heaviest symbols in
terms of llvm-lines

Before:
```
  Lines                  Copies               Function name
  -----                  ------               -------------
  1768469                40538                (TOTAL)
    10391 (0.6%,  0.6%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::RuleCodePrefix>::iter
     8250 (0.5%,  1.1%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::Rule>::noqa_code
     7427 (0.4%,  1.5%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::checkers::ast::Checker as ruff_python_ast[c4c9eadfa5741dd4]::visitor::Visitor>::visit_stmt
     6536 (0.4%,  1.8%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::<toml_edit[de4ca26332d39787]::de::spanned::SpannedDeserializer<toml_edit[de4ca26332d39787]::de::value::ValueDeserializer>>
     6536 (0.4%,  2.2%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::<toml_edit[de4ca26332d39787]::de::table::TableMapAccess>
     6533 (0.4%,  2.6%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::<toml_edit[de4ca26332d39787]::de::datetime::DatetimeDeserializer>
     5727 (0.3%,  2.9%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::checkers::ast::Checker as ruff_python_ast[c4c9eadfa5741dd4]::visitor::Visitor>::visit_expr
     4453 (0.3%,  3.2%)      1 (0.0%,  0.0%)  ruff[fa0f2e8ef07114da]::flake8_to_ruff::converter::convert
     3790 (0.2%,  3.4%)      1 (0.0%,  0.0%)  <&ruff[fa0f2e8ef07114da]::registry::Linter as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter
     3416 (0.2%,  3.6%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::registry::Linter>::code_for_rule
     3187 (0.2%,  3.7%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::Rule as core[da82827a87f140f9]::fmt::Debug>::fmt
     3185 (0.2%,  3.9%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<&ruff[fa0f2e8ef07114da]::codes::Rule>>::from
     3185 (0.2%,  4.1%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<ruff[fa0f2e8ef07114da]::codes::Rule>>::from
     3185 (0.2%,  4.3%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::Rule as core[da82827a87f140f9]::convert::AsRef<str>>::as_ref
     3183 (0.2%,  4.5%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::RuleIter>::get
     2718 (0.2%,  4.6%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_seq::<toml_edit[de4ca26332d39787]::de::array::ArraySeqAccess>
     2706 (0.2%,  4.8%)      1 (0.0%,  0.0%)  <&ruff[fa0f2e8ef07114da]::codes::Pylint as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter
```
After:
```
  Lines                  Copies               Function name
  -----                  ------               -------------
  1763380                40806                (TOTAL)
     8250 (0.5%,  0.5%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::Rule>::noqa_code
     7427 (0.4%,  0.9%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::checkers::ast::Checker as ruff_python_ast[c4c9eadfa5741dd4]::visitor::Visitor>::visit_stmt
     6536 (0.4%,  1.3%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::<toml_edit[de4ca26332d39787]::de::spanned::SpannedDeserializer<toml_edit[de4ca26332d39787]::de::value::ValueDeserializer>>
     6536 (0.4%,  1.6%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::<toml_edit[de4ca26332d39787]::de::table::TableMapAccess>
     6533 (0.4%,  2.0%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::<toml_edit[de4ca26332d39787]::de::datetime::DatetimeDeserializer>
     5727 (0.3%,  2.3%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::checkers::ast::Checker as ruff_python_ast[c4c9eadfa5741dd4]::visitor::Visitor>::visit_expr
     4453 (0.3%,  2.6%)      1 (0.0%,  0.0%)  ruff[fa0f2e8ef07114da]::flake8_to_ruff::converter::convert
     3790 (0.2%,  2.8%)      1 (0.0%,  0.0%)  <&ruff[fa0f2e8ef07114da]::registry::Linter as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter
     3416 (0.2%,  3.0%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::registry::Linter>::code_for_rule
     3187 (0.2%,  3.2%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::Rule as core[da82827a87f140f9]::fmt::Debug>::fmt
     3185 (0.2%,  3.3%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<&ruff[fa0f2e8ef07114da]::codes::Rule>>::from
     3185 (0.2%,  3.5%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<ruff[fa0f2e8ef07114da]::codes::Rule>>::from
     3185 (0.2%,  3.7%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::Rule as core[da82827a87f140f9]::convert::AsRef<str>>::as_ref
     3183 (0.2%,  3.9%)      1 (0.0%,  0.0%)  <ruff[fa0f2e8ef07114da]::codes::RuleIter>::get
     2718 (0.2%,  4.0%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::settings::options::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_seq::<toml_edit[de4ca26332d39787]::de::array::ArraySeqAccess>
     2706 (0.2%,  4.2%)      1 (0.0%,  0.0%)  <&ruff[fa0f2e8ef07114da]::codes::Pylint as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter
     2573 (0.1%,  4.3%)      1 (0.0%,  0.0%)  <<ruff[fa0f2e8ef07114da]::rules::isort::settings::Options as serde[1a28808d63625aed]::de::Deserialize>::deserialize::__Visitor as serde[1a28808d63625aed]::de::Visitor>::visit_map::<toml_edit[de4ca26332d39787]::de::spanned::SpannedDeserializer<toml_edit[de4ca26332d39787]::de::value::ValueDeserializer>>
```
I didn't measure the effect on binary size this time.

## Testing

`cargo test` which uses this to generate the schema didn't change
2023-06-18 12:39:06 +02:00
Evan Rittenhouse
653a0ebf2d Add Applicability to pyupgrade (#5162)
## Summary

Fixes some of #4184.
2023-06-17 19:33:11 +00:00
Evan Rittenhouse
95448ba669 Add Applicability to isort (#5161)
## Summary

Fixes some of #4184.
2023-06-17 19:08:11 +00:00
Charlie Marsh
f18e10183f Add some minor tweaks to latest docs (#5164) 2023-06-17 17:04:50 +00:00
Tom Kuson
98920909c6 Complete documentation for flake8-blind-except and flake8-raise rules (#5143)
## Summary

Completes the documentation for the `flake8-blind-except` and
`flake8-raise` rules.

Related to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-17 12:56:27 -04:00
Evan Rittenhouse
e1e1d2d341 Add Applicability to flynt (#5160)
## Summary

Fixes some of #4184.
2023-06-17 12:05:43 -04:00
David Szotten
4b9b6829dc format StmtBreak (#5158)
## Summary

format `StmtBreak`

trying to learn how to help out with the formatter. starting simple

## Test Plan

new snapshot test
2023-06-17 10:31:29 +02:00
Charlie Marsh
be107dad64 Add a PNG variant of the Astral badge (#5155) 2023-06-17 03:24:32 +00:00
Charlie Marsh
d0ad1ed0af Replace static CallPath vectors with matches! macros (#5148)
## Summary

After #5140, I audited the codebase for similar patterns (defining a
list of `CallPath` entities in a static vector, then looping over them
to pattern-match). This PR migrates all other such cases to use `match`
and `matches!` where possible.

There are a few benefits to this:

1. It more clearly denotes the intended semantics (branches are
exclusive).
2. The compiler can help deduplicate the patterns and detect unreachable
branches.
3. Performance: in the benchmark below, the all-rules performance is
increased by nearly 10%...

## Benchmarks

I decided to benchmark against a large file in the Airflow repository
with a lot of type annotations
([`views.py`](https://raw.githubusercontent.com/apache/airflow/f03f73100e8a7d6019249889de567cb00e71e457/airflow/www/views.py)):

```
linter/default-rules/airflow/views.py
                        time:   [10.871 ms 10.882 ms 10.894 ms]
                        thrpt:  [19.739 MiB/s 19.761 MiB/s 19.781 MiB/s]
                 change:
                        time:   [-2.7182% -2.5687% -2.4204%] (p = 0.00 < 0.05)
                        thrpt:  [+2.4805% +2.6364% +2.7942%]
                        Performance has improved.

linter/all-rules/airflow/views.py
                        time:   [24.021 ms 24.038 ms 24.062 ms]
                        thrpt:  [8.9373 MiB/s 8.9461 MiB/s 8.9527 MiB/s]
                 change:
                        time:   [-8.9537% -8.8516% -8.7527%] (p = 0.00 < 0.05)
                        thrpt:  [+9.5923% +9.7112% +9.8342%]
                        Performance has improved.
Found 12 outliers among 100 measurements (12.00%)
  5 (5.00%) high mild
  7 (7.00%) high severe
```

The impact is dramatic -- nearly a 10% improvement for `all-rules`.
2023-06-16 17:34:42 +00:00
Charlie Marsh
b3240dbfa2 Avoid propagating BindingKind::Global and BindingKind::Nonlocal (#5136)
## Summary

This PR fixes a small quirk in the semantic model. Typically, when we
see an import, like `import foo`, we create a `BindingKind::Importation`
for it. However, if `foo` has been declared as a `global`, then we
propagate the kind forward. So given:

```python
global foo

import foo
```

We'd create two bindings for `foo`, both with type `global`.

This was originally borrowed from Pyflakes, and it exists to help avoid
false-positives like:

```python
def f():
    global foo

    # Don't mark `foo` as "assigned but unused"! It's a global!
    foo = 1
```

This PR removes that behavior, and instead tracks "Does this binding
refer to a global?" as a flag. This is much cleaner, since it means we
don't "lose" the identity of various bindings.

As a very strange example of why this matters, consider:

```python
def foo():
    global Member

    from module import Member

    x: Member = 1
```

`Member` is only used in a typing context, so we should flag it and say
"move it to a `TYPE_CHECKING` block". However, when we go to analyze
`from module import Member`, it has `BindingKind::Global`. So we don't
even know that it's an import!
2023-06-16 11:06:59 -04:00
Charlie Marsh
fd1dfc3bfa Add support for global and nonlocal symbol renames (#5134)
## Summary

In #5074, we introduced an abstraction to support local symbol renames
("local" here refers to "within a module"). However, that abstraction
didn't support `global` and `nonlocal` symbols. This PR extends it to
those cases.

Broadly, there are considerations.

First, if we're renaming a symbol in a scope in which it is declared
`global` or `nonlocal`. For example, given:

```python
x = 1

def foo():
    global x
```

Then when renaming `x` in `foo`, we need to detect that it's `global`
and instead perform the rename starting from the module scope.

Second, when renaming a symbol, we need to determine the scopes in which
it is declared `global` or `nonlocal`. This is effectively the inverse
of the above: when renaming `x` in the module scope, we need to detect
that we should _also_ rename `x` in `foo`.

To support these cases, the renaming algorithm was adjusted as follows:

- When we start a rename in a scope, determine whether the symbol is
declared `global` or `nonlocal` by looking for a `global` or `nonlocal`
binding. If it is, start the rename in the defining scope. (This
requires storing the defining scope on the `nonlocal` binding, which is
new.)
- We then perform the rename in the defining scope.
- We then check whether the symbol was declared as `global` or
`nonlocal` in any scopes, and perform the rename in those scopes too.
(Thankfully, this doesn't need to be done recursively.)

Closes #5092.

## Test Plan

Added some additional snapshot tests.
2023-06-16 14:35:10 +00:00
Charlie Marsh
b9754bd5c5 Add autofix for Set-to-AbstractSet rewrite using reference tracking (#5074)
## Summary

This PR enables autofix behavior for the `flake8-pyi` rule that asks you
to alias `Set` to `AbstractSet` when importing `collections.abc.Set`.
It's not the most important rule, but it's a good isolated test-case for
local symbol renaming.

The renaming algorithm is outlined in-detail in the `renamer.rs` module.
But to demonstrate the behavior, here's the diff when running this fix
over a complex file that exercises a few edge cases:

```diff
--- a/foo.pyi
+++ b/foo.pyi
@@ -1,16 +1,16 @@
 if True:
-    from collections.abc import Set
+    from collections.abc import Set as AbstractSet
 else:
-    Set = 1
+    AbstractSet = 1

-x: Set = set()
+x: AbstractSet = set()

-x: Set
+x: AbstractSet

-del Set
+del AbstractSet

 def f():
-    print(Set)
+    print(AbstractSet)

     def Set():
         pass
```

Making this work required resolving a bunch of edge cases in the
semantic model that were causing us to "lose track" of references. For
example, the above wasn't possible with our previous approach to
handling deletions (#5071). Similarly, the `x: Set` "delayed annotation"
tracking was enabled via #5070. And many of these edits would've failed
if we hadn't changed `BindingKind` to always match the identifier range
(#5090). So it's really the culmination of a bunch of changes over the
course of the week.

The main outstanding TODO is that this doesn't support `global` or
`nonlocal` usages. I'm going to take a look at that tonight, but I'm
comfortable merging this as-is.

Closes #1106.

Closes #5091.
2023-06-16 14:12:33 +00:00
Charlie Marsh
307f7a735c Avoid allocations in lowercase comparisons (#5137)
## Summary

I noticed that we have a few hot comparisons that involve called
`s.to_lowercase()`. We can avoid an allocation by comparing characters
directly.
2023-06-16 08:57:43 -04:00
Charlie Marsh
3af9dfeb0a Rewrite suspicious_function_call as a match statement (#5140)
## Summary

@konstin mentioned that in profiling, this function accounted for a
non-trivial amount of time (0.33% of total execution, the most of any
rule). This PR attempts to rewrite it as a match statement for better
performance over a looping comparison.

## Test Plan

`cargo test`
2023-06-16 08:57:20 -04:00
Charlie Marsh
5526699535 Use const-singleton helpers in more rules (#5142) 2023-06-16 04:28:35 +00:00
Charlie Marsh
fab2a4adf7 Use matches! for insecure hash rule (#5141) 2023-06-16 04:18:32 +00:00
Charlie Marsh
13813dc1b1 Skip DJ008 enforcement in stub files (#5139)
Closes #5138.
2023-06-16 03:49:40 +00:00
Charlie Marsh
70c01257ca Minor formatting changes to Checker (#5135) 2023-06-15 22:42:21 -04:00
Evan Rittenhouse
26d19655db Add Applicability to flake8_tidy_imports (#5131)
## Summary
Fixes some of https://github.com/astral-sh/ruff/issues/4184
2023-06-15 18:09:00 -04:00
Charlie Marsh
1f856aa576 Don't treat straight imports of __future__ as __future__ imports (#5128)
## Summary

If you `import __future__`, it's not subject to the same rules as `from
__future__ import feature` -- i.e., this is fine:

```python
x = 1

import __future__
```

It doesn't really make sense to treat these as `__future__` imports
(though I can't imagine anyone ever does this anyway).
2023-06-15 20:53:02 +00:00
Evan Rittenhouse
1e383483f7 Add Applicability to flake8_quotes fixes (#5130)
## Summary

Fixes some of #4184
2023-06-15 16:50:54 -04:00
Evan Rittenhouse
89b328c6be Add Applicability to flake8_logging_format fixes (#5129)
## Summary

Fixes some of #4184
2023-06-15 16:50:19 -04:00
Evan Rittenhouse
6143065fc2 Add Applicability to flake8_comma fixes (#5127)
## Summary

Fixes some of #4184
2023-06-15 16:49:54 -04:00
Charlie Marsh
107a295af4 Allow async with in redefined-loop-name (#5125)
## Summary

Closes #5124.
2023-06-15 15:00:19 -04:00
konstin
c811213302 Allow space in filename for powershell + windows + python module (#5115)
Fixes #5077

## Summary

Previously, in a powershell on windows when using `python -m ruff`
instead of `ruff` a call such as `python -m ruff "a b.py"` would fail
because the space would be split into two arguments.

The python docs
[recommend](https://docs.python.org/3/library/os.html#os.spawnv) using
subprocess instead of os.spawn variants, which does fix the problem.

## Test Plan

I manually confirmed that the problem previously occurred and now
doesn't anymore. This only happens in a very specific environment
(maturin build, windows, powershell), so i could try adding a step on CI
for it but i don't think it's worth it.

```
(.venv) PS C:\Users\Konstantin\PycharmProjects\ruff> python -m ruff "a b.py"
warning: Detected debug build without --no-cache.
error: Failed to lint a: The system cannot find the file specified. (os error 2)
error: Failed to lint b.py: The system cannot find the file specified. (os error 2)
a:1:1: E902 The system cannot find the file specified. (os error 2)
b.py:1:1: E902 The system cannot find the file specified. (os error 2)
Found 2 errors.
(.venv) PS C:\Users\Konstantin\PycharmProjects\ruff> python -m ruff "a b.py"
warning: Detected debug build without --no-cache.
a b.py:2:5: F841 [*] Local variable `x` is assigned to but never used
Found 1 error.
[*] 1 potentially fixable with the --fix option.
```
2023-06-15 20:57:00 +02:00
Charlie Marsh
5ea3e42513 Always use identifier ranges to store bindings (#5110)
## Summary

At present, when we store a binding, we include a `TextRange` alongside
it. The `TextRange` _sometimes_ matches the exact range of the
identifier to which the `Binding` is linked, but... not always.

For example, given:

```python
x = 1
```

The binding we create _will_ use the range of `x`, because the left-hand
side is an `Expr::Name`, which has a valid range on it.

However, given:

```python
try:
  pass
except ValueError as e:
  pass
```

When we create a binding for `e`, we don't have a `TextRange`... The AST
doesn't give us one. So we end up extracting it via lexing.

This PR extends that pattern to the rest of the binding kinds, to ensure
that whenever we create a binding, we always use the range of the bound
name. This leads to better diagnostics in cases like pattern matching,
whereby the diagnostic for "unused variable `x`" here used to include
`*x`, instead of just `x`:

```python
def f(provided: int) -> int:
    match provided:
        case [_, *x]:
            pass
```

This is _also_ required for symbol renames, since we track writes as
bindings -- so we need to know the ranges of the bound symbols.

By storing these bindings precisely, we can also remove the
`binding.trimmed_range` abstraction -- since bindings already use the
"trimmed range".

To implement this behavior, I took some of our existing utilities (like
the code we had for `except ValueError as e` above), migrated them from
a full lexer to a zero-allocation lexer that _only_ identifies
"identifiers", and moved the behavior into a trait, so we can now do
`stmt.identifier(locator)` to get the range for the identifier.

Honestly, we might end up discarding much of this if we decide to put
ranges on all identifiers
(https://github.com/astral-sh/RustPython-Parser/pull/8). But even if we
do, this will _still_ be a good change, because the lexer introduced
here is useful beyond names (e.g., we use it find the `except` keyword
in an exception handler, to find the `else` after a `for` loop, and so
on). So, I'm fine committing this even if we end up changing our minds
about the right approach.

Closes #5090.

## Benchmarks

No significant change, with one statistically significant improvement
(-2.1654% on `linter/all-rules/large/dataset.py`):

```
linter/default-rules/numpy/globals.py
                        time:   [73.922 µs 73.955 µs 73.986 µs]
                        thrpt:  [39.882 MiB/s 39.898 MiB/s 39.916 MiB/s]
                 change:
                        time:   [-0.5579% -0.4732% -0.3980%] (p = 0.00 < 0.05)
                        thrpt:  [+0.3996% +0.4755% +0.5611%]
                        Change within noise threshold.
Found 6 outliers among 100 measurements (6.00%)
  4 (4.00%) low severe
  1 (1.00%) low mild
  1 (1.00%) high mild
linter/default-rules/pydantic/types.py
                        time:   [1.4909 ms 1.4917 ms 1.4926 ms]
                        thrpt:  [17.087 MiB/s 17.096 MiB/s 17.106 MiB/s]
                 change:
                        time:   [+0.2140% +0.2741% +0.3392%] (p = 0.00 < 0.05)
                        thrpt:  [-0.3380% -0.2734% -0.2136%]
                        Change within noise threshold.
Found 4 outliers among 100 measurements (4.00%)
  3 (3.00%) high mild
  1 (1.00%) high severe
linter/default-rules/numpy/ctypeslib.py
                        time:   [688.97 µs 691.34 µs 694.15 µs]
                        thrpt:  [23.988 MiB/s 24.085 MiB/s 24.168 MiB/s]
                 change:
                        time:   [-1.3282% -0.7298% -0.1466%] (p = 0.02 < 0.05)
                        thrpt:  [+0.1468% +0.7351% +1.3461%]
                        Change within noise threshold.
Found 15 outliers among 100 measurements (15.00%)
  1 (1.00%) low mild
  2 (2.00%) high mild
  12 (12.00%) high severe
linter/default-rules/large/dataset.py
                        time:   [3.3872 ms 3.4032 ms 3.4191 ms]
                        thrpt:  [11.899 MiB/s 11.954 MiB/s 12.011 MiB/s]
                 change:
                        time:   [-0.6427% -0.2635% +0.0906%] (p = 0.17 > 0.05)
                        thrpt:  [-0.0905% +0.2642% +0.6469%]
                        No change in performance detected.
Found 20 outliers among 100 measurements (20.00%)
  1 (1.00%) low severe
  2 (2.00%) low mild
  4 (4.00%) high mild
  13 (13.00%) high severe

linter/all-rules/numpy/globals.py
                        time:   [148.99 µs 149.21 µs 149.42 µs]
                        thrpt:  [19.748 MiB/s 19.776 MiB/s 19.805 MiB/s]
                 change:
                        time:   [-0.7340% -0.5068% -0.2778%] (p = 0.00 < 0.05)
                        thrpt:  [+0.2785% +0.5094% +0.7395%]
                        Change within noise threshold.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low mild
  1 (1.00%) high severe
linter/all-rules/pydantic/types.py
                        time:   [3.0362 ms 3.0396 ms 3.0441 ms]
                        thrpt:  [8.3779 MiB/s 8.3903 MiB/s 8.3997 MiB/s]
                 change:
                        time:   [-0.0957% +0.0618% +0.2125%] (p = 0.45 > 0.05)
                        thrpt:  [-0.2121% -0.0618% +0.0958%]
                        No change in performance detected.
Found 11 outliers among 100 measurements (11.00%)
  1 (1.00%) low severe
  3 (3.00%) low mild
  5 (5.00%) high mild
  2 (2.00%) high severe
linter/all-rules/numpy/ctypeslib.py
                        time:   [1.6879 ms 1.6894 ms 1.6909 ms]
                        thrpt:  [9.8478 MiB/s 9.8562 MiB/s 9.8652 MiB/s]
                 change:
                        time:   [-0.2279% -0.0888% +0.0436%] (p = 0.18 > 0.05)
                        thrpt:  [-0.0435% +0.0889% +0.2284%]
                        No change in performance detected.
Found 5 outliers among 100 measurements (5.00%)
  4 (4.00%) low mild
  1 (1.00%) high severe
linter/all-rules/large/dataset.py
                        time:   [7.1520 ms 7.1586 ms 7.1654 ms]
                        thrpt:  [5.6777 MiB/s 5.6831 MiB/s 5.6883 MiB/s]
                 change:
                        time:   [-2.5626% -2.1654% -1.7780%] (p = 0.00 < 0.05)
                        thrpt:  [+1.8102% +2.2133% +2.6300%]
                        Performance has improved.
Found 2 outliers among 100 measurements (2.00%)
  1 (1.00%) low mild
  1 (1.00%) high mild
```
2023-06-15 18:43:19 +00:00
konstin
66089e1a2e Fix a number of formatter errors from the cpython repository (#5089)
## Summary

This fixes a number of problems in the formatter that showed up with
various files in the [cpython](https://github.com/python/cpython)
repository. These problems surfaced as unstable formatting and invalid
code. This is not the entirety of problems discovered through cpython,
but a big enough chunk to separate it. Individual fixes are generally
individual commits. They were discovered with #5055, which i update as i
work through the output

## Test Plan

I added regression tests with links to cpython for each entry, except
for the two stubs that also got comment stubs since they'll be
implemented properly later.
2023-06-15 11:24:14 +00:00
Dhruv Manilawala
097823b56d Ability to perform integration test on Jupyter notebooks (#5076)
## Summary

Ability to perform integration test on Jupyter notebooks

Part of #1218

## Test Plan

`cargo test`
2023-06-15 08:04:27 +05:30
Charlie Marsh
ed8113267c Add autofix specification levels for a variety of rules (#5109) 2023-06-14 22:03:37 -04:00
Charlie Marsh
c654280d84 Use direct links for all PEP 8 references (#5108) 2023-06-14 21:12:23 -04:00
Charlie Marsh
99486b38f4 Disambiguate all Python documentation references (#5107) 2023-06-15 00:47:00 +00:00
Charlie Marsh
716cab2f19 Run rustfmt on nightly to clean up erroneous comments (#5106)
## Summary

This PR runs `rustfmt` with a few nightly options as a one-time fix to
catch some malformatted comments. I ended up just running with:

```toml
condense_wildcard_suffixes = true
edition = "2021"
max_width = 100
normalize_comments = true
normalize_doc_attributes = true
reorder_impl_items = true
unstable_features = true
use_field_init_shorthand = true
```

Since these all seem like reasonable things to fix, so may as well while
I'm here.
2023-06-15 00:19:05 +00:00
Charlie Marsh
9ab16fb417 Add target-version link to relevant rules (#5105) 2023-06-15 00:12:32 +00:00
Charlie Marsh
458beccf14 Uniformly put ## Options at the end of documentation (#5104) 2023-06-15 00:04:51 +00:00
Tom Kuson
ccbc863960 Complete pyupgrade documentation (#5096)
## Summary

Completes the documentation for the `pyupgrade` rules.

Related to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-14 23:43:12 +00:00
Charlie Marsh
71b3130ff1 Remove manual await detection (#5103)
We can just use `any_over_expr` instead.
2023-06-14 22:17:14 +00:00
Tom Kuson
08cd140ea6 Ignore reimplemented-builtin if in async context (#5101)
## Summary

Checks if `checker` is in an `async` context. If yes, return early.

Fixes #5098.

## Test Plan

`cargo test`
2023-06-14 18:00:30 -04:00
Charlie Marsh
848f184b8c Enable UTC-import for datetime-utc-alias fix (#5100)
## Summary

Small update to leverage `get_or_import_symbol` to fix `UP017` in more
cases (e.g., when we need to import `UTC`, or access it from an alias or
something).

## Test Plan

Check out the updated snapshot.
2023-06-14 21:13:36 +00:00
Charlie Marsh
56476dfd61 Use matches! for CallPath comparisons (#5099)
## Summary

This PR consistently uses `matches! for static `CallPath` comparisons.
In some cases, we can significantly reduce the number of cases or
checks.

## Test Plan

`cargo test `
2023-06-14 17:06:34 -04:00
Charlie Marsh
bae183b823 Rename semantic_model and model usages to semantic (#5097)
## Summary

As discussed in Discord, and similar to oxc, we're going to refer to
this as `.semantic()` everywhere.

While I was auditing usages of `model: &SemanticModel`, I also changed
as many function signatures as I could find to consistently take the
model as the _last_ argument, rather than the first.
2023-06-14 15:01:51 -04:00
Charlie Marsh
65dbfd2556 Improve names and documentation on scope API (#5095)
## Summary

Just minor improvements to improve consistency of method names and
availability.
2023-06-14 18:28:55 +00:00
Charlie Marsh
86ff1febea Re-export ruff_python_semantic members (#5094)
## Summary

This PR adds a more unified public API to `ruff_python_semantic`, so
that we don't need to do deeply nested imports all over the place.
2023-06-14 18:23:38 +00:00
Charlie Marsh
a33bbe6335 Track "delayed" annotations in the semantic model (#5070)
## Summary

This PR tackles a corner case that we'll need to support local symbol
renaming. It relates to a nuance in how we want handle annotations
(i.e., `AnnAssign` statements with no value, like `x: int` in a function
body).

When we see a statement like:

```python
x: int
```

We create a `BindingKind::Annotation` for `x`. This is a special
`BindingKind` that the resolver isn't allowed to return. For example,
given:

```python
x: int
print(x)
```

The second line will yield an `undefined-name` error.

So why does this `BindingKind` exist at all? In Pyflakes, to support the
`unused-annotation` lint:

```python
def f():
    x: int  # unused-annotation
```

If we don't track `BindingKind::Annotation`, we can't lint for unused
variables that are only "defined" via annotations.

There are a few other wrinkles to `BindingKind::Annotation`. One is
that, if a binding already exists in the scope, we actually just discard
the `BindingKind`. So in this case:

```python
x = 1
x: int
```

When we go to create the `BindingKind::Annotation` for the second
statement, we notice that (1) we're creating an annotation but (2) the
scope already has binding for the name -- so we just drop the binding on
the floor. This has the nice property that annotations aren't considered
to "shadow" another binding, which is important in a bunch of places
(e.g., if we have `import os; os: int`, we still consider `os` to be an
import, as we should). But it also means that these "delayed"
annotations are one of the few remaining references that we don't track
anywhere in the semantic model.

This PR adds explicit support for these via a new `delayed_annotations`
attribute on the semantic model. These should be extremely rare, but we
do need to track them if we want to support local symbol renaming.

### This isn't the right way to model this

This isn't the right way to model this.

Here's an alternative:

- Remove `BindingKind::Annotation`, and treat annotations as their own,
separate concept.
- Instead of storing a map from name to `BindingId` on each `Scope`,
store a map from name to... `SymbolId`.
- Introduce a `Symbol` abstraction, where a symbol can point to a
current binding, and a list of annotations, like:

```rust
pub struct Symbol {
  binding: Option<BindingId>,
  annotations: Vec<AnnotationId>
}
```

If we did this, we could appropriately model the semantics described
above. When we go to resolve a binding, we ignore annotations (always).
When we try to find unused variables, we look through the list of
symbols, and have sufficient information to discriminate between
annotations and bound variables. Etc.

The main downside of this `Symbol`-based approach is that it's going to
take a lot more work to implement, and it'll be less performant (we'll
be storing more data per symbol, and our binding lookups will have an
added layer of indirection).
2023-06-14 17:54:35 +00:00
Charlie Marsh
c992cfa76e Make some of ruff_python_semantic pub(crate) (#5093) 2023-06-14 17:49:37 +00:00
konstin
916f0889f8 Add pyproject.toml to include option doc (#5080)
Fixes an oversight where i didn't update this initially
2023-06-14 15:55:12 +00:00
MT BENTERKI
c1fd2c8a8e Update tutorial doc typo (#5088) 2023-06-14 11:17:35 -04:00
Charlie Marsh
732b0405d7 Remove FixMode::None (#5087)
## Summary

We now _always_ generate fixes, so `FixMode::None` and
`FixMode::Generate` are redundant. We can also remove the TODO around
`--fix-dry-run`, since that's our default behavior.

Closes #5081.
2023-06-14 11:17:09 -04:00
Thomas de Zeeuw
e7316c1cc6 Consider ignore-names in all pep8 naming rules (#5079)
## Summary

This changes all remaining pep8 naming rules to consider the
`ingore-names` argument.

Closes #5050

## Test Plan

Added new tests.
2023-06-14 16:57:09 +02:00
Charlie Marsh
6f10aeebaa Remove unused Scope#delete method (#5085)
## Summary

This is now intentionally unused and is now made impossible (via this
PR).
2023-06-14 14:15:14 +00:00
Charlie Marsh
c74ef77e85 Move binding accesses into SemanticModel method (#5084) 2023-06-14 14:07:46 +00:00
Charlie Marsh
1e497162d1 Add a dedicated read result for unbound locals (#5083)
## Summary

Small follow-up to #4888 to add a dedicated `ResolvedRead` case for
unbound locals, mostly for clarity and documentation purposes (no
behavior changes).

## Test Plan

`cargo test`
2023-06-14 09:58:48 -04:00
Charlie Marsh
aa41ffcfde Add BindingKind variants to represent deleted bindings (#5071)
## Summary

Our current mechanism for handling deletions (e.g., `del x`) is to
remove the symbol from the scope's `bindings` table. This "does the
right thing", in that if we then reference a deleted symbol, we're able
to determine that it's unbound -- but it causes a variety of problems,
mostly in that it makes certain bindings and references unreachable
after-the-fact.

Consider:

```python
x = 1
print(x)
del x
```

If we analyze this code _after_ running the semantic model over the AST,
we'll have no way of knowing that `x` was ever introduced in the scope,
much less that it was bound to a value, read, and then deleted --
because we effectively erased `x` from the model entirely when we hit
the deletion.

In practice, this will make it impossible for us to support local symbol
renames. It also means that certain rules that we want to move out of
the model-building phase and into the "check dead scopes" phase wouldn't
work today, since we'll have lost important information about the source
code.

This PR introduces two new `BindingKind` variants to model deletions:

- `BindingKind::Deletion`, which represents `x = 1; del x`.
- `BindingKind::UnboundException`, which represents:

```python
try:
  1 / 0
except Exception as e:
  pass
```

In the latter case, `e` gets unbound after the exception handler
(assuming it's triggered), so we want to handle it similarly to a
deletion.

The main challenge here is auditing all of our existing `Binding` and
`Scope` usages to understand whether they need to accommodate deletions
or otherwise behave differently. If you look one commit back on this
branch, you'll see that the code is littered with `NOTE(charlie)`
comments that describe the reasoning behind changing (or not) each of
those call sites. I've also augmented our test suite in preparation for
this change over a few prior PRs.

### Alternatives

As an alternative, I considered introducing a flag to `BindingFlags`,
like `BindingFlags::UNBOUND`, and setting that at the appropriate time.

This turned out to be a much more difficult change, because we tend to
match on `BindingKind` all over the place (e.g., we have a bunch of code
blocks that only run when a `BindingKind` is
`BindingKind::Importation`). As a result, introducing these new
`BindingKind` variants requires only a few changes at the client sites.
Adding a flag would've required a much wider-reaching change.
2023-06-14 09:27:24 -04:00
Dhruv Manilawala
bf5fbf8971 Add GitHub CODEOWNERS file (#5054)
## Summary

Add GitHub CODEOWNERS file.

Initiating this, we can discuss and iterate further.

https://help.github.com/articles/about-codeowners/

## Test Plan

Look out for review requests :)
2023-06-14 05:21:59 +00:00
Charlie Marsh
fc6580592d Use Expr::is_* methods at more call sites (#5075) 2023-06-14 04:02:39 +00:00
Tom Kuson
4d9b0b925d Add documentation to flake8-executable rules (#5063)
## Summary

Completes the documentation for the `flake8-executable` rules.

Related to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-14 01:31:06 +00:00
Charlie Marsh
0daeea1f42 Tweak exception-handler handling in AST visitor (#5069) 2023-06-14 01:00:42 +00:00
Charlie Marsh
3f6584b74f Fix erroneous kwarg reference (#5068) 2023-06-14 00:01:52 +00:00
Charlie Marsh
c2fa568b46 Use dedicated structs for excepthandler variants (#5065)
## Summary

Oversight from #5042.
2023-06-13 22:37:06 +00:00
Charlie Marsh
1895011ac2 Document some attributes on the semantic model (#5064) 2023-06-13 20:45:24 +00:00
Charlie Marsh
364bd82aee Don't treat annotations as resolved in forward references (#5060)
## Summary

This behavior dates back to a Pyflakes commit (5fc37cbd), which was used
to allow this test to pass:

```py
from __future__ import annotations
T: object
def f(t: T): pass
def g(t: 'T'): pass
```

But, I think this is an error. Mypy and Pyright don't accept it -- you
can only use variables as type annotations if they're type aliases
(i.e., annotated with `TypeAlias`), in which case, there has to be an
assignment on the right-hand side (see: [PEP
613](https://peps.python.org/pep-0613/)).
2023-06-13 14:47:29 -04:00
Charlie Marsh
f9f08d6b03 Add a few more tests for deletion behaviors (#5058) 2023-06-13 17:54:04 +00:00
Charlie Marsh
b0984a2868 Treat exception binding as explicit deletion (#5057)
## Summary

This PR corrects a misunderstanding I had related to Python's handling
of bound exceptions.

Previously, I thought this code ran without error:

```py
def f():
    x = 1

    try:
        1 / 0
    except Exception as x:
        pass

    print(x)
```

My understanding was that `except Exception as x` bound `x` within the
`except` block, but then restored the `x = 1` binding after exiting the
block.

In practice, however, this throws a `UnboundLocalError` error, because
`x` becomes "unbound" after exiting the exception handler. It's similar
to a `del` statement in this way.

This PR removes our behavior to "restore" the previous binding. This
could lead to faulty analysis in conditional blocks due to our lack of
control flow analysis, but those same problems already exist for `del`
statements.
2023-06-13 13:45:51 -04:00
Charlie Marsh
a431dd0368 Respect all __all__ definitions for docstring visibility (#5052)
## Summary

We changed the semantics around `__all__` in #4885, but didn't update
the docstring visibility code to match those changes.
2023-06-13 12:22:20 -04:00
Charlie Marsh
099a9152d1 Use .is_unbound() in flake8-errmsg fix (#5053)
## Summary

Trying to bring some more consistent to these APIs as I look to change
them to accommodate deletions.
2023-06-13 12:22:05 -04:00
Charlie Marsh
19f972a305 Use Scope#has in lieu of Scope#get (#5051)
## Summary

These usages don't actually need the `BindingId`.
2023-06-13 15:59:53 +00:00
Thomas de Zeeuw
b0f89fa814 Support glob patterns in pep8_naming ignore-names (#5024)
## Summary

 Support glob patterns in pep8_naming ignore-names.

Closes #2787

## Test Plan

Added new tests.
2023-06-13 17:37:13 +02:00
Charlie Marsh
65312bad01 Remove unannotated attributes from RUF008 (#5049)
## Summary

In a dataclass:

```py
from dataclasses import dataclass

@dataclass
class X:
    class_var = {}
    x: int
```

`class_var` isn't actually a dataclass attribute, since it's
unannotated. This PR removes such attributes from RUF008
(`mutable-dataclass-default`), but it does enforce them in RUF012
(`mutable-class-default`), since those should be annotated with
`ClassVar` like any other mutable class attribute.

Closes #5043.
2023-06-13 10:21:14 -04:00
Aarni Koskela
7b4dde0c6c Add JSON Lines (NDJSON) message serialization (#5048)
## Summary

This adds `json-lines` (https://jsonlines.org/ or http://ndjson.org/) as
an output format.

I'm sure you already know, but

* JSONL is more greppable (each record is a single line) than the pretty
JSON
* JSONL is faster to ingest piecewise (and/or in parallel) than JSON

## Test Plan

Snapshot test in the new module :)
2023-06-13 14:15:55 +00:00
Thomas de Zeeuw
e1fd3965a2 Start with Upper case in error messages (#5045)
## Summary

To be consistent with the format used by other errors.

## Test Plan

N/A.
2023-06-13 13:14:45 +02:00
konstin
95ee6dcb3b Add contributor docs to formatter (#5023)
I've written done my condensed learnings from working on the formatter
so that others can have an easier start working on it.

This is a pure docs change
2023-06-13 07:22:17 +00:00
Charlie Marsh
cc44349401 Use dedicated structs in comparable.rs (#5042)
## Summary

Updating to match the updated AST structure, for consistency.
2023-06-13 03:57:34 +00:00
qdegraaf
a477720f4e [perflint] Add perflint plugin, add first rule PERF102 (#4821)
## Summary

Adds boilerplate for implementing the
[perflint](https://github.com/tonybaloney/perflint/) plugin, plus a
first rule.

## Test Plan

Fixture added for PER8102

## Issue link

Refers: https://github.com/charliermarsh/ruff/issues/4789
2023-06-13 01:54:44 +00:00
Charlie Marsh
be2fa6d217 Increase density of Checker arms (#5041) 2023-06-13 01:08:23 +00:00
Charlie Marsh
cbd4c10fdd Support 'reason' argument to pytest.fail (#5040)
## Summary

Per the [API
reference](https://docs.pytest.org/en/7.1.x/reference/reference.html#pytest.fail),
`reason` was added in version 7, and is equivalent to `msg` (but
preferred going forward).

I also grepped for `msg` usages in `flake8_pytest_style`, but found no
others (apart from those that reference `unittest` APIs.)

Closes #3387.
2023-06-12 20:54:07 -04:00
Timofei Kukushkin
e2130707f5 Autofixer for ISC001 (#4853)
## Summary

This PR adds autofixer for rule ISC001 in cases where both string
literals are of the same kind and with same quotes (double / single).

Fixes #4829

## Test Plan

I added testcases with different combinations of string literals.
2023-06-12 23:28:57 +00:00
Charlie Marsh
780336db0a Include f-string prefixes in quote-stripping utilities (#5039)
Mentioned here:
https://github.com/astral-sh/ruff/pull/4853#discussion_r1217560348.

Generated with this hacky script:
https://gist.github.com/charliermarsh/8ecc4e55bc87d51dc27340402f33b348.
2023-06-12 18:25:47 -04:00
Charlie Marsh
7e37d8916c Remove lexer dependency from identifier_range (#5036)
## Summary

We run this quite a bit -- the new version is zero-allocation, though
it's not quite as nice as the lexer we have in the formatter.
2023-06-12 22:06:03 +00:00
Charlie Marsh
ab11dd08df Improve TypedDict conversion logic for shadowed builtins and dunder methods (#5038)
## Summary

This PR (1) avoids flagging `TypedDict` and `NamedTuple` conversions
when attributes are dunder methods, like `__dict__`, and (2) avoids
flagging the `A003` shadowed-attribute rule for `TypedDict` classes at
all, where it doesn't really apply (since those attributes are only
accessed via subscripting anyway).

Closes #5027.
2023-06-12 21:23:39 +00:00
Charlie Marsh
4080f36850 Handle decorators in class-parenthesis-modifying rules (#5034)
## Summary

A few of our rules look at the parentheses that follow a class
definition (e.g., `class Foo(object):`) and attempt to modify those
parentheses. Neither of those rules were behaving properly in the
presence of decorators, which were recently added to the statement
range.

## Test Plan

`cargo test` with a variety of new fixture tests.
2023-06-12 15:19:59 -04:00
Charlie Marsh
6d861743c8 Remove custom tests in rules/ruff/mod.rs (#5033) 2023-06-12 18:54:04 +00:00
Charlie Marsh
54e103fc99 Add a rule to remove unnecessary parentheses in class definitions (#5032)
Closes #2409.
2023-06-12 18:43:06 +00:00
Dhruv Manilawala
3470dee7d4 Add rule to disallow implicit optional with autofix (#4831)
## Summary

Add rule to disallow implicit optional with autofix.

Currently, I've added it under `RUF` category.

### Limitation

Type aliases could result in false positive:

```python
from typing import Optional

StrOptional = Optional[str]


def foo(arg: StrOptional = None):
	pass
```

## Test Plan

`cargo test`

resolves: #1983

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-06-12 18:12:10 +00:00
Dhruv Manilawala
cb4f086cbf Add roundtrip support for Jupyter notebook (#5028)
## Summary

Add roundtrip support for Jupyter notebook.

1. Read the notebook
2. Extract out the source code content
3. Use it to update the notebook itself (should be exactly the same [^1])
4. Serialize into JSON and print it to stdout

## Test Plan

`cargo run --all-features --bin ruff_dev --package ruff_dev --
round-trip <path/to/notebook.ipynb>`

<details><summary>Example output:</summary>
<p>

```
{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "f3c286e9-fa52-4440-816f-4449232f199a",
   "metadata": {},
   "source": [
    "# Ruff Test"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2b7bc6c-778a-4b07-86ae-dde5a2d9511e",
   "metadata": {},
   "source": [
    "Markdown block before the first import"
   ]
  },
  {
   "cell_type": "code",
   "id": "5e3ef98e-224c-450a-80e6-be442ad50907",
   "metadata": {
    "tags": []
   },
   "source": "",
   "execution_count": 1,
   "outputs": []
  },
  {
   "cell_type": "code",
   "id": "6bced3f8-e0a4-450c-ae7c-f60ad5671ee9",
   "metadata": {},
   "source": "import contextlib\n\nwith contextlib.suppress(ValueError):\n    print()\n",
   "outputs": []
  },
  {
   "cell_type": "code",
   "id": "d7102cfd-5bb5-4f5b-a3b8-07a7b8cca34c",
   "metadata": {},
   "source": "import random\n\nrandom.randint(10, 20)",
   "outputs": []
  },
  {
   "cell_type": "code",
   "id": "88471d1c-7429-4967-898f-b0088fcb4c53",
   "metadata": {},
   "source": "foo = 1\nif foo < 2:\n    msg = f\"Invalid foo: {foo}\"\n    raise ValueError(msg)",
   "outputs": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python (ruff-playground)",
   "name": "ruff-playground",
   "language": "python"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "pygments_lexer": "ipython3",
   "nbconvert_exporter": "python",
   "version": "3.11.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
```

</p>
</details> 

[^1]: The type in JSON might be different (https://github.com/astral-sh/ruff/pull/4665#discussion_r1212663495)

Part of #1218
2023-06-12 23:27:45 +05:30
Charlie Marsh
a77d2df934 Split mutable-class-defaults rules into separate modules (#5031) 2023-06-12 17:21:28 +00:00
Adam Pauls
638c18f007 Expand RUF008 to all classes, but to a new code (RUF012) (#4390)
AFAIK, there is no reason to limit RUF008 to just dataclasses -- mutable
defaults have the same problems for regular classes.

Partially addresses https://github.com/charliermarsh/ruff/issues/4053
and broken out from https://github.com/charliermarsh/ruff/pull/4096.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-06-12 16:54:27 +00:00
Addison Crump
70e6c212d9 Improve ruff_parse_simple to find UTF-8 violations (#5008)
Improves the `ruff_parse_simple` fuzz harness by adding checks for
parsed locations to ensure they all lie on UTF-8 character boundaries.
This will allow for faster identification of issues like #5004.

This also adds additional details for Apple M1 users and clarifies the
importance of using `init-fuzzer.sh` (thanks for the feedback,
@jasikpark 🙂).
2023-06-12 12:10:23 -04:00
Charlie Marsh
9db622afe1 Allow Options-to-Settings conversion to use TryFrom (#5025)
## Summary

This avoids a bad `expect()` call in the `copyright` conversion.

## Test Plan

`cargo test`
2023-06-12 15:31:50 +00:00
Thomas de Zeeuw
d3aa81a474 Suggest combining async with statements (#5022)
## Summary

Previously the rule for SIM117 explicitly ignored `async with`
statements as it would incorrectly suggestion to merge `async with` and
regular `with` statements as reported in issue #1902.

This partially reverts the fix for that (commit
396be5edea) by enabling the rules for
`async with` statements again, but with a check ensuring that the
statements are both of the same kind, i.e. both `async with` or both
(just) `with` statements.

Closes #3025

## Test Plan

Updated and existing test and added a new test case from #3025.
2023-06-12 16:33:18 +02:00
Dhruv Manilawala
d8f5d2d767 Add support for auto-fix in Jupyter notebooks (#4665)
## Summary

Add support for applying auto-fixes in Jupyter Notebook.

### Solution

Cell offsets are the boundaries for each cell in the concatenated source
code. They are represented using `TextSize`. It includes the start and
end offset as well, thus creating a range for each cell. These offsets
are updated using the `SourceMap` markers.

### SourceMap

`SourceMap` contains markers constructed from each edits which tracks
the original source code position to the transformed positions. The
following drawing might make it clear:

![SourceMap visualization](https://github.com/astral-sh/ruff/assets/67177269/3c94e591-70a7-4b57-bd32-0baa91cc7858)

The center column where the dotted lines are present are the markers
included in the `SourceMap`. The `Notebook` looks at these markers and
updates the cell offsets after each linter loop. If you notice closely,
the destination takes into account all of the markers before it.

The index is constructed only when required as it's only used to render
the diagnostics. So, a `OnceCell` is used for this purpose. The cell
offsets, cell content and the index will be updated after each iteration
of linting in the mentioned order. The order is important here as the
content is updated as per the new offsets and index is updated as per
the new content.

## Limitations

### 1

Styling rules such as the ones in `pycodestyle` will not be applicable
everywhere in Jupyter notebook, especially at the cell boundaries. Let's
take an example where a rule suggests to have 2 blank lines before a
function and the cells contains the following code:

```python
import something
# ---
def first():
	pass

def second():
	pass
```

(Again, the comment is only to visualize cell boundaries.)

In the concatenated source code, the 2 blank lines will be added but it
shouldn't actually be added when we look in terms of Jupyter notebook.
It's as if the function `first` is at the start of a file.

`nbqa` solves this by recording newlines before and after running
`autopep8`, then running the tool and restoring the newlines at the end
(refer https://github.com/nbQA-dev/nbQA/pull/807).

## Test Plan

Three commands were run in order with common flags (`--select=ALL
--no-cache --isolated`) to isolate which stage the problem is occurring:
1. Only diagnostics
2. Fix with diff (`--fix --diff`)
3. Fix (`--fix`)

### https://github.com/facebookresearch/segment-anything

```
-------------------------------------------------------------------------------
 Jupyter Notebooks       3            0            0            0            0
 |- Markdown             3           98            0           94            4
 |- Python               3          513          468            4           41
 (Total)                            611          468           98           45
-------------------------------------------------------------------------------
```

```console
$ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/segment-anything/**/*.ipynb --fix
...
Found 180 errors (89 fixed, 91 remaining).
```

### https://github.com/openai/openai-cookbook

```
-------------------------------------------------------------------------------
 Jupyter Notebooks      65            0            0            0            0
 |- Markdown            64         3475           12         2507          956
 |- Python              65         9700         7362         1101         1237
 (Total)                          13175         7374         3608         2193
===============================================================================
```

```console
$ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/openai-cookbook/**/*.ipynb --fix
error: Failed to parse /path/to/openai-cookbook/examples/vector_databases/Using_vector_databases_for_embeddings_search.ipynb:cell 4:29:18: unexpected token '-'
...
Found 4227 errors (2165 fixed, 2062 remaining).
```

### https://github.com/tensorflow/docs

```
-------------------------------------------------------------------------------
 Jupyter Notebooks     150            0            0            0            0
 |- Markdown             1           55            0           46            9
 |- Python               1          402          289           60           53
 (Total)                            457          289          106           62
-------------------------------------------------------------------------------
```

```console
$ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/tensorflow-docs/**/*.ipynb --fix
error: Failed to parse /path/to/tensorflow-docs/site/en/guide/extension_type.ipynb:cell 80:1:1: unexpected token Indent
error: Failed to parse /path/to/tensorflow-docs/site/en/r1/tutorials/eager/custom_layers.ipynb:cell 20:1:1: unexpected token Indent
error: Failed to parse /path/to/tensorflow-docs/site/en/guide/data.ipynb:cell 175:5:14: unindent does not match any outer indentation level
error: Failed to parse /path/to/tensorflow-docs/site/en/r1/tutorials/representation/unicode.ipynb:cell 30:1:1: unexpected token Indent
...
Found 12726 errors (5140 fixed, 7586 remaining).
```

### https://github.com/tensorflow/models

```
-------------------------------------------------------------------------------
 Jupyter Notebooks      46            0            0            0            0
 |- Markdown             1           11            0            6            5
 |- Python               1          328          249           19           60
 (Total)                            339          249           25           65
-------------------------------------------------------------------------------
```

```console
$ cargo run --all-features --bin ruff -- check --no-cache --isolated --select=ALL /path/to/tensorflow-models/**/*.ipynb --fix
...
Found 4856 errors (2690 fixed, 2166 remaining).
```

resolves: #1218
fixes: #4556
2023-06-12 14:14:15 +00:00
konstin
e586c27590 Format ExprTuple (#4963)
This implements formatting ExprTuple, including magic trailing comma. I
intentionally didn't change the settings mechanism but just added a
dummy global const flag.

Besides the snapshots, I added custom breaking/joining tests and a
deeply nested test case. The diffs look better than previously, proper
black compatibility depends on parentheses handling.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-06-12 12:55:47 +00:00
Thomas de Zeeuw
8161757229 [flake8-pyi] Implement PYI044 (#5021)
## Summary

This implements PYI044. This rule checks if `from __future__ import
annotations` is used in stub files as it has no effect in stub files, since type
checkers automatically treat stubs as having those semantics.

Updates https://github.com/astral-sh/ruff/issues/848

## Test Plan

Added a test case and snapshots.
2023-06-12 13:20:16 +02:00
Charlie Marsh
6a5f317362 Use use::* for rule re-exports (#5018) 2023-06-12 00:32:45 +00:00
Dhruv Manilawala
c3d1fa851e Ignore pyproject.toml for adding noqa directives (#5013)
## Summary

Ignore pyproject.toml file for adding noqa directives using `--add-noqa`

## Test Plan

`cargo run --bin ruff -- check --add-noqa .`

fixes: #5012
2023-06-11 20:21:24 -04:00
Charlie Marsh
eac3a0cc3d Update CONTRIBUTING.md guide (#5017) 2023-06-12 00:20:59 +00:00
Charlie Marsh
31067e6ce2 Update list of crates in CONTRIBUTING.md (#5016) 2023-06-11 20:10:37 -04:00
Charlie Marsh
68b6d30c46 Use consistent Cargo.toml metadata in all crates (#5015) 2023-06-12 00:02:40 +00:00
Ryan Yang
ab3c02342b Implement copyright notice detection (#4701)
## Summary

Add copyright notice detection to enforce the presence of copyright
headers in Python files.

Configurable settings include: the relevant regular expression, the
author name, and the minimum file size, similar to
[flake8-copyright](https://github.com/savoirfairelinux/flake8-copyright).

Closes https://github.com/charliermarsh/ruff/issues/3579

---------

Signed-off-by: ryan <ryang@waabi.ai>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-06-11 02:17:58 +00:00
Trevor Gross
9f7cc86a22 Add more details to E722 (bare-except) docs (#5007)
## Summary

Note that catching a bare `Exception` is better than catching no
specific exception.

## Test Plan

Documentation only.
2023-06-10 18:42:43 -04:00
Charlie Marsh
445e1723ab Use Stmt::parse in lieu of Suite unwraps (#5002) 2023-06-10 04:55:31 +00:00
Charlie Marsh
42c8054268 Implement autofix for revised RET504 rule (#4999)
## Summary

This PR enables autofix for the revised `RET504` rule, by changing:

```py
def f():
    x = 1
    return x
```

...to:

```py
def f():
    return 1
```

Closes #2263.

Closes #2788.
2023-06-10 04:32:03 +00:00
Charlie Marsh
2d597bc1fb Parenthesize expressions prior to lexing in F632 (#5001) 2023-06-10 04:23:43 +00:00
Charlie Marsh
7275c16d98 Extend revised RET504 implementation to with statements (#4998)
## Summary

This PR extends the new `RET504` implementation to handle cases like:

```py
def foo():
    with open("foo.txt", "r") as f:
        x = f.read()
    return x
```

This was originally suggested in
https://github.com/astral-sh/ruff/issues/2950#issuecomment-1433441503.
2023-06-10 04:15:35 +00:00
Charlie Marsh
02b8ce82af Refactor RET504 to only enforce assignment-then-return pattern (#4997)
## Summary

The `RET504` rule, which looks for unnecessary assignments before return
statements, is a frequent source of issues (#4173, #4236, #4242, #1606,
#2950). Over time, we've tried to refine the logic to handle more cases.
For example, we now avoid analyzing any functions that contain any
function calls or attribute assignments, since those operations can
contain side effects (and so we mark them as a "read" on all variables
in the function -- we could do a better job with code graph analysis to
handle this limitation, but that'd be a more involved change.) We also
avoid flagging any variables that are the target of multiple
assignments. Ultimately, though, I'm not happy with the implementation
-- we just can't do sufficiently reliable analysis of arbitrary code
flow given the limited logic herein, and the existing logic is very hard
to reason about and maintain.

This PR refocuses the rule to only catch cases of the form:

```py
def f():
    x = 1
    return x
```

That is, we now only flag returns that are immediately preceded by an
assignment to the returned variable. While this is more limiting, in
some ways, it lets us flag more cases vis-a-vis the previous
implementation, since we no longer "fully eject" when functions contain
function calls and other effect-ful operations.

Closes #4173.

Closes #4236.

Closes #4242.
2023-06-10 00:05:01 -04:00
Charlie Marsh
5abb8ec0dc Use Python whitespace utilities in ruff_textwrap (#4996)
## Summary

This change was intended to be included in #4994, but was somehow
dropped.
2023-06-10 02:32:42 +00:00
Charlie Marsh
f401050878 Introduce PythonWhitespace to confine trim operations to Python whitespace (#4994)
## Summary

We use `.trim()` and friends in a bunch of places, to strip whitespace
from source code. However, not all Unicode whitespace characters are
considered "whitespace" in Python, which only supports the standard
space, tab, and form-feed characters.

This PR audits our usages of `.trim()`, `.trim_start()`, `.trim_end()`,
and `char::is_whitespace`, and replaces them as appropriate with a new
`.trim_whitespace()` analogues, powered by a `PythonWhitespace` trait.

In general, the only place that should continue to use `.trim()` is
content within docstrings, which don't need to adhere to Python's
semantic definitions of whitespace.

Closes #4991.
2023-06-09 21:44:50 -04:00
Charlie Marsh
c1ac50093c Use super visibility in helpers (#4995) 2023-06-10 01:23:13 +00:00
Charlie Marsh
1d756dc3a7 Move Python whitespace utilities into new ruff_python_whitespace crate (#4993)
## Summary

`ruff_newlines` becomes `ruff_python_whitespace`, and includes the
existing "universal newline" handlers alongside the Python
whitespace-specific utilities.
2023-06-10 00:59:57 +00:00
Charlie Marsh
e86f12a1ec Rename some methods on SemanticModel (#4990) 2023-06-09 19:36:59 +00:00
Charlie Marsh
5c502a3320 Add documentation for BindingKind variants (#4989) 2023-06-09 18:32:50 +00:00
Micha Reiser
901bcb6f21 Fix line numbers in source frames (#4984) 2023-06-09 17:21:18 +02:00
Micha Reiser
111e1f93ca perf(formatter): Skip bodies without comments (#4978) 2023-06-09 11:33:57 +02:00
Micha Reiser
68d52da43b Track formatted comments (#4979) 2023-06-09 09:09:45 +00:00
Micha Reiser
646ab64850 Fix binary expression formatting with leading comments (#4964) 2023-06-09 09:02:50 +00:00
Micha Reiser
1accbeffd6 Format if statements (#4961) 2023-06-09 10:55:14 +02:00
konstin
548a3cbb3f Small CI improvements (#4953)
* Replace the unmaintained actions-rs/cargo that github actions
complains about using an old node version with plain cargo (this was the
original motivation for this PR)
* Use taiki-e/install-action to install critcmp directly
* Use a rust 1.70 nightly toolchain for udeps
* Cache python package build (this should cut a good chunk of ci time)
* yaml formatting courtesy of pycharm

Test Plan: CI itself
2023-06-09 09:03:34 +02:00
Charlie Marsh
16d1e63a5e Respect 'is not' operators split across newlines (#4977) 2023-06-09 05:07:45 +00:00
Charlie Marsh
d647105e97 Support concatenated string key removals (#4976) 2023-06-09 04:56:35 +00:00
Davide Canton
63fdcea29e Handled dict and set inside f-string (#4249) (#4563) 2023-06-09 04:53:13 +00:00
qdegraaf
2bb32ee943 [flake8-slots] Add plugin, add SLOT000, SLOT001 and SLOT002 (#4909) 2023-06-09 04:14:16 +00:00
rodjunger
ee1f094834 [ruff] Add a rule for static keys in dict comprehensions (#4929) 2023-06-09 02:06:34 +00:00
Tom Kuson
efd8f3bdab Complete flake8-simplify documentation (#4930) 2023-06-09 02:02:41 +00:00
Charlie Marsh
293889a352 Support concatenated literals in format-literals (#4974) 2023-06-09 01:29:19 +00:00
Tom Kuson
2c19000e4a Add Pylint rule comparison-with-itself (R0124) (#4957) 2023-06-09 00:57:50 +00:00
Charlie Marsh
aba073a791 Upgrade explicit-type-conversion rule (RUF010) to remove unnecessary str calls (#4971) 2023-06-08 20:02:57 +00:00
Charlie Marsh
d042eddccc Remove unwrap from none-comparison rule (#4969) 2023-06-08 18:21:56 +00:00
Charlie Marsh
775d247731 Allow private accesses within special dunder methods (#4968) 2023-06-08 17:36:49 +00:00
Charlie Marsh
58d08219e8 Allow re-assignments to __all__ (#4967) 2023-06-08 17:19:56 +00:00
Charlie Marsh
902c4e7d77 Make SIM118 a suggested fix (#4966) 2023-06-08 17:02:42 +00:00
Micha Reiser
68969240c5 Format Function definitions (#4951) 2023-06-08 16:07:33 +00:00
Dhruv Manilawala
07cc4bcb0f Update links to point to Astral org (#4949) 2023-06-08 11:43:40 -04:00
Micha Reiser
9c3fb23ace Simple lexer for formatter (#4922) 2023-06-08 17:37:39 +02:00
konstin
467df23e65 Implement StmtReturn (#4960)
* Implement StmtPass

This implements StmtPass as `pass`.

The snapshot diff is small because pass mainly occurs in bodies and function (#4951) and if/for bodies.

* Implement StmtReturn

This implements StmtReturn as `return` or `return {value}`.

The snapshot diff is small because return occurs in functions (#4951)
2023-06-08 16:29:39 +02:00
konstin
c8442e91ce Implement StmtPass (#4959)
This implements StmtPass as `pass`.

The snapshot diff is small because pass mainly occurs in bodies and function (#4951) and if/for bodies.
2023-06-08 16:29:27 +02:00
Micha Reiser
6bef347a8e Trailing own line comments before func or class (#4921) 2023-06-08 12:50:25 +00:00
Micha Reiser
c1cc6f3be1 Add basic Constant formatting (#4954) 2023-06-08 11:42:44 +00:00
Micha Reiser
83cf6d6e2f Implement Binary expression without best_fitting (#4952) 2023-06-08 12:45:03 +02:00
konstin
23abad0bd5 A basic StmtAssign formatter and better dummies for expressions (#4938)
* A basic StmtAssign formatter and better dummies for expressions

The goal of this PR was formatting StmtAssign since many nodes in the black tests (and in python in general) are after an assignment. This caused unstable formatting: The spacing of power op spacing depends on the type of the two involved expressions, but each expression was formatted as dummy string and re-parsed as a ExprName, so in the second round the different rules of ExprName were applied, causing unstable formatting.

This PR does not necessarily bring us closer to black's style, but it unlocks a good porting of black's test suite and is a basis for implementing the Expr nodes.

* fmt

* Review
2023-06-08 12:20:25 +02:00
konstin
651d89794c Use phf for confusables to reduce llvm lines (#4926)
* Use phf for confusables to reduce llvm lines

## Summary

This replaces FxHashMap for the confusables with a perfect hash map from the [phf crate](https://github.com/rust-phf/rust-phf) to reduce the generated llvm instructions.

A perfect hash function is one that doesn't have any collisions. We can build one because we know all keys at compile time. This improves hashmap efficiency, even though this is likely not noticeable in our case (except someone has a large non-english crate to test on).

The original hashmap contained a lot of duplicates, which i had to remove when phf_map complained, i did so by sorting the keys.

The important part that it reduces the llvm instructions generated (#3808, `RUSTFLAGS="-Csymbol-mangling-version=v0" cargo llvm-lines -p ruff --lib | head -20`):

```
  Lines                  Copies               Function name
  -----                  ------               -------------
  1740502                38973                (TOTAL)
    27423 (1.6%,  1.6%)      1 (0.0%,  0.0%)  ruff[cef4c65d96248843]::rules::ruff::rules::confusables::CONFUSABLES::{closure#0}
    10193 (0.6%,  2.2%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::codes::RuleCodePrefix>::iter
     8107 (0.5%,  2.6%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::codes::Rule>::noqa_code
     7345 (0.4%,  3.0%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::checkers::ast::Checker as ruff_python_ast[3778b140caf21545]::visitor::Visitor>::visit_stmt
     6412 (0.4%,  3.4%)      1 (0.0%,  0.0%)  <<ruff[cef4c65d96248843]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_map::<toml_edit[7e3a6c5e67260672]::de::spanned::SpannedDeserializer<toml_edit[7e3a6c5e67260672]::de::value::ValueDeserializer>>
     6412 (0.4%,  3.8%)      1 (0.0%,  0.0%)  <<ruff[cef4c65d96248843]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_map::<toml_edit[7e3a6c5e67260672]::de::table::TableMapAccess>
     6409 (0.4%,  4.2%)      1 (0.0%,  0.0%)  <<ruff[cef4c65d96248843]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_map::<toml_edit[7e3a6c5e67260672]::de::datetime::DatetimeDeserializer>
     5696 (0.3%,  4.5%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::checkers::ast::Checker as ruff_python_ast[3778b140caf21545]::visitor::Visitor>::visit_expr
     4448 (0.3%,  4.7%)      1 (0.0%,  0.0%)  ruff[cef4c65d96248843]::flake8_to_ruff::converter::convert
     3702 (0.2%,  4.9%)      1 (0.0%,  0.0%)  <&ruff[cef4c65d96248843]::registry::Linter as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter
     3349 (0.2%,  5.1%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::registry::Linter>::code_for_rule
     3132 (0.2%,  5.3%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::codes::Rule as core[da82827a87f140f9]::fmt::Debug>::fmt
     3130 (0.2%,  5.5%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<&ruff[cef4c65d96248843]::codes::Rule>>::from
     3130 (0.2%,  5.7%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<ruff[cef4c65d96248843]::codes::Rule>>::from
     3130 (0.2%,  5.9%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::codes::Rule as core[da82827a87f140f9]::convert::AsRef<str>>::as_ref
     3128 (0.2%,  6.0%)      1 (0.0%,  0.0%)  <ruff[cef4c65d96248843]::codes::RuleIter>::get
     2669 (0.2%,  6.2%)      1 (0.0%,  0.0%)  <<ruff[cef4c65d96248843]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_seq::<toml_edit[7e3a6c5e67260672]::de::array::ArraySeqAccess>
```
After:
```
  Lines                  Copies               Function name
  -----                  ------               -------------
  1710487                38900                (TOTAL)
    10193 (0.6%,  0.6%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::codes::RuleCodePrefix>::iter
     8107 (0.5%,  1.1%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::codes::Rule>::noqa_code
     7345 (0.4%,  1.5%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::checkers::ast::Checker as ruff_python_ast[5588cd60041c8605]::visitor::Visitor>::visit_stmt
     6412 (0.4%,  1.9%)      1 (0.0%,  0.0%)  <<ruff[52408f46d2058296]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_map::<toml_edit[7e3a6c5e67260672]::de::spanned::SpannedDeserializer<toml_edit[7e3a6c5e67260672]::de::value::ValueDeserializer>>
     6412 (0.4%,  2.2%)      1 (0.0%,  0.0%)  <<ruff[52408f46d2058296]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_map::<toml_edit[7e3a6c5e67260672]::de::table::TableMapAccess>
     6409 (0.4%,  2.6%)      1 (0.0%,  0.0%)  <<ruff[52408f46d2058296]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_map::<toml_edit[7e3a6c5e67260672]::de::datetime::DatetimeDeserializer>
     5696 (0.3%,  3.0%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::checkers::ast::Checker as ruff_python_ast[5588cd60041c8605]::visitor::Visitor>::visit_expr
     4448 (0.3%,  3.2%)      1 (0.0%,  0.0%)  ruff[52408f46d2058296]::flake8_to_ruff::converter::convert
     3702 (0.2%,  3.4%)      1 (0.0%,  0.0%)  <&ruff[52408f46d2058296]::registry::Linter as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter
     3349 (0.2%,  3.6%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::registry::Linter>::code_for_rule
     3132 (0.2%,  3.8%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::codes::Rule as core[da82827a87f140f9]::fmt::Debug>::fmt
     3130 (0.2%,  4.0%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<&ruff[52408f46d2058296]::codes::Rule>>::from
     3130 (0.2%,  4.2%)      1 (0.0%,  0.0%)  <&str as core[da82827a87f140f9]::convert::From<ruff[52408f46d2058296]::codes::Rule>>::from
     3130 (0.2%,  4.4%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::codes::Rule as core[da82827a87f140f9]::convert::AsRef<str>>::as_ref
     3128 (0.2%,  4.5%)      1 (0.0%,  0.0%)  <ruff[52408f46d2058296]::codes::RuleIter>::get
     2669 (0.2%,  4.7%)      1 (0.0%,  0.0%)  <<ruff[52408f46d2058296]::settings::options::Options as serde[d89b1b632568f5a3]::de::Deserialize>::deserialize::__Visitor as serde[d89b1b632568f5a3]::de::Visitor>::visit_seq::<toml_edit[7e3a6c5e67260672]::de::array::ArraySeqAccess>
     2659 (0.2%,  4.9%)      1 (0.0%,  0.0%)  <&ruff[52408f46d2058296]::codes::Pylint as core[da82827a87f140f9]::iter::traits::collect::IntoIterator>::into_iter
```

I'd assume this has a positive effect both on compile time and on runtime, but i don't know the actual effect on compile times and can't really measure.

## Test plan

Check CI for any performance regressions.

This should fix #3808 if we merge it.

* clippy

* Update update_ambiguous_characters.py
2023-06-08 08:13:20 +02:00
Micha Reiser
39a1f3980f Upgrade RustPython (#4900) 2023-06-08 05:53:14 +00:00
1686 changed files with 57233 additions and 38841 deletions

9
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,9 @@
# GitHub code owners file. For more info: https://help.github.com/articles/about-codeowners/
#
# - Comment lines begin with `#` character.
# - Each line is a file pattern followed by one or more owners.
# - The '*' pattern is global owners.
# - Order is important. The last matching pattern has the most precedence.
# Jupyter
/crates/ruff/src/jupyter/ @dhruvmanila

View File

@@ -14,7 +14,7 @@ jobs:
name: "Run | ${{ matrix.os }}"
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
@@ -29,10 +29,7 @@ jobs:
- uses: Swatinem/rust-cache@v2
- name: "PR - Build benchmarks"
uses: actions-rs/cargo@v1
with:
command: bench
args: -p ruff_benchmark --no-run
run: cargo bench -p ruff_benchmark --no-run
- name: "PR - Run benchmarks"
run: cargo benchmark --save-baseline=pr
@@ -47,10 +44,7 @@ jobs:
run: rustup show
- name: "Main - Build benchmarks"
uses: actions-rs/cargo@v1
with:
command: bench
args: -p ruff_benchmark --no-run
run: cargo bench -p ruff_benchmark --no-run
- name: "Main - Run benchmarks"
run: cargo benchmark --save-baseline=main
@@ -78,11 +72,10 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: taiki-e/install-action@cargo-binstall
- name: "Install critcmp"
run: cargo binstall critcmp -y
uses: taiki-e/install-action@v2
with:
tool: critcmp
- name: "Linux | Download PR benchmark results"
uses: actions/download-artifact@v3

View File

@@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [main]
branches: [ main ]
pull_request:
workflow_dispatch:
@@ -54,7 +54,7 @@ jobs:
cargo-test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}
name: "cargo test | ${{ matrix.os }}"
steps:
@@ -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:
@@ -194,12 +197,12 @@ jobs:
- uses: actions/checkout@v3
- name: "Install nightly Rust toolchain"
# Only pinned to make caching work, update freely
run: rustup toolchain install nightly-2023-03-30
run: rustup toolchain install nightly-2023-06-08
- uses: Swatinem/rust-cache@v2
- name: "Install cargo-udeps"
uses: taiki-e/install-action@cargo-udeps
- name: "Run cargo-udeps"
run: cargo +nightly-2023-03-30 udeps
run: cargo +nightly-2023-06-08 udeps
python-package:
@@ -211,18 +214,20 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- uses: Swatinem/rust-cache@v2
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- 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"
run: rm -rf target/wheels
pre-commit:
name: "pre-commit"
@@ -240,8 +245,8 @@ jobs:
- name: "Cache pre-commit"
uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: "Run pre-commit"
run: |
echo '```console' > $GITHUB_STEP_SUMMARY

View File

@@ -66,7 +66,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
target: [ x64, x86 ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -94,7 +94,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, i686]
target: [ x86_64, i686 ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -121,7 +121,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
target: [aarch64, armv7, s390x, ppc64le, ppc64]
target: [ aarch64, armv7, s390x, ppc64le, ppc64 ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4

View File

@@ -2,8 +2,8 @@ name: PR Check Comment
on:
workflow_run:
workflows: [CI, Benchmark]
types: [completed]
workflows: [ CI, Benchmark ]
types: [ completed ]
workflow_dispatch:
inputs:
workflow_run_id:

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 }}
@@ -221,7 +230,7 @@ jobs:
platform:
- target: aarch64-unknown-linux-gnu
arch: aarch64
# see https://github.com/charliermarsh/ruff/issues/3791
# see https://github.com/astral-sh/ruff/issues/3791
# and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: armv7-unknown-linux-gnueabihf
@@ -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

15
.gitignore vendored
View File

@@ -1,11 +1,26 @@
# 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,
# 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*
###
# Rust.gitignore

View File

@@ -3,6 +3,8 @@ fail_fast: true
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/.*
)$
@@ -37,29 +39,19 @@ repos:
name: cargo fmt
entry: cargo fmt --
language: system
types: [rust]
- id: clippy
name: clippy
entry: cargo clippy --workspace --all-targets --all-features -- -D warnings
language: system
pass_filenames: false
types: [ rust ]
pass_filenames: false # This makes it a lot faster
- id: ruff
name: ruff
entry: cargo run -p ruff_cli -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix
entry: cargo run --bin ruff -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix
language: system
types_or: [python, pyi]
types_or: [ python, pyi ]
require_serial: true
exclude: |
(?x)^(
crates/ruff/resources/.*|
crates/ruff_python_formatter/resources/.*
)$
- id: dev-generate-all
name: dev-generate-all
entry: cargo dev generate-all
language: system
pass_filenames: false
exclude: target
# Black
- repo: https://github.com/psf/black
@@ -68,4 +60,4 @@ repos:
- id: black
ci:
skip: [cargo-fmt, clippy, dev-generate-all]
skip: [ cargo-fmt, dev-generate-all ]

View File

@@ -2,7 +2,7 @@
## 0.0.268
### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/charliermarsh/ruff/pull/4427))
### The `keep-runtime-typing` setting has been removed ([#4427](https://github.com/astral-sh/ruff/pull/4427))
Enabling the `keep-runtime-typing` option, located under the `pyupgrade` section, is equivalent
to ignoring the `UP006` and `UP007` rules via Ruff's standard `ignore` mechanism. As there's no
@@ -11,9 +11,9 @@ removed.
## 0.0.267
### `update-check` is no longer a valid configuration option ([#4313](https://github.com/charliermarsh/ruff/pull/4313))
### `update-check` is no longer a valid configuration option ([#4313](https://github.com/astral-sh/ruff/pull/4313))
The `update-check` functionality was deprecated in [#2530](https://github.com/charliermarsh/ruff/pull/2530),
The `update-check` functionality was deprecated in [#2530](https://github.com/astral-sh/ruff/pull/2530),
in that the behavior itself was removed, and Ruff was changed to warn when that option was enabled.
Now, Ruff will throw an error when `update-check` is provided via a configuration file (e.g.,
@@ -22,7 +22,7 @@ this option from their configuration.
## 0.0.265
### `--fix-only` now exits with a zero exit code, unless `--exit-non-zero-on-fix` is specified ([#4146](https://github.com/charliermarsh/ruff/pull/4146))
### `--fix-only` now exits with a zero exit code, unless `--exit-non-zero-on-fix` is specified ([#4146](https://github.com/astral-sh/ruff/pull/4146))
Previously, `--fix-only` would exit with a non-zero exit code if any fixes were applied. This
behavior was inconsistent with `--fix`, and further, meant that `--exit-non-zero-on-fix` was
@@ -33,7 +33,7 @@ in which case it will exit with a non-zero exit code if any fixes were applied.
## 0.0.260
### Fixes are now represented as a list of edits ([#3709](https://github.com/charliermarsh/ruff/pull/3709))
### Fixes are now represented as a list of edits ([#3709](https://github.com/astral-sh/ruff/pull/3709))
Previously, Ruff represented each fix as a single edit, which prohibited Ruff from automatically
fixing violations that required multiple edits across a file. As such, Ruff now represents each
@@ -68,14 +68,14 @@ The updated representation instead includes a list of edits:
## 0.0.246
### `multiple-statements-on-one-line-def` (`E704`) was removed ([#2773](https://github.com/charliermarsh/ruff/pull/2773))
### `multiple-statements-on-one-line-def` (`E704`) was removed ([#2773](https://github.com/astral-sh/ruff/pull/2773))
This rule was introduced in v0.0.245. However, it turns out that pycodestyle and Flake8 ignore this
rule by default, as it is not part of PEP 8. As such, we've removed it from Ruff.
## 0.0.245
### Ruff's public `check` method was removed ([#2709](https://github.com/charliermarsh/ruff/pull/2709))
### Ruff's public `check` method was removed ([#2709](https://github.com/astral-sh/ruff/pull/2709))
Previously, Ruff exposed a `check` method as a public Rust API. This method was used by few,
if any clients, and was not well documented or supported. As such, it has been removed, with
@@ -83,11 +83,11 @@ the intention of adding a stable public API in the future.
## 0.0.238
### `select`, `extend-select`, `ignore`, and `extend-ignore` have new semantics ([#2312](https://github.com/charliermarsh/ruff/pull/2312))
### `select`, `extend-select`, `ignore`, and `extend-ignore` have new semantics ([#2312](https://github.com/astral-sh/ruff/pull/2312))
Previously, the interplay between `select` and its related options could lead to unexpected
behavior. For example, `ruff --select E501 --ignore ALL` and `ruff --select E501 --extend-ignore ALL`
behaved differently. (See [#2312](https://github.com/charliermarsh/ruff/pull/2312) for more
behaved differently. (See [#2312](https://github.com/astral-sh/ruff/pull/2312) for more
examples.)
When Ruff determines the enabled rule set, it has to reconcile `select` and `ignore` from a variety
@@ -113,14 +113,14 @@ ignore = ["F401"]
Running `ruff --select F` would previously have enabled all `F` rules, apart from `F401`. Now, it
will enable all `F` rules, including `F401`, as the command line's `--select` resets the resolution.
### `remove-six-compat` (`UP016`) has been removed ([#2332](https://github.com/charliermarsh/ruff/pull/2332))
### `remove-six-compat` (`UP016`) has been removed ([#2332](https://github.com/astral-sh/ruff/pull/2332))
The `remove-six-compat` rule has been removed. This rule was only useful for one-time Python 2-to-3
upgrades.
## 0.0.237
### `--explain`, `--clean`, and `--generate-shell-completion` are now subcommands ([#2190](https://github.com/charliermarsh/ruff/pull/2190))
### `--explain`, `--clean`, and `--generate-shell-completion` are now subcommands ([#2190](https://github.com/astral-sh/ruff/pull/2190))
`--explain`, `--clean`, and `--generate-shell-completion` are now implemented as subcommands:
@@ -163,14 +163,14 @@ no change in behavior. However, please note the following exceptions:
## 0.0.226
### `misplaced-comparison-constant` (`PLC2201`) was deprecated in favor of `SIM300` ([#1980](https://github.com/charliermarsh/ruff/pull/1980))
### `misplaced-comparison-constant` (`PLC2201`) was deprecated in favor of `SIM300` ([#1980](https://github.com/astral-sh/ruff/pull/1980))
These two rules contain (nearly) identical logic. To deduplicate the rule set, we've upgraded
`SIM300` to handle a few more cases, and deprecated `PLC2201` in favor of `SIM300`.
## 0.0.225
### `@functools.cache` rewrites have been moved to a standalone rule (`UP033`) ([#1938](https://github.com/charliermarsh/ruff/pull/1938))
### `@functools.cache` rewrites have been moved to a standalone rule (`UP033`) ([#1938](https://github.com/astral-sh/ruff/pull/1938))
Previously, `UP011` handled both `@functools.lru_cache()`-to-`@functools.lru_cache` conversions,
_and_ `@functools.lru_cache(maxsize=None)`-to-`@functools.cache` conversions. The latter has been
@@ -179,7 +179,7 @@ to reflect the change in rule code.
## 0.0.222
### `--max-complexity` has been removed from the CLI ([#1877](https://github.com/charliermarsh/ruff/pull/1877))
### `--max-complexity` has been removed from the CLI ([#1877](https://github.com/astral-sh/ruff/pull/1877))
The McCabe plugin's `--max-complexity` setting has been removed from the CLI, for consistency with
the treatment of other, similar settings.
@@ -194,7 +194,7 @@ max-complexity = 10
## 0.0.181
### Files excluded by `.gitignore` are now ignored ([#1234](https://github.com/charliermarsh/ruff/pull/1234))
### Files excluded by `.gitignore` are now ignored ([#1234](https://github.com/astral-sh/ruff/pull/1234))
Ruff will now avoid checking files that are excluded by `.ignore`, `.gitignore`,
`.git/info/exclude`, and global `gitignore` files. This behavior is powered by the [`ignore`](https://docs.rs/ignore/latest/ignore/struct.WalkBuilder.html#ignore-rules)
@@ -207,7 +207,7 @@ default.
## 0.0.178
### Configuration files are now resolved hierarchically ([#1190](https://github.com/charliermarsh/ruff/pull/1190))
### Configuration files are now resolved hierarchically ([#1190](https://github.com/astral-sh/ruff/pull/1190))
`pyproject.toml` files are now resolved hierarchically, such that for each Python file, we find
the first `pyproject.toml` file in its path, and use that to determine its lint settings.

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
@@ -21,18 +21,18 @@ Ruff welcomes contributions in the form of Pull Requests.
For small changes (e.g., bug fixes), feel free to submit a PR.
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
creating an [**issue**](https://github.com/charliermarsh/ruff/issues) outlining your proposed
change. You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with
the community.
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the
community.
If you're looking for a place to start, we recommend implementing a new lint rule (see:
[_Adding a new lint rule_](#example-adding-a-new-lint-rule), which will allow you to learn from and
pattern-match against the examples in the existing codebase. Many lint rules are inspired by
existing Python plugins, which can be used as a reference implementation.
As a concrete example: consider taking on one of the rules from the [`flake8-pyi`](https://github.com/charliermarsh/ruff/issues/848)
plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi)
for guidance.
As a concrete example: consider taking on one of the rules from the [`flake8-pyi`](https://github.com/astral-sh/ruff/issues/848)
plugin, and looking to the originating [Python source](https://github.com/PyCQA/flake8-pyi) for
guidance.
### Prerequisites
@@ -45,6 +45,12 @@ You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
cargo install cargo-insta
```
and pre-commit to run some validation checks:
```shell
pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
```
### Development
After cloning the repository, run Ruff locally with:
@@ -57,9 +63,9 @@ Prior to opening a pull request, ensure that your code has been auto-formatted,
and that it passes both the lint and test validation checks:
```shell
cargo fmt # Auto-formatting...
cargo clippy --fix --workspace --all-targets --all-features # Linting...
cargo test # Testing...
cargo clippy --workspace --all-targets --all-features -- -D warnings # Rust linting
RUFF_UPDATE_SCHEMA=1 cargo test # Rust testing and updating ruff.schema.json
pre-commit run --all-files --show-diff-on-failure # Rust and Python formatting, Markdown and Python linting, etc.
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
@@ -72,13 +78,6 @@ after running `cargo test` like so:
cargo insta review
```
If you have `pre-commit` [installed](https://pre-commit.com/#installation) then you can use it to
assist with formatting and linting. The following command will run the `pre-commit` hooks:
```shell
pre-commit run --all-files
```
Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration
prior to merging.
@@ -93,64 +92,89 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra
At time of writing, the repository includes the following crates:
- `crates/ruff`: library crate containing all lint rules and the core logic for running them.
- `crates/ruff_benchmark`: binary crate for running micro-benchmarks.
- `crates/ruff_cache`: library crate for caching lint results.
- `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g.,
`cargo dev generate-all`).
- `crates/ruff_diagnostics`: library crate for the lint diagnostics APIs.
- `crates/ruff_formatter`: library crate for generic code formatting logic based on an intermediate
representation.
- `crates/ruff_index`: library crate inspired by `rustc_index`.
- `crates/ruff_macros`: library crate containing macros used by Ruff.
- `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of
standard library modules by version).
- `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_formatter`: library crate containing Python-specific code formatting logic.
- `crates/ruff_python_semantic`: library crate containing Python-specific semantic analysis logic,
including Ruff's semantic model.
- `crates/ruff_python_stdlib`: library crate containing Python-specific standard library data.
- `crates/ruff_python_whitespace`: library crate containing Python-specific whitespace analysis
logic.
- `crates/ruff_rustpython`: library crate containing `RustPython`-specific utilities.
- `crates/ruff_testing_macros`: library crate containing macros used for testing Ruff.
- `crates/ruff_textwrap`: library crate to indent and dedent Python source code.
- `crates/ruff_wasm`: library crate for exposing Ruff as a WebAssembly module.
### Example: Adding a new lint rule
At a high level, the steps involved in adding a new lint rule are as follows:
1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention).
1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention)
(e.g., `AssertFalse`, as in, "allow `assert False`").
1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`).
1. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/assert_false.rs`).
1. In that file, define a violation struct. You can grep for `#[violation]` to see examples.
1. In that file, define a violation struct (e.g., `pub struct AssertFalse`). You can grep for
`#[violation]` to see examples.
1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `E402`).
1. In that file, define a function that adds the violation to the diagnostic list as appropriate
(e.g., `pub(crate) fn assert_false`) based on whatever inputs are required for the rule (e.g.,
an `ast::StmtAssert` node).
1. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast/mod.rs` (for
AST-based checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks),
`crates/ruff/src/checkers/lines.rs` (for text-based checks), or
`crates/ruff/src/checkers/filesystem.rs` (for filesystem-based checks).
1. Map the violation struct to a rule code in `crates/ruff/src/codes.rs` (e.g., `B011`).
1. Add proper [testing](#rule-testing-fixtures-and-snapshots) for your rule.
1. Update the generated files (documentation and generated code).
To define the violation, start by creating a dedicated file for your rule under the appropriate
rule linter (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should
contain a struct defined via `#[violation]`, along with a function that creates the violation
based on any required inputs.
To trigger the violation, you'll likely want to augment the logic in `crates/ruff/src/checkers/ast.rs`,
which defines the Python AST visitor, responsible for iterating over the abstract syntax tree and
collecting diagnostics as it goes.
To trigger the violation, you'll likely want to augment the logic in `crates/ruff/src/checkers/ast.rs`
to call your new function at the appropriate time and with the appropriate inputs. The `Checker`
defined therein is a Python AST visitor, which iterates over the AST, building up a semantic model,
and calling out to lint rule analyzer functions as it goes.
If you need to inspect the AST, you can run `cargo dev print-ast` with a Python file. Grep
for the `Check::new` invocations to understand how other, similar rules are implemented.
for the `Diagnostic::new` invocations to understand how other, similar rules are implemented.
Once you're satisfied with your code, add tests for your rule. See [rule testing](#rule-testing-fixtures-and-snapshots)
for more details.
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
Finally, regenerate the documentation and other generated assets (like our JSON Schema) with:
`cargo dev generate-all`.
#### Rule naming convention
The rule name should make sense when read as "allow _rule-name_" or "allow _rule-name_ items".
Like Clippy, Ruff's rule names should make grammatical and logical sense when read as "allow
${rule}" or "allow ${rule} items", as in the context of suppression comments.
This implies that rule names:
For example, `AssertFalse` fits this convention: it flags `assert False` statements, and so a
suppression comment would be framed as "allow `assert False`".
- should state the bad thing being checked for
As such, rule names should...
- should not contain instructions on what you should use instead
(these belong in the rule documentation and the `autofix_title` for rules that have autofix)
- Highlight the pattern that is being linted against, rather than the preferred alternative.
For example, `AssertFalse` guards against `assert False` statements.
When re-implementing rules from other linters, this convention is given more importance than
- _Not_ contain instructions on how to fix the violation, which instead belong in the rule
documentation and the `autofix_title`.
- _Not_ contain a redundant prefix, like `Disallow` or `Banned`, which are already implied by the
convention.
When re-implementing rules from other linters, we prioritize adhering to this convention over
preserving the original rule name.
#### Rule testing: fixtures and snapshots
@@ -247,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
@@ -258,10 +304,18 @@ python scripts/check_ecosystem.py path/to/your/ruff path/to/older/ruff
You can also run the Ecosystem CI check in a Docker container across a larger set of projects by
downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-aggregate/blob/master/data/known-github-tomls.jsonl)
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/charliermarsh/ruff/blob/main/scripts/Dockerfile.ecosystem).
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.
@@ -340,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:
@@ -383,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.

178
Cargo.lock generated
View File

@@ -171,6 +171,12 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "bincode"
version = "1.3.3"
@@ -256,7 +262,8 @@ dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"time",
"serde",
"time 0.1.45",
"wasm-bindgen",
"winapi",
]
@@ -555,6 +562,41 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "darling"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0558d22a7b463ed0241e993f76f09f30b126687447751a8638587b864e4b3944"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8bfa2e259f8ee1ce5e97824a3c55ec4404a0d772ca7fa96bf19f0752a046eb"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.18",
]
[[package]]
name = "darling_macro"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a"
dependencies = [
"darling_core",
"quote",
"syn 2.0.18",
]
[[package]]
name = "diff"
version = "0.1.13"
@@ -691,7 +733,7 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.272"
version = "0.0.274"
dependencies = [
"anyhow",
"clap",
@@ -814,6 +856,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hexf-parse"
version = "0.2.1"
@@ -843,6 +891,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.3.0"
@@ -1402,6 +1456,7 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [
"phf_macros",
"phf_shared",
]
@@ -1425,6 +1480,19 @@ dependencies = [
"rand",
]
[[package]]
name = "phf_macros"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92aacdc5f16768709a569e913f7451034034178b05bdc8acda226659a3dccc66"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "phf_shared"
version = "0.11.1"
@@ -1725,7 +1793,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.272"
version = "0.0.274"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1752,6 +1820,7 @@ dependencies = [
"path-absolutize",
"pathdiff",
"pep440_rs",
"phf",
"pretty_assertions",
"pyproject-toml",
"quick-junit",
@@ -1760,10 +1829,10 @@ dependencies = [
"ruff_cache",
"ruff_diagnostics",
"ruff_macros",
"ruff_newlines",
"ruff_python_ast",
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_python_whitespace",
"ruff_rustpython",
"ruff_text_size",
"ruff_textwrap",
@@ -1774,6 +1843,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"serde_with",
"shellexpand",
"similar",
"smallvec",
@@ -1810,6 +1880,7 @@ name = "ruff_cache"
version = "0.0.0"
dependencies = [
"filetime",
"glob",
"globset",
"itertools",
"regex",
@@ -1818,7 +1889,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.272"
version = "0.0.274"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1837,6 +1908,7 @@ dependencies = [
"glob",
"ignore",
"itertools",
"itoa",
"log",
"mimalloc",
"notify",
@@ -1871,17 +1943,20 @@ dependencies = [
"clap",
"itertools",
"libcst",
"log",
"once_cell",
"pretty_assertions",
"regex",
"ruff",
"ruff_cli",
"ruff_diagnostics",
"ruff_python_formatter",
"ruff_textwrap",
"rustpython-format",
"rustpython-parser",
"schemars",
"serde_json",
"similar",
"strum",
"strum_macros",
]
@@ -1930,14 +2005,6 @@ dependencies = [
"syn 2.0.18",
]
[[package]]
name = "ruff_newlines"
version = "0.0.0"
dependencies = [
"memchr",
"ruff_text_size",
]
[[package]]
name = "ruff_python_ast"
version = "0.0.0"
@@ -1952,7 +2019,7 @@ dependencies = [
"num-bigint",
"num-traits",
"once_cell",
"ruff_newlines",
"ruff_python_whitespace",
"ruff_text_size",
"rustc-hash",
"rustpython-ast",
@@ -1974,8 +2041,8 @@ dependencies = [
"itertools",
"once_cell",
"ruff_formatter",
"ruff_newlines",
"ruff_python_ast",
"ruff_python_whitespace",
"ruff_testing_macros",
"ruff_text_size",
"rustc-hash",
@@ -2009,6 +2076,14 @@ dependencies = [
"rustc-hash",
]
[[package]]
name = "ruff_python_whitespace"
version = "0.0.0"
dependencies = [
"memchr",
"ruff_text_size",
]
[[package]]
name = "ruff_rustpython"
version = "0.0.0"
@@ -2030,7 +2105,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"schemars",
"serde",
@@ -2040,7 +2115,7 @@ dependencies = [
name = "ruff_textwrap"
version = "0.0.0"
dependencies = [
"ruff_newlines",
"ruff_python_whitespace",
"ruff_text_size",
]
@@ -2108,7 +2183,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"is-macro",
"num-bigint",
@@ -2119,7 +2194,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"bitflags 2.3.1",
"itertools",
@@ -2131,7 +2206,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2143,7 +2218,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"anyhow",
"is-macro",
@@ -2166,9 +2241,10 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd#7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=08ebbe40d7776cac6e3ba66277d435056f2b8dca#08ebbe40d7776cac6e3ba66277d435056f2b8dca"
dependencies = [
"is-macro",
"memchr",
"ruff_text_size",
]
@@ -2293,7 +2369,6 @@ version = "1.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
@@ -2308,6 +2383,34 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
dependencies = [
"base64 0.21.2",
"chrono",
"hex",
"indexmap",
"serde",
"serde_json",
"serde_with_macros",
"time 0.3.21",
]
[[package]]
name = "serde_with_macros"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]]
name = "shellexpand"
version = "3.1.0"
@@ -2534,6 +2637,33 @@ dependencies = [
"winapi",
]
[[package]]
name = "time"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc"
dependencies = [
"itoa",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
[[package]]
name = "time-macros"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b"
dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
@@ -2752,7 +2882,7 @@ version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338b31dd1314f68f3aabf3ed57ab922df95ffcd902476ca7ba3c4ce7b908c46d"
dependencies = [
"base64",
"base64 0.13.1",
"flate2",
"log",
"once_cell",

View File

@@ -1,13 +1,15 @@
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.70"
homepage = "https://beta.ruff.rs/docs/"
documentation = "https://beta.ruff.rs/docs/"
repository = "https://github.com/charliermarsh/ruff"
homepage = "https://beta.ruff.rs/docs"
documentation = "https://beta.ruff.rs/docs"
repository = "https://github.com/astral-sh/ruff"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
anyhow = { version = "1.0.69" }
@@ -22,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" }
@@ -34,16 +35,11 @@ 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 = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" }
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd", default-features = false, features = ["all-nodes-with-ranges"]}
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd" }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "7a3eedbf6fb4ea7068a1bf7fe0e97e963ea95ffd", default-features = false, features = ["full-lexer", "all-nodes-with-ranges"] }
schemars = { version = "0.8.12" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.93", features = ["preserve_order"] }
serde_json = { version = "1.0.93" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1" }
similar = { version = "2.2.1", features = ["inline"] }
smallvec = { version = "1.10.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
@@ -51,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

48
LICENSE
View File

@@ -354,6 +354,29 @@ are:
SOFTWARE.
"""
- flake8-slots, licensed as follows:
"""
Copyright (c) 2021 Dominic Davis-Foster
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.
"""
- flake8-todos, licensed as follows:
"""
Copyright (c) 2019 EclecticIQ. All rights reserved.
@@ -1176,6 +1199,31 @@ are:
- flake8-django, licensed under the GPL license.
- perflint, licensed as follows:
"""
MIT License
Copyright (c) 2022 Anthony Shaw
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- rust-analyzer/text-size, licensed under the MIT license:
"""
Permission is hereby granted, free of charge, to any

View File

@@ -2,11 +2,11 @@
# Ruff
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/charliermarsh/ruff)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![image](https://img.shields.io/pypi/v/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/charliermarsh/ruff/workflows/CI/badge.svg)](https://github.com/charliermarsh/ruff/actions)
[![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions)
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://beta.ruff.rs/docs/) | [**Playground**](https://play.ruff.rs/)
@@ -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>
@@ -88,7 +88,7 @@ creator of [isort](https://github.com/PyCQA/isort):
> Just switched my first project to Ruff. Only one downside so far: it's so fast I couldn't believe
> it was working till I intentionally introduced some errors.
[**Tim Abbott**](https://github.com/charliermarsh/ruff/issues/465#issuecomment-1317400028), lead
[**Tim Abbott**](https://github.com/astral-sh/ruff/issues/465#issuecomment-1317400028), lead
developer of [Zulip](https://github.com/zulip/zulip):
> This is just ridiculously fast... `ruff` is amazing.
@@ -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
```
@@ -254,13 +254,14 @@ quality tools, including:
- [flake8-2020](https://pypi.org/project/flake8-2020/)
- [flake8-annotations](https://pypi.org/project/flake8-annotations/)
- [flake8-async](https://pypi.org/project/flake8-async)
- [flake8-bandit](https://pypi.org/project/flake8-bandit/) ([#1646](https://github.com/charliermarsh/ruff/issues/1646))
- [flake8-bandit](https://pypi.org/project/flake8-bandit/) ([#1646](https://github.com/astral-sh/ruff/issues/1646))
- [flake8-blind-except](https://pypi.org/project/flake8-blind-except/)
- [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/)
- [flake8-bugbear](https://pypi.org/project/flake8-bugbear/)
- [flake8-builtins](https://pypi.org/project/flake8-builtins/)
- [flake8-commas](https://pypi.org/project/flake8-commas/)
- [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
- [flake8-copyright](https://pypi.org/project/flake8-copyright/)
- [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
- [flake8-debugger](https://pypi.org/project/flake8-debugger/)
- [flake8-django](https://pypi.org/project/flake8-django/)
@@ -283,12 +284,13 @@ quality tools, including:
- [flake8-return](https://pypi.org/project/flake8-return/)
- [flake8-self](https://pypi.org/project/flake8-self/)
- [flake8-simplify](https://pypi.org/project/flake8-simplify/)
- [flake8-slots](https://pypi.org/project/flake8-slots/)
- [flake8-super](https://pypi.org/project/flake8-super/)
- [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
- [flake8-todos](https://pypi.org/project/flake8-todos/)
- [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
- [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/charliermarsh/ruff/issues/2102))
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/astral-sh/ruff/issues/2102))
- [isort](https://pypi.org/project/isort/)
- [mccabe](https://pypi.org/project/mccabe/)
- [pandas-vet](https://pypi.org/project/pandas-vet/)
@@ -311,8 +313,8 @@ You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).
## Support
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/charliermarsh/ruff/issues),
or feel free to [**open a new one**](https://github.com/charliermarsh/ruff/issues/new).
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
You can also ask for help on [**Discord**](https://discord.gg/c9MhzV8aU5).
@@ -334,7 +336,7 @@ and again draws on both the APIs and implementation details of [Rome](https://gi
Ruff is also influenced by a number of tools outside the Python ecosystem, like
[Clippy](https://github.com/rust-lang/rust-clippy) and [ESLint](https://github.com/eslint/eslint).
Ruff is the beneficiary of a large number of [contributors](https://github.com/charliermarsh/ruff/graphs/contributors).
Ruff is the beneficiary of a large number of [contributors](https://github.com/astral-sh/ruff/graphs/contributors).
Ruff is released under the MIT license.
@@ -413,21 +415,21 @@ Ruff is used by a number of major open-source projects and companies, including:
If you're using Ruff, consider adding the Ruff badge to project's `README.md`:
```md
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/charliermarsh/ruff)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
```
...or `README.rst`:
```rst
.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json
:target: https://github.com/charliermarsh/ruff
:target: https://github.com/astral-sh/ruff
:alt: Ruff
```
...or, as HTML:
```html
<a href="https://github.com/charliermarsh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
```
## License

View File

@@ -8,3 +8,4 @@ whos = "whos"
spawnve = "spawnve"
ned = "ned"
poit = "poit"
BA = "BA" # acronym for "Bad Allowed", used in testing.

BIN
assets/png/Astral.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -1,8 +1,16 @@
[package]
name = "flake8-to-ruff"
version = "0.0.272"
version = "0.0.274"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
[dependencies]
ruff = { path = "../ruff", default-features = false }

View File

@@ -1,7 +1,7 @@
# flake8-to-ruff
Convert existing Flake8 configuration files (`setup.cfg`, `tox.ini`, or `.flake8`) for use with
[Ruff](https://github.com/charliermarsh/ruff).
[Ruff](https://github.com/astral-sh/ruff).
Generates a Ruff-compatible `pyproject.toml` section.
@@ -96,4 +96,4 @@ MIT
## Contributing
Contributions are welcome and hugely appreciated. To get started, check out the
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTING.md).
[contributing guidelines](https://github.com/astral-sh/ruff/blob/main/CONTRIBUTING.md).

View File

@@ -23,7 +23,7 @@ description = "Convert existing Flake8 configuration to Ruff."
requires-python = ">=3.7"
[project.urls]
repository = "https://github.com/charliermarsh/ruff#subdirectory=crates/flake8_to_ruff"
repository = "https://github.com/astral-sh/ruff#subdirectory=crates/flake8_to_ruff"
[build-system]
requires = ["maturin>=1.0,<2.0"]

View File

@@ -1,14 +1,15 @@
[package]
name = "ruff"
version = "0.0.272"
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
documentation.workspace = true
homepage.workspace = true
repository.workspace = true
version = "0.0.274"
publish = false
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
homepage = { workspace = true }
documentation = { workspace = true }
repository = { workspace = true }
license = { workspace = true }
readme = "README.md"
license = "MIT"
[lib]
name = "ruff"
@@ -17,7 +18,7 @@ name = "ruff"
ruff_cache = { path = "../ruff_cache" }
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
ruff_macros = { path = "../ruff_macros" }
ruff_newlines = { path = "../ruff_newlines" }
ruff_python_whitespace = { path = "../ruff_python_whitespace" }
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
ruff_python_semantic = { path = "../ruff_python_semantic" }
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
@@ -52,6 +53,7 @@ path-absolutize = { workspace = true, features = [
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.1", features = ["serde"] }
phf = { version = "0.11", features = ["macros"] }
pyproject-toml = { version = "0.6.0" }
quick-junit = { version = "0.3.2" }
regex = { workspace = true }
@@ -63,7 +65,8 @@ schemars = { workspace = true, optional = true }
semver = { version = "1.0.16" }
serde = { workspace = true }
serde_json = { workspace = true }
similar = { workspace = true, features = ["inline"] }
serde_with = { version = "3.0.0" }
similar = { workspace = true }
shellexpand = { workspace = true }
smallvec = { workspace = true }
strum = { workspace = true }

View File

@@ -149,7 +149,7 @@ for group in groupby(items, key=lambda p: p[1]):
collect_shop_items("Joe", group[1])
# https://github.com/charliermarsh/ruff/issues/4050
# https://github.com/astral-sh/ruff/issues/4050
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
if _section == "greens":
for item in section_items:

View File

@@ -1,6 +1,6 @@
class MyClass:
ImportError = 4
id = 5
id: int
dir = "/"
def __init__(self):
@@ -10,3 +10,10 @@ class MyClass:
def str(self):
pass
from typing import TypedDict
class MyClass(TypedDict):
id: int

View File

@@ -1,13 +1,20 @@
x = set(x for x in range(3))
x = set(
x for x in range(3)
)
y = f'{set(a if a < 6 else 0 for a in range(3))}'
_ = '{}'.format(set(a if a < 6 else 0 for a in range(3)))
print(f'Hello {set(a for a in range(3))} World')
def set(*args, **kwargs):
return None
x = set(x for x in range(3))
y = f"{set(a if a < 6 else 0 for a in range(3))}"
_ = "{}".format(set(a if a < 6 else 0 for a in range(3)))
print(f"Hello {set(a for a in range(3))} World")
set(x for x in range(3))
def f(x):
return x
print(f'Hello {set(a for a in "abc")} World')
print(f"Hello {set(a for a in 'abc')} World")
print(f"Hello {set(f(a) for a in 'abc')} World")
print(f"{set(a for a in 'abc') - set(a for a in 'ab')}")
print(f"{ set(a for a in 'abc') - set(a for a in 'ab') }")
# The fix generated for this diagnostic is incorrect, as we add additional space
# around the set comprehension.
print(f"{ {set(a for a in 'abc')} }")

View File

@@ -5,3 +5,14 @@ dict(
dict(((x, x) for x in range(3)), z=3)
y = f'{dict((x, x) for x in range(3))}'
print(f'Hello {dict((x, x) for x in range(3))} World')
print(f"Hello {dict((x, x) for x in 'abc')} World")
print(f'Hello {dict((x, x) for x in "abc")} World')
print(f'Hello {dict((x,x) for x in "abc")} World')
f'{dict((x, x) for x in range(3)) | dict((x, x) for x in range(3))}'
f'{ dict((x, x) for x in range(3)) | dict((x, x) for x in range(3)) }'
def f(x):
return x
print(f'Hello {dict((x,f(x)) for x in "abc")} World')

View File

@@ -2,3 +2,14 @@ s = set([x for x in range(3)])
s = set(
[x for x in range(3)]
)
s = f"{set([x for x in 'ab'])}"
s = f'{set([x for x in "ab"])}'
def f(x):
return x
s = f"{set([f(x) for x in 'ab'])}"
s = f"{ set([x for x in 'ab']) | set([x for x in 'ab']) }"
s = f"{set([x for x in 'ab']) | set([x for x in 'ab'])}"

View File

@@ -1,2 +1,13 @@
dict([(i, i) for i in range(3)])
dict([(i, i) for i in range(3)], z=4)
def f(x):
return x
f'{dict([(s,s) for s in "ab"])}'
f"{dict([(s,s) for s in 'ab'])}"
f"{dict([(s, s) for s in 'ab'])}"
f"{dict([(s,f(s)) for s in 'ab'])}"
f'{dict([(s,s) for s in "ab"]) | dict([(s,s) for s in "ab"])}'
f'{ dict([(s,s) for s in "ab"]) | dict([(s,s) for s in "ab"]) }'

View File

@@ -16,3 +16,11 @@ set(
set(
[1,]
)
f"{set([1,2,3])}"
f"{set(['a', 'b'])}"
f'{set(["a", "b"])}'
f"{set(['a', 'b']) - set(['a'])}"
f"{ set(['a', 'b']) - set(['a']) }"
f"a {set(['a', 'b']) - set(['a'])} b"
f"a { set(['a', 'b']) - set(['a']) } b"

View File

@@ -10,3 +10,13 @@ def list():
a = list()
f"{dict(x='y')}"
f'{dict(x="y")}'
f"{dict()}"
f"a {dict()} b"
f"{dict(x='y') | dict(y='z')}"
f"{ dict(x='y') | dict(y='z') }"
f"a {dict(x='y') | dict(y='z')} b"
f"a { dict(x='y') | dict(y='z') } b"

View File

@@ -34,3 +34,19 @@ _ = (
b"abc"
b"def"
)
_ = """a""" """b"""
_ = """a
b""" """c
d"""
_ = f"""a""" f"""b"""
_ = f"a" "b"
_ = """a""" "b"
_ = 'a' "b"
_ = rf"a" rf"b"

View File

@@ -1,19 +1,19 @@
from collections.abc import Set as AbstractSet # Ok
def f():
from collections.abc import Set as AbstractSet # Ok
from collections.abc import Set # Ok
def f():
from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok
from collections.abc import (
Container,
Sized,
Set, # Ok
ValuesView
)
def f():
from collections.abc import Set # PYI025
from collections.abc import (
Container,
Sized,
Set as AbstractSet, # Ok
ValuesView
)
def f():
from collections.abc import Container, Sized, Set, ValuesView # PYI025
GLOBAL: Set[int] = set()
class Class:
member: Set[int]

View File

@@ -1,19 +1,50 @@
from collections.abc import Set as AbstractSet # Ok
def f():
from collections.abc import Set as AbstractSet # Ok
def f():
from collections.abc import Container, Sized, Set as AbstractSet, ValuesView # Ok
from collections.abc import Set # PYI025
def f():
from collections.abc import Set # PYI025
def f():
from collections.abc import Container, Sized, Set, ValuesView # PYI025
from collections.abc import (
Container,
Sized,
Set, # PYI025
ValuesView
)
def f():
"""Test: local symbol renaming."""
if True:
from collections.abc import Set
else:
Set = 1
from collections.abc import (
Container,
Sized,
Set as AbstractSet,
ValuesView # Ok
)
x: Set = set()
x: Set
del Set
def f():
print(Set)
def Set():
pass
print(Set)
from collections.abc import Set
def f():
"""Test: global symbol renaming."""
global Set
Set = 1
print(Set)
def f():
"""Test: nonlocal symbol renaming."""
from collections.abc import Set
def g():
nonlocal Set
Set = 1
print(Set)

View File

@@ -0,0 +1,7 @@
# Bad import.
from __future__ import annotations # Not PYI044 (not a stubfile).
# Good imports.
from __future__ import Something
import sys
from socket import AF_INET

View File

@@ -0,0 +1,7 @@
# Bad import.
from __future__ import annotations # PYI044.
# Good imports.
from __future__ import Something
import sys
from socket import AF_INET

View File

@@ -1,17 +1,25 @@
import pytest
def test_xxx():
pytest.fail("this is a failure") # Test OK arg
# OK
def f():
pytest.fail("this is a failure")
def test_xxx():
pytest.fail(msg="this is a failure") # Test OK kwarg
def f():
pytest.fail(msg="this is a failure")
def test_xxx(): # Error
def f():
pytest.fail(reason="this is a failure")
# Errors
def f():
pytest.fail()
pytest.fail("")
pytest.fail(f"")
pytest.fail(msg="")
pytest.fail(msg=f"")
pytest.fail(reason="")
pytest.fail(reason=f"")

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

@@ -79,7 +79,7 @@ def x():
return a
# ignore unpacking
# Ignore unpacking
def x():
b, a = [1, 2]
return a
@@ -109,7 +109,8 @@ def x():
# Considered OK, since functions can have side effects.
def x():
b, a = 1, 2
a = 1
b = 2
print(b)
return a
@@ -276,20 +277,25 @@ def str_to_bool(val):
# Mixed assignments
def function_assignment(x):
def f(): ...
def f():
...
return f
def class_assignment(x):
class Foo: ...
class Foo:
...
return Foo
def mixed_function_assignment(x):
if x:
def f(): ...
def f():
...
else:
f = 42
@@ -298,8 +304,56 @@ def mixed_function_assignment(x):
def mixed_class_assignment(x):
if x:
class Foo: ...
class Foo:
...
else:
Foo = 42
return Foo
# `with` statements
def foo():
with open("foo.txt", "r") as f:
x = f.read()
return x # RET504
def foo():
with open("foo.txt", "r") as f:
x = f.read()
print(x)
return x
def foo():
with open("foo.txt", "r") as f:
x = f.read()
print(x)
return x
# Autofix cases
def foo():
a = 1
b=a
return b # RET504
def foo():
a = 1
b =a
return b # RET504
def foo():
a = 1
b= a
return b # RET504
def foo():
a = 1 # Comment
return a

View File

@@ -53,6 +53,9 @@ class Foo(metaclass=BazMeta):
def __really_private_func(self, arg):
super().__really_private_func(arg)
def __eq__(self, other):
return self._private_thing == other._private_thing
foo = Foo()

View File

@@ -171,3 +171,17 @@ def f():
if x.isdigit():
return True
return False
async def f():
# OK
for x in iterable:
if await check(x):
return True
return False
async def f():
# SIM110
for x in iterable:
if check(x):
return True
return False

View File

@@ -33,17 +33,17 @@ with A() as a:
print("hello")
a()
# OK
# OK, can't merge async with and with.
async with A() as a:
with B() as b:
print("hello")
# OK
# OK, can't merge async with and with.
with A() as a:
async with B() as b:
print("hello")
# OK
# SIM117
async with A() as a:
async with B() as b:
print("hello")
@@ -99,4 +99,25 @@ with A("01ß9💣28901ß9💣28901ß9💣289") as a:
# SIM117 (not auto-fixable too long)
with A("01ß9💣28901ß9💣28901ß9💣2890") as a:
with B("01ß9💣28901ß9💣28901ß9💣289") as b:
print("hello")
print("hello")
# From issue #3025.
async def main():
async with A() as a: # SIM117.
async with B() as b:
print("async-inside!")
return 0
# OK. Can't merge across different kinds of with statements.
with a as a2:
async with b as b2:
with c as c2:
async with d as d2:
f(a2, b2, c2, d2)
# OK. Can't merge across different kinds of with statements.
async with b as b2:
with c as c2:
async with d as d2:
f(b2, c2, d2)

View File

@@ -0,0 +1,6 @@
class Bad(str): # SLOT000
pass
class Good(str): # Ok
__slots__ = ["foo"]

View File

@@ -0,0 +1,21 @@
class Bad(tuple): # SLOT001
pass
class Good(tuple): # Ok
__slots__ = ("foo",)
from typing import Tuple
class Bad(Tuple): # SLOT001
pass
class Bad(Tuple[str, int, float]): # SLOT001
pass
class Good(Tuple[str, int, float]): # OK
__slots__ = ("foo",)

View File

@@ -0,0 +1,14 @@
from collections import namedtuple
from typing import NamedTuple
class Bad(namedtuple("foo", ["str", "int"])): # SLOT002
pass
class Good(namedtuple("foo", ["str", "int"])): # OK
__slots__ = ("foo",)
class Good(NamedTuple): # Ok
pass

View File

@@ -1,6 +1,6 @@
# TDO003 - accepted
# TODO: this comment has a link
# https://github.com/charliermarsh/ruff/issues/3870
# https://github.com/astral-sh/ruff/issues/3870
# TODO: this comment has an issue
# TDO-3870

View File

@@ -164,3 +164,11 @@ def f():
)
x: DataFrame = 2
def f():
global Member
from module import Member
x: Member = 1

View File

@@ -0,0 +1,37 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"import math\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

@@ -0,0 +1,38 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"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

@@ -0,0 +1,8 @@
{
"execution_count": null,
"cell_type": "code",
"id": "1",
"metadata": {},
"outputs": [],
"source": ["def foo():\n", " pass\n", "\n", "%timeit foo()"]
}

View File

@@ -0,0 +1,6 @@
{
"cell_type": "markdown",
"id": "1",
"metadata": {},
"source": ["This is a markdown cell\n", "Some more content"]
}

View File

@@ -0,0 +1,8 @@
{
"execution_count": null,
"cell_type": "code",
"id": "1",
"metadata": {},
"outputs": [],
"source": ["def foo():\n", " pass"]
}

View File

@@ -0,0 +1,8 @@
{
"execution_count": null,
"cell_type": "code",
"id": "1",
"metadata": {},
"outputs": [],
"source": "%timeit print('hello world')"
}

View File

@@ -0,0 +1,51 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "0c7535f6-43cb-423f-bfe1-d263b8f55da0",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import random\n",
"import math"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "c066fa1a-5682-47af-8c17-5afec3cf4ad0",
"metadata": {},
"outputs": [],
"source": [
"from typing import Any\n",
"import collections\n",
"# Newline should be added here\n",
"def foo():\n",
" pass"
]
}
],
"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

@@ -0,0 +1,53 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "663ba955-baca-4f34-9ebb-840d2573ae3f",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import random\n",
"from pathlib import Path"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d0adfe23-8aea-47e9-bf67-d856cfcb96ea",
"metadata": {},
"outputs": [],
"source": [
"import collections\n",
"from typing import Any\n",
"\n",
"\n",
"# Newline should be added here\n",
"def foo():\n",
" pass"
]
}
],
"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

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

@@ -3,6 +3,16 @@
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"ExecuteTime": {
"end_time": "2023-03-08T23:01:09.782916Z",
"start_time": "2023-03-08T23:01:09.705831Z"
},
"collapsed": false,
"jupyter": {
"outputs_hidden": false
}
},
"outputs": [
{
"name": "stdout",
@@ -19,32 +29,26 @@
" print(f\"cell one: {y}\")\n",
"\n",
"unused_variable()"
],
"metadata": {
"collapsed": false,
"ExecuteTime": {
"start_time": "2023-03-08T23:01:09.705831Z",
"end_time": "2023-03-08T23:01:09.782916Z"
}
}
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's do another mistake"
],
"metadata": {
"collapsed": false
}
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": true,
"ExecuteTime": {
"start_time": "2023-03-08T23:01:09.733809Z",
"end_time": "2023-03-08T23:01:09.915760Z"
"end_time": "2023-03-08T23:01:09.915760Z",
"start_time": "2023-03-08T23:01:09.733809Z"
},
"collapsed": true,
"jupyter": {
"outputs_hidden": true
}
},
"outputs": [
@@ -62,27 +66,66 @@
"\n",
"mutable_argument()\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's create an empty cell"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Multi-line empty cell!"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"print(\"after empty cells\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"display_name": "Python (ruff)",
"language": "python",
"name": "python3"
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 2
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython2",
"version": "2.7.6"
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 0
"nbformat_minor": 4
}

View File

@@ -0,0 +1,11 @@
class badAllowed:
pass
class stillBad:
pass
class BAD_ALLOWED:
pass
class STILL_BAD:
pass

View File

@@ -0,0 +1,14 @@
import unittest
def badAllowed():
pass
def stillBad():
pass
class Test(unittest.TestCase):
def badAllowed(self):
return super().tearDown()
def stillBad(self):
return super().tearDown()

View File

@@ -0,0 +1,12 @@
def func(_, a, badAllowed):
return _, a, badAllowed
def func(_, a, stillBad):
return _, a, stillBad
class Class:
def method(self, _, a, badAllowed):
return _, a, badAllowed
def method(self, _, a, stillBad):
return _, a, stillBad

View File

@@ -0,0 +1,22 @@
from abc import ABCMeta
class Class:
def __init_subclass__(self, default_name, **kwargs):
...
@classmethod
def badAllowed(self, x, /, other):
...
@classmethod
def stillBad(self, x, /, other):
...
class MetaClass(ABCMeta):
def badAllowed(self):
pass
def stillBad(self):
pass

View File

@@ -0,0 +1,42 @@
from abc import ABCMeta
import pydantic
class Class:
def badAllowed(this):
pass
def stillBad(this):
pass
if False:
def badAllowed(this):
pass
def stillBad(this):
pass
@pydantic.validator
def badAllowed(cls, my_field: str) -> str:
pass
@pydantic.validator
def stillBad(cls, my_field: str) -> str:
pass
@pydantic.validator("my_field")
def badAllowed(cls, my_field: str) -> str:
pass
@pydantic.validator("my_field")
def stillBad(cls, my_field: str) -> str:
pass
class PosOnlyClass:
def badAllowed(this, blah, /, self, something: str):
pass
def stillBad(this, blah, /, self, something: str):
pass

View File

@@ -0,0 +1,6 @@
def assign():
badAllowed = 0
stillBad = 0
BAD_ALLOWED = 0
STILL_BAD = 0

View File

@@ -0,0 +1,13 @@
def __badAllowed__():
pass
def __stillBad__():
pass
def nested():
def __badAllowed__():
pass
def __stillBad__():
pass

View File

@@ -0,0 +1,5 @@
import mod.BAD_ALLOWED as badAllowed
import mod.STILL_BAD as stillBad
from mod import BAD_ALLOWED as badAllowed
from mod import STILL_BAD as stillBad

View File

@@ -0,0 +1,5 @@
import mod.badallowed as badAllowed
import mod.stillbad as stillBad
from mod import badallowed as BadAllowed
from mod import stillbad as StillBad

View File

@@ -0,0 +1,8 @@
import mod.BadAllowed as badallowed
import mod.stillBad as stillbad
from mod import BadAllowed as badallowed
from mod import StillBad as stillbad
from mod import BadAllowed as bad_allowed
from mod import StillBad as still_bad

View File

@@ -0,0 +1,8 @@
import mod.BadAllowed as BADALLOWED
import mod.StillBad as STILLBAD
from mod import BadAllowed as BADALLOWED
from mod import StillBad as STILLBAD
from mod import BadAllowed as BAD_ALLOWED
from mod import StillBad as STILL_BAD

View File

@@ -0,0 +1,19 @@
class C:
badAllowed = 0
stillBad = 0
_badAllowed = 0
_stillBad = 0
bad_Allowed = 0
still_Bad = 0
class D(TypedDict):
badAllowed: bool
stillBad: bool
_badAllowed: list
_stillBad: list
bad_Allowed: set
still_Bad: set

View File

@@ -0,0 +1,8 @@
badAllowed = 0
stillBad = 0
_badAllowed = 0
_stillBad = 0
bad_Allowed = 0
still_Bad = 0

View File

@@ -0,0 +1,5 @@
import mod.BadAllowed as BA
import mod.StillBad as SB
from mod import BadAllowed as BA
from mod import StillBad as SB

View File

@@ -0,0 +1,11 @@
class BadAllowed(Exception):
pass
class StillBad(Exception):
pass
class BadAllowed(AnotherError):
pass
class StillBad(AnotherError):
pass

View File

@@ -0,0 +1,71 @@
some_dict = {"a": 12, "b": 32, "c": 44}
for _, value in some_dict.items(): # PERF102
print(value)
for key, _ in some_dict.items(): # PERF102
print(key)
for weird_arg_name, _ in some_dict.items(): # PERF102
print(weird_arg_name)
for name, (_, _) in some_dict.items(): # PERF102
pass
for name, (value1, _) in some_dict.items(): # OK
pass
for (key1, _), (_, _) in some_dict.items(): # PERF102
pass
for (_, (_, _)), (value, _) in some_dict.items(): # PERF102
pass
for (_, key2), (value1, _) in some_dict.items(): # OK
pass
for ((_, key2), (value1, _)) in some_dict.items(): # OK
pass
for ((_, key2), (_, _)) in some_dict.items(): # PERF102
pass
for (_, _, _, variants), (r_language, _, _, _) in some_dict.items(): # OK
pass
for (_, _, (_, variants)), (_, (_, (r_language, _))) in some_dict.items(): # OK
pass
for key, value in some_dict.items(): # OK
print(key, value)
for _, value in some_dict.items(12): # OK
print(value)
for key in some_dict.keys(): # OK
print(key)
for value in some_dict.values(): # OK
print(value)
for name, (_, _) in (some_function()).items(): # PERF102
pass
for name, (_, _) in (some_function().some_attribute).items(): # PERF102
pass

View File

@@ -60,3 +60,9 @@ match *0, 1, *2:
#:
class Foo:
match: Optional[Match] = None
#: E702:2:4
while 1:
1;...
#: E703:2:1
0\
;

View File

@@ -0,0 +1,11 @@
"""Test that straight `__future__` imports are considered unused."""
def f():
import __future__
def f():
import __future__
print(__future__.absolute_import)

View File

@@ -7,3 +7,5 @@ hidden = {"a": "!"}
"%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
"%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)
'' % {'a''b' : ''} # F504 ("ab" not used)

View File

@@ -19,3 +19,9 @@ if ("123" != x) is 3:
if "123" != (x is 3):
pass
{2 is
not ''}
{2 is
not ''}

View File

@@ -1,13 +1,13 @@
from __future__ import annotations
# Test case for https://github.com/charliermarsh/ruff/issues/1552
# Test case for https://github.com/astral-sh/ruff/issues/1552
def f():
x = 0
list()[x:]
# Test case for https://github.com/charliermarsh/ruff/issues/2603
# Test case for https://github.com/astral-sh/ruff/issues/2603
def f():
KeyTupleT = tuple[str, ...]

View File

@@ -108,3 +108,42 @@ def f():
def f():
toplevel = tt = 1
def f(provided: int) -> int:
match provided:
case [_, *x]:
pass
def f(provided: int) -> int:
match provided:
case x:
pass
def f(provided: int) -> int:
match provided:
case Foo(bar) as x:
pass
def f(provided: int) -> int:
match provided:
case {"foo": 0, **x}:
pass
def f(provided: int) -> int:
match provided:
case {**x}:
pass
global CONSTANT
def f() -> None:
global CONSTANT
CONSTANT = 1
CONSTANT = 2

View File

@@ -1,4 +1,3 @@
if True:
import foo1; x = 1
import foo2; x = 1
@@ -11,7 +10,6 @@ if True:
import foo4 \
; x = 1
if True:
x = 1; import foo5
@@ -20,12 +18,10 @@ if True:
x = 1; \
import foo6
if True:
x = 1 \
; import foo7
if True:
x = 1; import foo8; x = 1
x = 1; import foo9; x = 1
@@ -40,12 +36,27 @@ if True:
;import foo11 \
;x = 1
if True:
x = 1; \
\
import foo12
if True:
x = 1; \
\
import foo13
if True:
x = 1; \
# \
import foo14
# Continuation, but not as the last content in the file.
x = 1; \
import foo12
import foo15
# Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax
# error.)
x = 1; \
import foo13
import foo16

View File

@@ -0,0 +1,45 @@
# Errors.
foo == foo
foo != foo
foo > foo
foo >= foo
foo < foo
foo <= foo
foo is foo
foo is not foo
foo in foo
foo not in foo
# Non-errors.
"foo" == "foo" # This is flagged by `comparison-of-constant` instead.
foo == "foo"
foo == bar
foo != bar
foo > bar
foo >= bar
foo < bar
foo <= bar
foo is bar
foo is not bar
foo in bar
foo not in bar

View File

@@ -73,3 +73,10 @@ def override_class():
pass
CLASS()
def multiple_assignment():
"""Should warn on every assignment."""
global CONSTANT # [global-statement]
CONSTANT = 1
CONSTANT = 2

View File

@@ -14,6 +14,12 @@ __all__ = (x for x in ["Hello", "world"]) # [invalid-all-format]
__all__ = {x for x in ["Hello", "world"]} # [invalid-all-format]
__all__ = foo # [invalid-all-format]
__all__ = foo.bar # [invalid-all-format]
__all__ = foo["bar"] # [invalid-all-format]
__all__ = ["Hello"]
__all__ = ("Hello",)
@@ -29,3 +35,8 @@ __all__ = list({"Hello", "world"})
__all__ = list(["Hello"]) + list(["world"])
__all__ = tuple(["Hello"]) + ("world",)
__all__ = __all__ + ["Hello"]
__all__ = __all__ + multiprocessing.__all__

View File

@@ -31,6 +31,21 @@ with None as i:
with None as j: # ok
pass
# Async with -> with, variable reused
async with None as i:
with None as i: # error
pass
# Async with -> with, different variable
async with None as i:
with None as j: # ok
pass
# Async for -> for, variable reused
async for i in []:
for i in []: # error
pass
# For -> for -> for, doubly nested variable reuse
for i in []:
for j in []:

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

@@ -134,6 +134,19 @@ class A(
...
class A(object, object):
...
@decorator()
class A(object):
...
@decorator() # class A(object):
class A(object):
...
object = A

View File

@@ -21,10 +21,6 @@ x = "foo {0}" \
"\N{snowman} {0}".format(1)
'{' '0}'.format(1)
# These will not change because we are waiting for libcst to fix this issue:
# https://github.com/Instagram/LibCST/issues/846
print(
'foo{0}'
'bar{1}'.format(1, 2)
@@ -34,3 +30,5 @@ print(
'foo{0}' # ohai\n"
'bar{1}'.format(1, 2)
)
'{' '0}'.format(1)

View File

@@ -13,3 +13,5 @@ f"{0}".format(a)
f"{0}".format(1)
print(f"{0}".format(1))
''.format(1)

View File

@@ -0,0 +1,43 @@
# Errors
class A():
pass
class A() \
:
pass
class A \
():
pass
@decorator()
class A():
pass
@decorator
class A():
pass
# OK
class A:
pass
class A(A):
pass
class A(metaclass=type):
pass
@decorator()
class A:
pass
@decorator
class A:
pass

View File

@@ -5,12 +5,11 @@ from typing import ClassVar, Sequence
KNOWINGLY_MUTABLE_DEFAULT = []
@dataclass()
@dataclass
class A:
mutable_default: list[int] = []
immutable_annotation: typing.Sequence[int] = []
without_annotation = []
ignored_via_comment: list[int] = [] # noqa: RUF008
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: typing.ClassVar[list[int]] = []
@@ -21,7 +20,6 @@ class B:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
ignored_via_comment: list[int] = [] # noqa: RUF008
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []

View File

@@ -0,0 +1,13 @@
data = ["some", "Data"]
# Ok
{value: value.upper() for value in data}
{value.lower(): value.upper() for value in data}
{v: v*v for v in range(10)}
{(0, "a", v): v*v for v in range(10)} # Tuple with variable
# Errors
{"key": value.upper() for value in data}
{True: value.upper() for value in data}
{0: value.upper() for value in data}
{(1, "a"): value.upper() for value in data} # constant tuple

View File

@@ -0,0 +1,49 @@
import typing
from typing import ClassVar, Sequence, Final
KNOWINGLY_MUTABLE_DEFAULT = []
class A:
mutable_default: list[int] = []
immutable_annotation: typing.Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: typing.ClassVar[list[int]] = []
final_variable: typing.Final[list[int]] = []
class B:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
from dataclasses import dataclass, field
@dataclass
class C:
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]] = []
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

@@ -0,0 +1,223 @@
import typing
from typing import Annotated, Any, Literal, Optional, Tuple, Union
def f(arg: int):
pass
def f(arg=None):
pass
def f(arg: Any = None):
pass
def f(arg: object = None):
pass
def f(arg: int = None): # RUF013
pass
def f(arg: str = None): # RUF013
pass
def f(arg: typing.List[str] = None): # RUF013
pass
def f(arg: Tuple[str] = None): # RUF013
pass
# Optional
def f(arg: Optional[int] = None):
pass
def f(arg: typing.Optional[int] = None):
pass
# Union
def f(arg: Union[None, int] = None):
pass
def f(arg: Union[str, None] = None):
pass
def f(arg: typing.Union[int, str, None] = None):
pass
def f(arg: Union[int, str, Any] = None):
pass
def f(arg: Union = None): # RUF013
pass
def f(arg: Union[int, str] = None): # RUF013
pass
def f(arg: typing.Union[int, str] = None): # RUF013
pass
# PEP 604 Union
def f(arg: None | int = None):
pass
def f(arg: int | None = None):
pass
def f(arg: int | float | str | None = None):
pass
def f(arg: int | float = None): # RUF013
pass
def f(arg: int | float | str | bytes = None): # RUF013
pass
# Literal
def f(arg: None = None):
pass
def f(arg: Literal[1, 2, None, 3] = None):
pass
def f(arg: Literal[1, "foo"] = None): # RUF013
pass
def f(arg: typing.Literal[1, "foo", True] = None): # RUF013
pass
# Annotated
def f(arg: Annotated[Optional[int], ...] = None):
pass
def f(arg: Annotated[Union[int, None], ...] = None):
pass
def f(arg: Annotated[Any, ...] = None):
pass
def f(arg: Annotated[int, ...] = None): # RUF013
pass
def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013
pass
# Multiple arguments
def f(
arg1: Optional[int] = None,
arg2: Union[int, None] = None,
arg3: Literal[1, 2, None, 3] = None,
):
pass
def f(
arg1: int = None, # RUF013
arg2: Union[int, float] = None, # RUF013
arg3: Literal[1, 2, 3] = None, # RUF013
):
pass
# Nested
def f(arg: Literal[1, "foo", Literal[True, None]] = None):
pass
def f(arg: Union[int, Union[float, Union[str, None]]] = None):
pass
def f(arg: Union[int, Union[float, Optional[str]]] = None):
pass
def f(arg: Union[int, Literal[True, None]] = None):
pass
def f(arg: Union[Annotated[int, ...], Annotated[Optional[float], ...]] = None):
pass
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

@@ -0,0 +1,5 @@
# No `typing.Optional` import
def f(arg: int = None): # RUF011
pass

View File

@@ -1,13 +1,13 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{bail, Result};
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustpython_parser::ast::{self, Excepthandler, Expr, Keyword, Ranged, Stmt};
use rustpython_parser::ast::{self, ExceptHandler, Expr, Keyword, Ranged, Stmt};
use rustpython_parser::{lexer, Mode, Tok};
use ruff_diagnostics::Edit;
use ruff_newlines::NewlineWithTrailingNewline;
use ruff_python_ast::helpers;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_whitespace::{is_python_whitespace, NewlineWithTrailingNewline, PythonWhitespace};
use crate::autofix::codemods;
@@ -29,7 +29,6 @@ pub(crate) fn delete_stmt(
parent: Option<&Stmt>,
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Edit {
if parent
.map(|parent| is_lone_child(stmt, parent))
@@ -39,18 +38,15 @@ pub(crate) fn delete_stmt(
// it with a `pass`.
Edit::range_replacement("pass".to_string(), stmt.range())
} else {
if let Some(semicolon) = trailing_semicolon(stmt, locator) {
if let Some(semicolon) = trailing_semicolon(stmt.end(), locator) {
let next = next_stmt_break(semicolon, locator);
Edit::deletion(stmt.start(), next)
} else if helpers::has_leading_content(stmt, locator) {
} else if helpers::has_leading_content(stmt.start(), locator) {
Edit::range_deletion(stmt.range())
} else if helpers::preceded_by_continuation(stmt, indexer, locator) {
if is_end_of_file(stmt, locator) && locator.is_at_start_of_line(stmt.start()) {
// Special-case: a file can't end in a continuation.
Edit::range_replacement(stylist.line_ending().to_string(), stmt.range())
} else {
Edit::range_deletion(stmt.range())
}
} else if let Some(start) =
helpers::preceded_by_continuations(stmt.start(), locator, indexer)
{
Edit::range_deletion(TextRange::new(start, stmt.end()))
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
@@ -64,11 +60,11 @@ 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, stylist)),
None => Ok(delete_stmt(stmt, parent, locator, indexer)),
Some(content) => Ok(Edit::range_replacement(content, stmt.range())),
}
}
@@ -218,7 +214,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
|| is_only(orelse, child)
|| is_only(finalbody, child)
|| handlers.iter().any(|handler| match handler {
Excepthandler::ExceptHandler(ast::ExcepthandlerExceptHandler {
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
body, ..
}) => is_only(body, child),
})
@@ -238,15 +234,15 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<TextSize> {
let contents = locator.after(stmt.end());
fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option<TextSize> {
let contents = locator.after(offset);
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim_start();
let trimmed = line.trim_whitespace_start();
if trimmed.starts_with(';') {
let colon_offset = line.text_len() - trimmed.text_len();
return Some(stmt.end() + line.start() + colon_offset);
return Some(offset + line.start() + colon_offset);
}
if !trimmed.starts_with('\\') {
@@ -262,7 +258,7 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
let trimmed = line.trim();
let trimmed = line.trim_whitespace();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
@@ -276,7 +272,7 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !c.is_whitespace()).unwrap();
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
@@ -284,16 +280,11 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
locator.line_end(start_location)
}
/// Return `true` if a `Stmt` occurs at the end of a file.
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
stmt.end() == locator.contents().text_len()
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_text_size::TextSize;
use rustpython_parser::ast::Suite;
use rustpython_parser::ast::{Ranged, Suite};
use rustpython_parser::Parse;
use ruff_python_ast::source_code::Locator;
@@ -306,19 +297,25 @@ mod tests {
let program = Suite::parse(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), None);
assert_eq!(trailing_semicolon(stmt.end(), &locator), None);
let contents = "x = 1; y = 1";
let program = Suite::parse(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(5)));
assert_eq!(
trailing_semicolon(stmt.end(), &locator),
Some(TextSize::from(5))
);
let contents = "x = 1 ; y = 1";
let program = Suite::parse(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(6)));
assert_eq!(
trailing_semicolon(stmt.end(), &locator),
Some(TextSize::from(6))
);
let contents = r#"
x = 1 \
@@ -328,7 +325,10 @@ x = 1 \
let program = Suite::parse(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(10)));
assert_eq!(
trailing_semicolon(stmt.end(), &locator),
Some(TextSize::from(10))
);
Ok(())
}

View File

@@ -2,23 +2,31 @@ use std::collections::BTreeSet;
use itertools::Itertools;
use nohash_hasher::IntSet;
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{TextLen, TextRange, TextSize};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, Edit, Fix, IsolationLevel};
use ruff_python_ast::source_code::Locator;
use crate::autofix::source_map::SourceMap;
use crate::linter::FixTable;
use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod source_map;
pub(crate) struct FixResult {
/// The resulting source code, after applying all fixes.
pub(crate) code: String,
/// The number of fixes applied for each [`Rule`].
pub(crate) fixes: FixTable,
/// Source map for the fixed source code.
pub(crate) source_map: SourceMap,
}
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub(crate) fn fix_file(
diagnostics: &[Diagnostic],
locator: &Locator,
) -> Option<(String, FixTable)> {
pub(crate) fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<FixResult> {
let mut with_fixes = diagnostics
.iter()
.filter(|diag| diag.fix.is_some())
@@ -35,12 +43,13 @@ pub(crate) fn fix_file(
fn apply_fixes<'a>(
diagnostics: impl Iterator<Item = &'a Diagnostic>,
locator: &'a Locator<'a>,
) -> (String, FixTable) {
) -> FixResult {
let mut output = String::with_capacity(locator.len());
let mut last_pos: Option<TextSize> = None;
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
let mut isolated: IntSet<u32> = IntSet::default();
let mut fixed = FxHashMap::default();
let mut source_map = SourceMap::default();
for (rule, fix) in diagnostics
.filter_map(|diagnostic| {
@@ -84,9 +93,15 @@ fn apply_fixes<'a>(
let slice = locator.slice(TextRange::new(last_pos.unwrap_or_default(), edit.start()));
output.push_str(slice);
// Add the start source marker for the patch.
source_map.push_start_marker(edit, output.text_len());
// Add the patch itself.
output.push_str(edit.content().unwrap_or_default());
// Add the end source marker for the added patch.
source_map.push_end_marker(edit, output.text_len());
// Track that the edit was applied.
last_pos = Some(edit.end());
applied.insert(edit);
@@ -99,7 +114,11 @@ fn apply_fixes<'a>(
let slice = locator.after(last_pos.unwrap_or_default());
output.push_str(slice);
(output, fixed)
FixResult {
code: output,
fixes: fixed,
source_map,
}
}
/// Compare two fixes.
@@ -130,7 +149,8 @@ mod tests {
use ruff_diagnostics::Fix;
use ruff_python_ast::source_code::Locator;
use crate::autofix::apply_fixes;
use crate::autofix::source_map::SourceMarker;
use crate::autofix::{apply_fixes, FixResult};
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
#[allow(deprecated)]
@@ -150,9 +170,59 @@ mod tests {
fn empty_file() {
let locator = Locator::new(r#""#);
let diagnostics = create_diagnostics([]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(contents, "");
assert_eq!(fixed.values().sum::<usize>(), 0);
let FixResult {
code,
fixes,
source_map,
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(code, "");
assert_eq!(fixes.values().sum::<usize>(), 0);
assert!(source_map.markers().is_empty());
}
#[test]
fn apply_one_insertion() {
let locator = Locator::new(
r#"
import os
print("hello world")
"#
.trim(),
);
let diagnostics = create_diagnostics([Edit::insertion(
"import sys\n".to_string(),
TextSize::new(10),
)]);
let FixResult {
code,
fixes,
source_map,
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
code,
r#"
import os
import sys
print("hello world")
"#
.trim()
);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 10.into(),
dest: 10.into(),
},
SourceMarker {
source: 10.into(),
dest: 21.into(),
},
]
);
}
#[test]
@@ -169,16 +239,33 @@ class A(object):
TextSize::new(8),
TextSize::new(14),
)]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
let FixResult {
code,
fixes,
source_map,
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
contents,
code,
r#"
class A(Bar):
...
"#
.trim(),
);
assert_eq!(fixed.values().sum::<usize>(), 1);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 8.into(),
dest: 8.into(),
},
SourceMarker {
source: 14.into(),
dest: 11.into(),
},
]
);
}
#[test]
@@ -191,16 +278,33 @@ class A(object):
.trim(),
);
let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
let FixResult {
code,
fixes,
source_map,
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
contents,
code,
r#"
class A:
...
"#
.trim()
);
assert_eq!(fixed.values().sum::<usize>(), 1);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 7.into(),
dest: 7.into()
},
SourceMarker {
source: 15.into(),
dest: 7.into()
}
]
);
}
#[test]
@@ -216,17 +320,42 @@ class A(object, object, object):
Edit::deletion(TextSize::from(8), TextSize::from(16)),
Edit::deletion(TextSize::from(22), TextSize::from(30)),
]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
let FixResult {
code,
fixes,
source_map,
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
contents,
code,
r#"
class A(object):
...
"#
.trim()
);
assert_eq!(fixed.values().sum::<usize>(), 2);
assert_eq!(fixes.values().sum::<usize>(), 2);
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 8.into(),
dest: 8.into()
},
SourceMarker {
source: 16.into(),
dest: 8.into()
},
SourceMarker {
source: 22.into(),
dest: 14.into(),
},
SourceMarker {
source: 30.into(),
dest: 14.into(),
}
]
);
}
#[test]
@@ -242,15 +371,32 @@ class A(object):
Edit::deletion(TextSize::from(7), TextSize::from(15)),
Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)),
]);
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
let FixResult {
code,
fixes,
source_map,
} = apply_fixes(diagnostics.iter(), &locator);
assert_eq!(
contents,
code,
r#"
class A:
...
"#
.trim(),
);
assert_eq!(fixed.values().sum::<usize>(), 1);
assert_eq!(fixes.values().sum::<usize>(), 1);
assert_eq!(
source_map.markers(),
&[
SourceMarker {
source: 7.into(),
dest: 7.into(),
},
SourceMarker {
source: 15.into(),
dest: 7.into(),
}
]
);
}
}

View File

@@ -0,0 +1,59 @@
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;
/// Lightweight sourcemap marker representing the source and destination
/// position for an [`Edit`].
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct SourceMarker {
/// Position of the marker in the original source.
pub(crate) source: TextSize,
/// Position of the marker in the transformed code.
pub(crate) dest: TextSize,
}
/// A collection of [`SourceMarker`].
///
/// Sourcemaps are used to map positions in the original source to positions in
/// the transformed code. Here, only the boundaries of edits are tracked instead
/// of every single character.
#[derive(Default, PartialEq, Eq)]
pub(crate) struct SourceMap(Vec<SourceMarker>);
impl SourceMap {
/// Returns a slice of all the markers in the sourcemap in the order they
/// were added.
pub(crate) fn markers(&self) -> &[SourceMarker] {
&self.0
}
/// Push the start marker for an [`Edit`].
///
/// The `output_length` is the length of the transformed string before the
/// edit is applied.
pub(crate) fn push_start_marker(&mut self, edit: &Edit, output_length: TextSize) {
self.0.push(SourceMarker {
source: edit.start(),
dest: output_length,
});
}
/// Push the end marker for an [`Edit`].
///
/// The `output_length` is the length of the transformed string after the
/// edit has been applied.
pub(crate) fn push_end_marker(&mut self, edit: &Edit, output_length: TextSize) {
if edit.is_insertion() {
self.0.push(SourceMarker {
source: edit.start(),
dest: output_length,
});
} else {
// Deletion or replacement
self.0.push(SourceMarker {
source: edit.end(),
dest: output_length,
});
}
}
}

View File

@@ -1,13 +1,14 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::Expr;
use ruff_python_semantic::model::Snapshot;
use ruff_python_semantic::{ScopeId, Snapshot};
/// A collection of AST nodes that are deferred for later analysis.
/// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all
/// module-level definitions have been analyzed.
#[derive(Debug, Default)]
pub(crate) struct Deferred<'a> {
pub(crate) scopes: Vec<ScopeId>,
pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>,
pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>,
pub(crate) functions: Vec<Snapshot>,

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,9 @@ pub(crate) fn check_file_path(
// pep8-naming
if settings.rules.enabled(Rule::InvalidModuleName) {
if let Some(diagnostic) = invalid_module_name(path, package) {
if let Some(diagnostic) =
invalid_module_name(path, package, &settings.pep8_naming.ignore_names)
{
diagnostics.push(diagnostic);
}
}

View File

@@ -16,6 +16,7 @@ use crate::registry::Rule;
use crate::rules::isort;
use crate::rules::isort::block::{Block, BlockBuilder};
use crate::settings::Settings;
use crate::source_kind::SourceKind;
fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option<ImportMap> {
let Some(package) = package else {
@@ -83,12 +84,13 @@ pub(crate) fn check_imports(
stylist: &Stylist,
path: &Path,
package: Option<&Path>,
source_kind: Option<&SourceKind>,
) -> (Vec<Diagnostic>, Option<ImportMap>) {
let is_stub = is_python_stub_file(path);
// Extract all import blocks from the AST.
let tracker = {
let mut tracker = BlockBuilder::new(locator, directives, is_stub);
let mut tracker = BlockBuilder::new(locator, directives, is_stub, source_kind);
tracker.visit_body(python_ast);
tracker
};

View File

@@ -1,13 +1,14 @@
//! Lint rules based on checking physical lines.
use ruff_text_size::TextSize;
use std::path::Path;
use ruff_text_size::TextSize;
use ruff_diagnostics::Diagnostic;
use ruff_newlines::StrExt;
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_whitespace::UniversalNewlines;
use crate::registry::Rule;
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,
@@ -49,6 +50,7 @@ pub(crate) fn check_physical_lines(
let enforce_blank_line_contains_whitespace =
settings.rules.enabled(Rule::BlankLineWithWhitespace);
let enforce_tab_indentation = settings.rules.enabled(Rule::TabIndentation);
let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice);
let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration);
let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace);
@@ -144,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);
}
}
@@ -172,14 +174,21 @@ pub(crate) fn check_physical_lines(
}
}
if enforce_copyright_notice {
if let Some(diagnostic) = missing_copyright_notice(locator, settings) {
diagnostics.push(diagnostic);
}
}
diagnostics
}
#[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};

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