Compare commits

...

250 Commits

Author SHA1 Message Date
Zanie
d2b0b1ca26 Simplify comment 2023-08-22 10:23:31 -05:00
Zanie
6d88ac4ca2 Restore unrelated snapshot 2023-08-22 10:22:34 -05:00
Zanie
268be0cf8e Add more test cases; check format types that occur after placeholders 2023-08-22 10:08:08 -05:00
Zanie
5df9ba716f Consume all remaining placeholders at end of format spec parsing 2023-08-22 09:57:13 -05:00
Zanie
0e3284244b Add failing test case for bad-string-format-character 2023-08-22 09:37:26 -05:00
Zanie
d272874dfd Clean up implementation 2023-08-22 09:36:48 -05:00
Micha Reiser
ccac9681e1 Preserve yield parentheses (#6766) 2023-08-22 10:27:20 +00:00
Micha Reiser
b52cc84df6 Omit tuple parentheses in for statements except when absolutely necessary (#6765) 2023-08-22 12:18:59 +02:00
Micha Reiser
fec6fc2fab Preserve empty lines between try clause headers (#6759) 2023-08-22 11:50:28 +02:00
konsti
ba4c27598a Document IO Error (#6712)
`IOError` is special, it is not actually a lint but an error before
linting. I'm not entirely sure how to document it since it does not
match the general lint rule pattern (`Checks that the file can be read
in its entirety.` is imho worse).

I added the in my experience two most common reasons for io errors on
unix systems and linked two tutorials on how to fix them.

See https://github.com/astral-sh/ruff/issues/2646

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-08-22 11:46:18 +02:00
Victor Hugo Gomes
0f9ccfcad9 Format PatternMatchSingleton (#6741) 2023-08-22 08:23:47 +02:00
Charlie Marsh
fa32cd9b6f Truncate some messages in diagnostics (#6748)
## Summary

I noticed this in the ecosystem CI check from
https://github.com/astral-sh/ruff/pull/6742. If we include source code
directly in a diagnostic, we need to be careful to avoid rendering
multi-line diagnostics or even excessively long diagnostics.

## Test Plan

`cargo test`
2023-08-21 23:46:24 -04:00
Victor Hugo Gomes
0aad0c41f6 [pylint] Implement no-self-use (R6301) (#6574) 2023-08-22 03:44:38 +00:00
Charlie Marsh
424b8d4ad2 Use a single node hierarchy to track statements and expressions (#6709)
## Summary

This PR is a follow-up to the suggestion in
https://github.com/astral-sh/ruff/pull/6345#discussion_r1285470953 to
use a single stack to store all statements and expressions, rather than
using separate vectors for each, which gives us something closer to a
full-fidelity chain. (We can then generalize this concept to include all
other AST nodes too.)

This is in part made possible by the removal of the hash map from
`&Stmt` to `StatementId` (#6694), which makes it much cheaper to store
these using a single interface (since doing so no longer introduces the
requirement that we hash all expressions).

I'll follow-up with some profiling, but a few notes on how the data
requirements have changed:

- We now store a `BranchId` for every expression, not just every
statement, so that's an extra `u32`.
- We now store a single `NodeId` on every snapshot, rather than separate
`StatementId` and `ExpressionId` IDs, so that's one fewer `u32` for each
snapshot.
- We're probably doing a few more lookups in general, since any calls to
`current_statement()` etc. now have to iterate up the node hierarchy
until they identify the first statement.

## Test Plan

`cargo test`
2023-08-21 21:32:57 -04:00
Charlie Marsh
abc5065fc7 Avoid E231 if comma is at end-of-line (#6747)
## Summary

I don't know how this could come up in valid Python, but anyway...

Closes https://github.com/astral-sh/ruff/issues/6738.
2023-08-21 20:47:20 -04:00
Victor Hugo Gomes
37f4920e1e Don't trigger eq-without-hash when __hash__ is explicitly set to None (#6739) 2023-08-21 23:51:21 +00:00
Charlie Marsh
c0df99b965 Avoid attempting to fix unconventional submodule imports (#6745)
## Summary

Avoid attempting to rewrite `import matplotlib.pyplot` as `import
matplotlib.pyplot as plt`. We can't support these right now, since we
don't track references at the attribute level (like
`matplotlib.pyplot`).

Closes https://github.com/astral-sh/ruff/issues/6719.
2023-08-21 23:45:32 +00:00
Charlie Marsh
7650c6ee45 Support C419 autofixes for set comprehensions (#6744)
Closes https://github.com/astral-sh/ruff/issues/6713.
2023-08-21 23:41:13 +00:00
Charlie Marsh
7b14d17e39 Ignore star imports when importing symbols in fixes (#6743)
## Summary

Given:

```python
from sys import *

exit(0)
```

We can't add `exit` to `from sys import *`, so we should just ignore it.
Ideally, we'd just resolve `exit` in the first place (since it's
imported from `from sys import *`), but as long as we don't support
wildcard imports, this is more consistent.

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

## Test Plan

`cargo test`
2023-08-21 23:31:30 +00:00
Charlie Marsh
4678f7dafe Remove parenthesis lexing in RSE102 (#6732)
## Summary

Now that we have an `Arguments` node, we can just use the range of the
arguments directly to find the parentheses in `raise Error()`.
2023-08-21 20:59:06 +00:00
konsti
b182368008 Simplify suite formatting (#6722)
Avoid the nesting in a macro by using the new `WithNodeLevel` to
`PyFormatter` deref. No changes otherwise.

I wanted to follow this up with quickly fixing the typeshed empty line
rules but they turned out a lot more complex than i had anticipated.
2023-08-21 21:01:51 +02:00
Charlie Marsh
e032fbd2e7 Remove remove_super_arguments (#6735)
Now that we have an `Arguments` node, we can use it directly to get the
range.
2023-08-21 13:04:07 -04:00
dependabot[bot]
575b77aa52 ci(deps): bump cloudflare/wrangler-action from 3.0.2 to 3.1.0 (#6736)
Bumps
[cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action)
from 3.0.2 to 3.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/releases">cloudflare/wrangler-action's
releases</a>.</em></p>
<blockquote>
<h2>v3.1.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/154">#154</a>
<a
href="3f40637a1c"><code>3f40637</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- feat: Quiet mode
Some of the stderr, stdout, info &amp; groupings can be a little noisy
for some users and use cases.
This feature allows for a option to be passed 'quiet: true' this would
significantly reduce the noise.</p>
<p>There will still be output that lets the user know Wrangler Installed
and Wrangler Action completed successfully.
Any failure status will still be output to the user as well, to prevent
silent failures.</p>
<p>resolves <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/142">#142</a></p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md">cloudflare/wrangler-action's
changelog</a>.</em></p>
<blockquote>
<h2>3.1.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/154">#154</a>
<a
href="3f40637a1c"><code>3f40637</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- feat: Quiet mode
Some of the stderr, stdout, info &amp; groupings can be a little noisy
for some users and use cases.
This feature allows for a option to be passed 'quiet: true' this would
significantly reduce the noise.</p>
<p>There will still be output that lets the user know Wrangler Installed
and Wrangler Action completed successfully.
Any failure status will still be output to the user as well, to prevent
silent failures.</p>
<p>resolves <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/142">#142</a></p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c25aadc965"><code>c25aadc</code></a>
Automatic compilation</li>
<li><a
href="fcf648c789"><code>fcf648c</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/158">#158</a>
from cloudflare/changeset-release/main</li>
<li><a
href="fcbabec21e"><code>fcbabec</code></a>
Version Packages</li>
<li><a
href="0aa12f0c2b"><code>0aa12f0</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/154">#154</a>
from cloudflare/jacobmgevans/silence-mode</li>
<li><a
href="ad7441b6ad"><code>ad7441b</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/157">#157</a>
from EstebanBorai/main</li>
<li><a
href="3f40637a1c"><code>3f40637</code></a>
Quiet feature</li>
<li><a
href="4132892387"><code>4132892</code></a>
fix: use <code>wrangler@3.5.1</code> by default</li>
<li><a
href="62ce9d23a3"><code>62ce9d2</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/155">#155</a>
from ethanppl/fix-readme</li>
<li><a
href="f089b0a195"><code>f089b0a</code></a>
Update README.md</li>
<li><a
href="4318a2fb97"><code>4318a2f</code></a>
Fix examples in README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/cloudflare/wrangler-action/compare/v3.0.2...v3.1.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cloudflare/wrangler-action&package-manager=github_actions&previous-version=3.0.2&new-version=3.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 16:35:59 +00:00
Micha Reiser
17a26e6ff3 Fix fmt:skip for function with return type (#6733) 2023-08-21 17:45:23 +02:00
Charlie Marsh
d5a51b4e45 Allow ctypes.WinError() in flake8-raise (#6731)
Closes https://github.com/astral-sh/ruff/issues/6730.
2023-08-21 14:57:34 +00:00
Charlie Marsh
83f68891e0 Allow next in FBT exclusions (#6729)
Closes https://github.com/astral-sh/ruff/issues/6711.
2023-08-21 14:56:38 +00:00
konsti
aafde6db28 Remove some indexing (#6728)
**Summary** A common pattern in the code used to be
```rust
if statements.len() != 1 {
    return;
}
use_single_entry(statements[0])?;
```
which can be better expressed as
```rust
let [statement] = statements else {
    return;
};
use_single_entry(statements)?;
```

Direct indexing can cause panics if you don't manually take care of
checking the length, while matching (such as if-let or let-else) can
never panic.

This isn't a complete refactor, i've just removed some of the obvious
cases. I've specifically looked for `.len() != 1` and fixed those.

**Test Plan** No functional changes
2023-08-21 16:56:15 +02:00
Charlie Marsh
2405536d03 Remove unnecessary LibCST usage in key-in-dict (#6727)
## Summary

We're using LibCST to ensure that we return the full parenthesized range
of an expression, for display purposes. We can just use
`parenthesized_range` which is more efficient and removes one LibCST
dependency.

## Test Plan

`cargo test`
2023-08-21 10:32:09 -04:00
Micha Reiser
f017555d53 Parenthesize NamedExpr if target breaks (#6714) 2023-08-21 16:29:26 +02:00
Charlie Marsh
be96e0041a Accept empty inner calls in C414 (#6725)
Closes https://github.com/astral-sh/ruff/issues/6716.
2023-08-21 14:05:09 +00:00
Harutaka Kawamura
3c2dd5e42e Remove confusing comment on get_parametrize_name_range (#6724) 2023-08-21 08:52:48 -04:00
Micha Reiser
8b347cdaa9 Simplify IfRequired needs parentheses condition (#6678) 2023-08-21 07:11:31 +00:00
Tom Kuson
2a8d24dd4b Format function and class definitions into a single line if its body is an ellipsis (#6592) 2023-08-21 09:02:23 +02:00
Charlie Marsh
bb5fbb1b5c Use simple lexer for argument removal (#6710) 2023-08-21 04:16:29 +00:00
Harutaka Kawamura
086e11087f [flake8-pytest-style] Autofix PT014 (#6698) 2023-08-21 03:45:12 +00:00
Charlie Marsh
1b7e4a12a9 Refactor remove_unused_variable to take &Binding (#6707) 2023-08-20 15:50:57 +00:00
Charlie Marsh
da1697121e Add BranchId to the model snapshot (#6706)
This _probably_ never matters given the set of rules we support and in
fact I'm having trouble thinking of a test-case for it, but it's
definitely incorrect _not_ to pass on the `BranchId` here.
2023-08-20 15:35:49 +00:00
Harutaka Kawamura
419615f29b Add docs for E275, E231, E251, and E252 (#6700) 2023-08-20 14:51:50 +00:00
Charlie Marsh
a742a562fd Ignore multi-comparisons in repeated-equality-comparison-target (#6705)
Given `foo == "a" == "b" or foo == "c"`, we were suggesting `foo in
{"a", "b", "c"}`.
2023-08-20 14:41:10 +00:00
Harutaka Kawamura
129b19050a Refactor flake8_pytest_style/rules/parametrize.rs (#6703) 2023-08-20 14:30:26 +00:00
Konrad Listwan-Ciesielski
0dc23da1d0 Add docs for DTZ011 and DTZ012 (#6688) 2023-08-20 10:21:10 -04:00
Harutaka Kawamura
c62e544cba Add doc for E999 (#6699) 2023-08-20 14:14:22 +00:00
Charlie Marsh
7e9023b6f8 Use typing_extensions.TypeAlias for PYI026 fixes on pre-3.10 (#6696)
Closes https://github.com/astral-sh/ruff/issues/6695.
2023-08-19 22:16:44 +00:00
Harutaka Kawamura
a489b96a65 [flake8-pie] Implement unnecessary-range-start (PIE808) (#6690) 2023-08-19 21:59:11 +00:00
Charlie Marsh
17af12e57c Add branch detection to the semantic model (#6694)
## Summary

We have a few rules that rely on detecting whether two statements are in
different branches -- for example, different arms of an `if`-`else`.
Historically, the way this was implemented is that, given two statement
IDs, we'd find the common parent (by traversing upwards via our
`Statements` abstraction); then identify branches "manually" by matching
the parents against `try`, `if`, and `match`, and returning iterators
over the arms; then check if there's an arm for which one of the
statements is a child, and the other is not.

This has a few drawbacks:

1. First, the code is generally a bit hard to follow (Konsti mentioned
this too when working on the `ElifElseClause` refactor).

2. Second, this is the only place in the codebase where we need to go
from `&Stmt` to `StatementID` -- _everywhere_ else, we only need to go
in the _other_ direction. Supporting these lookups means we need to
maintain a mapping from `&Stmt` to `StatementID` that includes every
`&Stmt` in the program. (We _also_ end up maintaining a `depth` level
for every statement.) I'd like to get rid of these requirements to
improve efficiency, reduce complexity, and enable us to treat AST modes
more generically in the future. (When I looked at adding the `&Expr` to
our existing statement-tracking infrastructure, maintaining a hash map
with all the statements noticeably hurt performance.)

The solution implemented here instead makes branches a first-class
concept in the semantic model. Like with `Statements`, we now have a
`Branches` abstraction, where each branch points to its optional parent.
When we store statements, we store the `BranchID` alongside each
statement. When we need to detect whether two statements are in the same
branch, we just realize each statement's branch path and compare the
two. (Assuming that the two statements are in the same scope, then
they're on the same branch IFF one branch path is a subset of the other,
starting from the top.) We then add some calls to the visitor to push
and pop branches in the appropriate places, for `if`, `try`, and `match`
statements.

Note that a branch is not 1:1 with a statement; instead, each branch is
closer to a suite, but not _every_ suite is a branch. For example, each
arm in an `if`-`elif`-`else` is a branch, but the `else` in a `for` loop
is not considered a branch.

In addition to being much simpler, this should also be more efficient,
since we've shed the entire `&Stmt` hash map, plus the `depth` that we
track on `StatementWithParent` in favor of a single `Option<BranchID>`
on `StatementWithParent` plus a single vector for all branches. The
lookups should be faster too, since instead of doing a bunch of jumps
around with the hash map + repeated recursive calls to find the common
parents, we instead just do a few simple lookups in the `Branches`
vector to realize and compare the branch paths.

## Test Plan

`cargo test` -- we have a lot of coverage for this, which we inherited
from PyFlakes
2023-08-19 21:28:17 +00:00
Chris Pryer
648333b8b2 ruff_formatter crate doc comment fixes (#6677) 2023-08-19 17:42:02 +01:00
Charlie Marsh
3849fa0cf1 Rewrite yield-in-for-loop to avoid recursing over body (#6692)
## Summary

This is much simpler and avoids (1) multiple passes over the entire
function body, (2) requiring the rule to do its own binding tracking (we
can just use the semantic model), and (3) a usage of `StatementKey`.

In general, where we can, we should try to remove these kinds of custom
visitors that track name references, and instead rely on the semantic
model.

## Test Plan

`cargo test`
2023-08-19 11:25:29 -04:00
Victor Hugo Gomes
59e533047a Fix typo in ruff_python_formatter documentation (#6687)
## Summary

In the documentation was written `Javascript` but we are working with
`Python` here :)

## Test Plan

n/a
2023-08-18 19:16:09 -04:00
Charlie Marsh
053b1145f0 Avoid panic in unused arguments rule for parameter-free lambda (#6679)
## Summary

This was just a mistake in pattern-matching with no test coverage.

## Test Plan

`cargo test`
2023-08-18 18:29:31 +00:00
Charlie Marsh
6a5acde226 Make Parameters an optional field on ExprLambda (#6669)
## Summary

If a lambda doesn't contain any parameters, or any parameter _tokens_
(like `*`), we can use `None` for the parameters. This feels like a
better representation to me, since, e.g., what should the `TextRange` be
for a non-existent set of parameters? It also allows us to remove
several sites where we check if the `Parameters` is empty by seeing if
it contains any arguments, so semantically, we're already trying to
detect and model around this elsewhere.

Changing this also fixes a number of issues with dangling comments in
parameter-less lambdas, since those comments are now automatically
marked as dangling on the lambda. (As-is, we were also doing something
not-great whereby the lambda was responsible for formatting dangling
comments on the parameters, which has been removed.)

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

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

## Test Plan

`cargo test`
2023-08-18 15:34:54 +00:00
Micha Reiser
ea72d5feba Refactor SourceKind to store file content (#6640) 2023-08-18 13:45:38 +00:00
Charlie Marsh
2aeb27334d Avoid cloning source code multiple times (#6629)
## Summary

In working on https://github.com/astral-sh/ruff/pull/6628, I noticed
that we clone the source code contents, potentially multiple times,
prior to linting. The issue is that `SourceKind::Python` takes a
`String`, so we first have to provide it with a `String`. In the stdin
case, that means cloning. However, on top of this, we then have to clone
`source_kind.contents()` because `SourceKind` gets mutated. So for
stdin, we end up cloning twice. For non-stdin, we end up cloning once,
but unnecessarily (since the _contents_ don't get mutated, only the
kind).

This PR removes the `String` from `source_kind`, instead requiring that
we parse it out elsewhere. It reduces the number of clones down to 1 for
Jupyter Notebooks, and zero otherwise.
2023-08-18 09:32:18 -04:00
Micha Reiser
0cea4975fc Rename Comments methods (#6649) 2023-08-18 06:37:01 +00:00
Charlie Marsh
3ceb6fbeb0 Remove some unnecessary ampersands in the formatter (#6667) 2023-08-18 04:18:26 +00:00
Charlie Marsh
8e18f8018f Remove some trailing commas in write calls (#6666) 2023-08-18 00:14:44 -04:00
Charlie Marsh
8228429a70 Convert comment to rustdoc in placement.rs (#6665) 2023-08-18 04:11:38 +00:00
Charlie Marsh
1811312722 Improve with statement comment handling and expression breaking (#6621)
## Summary

The motivating code here was:

```python
with test as (
    # test
foo):
    pass
```

Which we were formatting as:

```python
with test as
# test
(foo):
    pass
```

`with` statements are oddly difficult. This PR makes a bunch of subtle
modifications and adds a more extensive test suite. For example, we now
only preserve parentheses if there's more than one `WithItem` _or_ a
trailing comma; before, we always preserved.

Our formatting is_not_ the same as Black, but here's a diff of our
formatted code vs. Black's for the `with.py` test suite. The primary
difference is that we tend to break parentheses when they contain
comments rather than move them to the end of the life (this is a
consistent difference that we make across the codebase):

```diff
diff --git a/crates/ruff_python_formatter/foo.py b/crates/ruff_python_formatter/foo.py
index 85e761080..31625c876 100644
--- a/crates/ruff_python_formatter/foo.py
+++ b/crates/ruff_python_formatter/foo.py
@@ -1,6 +1,4 @@
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-), aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
     ...
     # trailing
 
@@ -16,28 +14,33 @@ with (
     # trailing
 
 
-with a, b:  # a  # comma  # c  # colon
+with (
+    a,  # a  # comma
+    b,  # c
+):  # colon
     ...
 
 
 with (
-    a as  # a  # as
-    # own line
-    b,  # b  # comma
+    a as (  # a  # as
+        # own line
+        b
+    ),  # b  # comma
     c,  # c
 ):  # colon
     ...  # body
     # body trailing own
 
-with (
-    a as  # a  # as
+with a as (  # a  # as
     # own line
-    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb  # b
-):
+    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+):  # b
     pass
 
 
-with (a,):  # magic trailing comma
+with (
+    a,
+):  # magic trailing comma
     ...
 
 
@@ -47,6 +50,7 @@ with a:  # should remove brackets
 with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
     ...
 
+
 with (
     # leading comment
     a
@@ -74,8 +78,7 @@ with (
 with (
     a  # trailing same line comment
     # trailing own line comment
-    as b
-):
+) as b:
     ...
 
 with (
@@ -87,7 +90,9 @@ with (
 with (
     a
     # trailing own line comment
-) as b:  # trailing as same line comment  # trailing b same line comment
+) as (  # trailing as same line comment
+    b
+):  # trailing b same line comment
     ...
 
 with (
@@ -124,18 +129,24 @@ with (  # comment
     ...
 
 with (  # outer comment
-    CtxManager1() as example1,  # inner comment
+    (  # inner comment
+        CtxManager1()
+    ) as example1,
     CtxManager2() as example2,
     CtxManager3() as example3,
 ):
     ...
 
-with CtxManager() as example:  # outer comment
+with (  # outer comment
+    CtxManager()
+) as example:
     ...
 
 with (  # outer comment
     CtxManager()
-) as example, CtxManager2() as example2:  # inner comment
+) as example, (  # inner comment
+    CtxManager2()
+) as example2:
     ...
 
 with (  # outer comment
@@ -145,7 +156,9 @@ with (  # outer comment
     ...
 
 with (  # outer comment
-    (CtxManager1()),  # inner comment
+    (  # inner comment
+        CtxManager1()
+    ),
     CtxManager2(),
 ) as example:
     ...
@@ -179,7 +192,9 @@ with (
 ):
     pass
 
-with a as (b):  # foo
+with a as (  # foo
+    b
+):
     pass
 
 with f(
@@ -209,17 +224,13 @@ with f(
 ) as b, c as d:
     pass
 
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b:
     pass
 
 with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b:
     pass
 
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b, c as d:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, c as d:
     pass
 
 with (
@@ -230,6 +241,8 @@ with (
     pass
 
 with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b, c as d:
+    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b,
+    c as d,
+):
     pass
```

Closes https://github.com/astral-sh/ruff/issues/6600.
## Test Plan

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75473          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74292          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

After:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75473          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74292          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

`cargo test`
2023-08-18 03:30:38 +00:00
Charlie Marsh
26bba11be6 Manually format comments around := in named expressions (#6634)
## Summary

Attaches comments around the `:=` operator in a named expression as
dangling, and formats them manually in the `named_expr.rs` formatter.

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

## Test Plan

`cargo test`
2023-08-18 03:10:45 +00:00
Shantanu
a128fe5148 Apply RUF017 when start is passed via position (#6664)
As discussed in
https://github.com/astral-sh/ruff/pull/6489#discussion_r1297858919.
Linking https://github.com/astral-sh/ruff/issues/5073
2023-08-17 20:10:07 -04:00
Zanie Blue
5892c691ea Bump version to 0.0.285 (#6660)
Requires
- https://github.com/astral-sh/ruff/pull/6655
- https://github.com/astral-sh/ruff/pull/6657
2023-08-17 15:46:28 -05:00
Zanie Blue
82e0a97b34 Clarify behavior of PLW3201 (#6657)
Otherwise it is unclear that violations will be raised for methods like
`_foo_`
2023-08-17 14:41:55 -05:00
Zanie Blue
a8d7bbae6f Remove experimental label from Jupyter docs (#6655) 2023-08-17 14:40:50 -05:00
Charlie Marsh
1050142a58 Expand expressions to include parentheses in E712 (#6575)
## Summary

This PR exposes our `is_expression_parenthesized` logic such that we can
use it to expand expressions when autofixing to include their
parenthesized ranges.

This solution has a few drawbacks: (1) we need to compute parenthesized
ranges in more places, which also relies on backwards lexing; and (2) we
need to make use of this in any relevant fixes.

However, I still think it's worth pursuing. On (1), the implementation
is very contained, so IMO we can easily swap this out for a more
performant solution in the future if needed. On (2), this improves
correctness and fixes some bad syntax errors detected by fuzzing, which
means it has value even if it's not as robust as an _actual_
`ParenthesizedExpression` node in the AST itself.

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

## Test Plan

`cargo test` with new cases that previously failed the fuzzer.
2023-08-17 15:51:09 +00:00
Charlie Marsh
db1c556508 Implement Ranged on more structs (#6639)
## Summary

I noticed some inconsistencies around uses of `.range.start()`, structs
that have a `TextRange` field but don't implement `Ranged`, etc.

## Test Plan

`cargo test`
2023-08-17 11:22:39 -04:00
Charlie Marsh
a70807e1e1 Expand NamedExpr range to include full range of parenthesized value (#6632)
## Summary

Given:

```python
if (
    x
    :=
    (  # 4
        y # 5
    )  # 6
):
    pass
```

It turns out the parser ended the range of the `NamedExpr` at the end of
`y`, rather than the end of the parenthesis that encloses `y`. This just
seems like a bug -- the range should be from the start of the name on
the left, to the end of the parenthesized node on the right.

## Test Plan

`cargo test`
2023-08-17 14:34:05 +00:00
dependabot[bot]
d9bb51dee4 ci(deps): bump cloudflare/wrangler-action from 3.0.0 to 3.0.2 (#6565)
Bumps
[cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action)
from 3.0.0 to 3.0.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/releases">cloudflare/wrangler-action's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Added more error logging when a command fails to execute
Previously, we prevented any error logs from propagating too far to
prevent leaking of any potentially sensitive information. However, this
made it difficult for developers to debug their code.</p>
<p>In this release, we have updated our error handling to allow for more
error messaging from pre/post and custom commands. We still discourage
the use of these commands for secrets or other sensitive information,
but we believe this change will make it easier for developers to debug
their code.</p>
<p>Relates to <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/137">#137</a></p>
</li>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Adding Changesets</p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/HEAD/#version-300">Version
3.0.0</a></p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/HEAD/#version-200">Version
2.0.0</a></p>
</li>
</ul>
<h2>v3.0.1</h2>
<p>Automating Build &amp; Release</p>
<h2>What's Changed</h2>
<ul>
<li>Artifacts are now ESM supported with <code>.mjs</code></li>
<li>Update publish to deploy by <a
href="https://github.com/lrapoport-cf"><code>@​lrapoport-cf</code></a>
in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/124">cloudflare/wrangler-action#124</a></li>
<li>Automatically add issues to workers-sdk GH project by <a
href="https://github.com/lrapoport-cf"><code>@​lrapoport-cf</code></a>
in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/127">cloudflare/wrangler-action#127</a></li>
<li>Automate Action Release by <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>
in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/128">cloudflare/wrangler-action#128</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/lrapoport-cf"><code>@​lrapoport-cf</code></a>
made their first contribution in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/124">cloudflare/wrangler-action#124</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cloudflare/wrangler-action/compare/3.0.0...3.0.1">https://github.com/cloudflare/wrangler-action/compare/3.0.0...3.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md">cloudflare/wrangler-action's
changelog</a>.</em></p>
<blockquote>
<h2>3.0.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Added more error logging when a command fails to execute
Previously, we prevented any error logs from propagating too far to
prevent leaking of any potentially sensitive information. However, this
made it difficult for developers to debug their code.</p>
<p>In this release, we have updated our error handling to allow for more
error messaging from pre/post and custom commands. We still discourage
the use of these commands for secrets or other sensitive information,
but we believe this change will make it easier for developers to debug
their code.</p>
<p>Relates to <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/137">#137</a></p>
</li>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Adding Changesets</p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/main/#version-300">Version
3.0.0</a></p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/main/#version-200">Version
2.0.0</a></p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="80501a7b4d"><code>80501a7</code></a>
Automatic compilation</li>
<li><a
href="3252711404"><code>3252711</code></a>
hotfix</li>
<li><a
href="2faabf36a2"><code>2faabf3</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/150">#150</a>
from cloudflare/jacobmgevans/changesets-tag-cli</li>
<li><a
href="b734d85d74"><code>b734d85</code></a>
Changesets needs a tag created for release</li>
<li><a
href="8ca2ff1612"><code>8ca2ff1</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/148">#148</a>
from cloudflare/changeset-release/main</li>
<li><a
href="7d08a8657e"><code>7d08a86</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/149">#149</a>
from cloudflare/jacobmgevans/changesets-needs-publish</li>
<li><a
href="e193627f19"><code>e193627</code></a>
Spoofing publish for Changeset Action</li>
<li><a
href="55b80c5f62"><code>55b80c5</code></a>
Version Packages</li>
<li><a
href="f61dc4d5a9"><code>f61dc4d</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/147">#147</a>
from cloudflare/revert-146-changeset-release/main</li>
<li><a
href="58f274b9f7"><code>58f274b</code></a>
Revert &quot;Version Packages&quot;</li>
<li>Additional commits viewable in <a
href="https://github.com/cloudflare/wrangler-action/compare/3.0.0...v3.0.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cloudflare/wrangler-action&package-manager=github_actions&previous-version=3.0.0&new-version=3.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-17 09:11:04 -05:00
Zanie Blue
d0f2a8e424 Add support for nested replacements inside format specifications (#6616)
Closes https://github.com/astral-sh/ruff/issues/6442

Python string formatting like `"hello {place}".format(place="world")`
supports format specifications for replaced content such as `"hello
{place:>10}".format(place="world")` which will align the text to the
right in a container filled up to ten characters.

Ruff parses formatted strings into `FormatPart`s each of which is either
a `Field` (content in `{...}`) or a `Literal` (the normal content).
Fields are parsed into name and format specifier sections (we'll ignore
conversion specifiers for now).

There are a myriad of specifiers that can be used in a `FormatSpec`.
Unfortunately for linters, the specifier values can be dynamically set.
For example, `"hello {place:{align}{width}}".format(place="world",
align=">", width=10)` and `"hello {place:{fmt}}".format(place="world",
fmt=">10")` will yield the same string as before but variables can be
used to determine the formatting. In this case, when parsing the format
specifier we can't know what _kind_ of specifier is being used as their
meaning is determined by both position and value.

Ruff does not support nested replacements and our current data model
does not support the concept. Here the data model is updated to support
this concept, although linting of specifications with replacements will
be inherently limited. We could split format specifications into two
types, one without any replacements that we can perform lints with and
one with replacements that we cannot inspect. However, it seems
excessive to drop all parsing of format specifiers due to the presence
of a replacement. Instead, I've opted to parse replacements eagerly and
ignore their possible effect on other format specifiers. This will allow
us to retain a simple interface for `FormatSpec` and most syntax checks.
We may need to add some handling to relax errors if a replacement was
seen previously.

It's worth noting that the nested replacement _can_ also include a
format specification although it may fail at runtime if you produce an
invalid outer format specification. For example, `"hello
{place:{fmt:<2}}".format(place="world", fmt=">10")` is valid so we need
to represent each nested replacement as a full `FormatPart`.

## Test plan

Adding unit tests for `FormatSpec` parsing and snapshots for PLE1300
2023-08-17 09:07:30 -05:00
Charlie Marsh
1334232168 Introduce ExpressionRef (#6637)
## Summary

This PR revives the `ExpressionRef` concept introduced in
https://github.com/astral-sh/ruff/pull/5644, motivated by the change we
want to make in https://github.com/astral-sh/ruff/pull/6575 to narrow
the type of the expression that can be passed to `parenthesized_range`.

## Test Plan

`cargo test`
2023-08-17 10:07:16 -04:00
Micha Reiser
fa7442da2f Support fmt: skip on compound statements (#6593) 2023-08-17 06:05:41 +00:00
Micha Reiser
4dc32a00d0 Support fmt: skip for simple-statements and decorators (#6561) 2023-08-17 05:58:19 +00:00
Evan Rittenhouse
e3ecbe660e [ruff] Implement quadratic-list-summation rule (RUF017) (#6489)
## Summary

Adds `RUF017`. Closes #5073 

## Test Plan

`cargo t`
2023-08-16 23:13:05 -04:00
Harutaka Kawamura
8c3a8c4fc6 Support glob patterns for raises_require_match_for and raises_require_match_for (#6635)
## Summary

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

Support glob patterns for `raises_require_match_for` and
`raises_require_match_for`. Resolve #6473

## Test Plan

New tests + existing tests
2023-08-17 02:15:50 +00:00
Charlie Marsh
dcc7226685 Make lambda-assignment fix always-manual in class bodies (#6626)
## Summary

Related to https://github.com/astral-sh/ruff/issues/6620 (although that
will only be truly closed once we respect manual fixes on the CLI).
2023-08-16 21:24:48 -04:00
Charlie Marsh
036035bc50 Refactor literal-comparison and not-test rules (#6636)
## Summary

No behavior changes, but these need some refactoring to support
https://github.com/astral-sh/ruff/pull/6575 (namely, they need to take
the `ast::ExprCompare` or similar node instead of the attribute fields),
and I don't want to muddy that PR.

## Test Plan

`cargo test`
2023-08-17 01:02:30 +00:00
Charlie Marsh
97ae9e7433 Don't detect pandas#values for stores, deletes, or class accesses (#6631)
## Summary

Ensures we avoid cases like:

```python
x.values = 1
```

Since Pandas doesn't even expose a setter for that. We also avoid cases
like:

```python
print(self.values)
```

Since it's overwhelming likely to be a false positive.

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

## Test Plan

`cargo test`
2023-08-16 17:13:33 -04:00
Charlie Marsh
98b9f2e705 Respect .ipynb and .pyi sources when linting from stdin (#6628)
## Summary

When running Ruff from stdin, we were always falling back to the default
source type, even if the user specified a path (as is the case when
running from the LSP). This PR wires up the source type inference, which
means we now get the expected result when checking `.pyi` and `.ipynb`
files.

Closes #6627.

## Test Plan

Verified that `cat
crates/ruff/resources/test/fixtures/jupyter/valid.ipynb | cargo run -p
ruff_cli -- --force-exclude --no-cache --no-fix --isolated --select ALL
--stdin-filename foo.ipynb -` yielded the expected results (and differs
from the errors you get if you omit the filename).

Verified that `cat foo.pyi | cargo run -p ruff_cli -- --force-exclude
--no-cache --no-fix --format json --isolated --select TCH
--stdin-filename path/to/foo.pyi -` yielded no errors.
2023-08-16 20:33:59 +00:00
Zanie Blue
6253d8e2c8 Remove unused runtime string formatting logic (#6624)
In https://github.com/astral-sh/ruff/pull/6616 we are adding support for
nested replacements in format specifiers which makes actually formatting
strings infeasible without a great deal of complexity. Since we're not
using these functions (they just exist for runtime use in RustPython),
we can just remove them.
2023-08-16 17:38:33 +00:00
Charlie Marsh
0a5be74be3 Fix transformers checkout in scripts/formatter_ecosystem_checks.sh (#6622)
## Summary

In #6387, we accidentally added `git -C "$dir/django" checkout
95e4d6b81312fdd9f8ebf3385be1c1331168b5cf` as the transformers checkout
(duplicated line from the Django case). This PR fixes the SHA, and
spaces out the cases to make it more visible. I _think_ the net effect
here is that we've been formatting `main` on transformers, rather than
the SHA?
2023-08-16 12:25:46 -05:00
Micha Reiser
fdbb2fbdba Fix unreachable in playground (#6623) 2023-08-16 18:54:42 +02:00
Charlie Marsh
d0b8e4f701 Update Black tests (#6618)
## Summary

Pulls in some tests that we previously couldn't support

## Test Plan

`cargo test`
2023-08-16 15:05:51 +00:00
Charlie Marsh
12f3c4c931 Fix comment formatting for yielded tuples (#6603)
## Summary
Closes https://github.com/astral-sh/ruff/issues/6384, although I think
the issue was fixed already on main, for the most part.

The linked issue is around formatting expressions like:

```python
def test():
    (
        yield 
        #comment 1
        * # comment 2
        # comment 3
        test # comment 4
    )

```

On main, prior to this PR, we now format like:

```python
def test():
    (
        yield (
            # comment 1
            # comment 2
            # comment 3
            *test
        )  # comment 4
    )
```

Which strikes me as reasonable. (We can't test this, since it's a syntax
error after for our parser, despite being a syntax error in both cases
from CPython's perspective.)

Meanwhile, Black does:

```python
def test():
    (
        yield
        # comment 1
        *  # comment 2
        # comment 3
        test  # comment 4
    )
```

So our formatting differs in that we move comments between the star and
the expression above the star.

As of this PR, we also support formatting this input, which is valid:

```python
def test():
    (
        yield 
        #comment 1
        * # comment 2
        # comment 3
        test, # comment 4
        1
    )
```

Like:

```python
def test():
    (
        yield (
            # comment 1
            (
                # comment 2
                # comment 3
                *test,  # comment 4
                1,
            )
        )
    )
```

There were two fixes here: (1) marking starred comments as dangling and
formatting them properly; and (2) supporting parenthesized comments for
tuples that don't contain their own parentheses, as is often the case
for yielded tuples (previously, we hit a debug assert).

Note that this diff

## Test Plan
cargo test
2023-08-16 13:41:07 +00:00
Micha Reiser
7ee2ae8395 Estimate expected VecBuffer size (#6612) 2023-08-16 15:31:31 +02:00
Charlie Marsh
95f78821ad Fix parenthesized detection for tuples (#6599)
## Summary

This PR fixes our code for detecting whether a tuple has its own
parentheses, which is necessary when attempting to preserve parentheses.
As-is, we were getting some cases wrong, like `(a := 1), (b := 3))` --
the detection code inferred that this _was_ parenthesized, and so
wrapped the entire thing in an unnecessary set of parentheses.

## Test Plan

`cargo test`

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75472          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74288          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

After:
| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75473          |
| django       | 0.99804 |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74288          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |
2023-08-16 13:20:48 +00:00
Micha Reiser
daac31d2b9 Make Buffer::write_element non-failable (#6613) 2023-08-16 15:13:07 +02:00
Charlie Marsh
86ccdcc9d9 Add support for multi-character operator tokens to SimpleTokenizer (#6563)
## Summary

Allows for proper lexing of tokens like `->`.

The main challenge is to ensure that our forward and backwards
representations are the same for cases like `===`. Specifically, we want
that to lex as `==` followed by `=` regardless of whether it's a
forwards or backwards lex. To do so, we identify the range of the
sequential characters (the full span of `===`), lex it forwards, then
return the last token.

## Test Plan

`cargo test`
2023-08-16 09:09:19 -04:00
Micha Reiser
e28858bb29 Fast path for ASCII only identifiers start (#6609) 2023-08-16 10:22:44 +02:00
Charlie Marsh
2d86e78bfc Allow top-level await in Jupyter notebooks (#6607)
## Summary

Top-level `await` is allowed in Jupyter notebooks (see:
[autoawait](https://ipython.readthedocs.io/en/stable/interactive/autoawait.html)).

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

## Test Plan

Had to test this manually. Created a notebook, verified that the `yield`
was flagged but the `await` was not.

<img width="868" alt="Screen Shot 2023-08-15 at 11 40 19 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/b2853651-30a6-4dc6-851c-9fe7f694b8e8">
2023-08-15 23:59:05 -04:00
Harutaka Kawamura
d9a81f4fbb [flake8-pytest-style] Implement duplicate parameterized fixture detection (PT014) (#6598) 2023-08-16 03:35:46 +00:00
Micha Reiser
897cce83b3 Call pattern formatting (#6594) 2023-08-16 08:31:25 +05:30
Anton Grouchtchak
9bf6713b76 Change rule count from 500 to 600 (#6605)
Fixes missing change from PR #6579.
2023-08-15 19:17:52 -05:00
Charlie Marsh
3f1658a25b Remove pylint's duplicate_value.rs (#6604)
This was moved to bugbear, but we forgot to delete the file.
2023-08-16 00:10:24 +00:00
Zanie Blue
097db2fcce Fix docs for PLW1508 (#6602) 2023-08-15 15:29:29 -05:00
Charlie Marsh
a3d4f08f29 Add general support for parenthesized comments on expressions (#6485)
## Summary

This PR adds support for parenthesized comments. A parenthesized comment
is a comment that appears within a parenthesis, but not within the range
of the expression enclosed by the parenthesis. For example, the comment
here is a parenthesized comment:

```python
if (
    # comment
    True
):
    ...
```

The parentheses enclose the `True`, but the range of `True` doesn’t
include the `# comment`.

There are at least two problems associated with parenthesized comments:
(1) associating the comment with the correct (i.e., enclosed) node; and
(2) formatting the comment correctly, once it has been associated with
the enclosed node.

The solution proposed here for (1) is to search for parentheses between
preceding and following node, and use open and close parentheses to
break ties, rather than always assigning to the preceding node.

For (2), we handle these special parenthesized comments in `FormatExpr`.
The biggest risk with this approach is that we forget some codepath that
force-disables parenthesization (by passing in `Parentheses::Never`).
I've audited all usages of that enum and added additional handling +
test coverage for such cases.

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

## Test Plan

`cargo test` with new cases.

Before:

| project      | similarity index |
|--------------|------------------|
| build        | 0.75623          |
| cpython      | 0.75472          |
| django       | 0.99804          |
| transformers | 0.99618          |
| typeshed     | 0.74233          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

After:

| project      | similarity index |
|--------------|------------------|
| build        | 0.75623          |
| cpython      | 0.75472          |
| django       | 0.99804          |
| transformers | 0.99618          |
| typeshed     | 0.74237          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |
2023-08-15 18:59:18 +00:00
Micha Reiser
29c0b9f91c Use single lookup for leading, dangling, and trailing comments (#6589) 2023-08-15 17:39:45 +02:00
Harutaka Kawamura
81b1176f99 Fix PT005 doc (#6596) 2023-08-15 12:48:44 +00:00
Charlie Marsh
b1c4c7be69 Add trailing comma for single-element import-from groups (#6583)
## Summary

Unlike other statements, Black always adds a trailing comma if an
import-from statement breaks with a single import member. I believe this
is for compatibility with isort -- see
09f5ee3a19,
https://github.com/psf/black/issues/127, or
66648c528a/src/black/linegen.py (L1452)
for the current version.

## Test Plan

`cargo test`, notice that a big chunk of the compatibility suite is
removed.

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75472          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74233          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

After:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75472          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74260          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |
2023-08-15 07:15:33 -04:00
Tom Kuson
84d178a219 Use one line between top-level items if formatting a stub file (#6501)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-08-15 09:33:57 +02:00
Micha Reiser
455db84a59 Replace inline(always) with inline (#6590) 2023-08-15 08:58:11 +02:00
Micha Reiser
232b44a8ca Indent statements in suppressed ranges (#6507) 2023-08-15 08:00:35 +02:00
Harutaka Kawamura
e1e213decf Import pytest in flake8-pytest-style docs (#6580) 2023-08-14 23:08:15 -04:00
Charlie Marsh
5f709cd3e0 Bump rule count to 600+ in the docs (#6579)
My informal count yielded 679 rules as of yesterday.
2023-08-15 00:11:32 +00:00
Charlie Marsh
17e7eae2f9 Avoid unused argument rules when functions call locals() (#6578)
Closes https://github.com/astral-sh/ruff/issues/6576.
2023-08-14 19:48:20 -04:00
Charlie Marsh
7f7df852e8 Remove some extraneous newlines in Cargo.toml (#6577) 2023-08-14 23:39:41 +00:00
Nok Lam Chan
9a0d2f5afd Add regular expression example for per-file-ignores (#6573)
## Summary
Hi! This is my first PR to `ruff` and thanks for this amazing project.
While I am working on my project, I need to set different rules for my
`test/` folder and the main `src` package.

It's not immediately obvious that the
[`tool.ruff.per-file-ignores`](https://beta.ruff.rs/docs/settings/#per-file-ignores)
support regular expression. It is useful to set rules on directory
level. The PR add a simple example to make it clear this support regex.
2023-08-14 22:02:40 +00:00
Harutaka Kawamura
ebda5fcd99 Add PT002 ~ PT005 docs (#6521) 2023-08-14 21:29:03 +00:00
Charlie Marsh
b1870b2b16 Add deprecated unittest assertions to PT009 (#6572)
## Summary

This rule was missing `self.failIf` and friends.

## Test Plan

`cargo test`
2023-08-14 21:08:02 +00:00
Harutaka Kawamura
a51d1ac980 Add PT006 and PT007 docs (#6531) 2023-08-14 17:03:42 -04:00
Evan Rittenhouse
1a52b548e7 Ignore PERF203 if try contains loop control flow statements (#6536) 2023-08-14 20:47:37 +00:00
konsti
a3bf6d9cb7 Formatter ecosystem checks: Use twine instead of build (#6559) 2023-08-14 22:40:56 +02:00
Harutaka Kawamura
70696061cd [flake8-pytest-style] Implement pytest-unittest-raises-assertion (PT027) (#6554) 2023-08-14 20:25:23 +00:00
Charlie Marsh
cd634a9489 Expand documentation around flake8-type-checking rules for SQLAlchemy (#6570)
## Summary

Not addressing the root issue as much as improving the documentation.

Closes https://github.com/astral-sh/ruff/issues/6510.
2023-08-14 19:47:10 +00:00
Charlie Marsh
5ddf143cae Clarify FBT documentation and refine rule names (#6567)
Closes https://github.com/astral-sh/ruff/issues/6530.
2023-08-14 15:24:16 -04:00
Charlie Marsh
46862473b9 Omit NotImplementedError from TRY003 (#6568)
Closes https://github.com/astral-sh/ruff/issues/6528.
2023-08-14 18:24:44 +00:00
Charlie Marsh
96d310fbab Remove Stmt::TryStar (#6566)
## Summary

Instead, we set an `is_star` flag on `Stmt::Try`. This is similar to the
pattern we've migrated towards for `Stmt::For` (removing
`Stmt::AsyncFor`) and friends. While these are significant differences
for an interpreter, we tend to handle these cases identically or nearly
identically.

## Test Plan

`cargo test`
2023-08-14 13:39:44 -04:00
Micha Reiser
09c8b17661 fmt: off..on suppression comments (#6477) 2023-08-14 15:57:36 +00:00
qdegraaf
278a4f6e14 Formatter: Fix posonlyargs for expr_lambda (#6562) 2023-08-14 17:38:56 +02:00
Charlie Marsh
c3a9151eb5 Handle comments on open parentheses in with statements (#6515)
## Summary

This PR adds handling for comments on open parentheses in parenthesized
context managers. For example, given:

```python
with (  # comment
    CtxManager1() as example1,
    CtxManager2() as example2,
    CtxManager3() as example3,
):
    ...
```

We want to preserve that formatting. (Black does the same.) On `main`,
we format as:

```python
with (
    # comment
    CtxManager1() as example1,
    CtxManager2() as example2,
    CtxManager3() as example3,
):
    ...
```

It's very similar to how `StmtImportFrom` is handled.

Note that this case _isn't_ covered by the "parenthesized comment"
proposal, since this is a common on the statement that would typically
be attached to the first `WithItem`, and the `WithItem` _itself_ can
have parenthesized comments, like:

```python
with (  # comment
    (
        CtxManager1()  # comment
    ) as example1,
    CtxManager2() as example2,
    CtxManager3() as example3,
):
    ...
```

## Test Plan

`cargo test`

Confirmed no change in similarity score.
2023-08-14 15:11:03 +00:00
Charlie Marsh
3711f8ad59 Expand SimpleTokenizer to all keywords and single-character tokens (#6518)
## Summary

For #6485, I need to be able to use the `SimpleTokenizer` to lex the
space between any two adjacent expressions (i.e., the space between a
preceding and following node). This requires that we support a wider
range of keywords (like `and`, to connect the pieces of `x and y`), and
some additional single-character tokens (like `-` and `>`, to support
`->`). Note that the `SimpleTokenizer` does not support multi-character
tokens, so the `->` in a function signature is lexed as a `-` followed
by a `>` -- but this is fine for our purposes.
2023-08-14 10:35:31 -04:00
Charlie Marsh
a7cf8f0b77 Replace dynamic implicit concatenation detection with parser flag (#6513)
## Summary

In https://github.com/astral-sh/ruff/pull/6512, we added a flag to the
AST to mark implicitly-concatenated string expressions. This PR makes
use of that flag to remove the `is_implicit_concatenation` method.

## Test Plan

`cargo test`
2023-08-14 10:27:17 -04:00
Charlie Marsh
40407dcce5 Avoid marking inner-parenthesized comments as dangling bracket comments (#6517)
## Summary

The bracketed-end-of-line comment rule is meant to assign comments like
this as "immediately following the bracket":

```python
f(  # comment
    1
)
```

However, the logic was such that we treated this equivalently:

```python
f(
    (  # comment
        1
    )
)
```

This PR modifies the placement logic to ensure that we only skip the
opening bracket, and not any nested brackets. The above is now formatted
as:

```python
f(
    (
        # comment
        1
    )
)
```

(But will be corrected once we handle parenthesized comments properly.)

## Test Plan

`cargo test`

Confirmed no change in similarity score.
2023-08-14 09:52:19 -04:00
Charlie Marsh
f16e780e0a Add an implicit concatenation flag to string and bytes constants (#6512)
## Summary

Per the discussion in
https://github.com/astral-sh/ruff/discussions/6183, this PR adds an
`implicit_concatenated` flag to the string and bytes constant variants.
It's not actually _used_ anywhere as of this PR, but it is covered by
the tests.

Specifically, we now use a struct for the string and bytes cases, along
with the `Expr::FString` node. That struct holds the value, plus the
flag:

```rust
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
pub enum Constant {
    Str(StringConstant),
    Bytes(BytesConstant),
    ...
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct StringConstant {
    /// The string value as resolved by the parser (i.e., without quotes, or escape sequences, or
    /// implicit concatenations).
    pub value: String,
    /// Whether the string contains multiple string tokens that were implicitly concatenated.
    pub implicit_concatenated: bool,
}

impl Deref for StringConstant {
    type Target = str;
    fn deref(&self) -> &Self::Target {
        self.value.as_str()
    }
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BytesConstant {
    /// The bytes value as resolved by the parser (i.e., without quotes, or escape sequences, or
    /// implicit concatenations).
    pub value: Vec<u8>,
    /// Whether the string contains multiple string tokens that were implicitly concatenated.
    pub implicit_concatenated: bool,
}

impl Deref for BytesConstant {
    type Target = [u8];
    fn deref(&self) -> &Self::Target {
        self.value.as_slice()
    }
}
```

## Test Plan

`cargo test`
2023-08-14 13:46:54 +00:00
Micha Reiser
fc0c9507d0 Override fmt_dangling_comments for frequent nodes (#6551) 2023-08-14 15:29:05 +02:00
Tom Kuson
680d171ae5 Tweak documentation for FBT002 (#6556) 2023-08-14 09:22:48 -04:00
konsti
01eceaf0dc Format docstrings (#6452)
**Summary** Implement docstring formatting

**Test Plan** Matches black's `docstring.py` fixture exactly, added some
new cases for what is hard to debug with black and with what black
doesn't cover.

similarity index:

main:
zulip: 0.99702
django: 0.99784
warehouse: 0.99585
build: 0.75623
transformers: 0.99469
cpython: 0.75989
typeshed: 0.74853

this branch:

zulip: 0.99702
django: 0.99784
warehouse: 0.99585
build: 0.75623
transformers: 0.99464
cpython: 0.75517
typeshed: 0.74853

The regression in transformers is actually an improvement in a file they
don't format with black (they run `black examples tests src utils
setup.py conftest.py`, the difference is in hubconf.py). cpython doesn't
use black.

Closes #6196
2023-08-14 12:28:58 +00:00
Micha Reiser
910dbbd9b6 Printer: Reserve buffer upfront (#6550) 2023-08-14 12:15:36 +00:00
Micha Reiser
9584f613b9 Remove allow(pedantic) from formatter (#6549) 2023-08-14 14:02:06 +02:00
konsti
c39bcbadff Always run check-formatter-ecosystem on main (#6503)
This makes it easier to get the latest similarity numbers from main
2023-08-14 14:01:26 +02:00
Micha Reiser
24f42f0894 Printer: Remove unused state fields (#6548) 2023-08-14 11:08:00 +02:00
Micha Reiser
51ae47ad56 Remove lex and parsing from formatter benchmark (#6547) 2023-08-14 10:25:37 +02:00
Charlie Marsh
1a9536c4e2 Remove SemanticModel#find_binding (#6546)
## Summary

This method is almost never what you actually want, because it doesn't
respect Python's scoping semantics. For example, if you call this within
a class method, it will return class attributes, whereas Python actually
_skips_ symbols in classes unless the load occurs within the class
itself. I also want to move away from these kinds of dynamic lookups and
more towards `resolve_name`, which performs a lookup based on the stored
`BindingId` at the time of symbol resolution, and will make it much
easier for us to separate model building from linting in the near
future.

## Test Plan

`cargo test`
2023-08-14 00:09:05 -04:00
Charlie Marsh
bf4c6473c8 Remove unnecessary expr_name function (#6544) 2023-08-13 23:51:36 -04:00
Charlie Marsh
768686148f Add support for unions to our Python builtins type system (#6541)
## Summary

Fixes some TODOs introduced in
https://github.com/astral-sh/ruff/pull/6538. In short, given an
expression like `1 if x > 0 else "Hello, world!"`, we now return a union
type that says the expression can resolve to either an `int` or a `str`.
The system remains very limited, it only works for obvious primitive
types, and there's no attempt to do inference on any more complex
variables. (If any expression yields `Unknown` or `TypeError`, we
propagate that result throughout and abort on the client's end.)
2023-08-13 18:00:50 -04:00
Charlie Marsh
eb24f5a0b9 Add some additional projects to the ecosystem CI (#6542)
Adding five new projects. Some of these have seen issues filed, the
others, I just tabbed through our dependency pain and looked for some
reasonably-large projects that enabled rules beyond the default rule
set.
2023-08-13 21:15:54 +00:00
Charlie Marsh
446ceed1ad Support IfExp with dual string arms in invalid-envvar-value (#6538)
## Summary

Closes https://github.com/astral-sh/ruff/issues/6537. We need to improve
the `PythonType` algorithm, so this also documents some of its
limitations as TODOs.
2023-08-13 15:52:10 -04:00
Takuma Watanabe
8660e5057c Fix minor document errors (#6533)
## Summary

Fix minor errors in the sample codes of some rules.

## Test Plan

N/A (Just fix document typos.)
2023-08-13 13:35:30 -04:00
Konrad Listwan-Ciesielski
808e09180e Add docs for DTZ005 and DTZ006 (#6529)
Changes:
- Adds docs for `DTZ005`
- Adds docs for `DTZ006`

Related to: https://github.com/astral-sh/ruff/issues/2646
2023-08-12 21:29:32 -04:00
Presley Graham
dbf003fde4 importer: skip whitespace between comments at start of file (#6523)
## Summary

When adding an import, such as when fixing `I002`, ruff doesn't skip
whitespace between comments, but isort does. See this issue for more
detail: https://github.com/astral-sh/ruff/issues/6504

This change would fix that by skipping whitespace between comments in
`Insertion.start_of_file()`.

## Test Plan

I added a new test, `comments_and_newlines`, to verify this behavior. I
also ran `cargo test` and no existing tests broke. That being said, this
is technically a breaking change, as it's possible that someone was
relying on the previous behavior.
2023-08-12 16:37:56 -04:00
Charlie Marsh
010293ddcc Use a unified policy abstraction for the flake8-tidy-imports rules (#6527)
## Summary

Generalizes the abstractions for name matching introduced in
https://github.com/astral-sh/ruff/pull/6378 and applies them to the
existing `banned_api` rule, such that both rules have a uniform API and
implementation.

## Test Plan

`cargo test`
2023-08-12 16:32:09 -04:00
James Braza
4974964ad3 Clarifying target-version in flake8-future-annotations docs (#6520) 2023-08-12 19:01:03 +00:00
Charlie Marsh
b49c80f8c8 Use top-level semantic detection for E402 (#6526)
## Summary

Noticed in https://github.com/astral-sh/ruff/pull/6378. Given `import h;
import i`, we don't consider `import i` to be a "top-level" import for
E402 purposes, which is wrong. Similarly, we _do_ consider `import k` to
be a "top-level" import in:

```python
if __name__ == "__main__":
    import j; \
import k
```

Using the semantic detection, rather than relying on newline position,
fixes both cases.

## Test Plan

`cargo test`
2023-08-12 18:52:44 +00:00
Presley Graham
c03e2acadb [flake8-tidy-imports] Add TID253 (#6378)
## Summary

Add a new rule `TID253` (`banned-module-level-imports`), to ban a
user-specified list of imports from appearing at module level. This rule
doesn't exist in `flake8-tidy-imports`, so it's unique to Ruff. The
implementation is pretty similar to `TID251`.

Briefly discussed
[here](https://github.com/astral-sh/ruff/discussions/6370).

## Test Plan

Added a new test case, checking that inline imports are allowed and that
non-inline imports from the banned list are disallowed.
2023-08-12 18:45:34 +00:00
Charlie Marsh
a1da9da0ef Avoid JSON parse error on playground load (#6519)
## Summary

On page load, the playground very briefly flickers a JSON parse error.
Due to our use of `useDeferredValue`, we attempt to parse the empty JSON
string settings, since after `const initialized = ruffVersion != null;`
returns true, we get one render with the stale deferred value.

This PR refactors the state, such that we start by storing `null` for
the `Source`, and use the `Source` itself to determine initialization
status.

## Test Plan

Set a breakpoint in the `catch` path in `Editor`; verified that it no
longer triggers on load (but did on `main`).
2023-08-12 04:11:44 +00:00
Harutaka Kawamura
c6ad364d8b Add PT008 and PT009 docs (#6479) 2023-08-11 23:44:48 -04:00
Zanie Blue
5b47350c25 Document default behavior of W505 in setting (#6463)
Addresses https://github.com/astral-sh/ruff/discussions/6459
2023-08-11 16:41:31 -05:00
Charlie Marsh
e91caea490 Add test case for walrus operators in return types (#6438)
## Summary

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

## Test Plan

`cargo test`
2023-08-11 18:28:48 +00:00
Charlie Marsh
53246b725e Allow return type annotations to use their own parentheses (#6436)
## Summary

This PR modifies our logic for wrapping return type annotations.
Previously, we _always_ wrapped the annotation in parentheses if it
expanded; however, Black only exhibits this behavior when the function
parameters is empty (i.e., it doesn't and can't break). In other cases,
it uses the normal parenthesization rules, allowing nodes to bring their
own parentheses.

For example, given:

```python
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
]:
    ...

def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(x) -> Set[
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
]:
    ...
```

Black will format as:

```python
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
    Set[
        "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    ]
):
    ...


def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
    x,
) -> Set[
    "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
]:
    ...
```

Whereas, prior to this PR, Ruff would format as:

```python
def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> (
    Set[
        "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    ]
):
    ...


def xxxxxxxxxxxxxxxxxxxxxxxxxxxx(
    x,
) -> (
    Set[
        "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
    ]
):
    ...
```

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

## Test Plan

Before:

- `zulip`: 0.99702
- `django`: 0.99784
- `warehouse`: 0.99585
- `build`: 0.75623
- `transformers`: 0.99470
- `cpython`: 0.75988
- `typeshed`: 0.74853

After:

- `zulip`: 0.99724
- `django`: 0.99791
- `warehouse`: 0.99586
- `build`: 0.75623
- `transformers`: 0.99474
- `cpython`: 0.75956
- `typeshed`: 0.74857
2023-08-11 18:19:21 +00:00
Charlie Marsh
d616c9b870 Avoid omitting optional parentheses for argument-less parentheses (#6484)
## Summary

This PR fixes some misformattings around optional parentheses for
expressions.

I first noticed that we were misformatting this:

```python
return (
    unicodedata.normalize("NFKC", s1).casefold()
    == unicodedata.normalize("NFKC", s2).casefold()
)
```

The above is stable Black formatting, but we were doing:
```python
return unicodedata.normalize("NFKC", s1).casefold() == unicodedata.normalize(
    "NFKC", s2
).casefold()
```

Above, the "last" expression is a function call, so our
`can_omit_optional_parentheses` was returning `true`...

However, it turns out that Black treats function calls differently
depending on whether or not they have arguments -- presumedly because
they'll never split empty parentheses, and so they're functionally
non-useful. On further investigation, I believe this applies to all
parenthesized expressions. If Black can't split on the parentheses, it
doesn't leverage them when removing optional parentheses.

## Test Plan

Nice increase in similarity scores.

Before:

- `zulip`: 0.99702
- `django`: 0.99784
- `warehouse`: 0.99585
- `build`: 0.75623
- `transformers`: 0.99470
- `cpython`: 0.75989
- `typeshed`: 0.74853

After:

- `zulip`: 0.99705
- `django`: 0.99795
- `warehouse`: 0.99600
- `build`: 0.75623
- `transformers`: 0.99471
- `cpython`: 0.75989
- `typeshed`: 0.74853
2023-08-11 17:58:42 +00:00
Chris Pryer
7c4aa3948b Fix typo in MeasureMode comment (#6508) 2023-08-11 17:46:59 +00:00
konsti
0c9ded9d84 Use a faster diffing library for the formatter ecosystem checks (#6497)
**Summary** Some files seems notoriously slow in the formatter (secons in debug mode). This time was however almost exclusively spent in the diff algorithm to collect the similarity index, so i replaced that. I kept `similar` for printing actual diff to avoid rewriting that too, with the disadvantage that we now have to diff libraries in format_dev.

I used this PR to remove the spinner from tracing-indicatif and changed `flamegraph --perfdata perf.data` to `flamegraph --perfdata perf.data --no-inline` as the former wouldn't finish for me on release builds with debug info.
2023-08-11 15:51:54 +02:00
Dhruv Manilawala
c434bdd2bd Add formatting for MatchCase (#6360)
## Summary

This PR adds formatting support for `MatchCase` node with subs for the
`Pattern`
nodes.

## Test Plan

Added test cases for case node handling with comments, newlines.

resolves: #6299
2023-08-11 19:20:25 +05:30
konsti
8b24238d19 Show a pretty markdown table in formatter ecosystem checks (#6496)
**Summary** The formatter ecosystem checks will now print a markdown table you can copy&paste into your PR description. 

![image](https://github.com/astral-sh/ruff/assets/6826232/80289ed9-9d2b-400e-a994-de63dca0b065)

copied markdown:

| project      | similarity index |
|--------------|------------------|
| build        | 0.75623          |
| cpython      | 0.75989          |
| django       | 0.99784          |
| transformers | 0.99470          |
| typeshed     | 0.74853          |
| warehouse    | 0.99585          |
| zulip        | 0.99702          |

raw markdown:
```markdown
| project      | similarity index |
|--------------|------------------|
| build        | 0.75623          |
| cpython      | 0.75989          |
| django       | 0.99784          |
| transformers | 0.99470          |
| typeshed     | 0.74853          |
| warehouse    | 0.99585          |
| zulip        | 0.99702          |
```
2023-08-11 15:37:21 +02:00
Charlie Marsh
f2939c678b Avoid breaking call chains unnecessarily (#6488)
## Summary

This PR attempts to fix the formatting of the following expression:

```python
max_message_id = (
    Message.objects.filter(recipient=recipient).order_by("id").reverse()[0].id
)
```

Specifically, Black preserves _that_ formatting, while we do:

```python
max_message_id = (
    Message.objects.filter(recipient=recipient)
    .order_by("id")
    .reverse()[0]
    .id
)
```

The fix here is to add a group around the entire call chain.

## Test Plan

Before:

- `zulip`: 0.99702
- `django`: 0.99784
- `warehouse`: 0.99585
- `build`: 0.75623
- `transformers`: 0.99470
- `cpython`: 0.75989
- `typeshed`: 0.74853

After:

- `zulip`: 0.99703
- `django`: 0.99791
- `warehouse`: 0.99586
- `build`: 0.75623
- `transformers`: 0.99470
- `cpython`: 0.75989
- `typeshed`: 0.74853
2023-08-11 13:33:15 +00:00
Victor Hugo Gomes
b05574babd Fix formatter instability with half-indented comment (#6460)
## Summary
The bug was happening in this
[loop](75f402eb82/crates/ruff_python_formatter/src/comments/placement.rs (L545)).

Basically, In the first iteration of the loop, the `comment_indentation`
is bigger than `child_indentation` (`comment_indentation` is 7 and
`child_indentation` is 4) making the `Ordering::Greater` branch execute.
Inside the `Ordering::Greater` branch, the `if` block gets executed,
resulting in the update of these variables.
```rust
parent_body = current_body;                    
current_body = Some(last_child_in_current_body);
last_child_in_current_body = nested_child;
```
In the second iteration of the loop, `comment_indentation` is smaller
than `child_indentation` (`comment_indentation` is 7 and
`child_indentation` is 8) making the `Ordering::Less` branch execute.
Inside the `Ordering::Less` branch, the `if` block gets executed, this
is where the bug was happening. At this point `parent_body` should be a
`StmtFunctionDef` but it was a `StmtClassDef`. Causing the comment to be
incorrectly formatted.

That happened for the following code:
```python
class A:
    def f():
        pass
       # strangely indented comment

print()
```

There is only one problem that I couldn't figure it out a solution, the
variable `current_body` in this
[line](75f402eb82/crates/ruff_python_formatter/src/comments/placement.rs (L542C5-L542C49))
now gives this warning _"value assigned to `current_body` is never read
maybe it is overwritten before being read?"_
Any tips on how to solve that?

Closes #5337

## Test Plan

Add new test case.

---------

Co-authored-by: konstin <konstin@mailbox.org>
2023-08-11 11:21:16 +00:00
konsti
0ef6af807b Implement DerefMut for WithNodeLevel (#6443)
**Summary** Implement `DerefMut` for `WithNodeLevel` so it can be used
in the same way as `PyFormatter`. I want this for my WIP upstack branch
to enable `.fmt(f)` on `WithNodeLevel` context. We could extend this to
remove the other two method from `WithNodeLevel`.
2023-08-11 10:41:48 +00:00
David Szotten
f091b46497 move comments from expressions in f-strings out (#6481) 2023-08-11 09:22:30 +02:00
Charlie Marsh
2cedb401bd Force parentheses for named expressions in more contexts (#6494)
See:
https://github.com/astral-sh/ruff/pull/6436#issuecomment-1673583888.
2023-08-11 01:54:46 -04:00
Charlie Marsh
2e5c81b202 Ensure that B006 autofix respects docstrings (#6493)
## Summary

Some follow-ups to https://github.com/astral-sh/ruff/pull/6131 to ensure
that fixes are inserted _after_ function docstrings, and that fixes are
robust to a bunch of edge cases.

## Test Plan

`cargo test`
2023-08-11 01:03:56 -04:00
Charlie Marsh
cc151c35a8 Respect dummy-variable-rgx for unused bound exceptions (#6492)
## Summary

This PR respects our unused variable regex when flagging bound
exceptions, so that you no longer get a violation for, e.g.:

```python
def f():
    try:
        pass
    except Exception as _:
        pass
```

This is an odd pattern, but I think it's surprising that the regex
_isn't_ respected here.

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

## Test Plan

`cargo test`
2023-08-11 04:02:02 +00:00
Charlie Marsh
563374503f Enable short URLs in the playground (#6383)
## Summary

This PR adds a [Workers
KV](https://developers.cloudflare.com/workers/runtime-apis/kv/)-based
database to the playground, which enables us to associate shared
snippets with a stable ID, which in turn allows us to generate short
URLs, rather than our existing extremely-long URLs.

For now, the URLs are based on UUID, so they look like
https://play.ruff.rs/a1c40d58-f643-4a3e-bc23-15021e16acef. (This URL
isn't expected to work, as the playground isn't deployed; it's only
included as an example.)

There are no visible changes in the UI here -- you still click the
"Share" button, which copies the link to your URL. There's no
user-visible latency either -- KV is very fast.

For context, with Workers KV, we provision a Workers KV store in our
Cloudflare account (`wrangler kv:namespace create "PLAYGROUND"`), and
then create a Cloudflare Worker that's bound to the KV store via the
`wrangler.toml`:

```toml
name = "db"
main = "src/index.ts"
compatibility_date = "2023-08-07"

kv_namespaces = [
  { binding = "PLAYGROUND", id = "672e16c4fb5e4887845973bf0e9f6021", preview_id = "0a96477e116540e5a6e1eab6d6e7523e" }
]
```

The KV store exists in perpetuity, while the Worker can be updated,
deployed, removed, etc. independently of the KV store. The Worker itself
has unfettered access to the KV store. The Worker is exposed publicly,
and just does some basic verification against the request host.
2023-08-11 02:31:09 +00:00
Charlie Marsh
95dea5c868 Respect tab width in line-length heuristic (#6491)
## Summary

In https://github.com/astral-sh/ruff/pull/5811, I suggested that we add
a heuristic to the overlong-lines check such that if the line had fewer
bytes than the character limit, we return early -- the idea being that a
single byte per character was the "worst case". I overlooked that this
isn't true for tabs -- with tabs, the "worst case" scenario is that
every byte is a tab, which can have a width greater than 1.

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

## Test Plan

`cargo test` with a new fixture borrowed from the issue, plus manual
testing.
2023-08-10 22:28:25 -04:00
Victor Hugo Gomes
eb68addf97 [pylint] Implement bad-dunder-name (W3201) (#6486)
## Summary

Checks for any misspelled dunder name method and for any method defined
with `__...__` that's not one of the pre-defined methods.

The pre-defined methods encompass all of Python's standard dunder
methods.

ref: #970

## Test Plan
Snapshots and manual runs of pylint.
2023-08-11 01:31:16 +00:00
Tom Kuson
9ff80a82b4 [pylint] Implement subprocess-run-check (W1510) (#6487)
## Summary

Implements [`subprocess-run-check`
(`W1510`)](https://pylint.readthedocs.io/en/latest/user_guide/messages/warning/subprocess-run-check.html)
as `subprocess-run-without-check` (`PLW1510`). Includes documentation.

Related to #970.

## Test Plan

`cargo test`
2023-08-10 20:54:53 -04:00
Charlie Marsh
84ae00c395 Allow os._exit accesses in SLF001 (#6490)
Closes https://github.com/astral-sh/ruff/issues/6483.
2023-08-11 00:54:38 +00:00
Zanie Blue
1050c4e104 Extend target-version documentation (#6482)
Closes https://github.com/astral-sh/ruff/issues/6462
2023-08-10 12:11:37 -05:00
Charlie Marsh
6706ae4828 Respect scoping rules when identifying builtins (#6468)
## Summary

Our `is_builtin` check did a naive walk over the parent scopes; instead,
it needs to (e.g.) skip symbols in a class scope if being called outside
of the class scope itself.

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

## Test Plan

`cargo test`
2023-08-10 10:20:09 -04:00
magic-akari
dc3275fe7f Improve Ruff Formatter Interoperability (#6472) 2023-08-10 14:39:53 +02:00
qdegraaf
50dab9cea6 [flake8-bugbear] Add autofix for B006 (#6131)
## Summary

Reopening of https://github.com/astral-sh/ruff/pull/4880 

One open TODO as described in:
https://github.com/astral-sh/ruff/pull/4880#discussion_r1265110215

FYI @charliermarsh seeing as you commented you wanted to do final review
and merge. @konstin @dhruvmanila @MichaReiser as previous reviewers.

# Old Description
## Summary

Adds an autofix for B006 turning mutable argument defaults into None and
setting their original value back in the function body if still `None`
at runtime like so:
```python
def before(x=[]):
    pass
    
def after(x=None):
    if x is None:
        x = []
    pass
```

## Test Plan

Added an extra test case to existing fixture with more indentation.
Checked results for all old examples.

NOTE: Also adapted the jupyter notebook test as this checked for B006 as
well.

## Issue link

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

---------

Co-authored-by: konstin <konstin@mailbox.org>
2023-08-10 11:06:40 +00:00
konsti
4811af0f0b Formatter: Add test cases for comments after opening parentheses (#6420)
**Summary** I collected all examples of end-of-line comments after
opening parentheses that i could think of so we get a comprehensive view
at the state of their formatting (#6390).

This PR intentionally only adds tests cases without any changes in
formatting. We need to decide which exact formatting we want, ideally in
terms of these test files, and implement this in follow-up PRs.

~~One stability check is still deactivated pending
https://github.com/astral-sh/ruff/pull/6386.~~
2023-08-10 08:34:03 +00:00
konsti
39beeb61f7 Track formatting all comments
We currently don't format all comments as match statements are not yet implemented. We can work around this for the top level match statement by setting them manually formatted but the mocked-out top level match doesn't call into its children so they would still have unformatted comments
2023-08-10 09:19:27 +02:00
Micha Reiser
e2f7862404 Preserve dangling f-string comments
<!--
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 fixes the issue where the FString formatting dropped dangling comments between the string parts.

```python
result_f = (
    f'  File "{__file__}", line {lineno_f+1}, in f\n'
    '    f()\n'
    # XXX: The following line changes depending on whether the tests
    # are run through the interactive interpreter or with -m
    # It also varies depending on the platform (stack size)
    # Fortunately, we don't care about exactness here, so we use regex
    r'  \[Previous line repeated (\d+) more times\]' '\n'
    'RecursionError: maximum recursion depth exceeded\n'
)
```

The solution here isn't ideal because it re-introduces the `enclosing_parent` on `DecoratedComment` but it is the easiest fix that I could come up. 
I didn't spend more time finding another solution becaues I think we have to re-write most of the fstring formatting with the upcoming Python 3.12 support (because lexing the individual parts as we do now will no longer work).

closes #6440

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

## Test Plan

`cargo test`

The child PR testing that all comments are formatted should now pass
2023-08-10 09:11:25 +02:00
Micha Reiser
ac5c8bb3b6 Add AnyNodeRef.visit_preorder
<!--
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 the `AnyNodeRef.visit_preorder` method. I'll need this method to mark all comments of a suppressed node's children as formatted (in debug builds). 

I'm not super happy with this because it now requires a double-dispatch where the `walk_*` methods call into `node.visit_preorder` and the `visit_preorder` then calls back into the visitor. Meaning,
the new implementation now probably results in way more function calls. The other downside is that `AnyNodeRef` now contains code that is difficult to auto-generate. This could be mitigated by extracting the `visit_preorder` method into its own `VisitPreorder` trait. 

Anyway, this approach solves the need and avoids duplicating the visiting code once more. 

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

## Test Plan

`cargo test`

<!-- How was it tested? -->
2023-08-10 08:35:09 +02:00
Micha Reiser
c1bc67686c Use SimpleTokenizer in max_lines (#6451) 2023-08-10 08:13:14 +02:00
Charlie Marsh
7eea0e94a2 Add containers to E721 types (#6469)
Related to https://github.com/astral-sh/ruff/issues/6465.
2023-08-10 02:34:51 +00:00
Charlie Marsh
0252995973 Document FormatSpec fields (#6458) 2023-08-09 18:13:29 -04:00
Charlie Marsh
627f475b91 Avoid applying PYI055 to runtime-evaluated annotations (#6457)
## Summary

The use of `|` as a union operator is not always safe, if a type
annotation is evaluated in a runtime context. For example, this code
errors at runtime:

```python
import httpretty
import requests_mock

item: type[requests_mock.Mocker | httpretty] = requests_mock.Mocker
```

However, it's fine in a `.pyi` file, with `__future__` annotations`, or
if the annotation is in a non-evaluated context, like:

```python
def func():
    item: type[requests_mock.Mocker | httpretty] = requests_mock.Mocker
```

This PR modifies the rule to avoid enforcing in those invalid,
runtime-evaluated contexts.

Closes https://github.com/astral-sh/ruff/issues/6455.
2023-08-09 16:46:41 -04:00
Charlie Marsh
395bb31247 Improve counting of message arguments when msg is provided as a keyword (#6456)
Closes https://github.com/astral-sh/ruff/issues/6454.
2023-08-09 20:39:10 +00:00
Zanie Blue
3ecd263b4d Bump version to 0.0.284 (#6453)
## What's Changed

This release fixes a few bugs, notably the previous release announced a
breaking change where the default target
Python version changed from 3.10 to 3.8 but it was not applied. Thanks
to @rco-ableton for fixing this in
https://github.com/astral-sh/ruff/pull/6444

### Bug Fixes
* Do not trigger `S108` if path is inside `tempfile.*` call by
@dhruvmanila in https://github.com/astral-sh/ruff/pull/6416
* Do not allow on zero tab width by @tjkuson in
https://github.com/astral-sh/ruff/pull/6429
* Fix false-positive in submodule resolution by @charliermarsh in
https://github.com/astral-sh/ruff/pull/6435

## New Contributors
* @rco-ableton made their first contribution in
https://github.com/astral-sh/ruff/pull/6444

**Full Changelog**:
https://github.com/astral-sh/ruff/compare/v0.0.283...v0.0.284
2023-08-09 13:32:33 -05:00
Charlie Marsh
6acf07c5c4 Use latest Python version by default in tests (#6448)
## Summary

Use the same Python version by default for all tests (our
latest-supported version).

## Test Plan

`cargo test`

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-08-09 15:22:39 +00:00
Charlie Marsh
38b9fb8bbd Set a default on PythonVersion (#6446)
## Summary

I think it makes sense for `PythonVersion::default()` to return our
minimum-supported non-EOL version.

## Test Plan

`cargo test`

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-08-09 15:19:27 +00:00
dependabot[bot]
e4f57434a2 ci(deps): bump cloudflare/wrangler-action from 2.0.0 to 3.0.0 (#6398)
Bumps
[cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action)
from 2.0.0 to 3.0.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="089567dec4"><code>089567d</code></a>
feat: rewrite Wrangler Action in TypeScript</li>
<li>See full diff in <a
href="https://github.com/cloudflare/wrangler-action/compare/2.0.0...3.0.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cloudflare/wrangler-action&package-manager=github_actions&previous-version=2.0.0&new-version=3.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 10:17:43 -05:00
Dhruv Manilawala
6a64f2289b Rename Magic* to IpyEscape* (#6395)
## Summary

This PR renames the `MagicCommand` token to `IpyEscapeCommand` token and
`MagicKind` to `IpyEscapeKind` type to better reflect the purpose of the
token and type. Similarly, it renames the AST nodes from `LineMagic` to
`IpyEscapeCommand` prefixed with `Stmt`/`Expr` wherever necessary.

It also makes renames from using `jupyter_magic` to
`ipython_escape_commands` in various function names.

The mode value is still `Mode::Jupyter` because the escape commands are
part of the IPython syntax but the lexing/parsing is done for a Jupyter
notebook.

### Motivation behind the rename:
* IPython codebase defines it as "EscapeCommand" / "Escape Sequences":
* Escape Sequences:
292e3a2345/IPython/core/inputtransformer2.py (L329-L333)
* Escape command:
292e3a2345/IPython/core/inputtransformer2.py (L410-L411)
* The word "magic" is used mainly for the actual magic commands i.e.,
the ones starting with `%`/`%%`
(https://ipython.readthedocs.io/en/stable/interactive/reference.html#magic-command-system).
So, this avoids any confusion between the Magic token (`%`, `%%`) and
the escape command itself.
## Test Plan

* `cargo test` to make sure all renames are done correctly.
* `grep` for `jupyter_escape`/`magic` to make sure all renames are done
correctly.
2023-08-09 13:28:18 +00:00
Charlie Marsh
3bf1c66cda Group function definition parameters with return type annotations (#6410)
## Summary

This PR removes the group around function definition parameters, instead
grouping the parameters with the type parameters and return type
annotation.

This increases Zulip's similarity score from 0.99385 to 0.99699, so it's
a meaningful improvement. However, there's at least one stability error
that I'm working on, and I'm really just looking for high-level feedback
at this point, because I'm not happy with the solution.

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

## Test Plan

Before:

- `zulip`: 0.99396
- `django`: 0.99784
- `warehouse`: 0.99578
- `build`: 0.75436
- `transformers`: 0.99407
- `cpython`: 0.75987
- `typeshed`: 0.74432

After:

- `zulip`: 0.99702
- `django`: 0.99784
- `warehouse`: 0.99585
- `build`: 0.75623
- `transformers`: 0.99470
- `cpython`: 0.75988
- `typeshed`: 0.74853
2023-08-09 12:13:58 +00:00
rco-ableton
eaada0345c Set default version to py38 (#6444)
## Summary

In https://github.com/astral-sh/ruff/pull/6397, the documentation was
updated stating that the default target-version is now "py38", but the
actual default value wasn't updated and remained py310. This commit
updates the default value to match what the documentation says.
2023-08-09 12:08:47 +00:00
Micha Reiser
a39dd76d95 Add enter and leave_node methods to Preoder visitor (#6422) 2023-08-09 09:09:00 +00:00
Dhruv Manilawala
e257c5af32 Add support for help end IPython escape commands (#6358)
## Summary

This PR adds support for a stricter version of help end escape
commands[^1] in the parser. By stricter, I mean that the escape tokens
are only at the end of the command and there are no tokens at the start.
This makes it difficult to implement it in the lexer without having to
do a lot of look aheads or keeping track of previous tokens.

Now, as we're adding this in the parser, the lexer needs to recognize
and emit a new token for `?`. So, `Question` token is added which will
be recognized only in `Jupyter` mode.

The conditions applied are the same as the ones in the original
implementation in IPython codebase (which is a regex):
* There can only be either 1 or 2 question mark(s) at the end
* The node before the question mark can be a `Name`, `Attribute`,
`Subscript` (only with integer constants in slice position), or any
combination of the 3 nodes.

## Test Plan

Added test cases for various combination of the possible nodes in the
command value position and update the snapshots.

fixes: #6359
fixes: #5030 (This is the final piece)

[^1]: https://github.com/astral-sh/ruff/pull/6272#issue-1833094281
2023-08-09 10:28:52 +05:30
Dhruv Manilawala
887a47cad9 Avoid S108 if path is inside tempfile.* call (#6416) 2023-08-09 10:22:31 +05:30
Charlie Marsh
a2758513de Fix false-positive in submodule resolution (#6435)
Closes https://github.com/astral-sh/ruff/issues/6433.
2023-08-09 02:36:39 +00:00
Tom Kuson
1b9fed8397 Error on zero tab width (#6429)
## Summary

Error if `tab-size` is set to zero (it is used as a divisor). Closes
#6423.

Also fixes a typo.

## Test Plan

Running ruff with a config

```toml
[tool.ruff]
tab-size = 0
```

returns an error message to the user saying that `tab-size` must be
greater than zero.
2023-08-08 16:51:37 -04:00
Charlie Marsh
55d6fd53cd Treat comments on open parentheses in return annotations as dangling (#6413)
## Summary

Given:

```python
def double(a: int) -> ( # Hello
    int
):
    return 2*a
```

We currently treat `# Hello` as a trailing comment on the parameters
(`(a: int)`). This PR adds a placement method to instead treat it as a
dangling comment on the function definition itself, so that it gets
formatted at the end of the definition, like:

```python
def double(a: int) -> int:  # Hello
    return 2*a
```

The formatting in this case is unchanged, but it's incorrect IMO for
that to be a trailing comment on the parameters, and that placement
leads to an instability after changing the grouping in #6410.

Fixing this led to a _different_ instability related to tuple return
type annotations, like:

```python
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> (  # type: ignore[override]
):
    ...
```

(This is a real example.)

To fix, I had to special-case tuples in that spot, though I'm not
certain that's correct.
2023-08-08 16:48:38 -04:00
Zanie Blue
d33618062e Improve documentation for PLE1300 (#6430) 2023-08-08 20:16:36 +00:00
Charlie Marsh
c7703e205d Move empty_parenthesized into the parentheses.rs (#6403)
## Summary

This PR moves `empty_parenthesized` such that it's peer to
`parenthesized`, and changes the API to better match that of
`parenthesized` (takes `&str` rather than `StaticText`, has a
`with_dangling_comments` method, etc.).

It may be intentionally _not_ part of `parentheses.rs`, but to me
they're so similar that it makes more sense for them to be in the same
module, with the same API, etc.
2023-08-08 19:17:17 +00:00
Zanie Blue
fe9590f39f Bump version number to 0.0.283 (#6407) 2023-08-08 12:31:30 -05:00
konsti
e769c74899 Check .git in formatter progress checkouts for build (#6387)
From the formatter progress CI logs:
```
2023-08-07T03:49:02.5178602Z + mkdir -p /home/runner/work/ruff/ruff/target/progress_projects
2023-08-07T03:49:02.5193474Z + '[' '!' -d /home/runner/work/ruff/ruff/target/progress_projects/build ']'
2023-08-07T03:49:02.5194228Z + '[' '!' -d /home/runner/work/ruff/ruff/target/progress_projects/django ']'
2023-08-07T03:49:02.5194966Z + git clone --filter=tree:0 https://github.com/django/django /home/runner/work/ruff/ruff/target/progress_projects/django
2023-08-07T03:49:02.5209260Z Cloning into '/home/runner/work/ruff/ruff/target/progress_projects/django'...
```
```
2023-08-07T03:51:17.4726088Z 2023-08-07T03:51:17.472404Z ERROR Failed /home/runner/work/ruff/ruff/target/progress_projects/build: no python files in ["/home/runner/work/ruff/ruff/target/progress_projects/build"]
```

Seems that build exists but is an empty cached folder. These changes
should fix this by a) checking for `.git` instead of just the folder
existing b) running the commit checkout unconditionally. The latter is
also important if we ever want to update the SHAs.
2023-08-08 17:46:46 +02:00
Dhruv Manilawala
d815a25b11 Update StmtMatch formatting snapshots (#6427) 2023-08-08 16:45:02 +02:00
Dhruv Manilawala
001aa486df Add formatting for StmtMatch (#6286)
## Summary

This PR adds support for `StmtMatch` with subs for `MatchCase`.

## Test Plan

Add a few additional test cases around `match` statement, comments, line
breaks.

resolves: #6298
2023-08-08 18:48:49 +05:30
Charlie Marsh
87984e9ac7 Expand parents whenever open-parenthesis comments are present (#6389)
## Summary

This PR modifies our dangling-open-parenthesis handling to _always_
expand the parent expression.

So, for example, given:

```python
a = int(  # type: ignore
    int(  # type: ignore
        int(  # type: ignore
            6
        )
    )
)
```

We now retain that as stable formatting, instead of truncating like:

```python
a = int(int(int(6)))  # comment  # comment  # comment
```

Note that Black _does_ collapse comments like this _unless_ they're `#
type: ignore` comments, and perhaps in some other cases, so this is an
intentional deviation
([playground](https://black.vercel.app/?version=main&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4AFEAHpdAD2IimZxl1N_WlOfrjryFgvD4ScVsKPztqdHDGJUg5knO0JCdpUfW1IrWSNmIJPx95s0hP-pRNkCQNH64-eIznIvXjeWBQ5-qax0oNw4yMOuhwr2azvMRZaEB5r8IXVPHmRCJp7fe7y4290u1zzxqK_nAi6q_5sI-jsAAAAA8HgZ9V7hG3QAAZYBxQIAAGnCHXexxGf7AgAAAAAEWVo=)).
2023-08-08 08:45:20 -04:00
Piotr
6aefe71c56 Fix name of rule in example of extend-per-file-ignores in options.rs (#6417)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

-->

## Summary

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

Fix name of rule in example of `extend-per-file-ignores` in `options.rs`
file.

It was `E401` but in configuration example `E402` was listed. Just a
tiny mismatch.

## Test Plan

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

Just by my eyes :).
2023-08-08 11:24:41 +02:00
konsti
90ba40c23c Fix zulip unstable formatting with end-of-line comments (#6386)
## Bug

Given
```python
x = () - (#
)
```
the comment is a dangling comment of the empty tuple. This is an
end-of-line comment so it may move after the expression. It still
expands the parent, so the operator breaks:
```python
x = (
    ()
    - ()  #
)
```
In the next formatting pass, the comment is not a trailing tuple but a
trailing bin op comment, so the bin op doesn't break anymore. The
comment again expands the parent, so we still add the superfluous
parentheses
```python
x = (
    () - ()  #
)
```

## Fix

The new formatting is to keep the comment on the empty tuple. This is a
log uglier and again has additional outer parentheses, but it's stable:
```python
x = (
    ()
    - (  #
    )
)
```

## Alternatives

Black formats all the examples above as
```python
x = () - ()  #
```
which i find better. 

I would be happy about any suggestions for better solutions than the
current one. I'd mainly need a workaround for expand parent having an
effect on the bin op instead of first moving the comment to the end and
then applying expand parent to the assign statement.
2023-08-08 09:15:35 +00:00
Micha Reiser
2bd345358f Simplify parenthesized formatting (#6419) 2023-08-08 08:50:57 +00:00
Dhruv Manilawala
289d1e85bf Manually parenthesize tuple expr in B014 autofix (#6415)
## Summary

Manually add the parentheses around tuple expressions for the autofix in
`B014`.
This is also done in various other autofixes as well such as for
[`RUF005`](6df5ab4098/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs (L183-L184)),
[`UP024`](6df5ab4098/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs (L137-L137)).

### Alternate Solution

An alternate solution would be to fix this in the `Generator` itself by
checking
if the tuple expression needs to be generated at the top-level or not.
If so,
then always add the parentheses.

```rust
                } else if level == 0 {
                    // Top-level tuples are always parenthesized.
                    self.p("(");
                    let mut first = true;
                    for elt in elts {
                        self.p_delim(&mut first, ", ");
                        self.unparse_expr(elt, precedence::COMMA);
                    }
                    self.p_if(elts.len() == 1, ",");
                    self.p(")");
```

## Test Plan

Add a regression test for this case in `B014`.

fixes: #6412
2023-08-08 09:14:18 +05:30
Anders Kaseorg
6df5ab4098 Remove duplicate line from project structure docs (#6408)
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-08-07 20:08:51 -04:00
Zanie Blue
90c9aa2992 Add support for simple generic type variables to UP040 (#6314)
Extends #6289 to support moving type variable usage in type aliases to
use PEP-695.

Does not remove the possibly unused type variable declaration.
Presumably this is handled by other rules, but is not working for me.

Does not handle type variables with bounds or variance declarations yet.

Part of #4617
2023-08-07 16:22:06 -05:00
Charlie Marsh
927cfc9564 Respect file-level # ruff: noqa suppressions for unused-noqa rule (#6405)
## Summary

We weren't respecting `# ruff: noqa: RUF100`, i.e., file-level
suppressions for the `unused-noqa` rule itself.

Closes https://github.com/astral-sh/ruff/issues/6385.
2023-08-07 16:33:01 -04:00
Charlie Marsh
3d06fe743d Change model: &SemanticModel to semantic: &SemanticModel (#6406)
Use the same naming conventions everywhere. See:
https://github.com/astral-sh/ruff/pull/6314/files#r1284457874.
2023-08-07 16:32:55 -04:00
Charlie Marsh
404e334fec Rename ArgumentSeparator to ParameterSeparator (#6404)
To mirror the rename from `Arguments` to `Parameters`.
2023-08-07 15:46:28 -04:00
Charlie Marsh
26098b8d91 Extend nested union detection to handle bitwise or Union expressions (#6399)
## Summary

We have some logic in the expression analyzer method to avoid
re-checking the inner `Union` in `Union[Union[...]]`, since the methods
that analyze `Union` expressions already recurse. Elsewhere, we have
logic to avoid re-checking the inner `|` in `int | (int | str)`, for the
same reason.

This PR unifies that logic into a single method _and_ ensures that, just
as we recurse over both `Union` and `|`, we also detect that we're in
_either_ kind of nested union.

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

## Test Plan

Added some new snapshots.
2023-08-07 15:17:26 -04:00
Charlie Marsh
98d4657961 Avoid attempting to fix .format(...) calls with too-few-arguments (#6401)
## Summary

We can anticipate earlier that this will error, so we should avoid
flagging the error at all. Specifically, we're talking about cases like
`"{1} {0}".format(*args)"`, in which we'd need to reorder the arguments
in order to remove the `1` and `0`, but we _can't_ reorder the arguments
since they're not statically analyzable.

Closes https://github.com/astral-sh/ruff/issues/6388.
2023-08-07 19:13:22 +00:00
Charlie Marsh
8919b6ad9a Add a with_dangling_comments to the parenthesized formatter (#6402)
See: https://github.com/astral-sh/ruff/pull/6376#discussion_r1285514328.
2023-08-07 19:12:12 +00:00
Zanie Blue
bb96647d66 Assume Python 3.8 instead of 3.10 for target version (#6397)
The target version should be the oldest supported version instead of an
arbitary version. Since 3.7 is EOL, we should use 3.8. I would like to
follow this up with more comprehensive default detection based on the
environment.
2023-08-07 13:48:06 -05:00
Charlie Marsh
df1591b3c2 Remove outdated TODO (#6400)
See: https://github.com/astral-sh/ruff/pull/6376#discussion_r1285539278.
2023-08-07 18:33:18 +00:00
Charlie Marsh
a637b8b3a3 Fixup comment handling on opening parenthesis in function definition (#6381)
## Summary

I noticed some deviations in how we treat dangling comments that hug the
opening parenthesis for function definitions.

For example, given:

```python
def f(  # first
    # second
):  # third
    ...
```

We currently format as:

```python
def f(
      # first
    # second
):  # third
    ...
```

This PR adds the proper opening-parenthesis dangling comment handling
for function parameters. Specifically, as with all other parenthesized
nodes, we now detect that dangling comment in `placement.rs` and handle
it in `parameters.rs`. We have to take some care in that file, since we
have multiple "kinds" of dangling comments, but I added a bunch of test
cases that we now format identically to Black.

## Test Plan

`cargo test`

Before:

- `zulip`: 0.99388
- `django`: 0.99784
- `warehouse`: 0.99504
- `transformers`: 0.99404
- `cpython`: 0.75913
- `typeshed`: 0.74364

After:

- `zulip`: 0.99386
- `django`: 0.99784
- `warehouse`: 0.99504
- `transformers`: 0.99404
- `cpython`: 0.75913
- `typeshed`: 0.74409

Meaningful improvement on `typeshed`, minor decrease on `zulip`.
2023-08-07 14:04:56 -04:00
Charlie Marsh
3f0eea6d87 Rename JoinedStr to FString in the AST (#6379)
## Summary

Per the proposal in https://github.com/astral-sh/ruff/discussions/6183,
this PR renames the `JoinedStr` node to `FString`.
2023-08-07 17:33:17 +00:00
Zanie Blue
999d88e773 Fix formatting of chained boolean operations (#6394)
Closes https://github.com/astral-sh/ruff/issues/6068

These commits are kind of a mess as I did some stumbling around here. 

Unrolls formatting of chained boolean operations to prevent nested
grouping which gives us Black-compatible formatting where each boolean
operation is on a new line.
2023-08-07 12:22:33 -05:00
Charlie Marsh
63ffadf0b8 Avoid omitting parentheses for trailing attributes on call expressions (#6322)
## Summary

This PR modifies our `can_omit_optional_parentheses` rules to ensure
that if we see a call followed by an attribute, we treat that as an
attribute access rather than a splittable call expression.

This in turn ensures that we wrap like:

```python
ct_match = aaaaaaaaaaact_id == self.get_content_type(
    obj=rel_obj, using=instance._state.db
)
```

For calls, but:

```python
ct_match = (
    aaaaaaaaaaact_id == self.get_content_type(obj=rel_obj, using=instance._state.db).id
)
```

For calls with trailing attribute accesses.

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

## Test Plan

Similarity index before:

- `zulip`: 0.99436
- `django`: 0.99779
- `warehouse`: 0.99504
- `transformers`: 0.99403
- `cpython`: 0.75912
- `typeshed`: 0.72293

And after:

- `zulip`: 0.99436
- `django`: 0.99780
- `warehouse`: 0.99504
- `transformers`: 0.99404
- `cpython`: 0.75913
- `typeshed`: 0.72293
2023-08-07 13:18:58 -04:00
Charlie Marsh
c439435615 Use dedicated AST nodes on MemberKind (#6374)
## Summary

This PR leverages the unified function definition node to add precise
AST node types to `MemberKind`, which is used to power our docstring
definition tracking (e.g., classes and functions, whether they're
methods or functions or nested functions and so on, whether they have a
docstring, etc.). It was painful to do this in the past because the
function variants needed to support a union anyway, but storing precise
nodes removes like a dozen panics.

No behavior changes -- purely a refactor.

## Test Plan

`cargo test`
2023-08-07 17:17:58 +00:00
Charlie Marsh
daefa74e9a Remove async AST node variants for with, for, and def (#6369)
## Summary

Per the suggestion in
https://github.com/astral-sh/ruff/discussions/6183, this PR removes
`AsyncWith`, `AsyncFor`, and `AsyncFunctionDef`, replacing them with an
`is_async` field on the non-async variants of those structs. Unlike an
interpreter, we _generally_ have identical handling for these nodes, so
separating them into distinct variants adds complexity from which we
don't really benefit. This can be seen below, where we get to remove a
_ton_ of code related to adding generic `Any*` wrappers, and a ton of
duplicate branches for these cases.

## Test Plan

`cargo test` is unchanged, apart from parser snapshots.
2023-08-07 16:36:02 +00:00
Charlie Marsh
c895252aae Remove RefEquality (#6393)
## Summary

See discussion in
https://github.com/astral-sh/ruff/pull/6351#discussion_r1284996979. We
can remove `RefEquality` entirely and instead use a text offset for
statement keys, since no two statements can start at the same text
offset.

## Test Plan

`cargo test`
2023-08-07 16:04:50 +00:00
Charlie Marsh
9328606843 Remove Statements#parent (#6392)
Discussed in
https://github.com/astral-sh/ruff/pull/6351#discussion_r1284997065.
2023-08-07 15:41:02 +00:00
Dhruv Manilawala
e4a4660925 Support help end escape command with priority (#6272)
## Summary

This PR adds support for help end escape command in the lexer.

### What are "help end escape commands"?

First, the escape commands are special IPython syntax which enhances the
functionality for the IPython REPL. There are 9 types of escape kinds
which are recognized by the tokens which are present at the start of the
command (`?`, `??`, `!`, `!!`, etc.).

Here, the help command is using either the `?` or `??` token at the
start (`?str.replace` for example). Those 2 tokens are also supported
when they're at the end of the command (`str.replace?`), but the other
tokens aren't supported in that position.

There are mainly two types of help end escape commands:
1. Ending with either `?` or `??`, but it also starts with one of the
escape tokens (`%matplotlib?`)
2. On the other hand, there's a stricter version for (1) which doesn't
start with any escape tokens (`str.replace?`)

This PR adds support for (1) while (2) will be supported in the parser.

### Priority

Now, if the command starts and ends with an escape token, how do we
decide the kind of this command? This is where priority comes into
picture. This is simple as there's only one priority where `?`/`??` at
the end takes priority over any other escape token and all of the other
tokens are at the same priority. Remember that only `?`/`??` at the end
is considered valid.

This is mainly useful in the case where someone would want to invoke the
help command on the magic command itself. For example, in `%matplotlib?`
the help command takes priority which means that we want help for the
`matplotlib` magic function instead of calling the magic function
itself.

### Specification

Here's where things get a bit tricky. What if there are question mark
tokens at both ends. How do we decide if it's `Help` (`?`) kind or
`Help2` (`??`) kind?

|     | Magic       | Value     | Kind    |
| --- | ---         | ---       | ---     |
| 1   | `?foo?`     | `foo`     | `Help`  |
| 2   | `??foo?`    | `foo`     | `Help`  |
| 3   | `?foo??`    | `foo`     | `Help2` |
| 4   | `??foo??`   | `foo`     | `Help2` |
| 5   | `???foo??`  | `foo`     | `Help2` |
| 6   | `??foo???`  | `foo???`  | `Help2` |
| 7   | `???foo???` | `?foo???` | `Help2` |

Looking at the above table:

- The question mark tokens on the right takes priority over the ones on
the left but only if the number of question mark on the right is 1 or 2.
- If there are more than 2 question mark tokens on the right side, then
the left side is used to determine the same.
- If the right side is used to determine the kind, then all of the
question marks and whitespaces on the left side are ignored in the
`value`, but if it’s the other way around, then all of the extra
question marks are part of the `value`.

### References

- IPython implementation using the regex:
292e3a2345/IPython/core/inputtransformer2.py (L454-L462)
- Priorities:
292e3a2345/IPython/core/inputtransformer2.py (L466-L469)

## Test Plan

Add a bunch of test cases for the lexer and verify that it matches the
behavior of
IPython transformer.

resolves: #6357
2023-08-07 21:01:02 +05:30
Charlie Marsh
b21abe0a57 Use separate structs for expression and statement tracking (#6351)
## Summary

This PR fixes the performance degradation introduced in
https://github.com/astral-sh/ruff/pull/6345. Instead of using the
generic `Nodes` structs, we now use separate `Statement` and
`Expression` structs. Importantly, we can avoid tracking a bunch of
state for expressions that we need for parents: we don't need to track
reference-to-ID pointers (we just have no use-case for this -- I'd
actually like to remove this from statements too, but we need it for
branch detection right now), we don't need to track depth, etc.

In my testing, this entirely removes the regression on all-rules, and
gets us down to 2ms slower on the default rules (as a crude hyperfine
benchmark, so this is within margin of error IMO).

No behavioral changes.
2023-08-07 15:27:42 +00:00
Charlie Marsh
61d3977f95 Make the statement vector private on SemanticModel (#6348)
## Summary

Instead, expose these as methods, now that we can use a reasonable
nomenclature on the API.
2023-08-07 15:02:14 +00:00
Charlie Marsh
bae87fa016 Rename semantic model methods to use current_* prefix (#6347)
## Summary

This PR attempts to draw a clearer divide between "methods that take
(e.g.) an expression or statement as input" and "methods that rely on
the _current_ expression or statement" in the semantic model, by
renaming methods like `stmt()` to `current_statement()`.

This had led to confusion in the past. For example, prior to this PR, we
had `scope()` (which returns the current scope), and `parent_scope`,
which returns the parent _of a scope that's passed in_. Now, the API is
clearer: `current_scope` returns the current scope, and `parent_scope`
takes a scope as argument and returns its parent.

Per above, I also changed `stmt` to `statement` and `expr` to
`expression`.
2023-08-07 14:44:49 +00:00
Charlie Marsh
b763973357 Avoid hard line break after dangling open-parenthesis comments (#6380)
## Summary

Given:

```python
[  # comment
    first,
    second,
    third
]  # another comment
```

We were adding a hard line break as part of the formatting of `#
comment`, which led to the following formatting:

```python
[first, second, third]  # comment
  # another comment
```

Closes https://github.com/astral-sh/ruff/issues/6367.
2023-08-07 14:15:32 +00:00
Charlie Marsh
63692b3798 Use parenthesized_with_dangling_comments in arguments formatter (#6376)
## Summary

Fixes an instability whereby this:

```python
def get_recent_deployments(threshold_days: int) -> Set[str]:
    # Returns a list of deployments not older than threshold days
    # including `/root/zulip` directory if it exists.
    recent = set()
    threshold_date = datetime.datetime.now() - datetime.timedelta(  # noqa: DTZ005
        days=threshold_days
    )
```

Was being formatted as:

```python
def get_recent_deployments(threshold_days: int) -> Set[str]:
    # Returns a list of deployments not older than threshold days
    # including `/root/zulip` directory if it exists.
    recent = set()
    threshold_date = (
        datetime.datetime.now()
        - datetime.timedelta(days=threshold_days)  # noqa: DTZ005
    )
```

Which was in turn being formatted as:

```python
def get_recent_deployments(threshold_days: int) -> Set[str]:
    # Returns a list of deployments not older than threshold days
    # including `/root/zulip` directory if it exists.
    recent = set()
    threshold_date = (
        datetime.datetime.now() - datetime.timedelta(days=threshold_days)  # noqa: DTZ005
    )
```

The second-to-third formattings still differs from Black because we
aren't taking the line suffix into account when splitting
(https://github.com/astral-sh/ruff/issues/6377), but the first
formatting is correct and should be unchanged (i.e., the first-to-second
formattings is incorrect, and fixed here).

## Test Plan

`cargo run --bin ruff_dev -- format-dev --stability-check ../zulip`
2023-08-07 09:43:57 -04:00
Charlie Marsh
89e4e038b0 Store expression hierarchy in semantic model snapshots (#6345)
## Summary

When we iterate over the AST for analysis, we often process nodes in a
"deferred" manner. For example, if we're analyzing a function, we push
the function body onto a deferred stack, along with a snapshot of the
current semantic model state. Later, when we analyze the body, we
restore the semantic model state from the snapshot. This ensures that we
know the correct scope, hierarchy of statement parents, etc., when we go
to analyze the function body.

Historically, we _haven't_ included the _expression_ hierarchy in the
model snapshot -- so we track the current expression parents in the
visitor, but we never save and restore them when processing deferred
nodes. This can lead to subtle bugs, in that methods like
`expr_parent()` aren't guaranteed to be correct, if you're in a deferred
visitor.

This PR migrates expression tracking to mirror statement tracking
exactly. So we push all expressions onto an `IndexVec`, and include the
current expression on the snapshot. This ensures that `expr_parent()`
and related methods are "always correct" rather than "sometimes
correct".

There's a performance cost here, both at runtime and in terms of memory
consumption (we now store an additional pointer for every expression).
In my hyperfine testing, it's about a 1% performance decrease for
all-rules on CPython (up to 533.8ms, from 528.3ms) and a 4% performance
decrease for default-rules on CPython (up to 212ms, from 204ms).
However... I think this is worth it given the incorrectness of our
current approach. In the future, we may want to reconsider how we do
these upward traversals (e.g., with something like a red-green tree).
(**Note**: in https://github.com/astral-sh/ruff/pull/6351, the slowdown
seems to be entirely removed.)
2023-08-07 09:42:04 -04:00
Tom Kuson
5d2a4ebc99 Add documentation to subprocess-with[out]-shell-equals-true rules (#6373) 2023-08-07 03:48:36 +00:00
Harutaka Kawamura
9c3fbcdf4a Add PT011 and PT012 docs (#6362) 2023-08-06 21:28:24 -04:00
Konrad Listwan-Ciesielski
61532e8aad Add DTZ003 and DTZ004 docs (#6223)
Changes:
- Fixes typo and repeated phrase in `DTZ002`
- Adds docs for `DTZ003`
- Adds docs for `DTZ004`
- Adds example for <=Python3.10 in `DTZ001`

Related to: https://github.com/astral-sh/ruff/issues/2646
2023-08-07 01:21:14 +00:00
Charlie Marsh
9171e97d15 Avoid allocation in no-signature (#6375) 2023-08-06 15:27:56 +00:00
Charlie Marsh
a5a29bb8d6 Revert change to require_git(false) in WalkBuilder (#6368)
## Summary

This was changed to fix https://github.com/astral-sh/ruff/issues/5930
(respect `.gitignore` for unzipped source repositories), but led to
undesirable behavior whereby `.gitignore` files in parent directories
are respected regardless of whether you're working in a child git
repository (see: https://github.com/astral-sh/ruff/issues/6335). The
latter is a bigger problem than the former is an important use-case to
support, so pragmatically erring on the side of a revert.

Closes https://github.com/astral-sh/ruff/issues/6335.
2023-08-05 19:45:50 +00:00
Zixuan Li
be657f5e7e Respect typing_extensions imports of Annotated for B006. (#6361)
`typing_extensions.Annotated` should be treated the same way as
`typing.Annotated`.
2023-08-05 17:39:52 +00:00
Charlie Marsh
76148ddb76 Store call paths rather than stringified names (#6102)
## Summary

Historically, we've stored "qualified names" on our
`BindingKind::Import`, `BindingKind::SubmoduleImport`, and
`BindingKind::ImportFrom` structs. In Ruff, a "qualified name" is a
dot-separated path to a symbol. For example, given `import foo.bar`, the
"qualified name" would be `"foo.bar"`; and given `from foo.bar import
baz`, the "qualified name" would be `foo.bar.baz`.

This PR modifies the `BindingKind` structs to instead store _call paths_
rather than qualified names. So in the examples above, we'd store
`["foo", "bar"]` and `["foo", "bar", "baz"]`. It turns out that this
more efficient given our data access patterns. Namely, we frequently
need to convert the qualified name to a call path (whenever we call
`resolve_call_path`), and it turns out that we do this operation enough
that those conversations show up on benchmarks.

There are a few other advantages to using call paths, rather than
qualified names:

1. The size of `BindingKind` is reduced from 32 to 24 bytes, since we no
longer need to store a `String` (only a boxed slice).
2. All three import types are more consistent, since they now all store
a boxed slice, rather than some storing an `&str` and some storing a
`String` (for `BindingKind::ImportFrom`, we needed to allocate a
`String` to create the qualified name, but the call path is a slice of
static elements that don't require that allocation).
3. A lot of code gets simpler, in part because we now do call path
resolution "earlier". Most notably, for relative imports (`from .foo
import bar`), we store the _resolved_ call path rather than the relative
call path, so the semantic model doesn't have to deal with that
resolution. (See that `resolve_call_path` is simpler, fewer branches,
etc.)

In my testing, this change improves the all-rules benchmark by another
4-5% on top of the improvements mentioned in #6047.
2023-08-05 15:21:50 +00:00
Harutaka Kawamura
501f537cb8 Avoid auto-fixing UP031 if there are comments within the right-hand side (#6364) 2023-08-05 11:14:29 -04:00
Dhruv Manilawala
1ac2699b5e Update F841 autofix to not remove line magic expr (#6141)
## Summary

Update `F841` autofix to not remove line magic expr

## Test Plan

Added test case for assignment statement with and without type
annotation

fixes: #6116
2023-08-05 00:45:01 +00:00
Dhruv Manilawala
32fa05765a Use Jupyter mode while parsing Notebook files (#5552)
## Summary

Enable using the new `Mode::Jupyter` for the tokenizer/parser to parse
Jupyter line magic tokens.

The individual call to the lexer i.e., `lex_starts_at` done by various
rules should consider the context of the source code (is this content
from a Jupyter Notebook?). Thus, a new field `source_type` (of type
`PySourceType`) is added to `Checker` which is being passed around as an
argument to the relevant functions. This is then used to determine the
`Mode` for the lexer.

## Test Plan

Add new test cases to make sure that the magic statement is considered
while generating the diagnostic and autofix:
* For `I001`, if there's a magic statement in between two import blocks,
they should be sorted independently

fixes: #6090
2023-08-05 00:32:07 +00:00
Charlie Marsh
d788957ec4 Allow capitalized names for logger candidate heuristic match (#6356)
Closes https://github.com/astral-sh/ruff/issues/6353.
2023-08-04 23:25:34 +00:00
Victor Hugo Gomes
78a370303b [flake8-pyi] Add tests cases for bad imports from PYI027 to PYI022 (UP035) (#6354)
## Summary
As of version
[23.1.0](2a86db8271/CHANGELOG.md (L158-L160)),
`flake8-pyi` remove the rule `Y027`.

The errors that resulted in `PYI027` are now being emitted by `PYI022`
(`UP035`).

ref: #848 

## Test Plan

Add new tests cases.
2023-08-04 19:00:33 -04:00
Charlie Marsh
5e73345a1c Avoid panic with positional-only arguments in PYI019 (#6350)
## Summary

Previously, failed on methods like:

```python
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ...  # PYI019
```

Since we check if there are any positional-only or non-positional
arguments, but then do an unsafe access on `parameters.args`.

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

## Test Plan

`cargo test` (verified that `main` panics on the new fixtures)
2023-08-04 18:37:07 +00:00
Charlie Marsh
b8fd69311c Remove ruff_python_ast prefix in fixes.rs (#6346) 2023-08-04 16:48:20 +00:00
Charlie Marsh
fa5c9cced9 Ignore same-line docstrings for lines-before and lines-after rules (#6344)
These rules assume that the docstring is on its own line. pydocstyle
treats them inconsistently, so I'm just going to disable them in this
case.

Closes https://github.com/astral-sh/ruff/issues/6329.
2023-08-04 16:08:36 +00:00
Harutaka Kawamura
08dd87e04d Avoid auto-fixing UP032 if comments are present around format call arguments (#6342) 2023-08-04 15:37:23 +00:00
konsti
9bb21283ca More similarity index digits (#6343)
**Summary** We were at similarity index 0.998 for django, we need more
decimal places, now we're at 0.99779.

**Test Plan** n/a
2023-08-04 17:12:33 +02:00
Charlie Marsh
4d47dfd6c0 Tweak breaking groups for comprehensions (#6321)
## Summary

Fixes some comprehension formatting by avoiding creating the group for
the comprehension itself (so that if it breaks, all parts break on their
own lines, e.g. the `for` and the `if` clauses).

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

## Test Plan

Bunch of new fixtures.
2023-08-04 14:00:54 +00:00
konsti
99baad12d8 Call chain formatting in fluent style (#6151)
Implement fluent style/call chains. See the `call_chains.py` formatting
for examples.

This isn't fully like black because in `raise A from B` they allow `A`
breaking can influence the formatting of `B` even if it is already
multiline.

Similarity index:

| project      | main  | PR    |
|--------------|-------|-------|
| build        | ???   | 0.753 |
| django       | 0.991 | 0.998 |
| transformers | 0.993 | 0.994 |
| typeshed     | 0.723 | 0.723 |
| warehouse    | 0.978 | 0.994 |
| zulip        | 0.992 | 0.994 |

Call chain formatting is affected by
https://github.com/astral-sh/ruff/issues/627, but i'm cutting scope
here.

Closes #5343

**Test Plan**:
 * Added a dedicated call chains test file
 * The ecosystem checks found some bugs
 * I manually check django and zulip formatting

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-08-04 13:58:01 +00:00
Charlie Marsh
35bdbe43a8 Flag comparison-with-itself on builtin calls (#6324)
## Summary

Extends `comparison-with-itself` to cover simple function calls on
known-pure functions, like `id`. For example, we now flag `id(x) ==
id(x)`.

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

## Test Plan

`cargo test`
2023-08-04 09:51:41 -04:00
Charlie Marsh
3a985dd71e Rename CommentPlacement#then_with to or_else (#6341)
Per nits in the PR.
2023-08-04 13:50:57 +00:00
Charlie Marsh
1e3fe67ca5 Refactor and rename skip_trailing_trivia (#6312)
Based on feedback here:
https://github.com/astral-sh/ruff/pull/6274#discussion_r1282747964.
2023-08-04 13:30:53 +00:00
Charlie Marsh
38a96c88c1 Add missing enable check for bad-string-format-character (#6340) 2023-08-04 13:27:53 +00:00
Micha Reiser
f4831d5a26 Formatter comment handling nits (#6339) 2023-08-04 13:22:16 +00:00
konsti
1031bb6550 Formatter: Add SourceType to context to enable special formatting for stub files (#6331)
**Summary** This adds the information whether we're in a .py python
source file or in a .pyi stub file to enable people working on #5822 and
related issues.

I'm not completely happy with `Default` for something that depends on
the input.

**Test Plan** None, this is currently unused, i'm leaving this to first
implementation of stub file specific formatting.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-08-04 11:52:26 +00:00
933 changed files with 78926 additions and 52324 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/check_ecosystem.py
- scripts/*
formatter:
- Cargo.toml
@@ -56,6 +56,7 @@ jobs:
- crates/ruff_text_size/**
- crates/ruff_python_parser/**
- crates/ruff_dev/**
- scripts/*
cargo-fmt:
name: "cargo fmt"
@@ -327,7 +328,7 @@ jobs:
name: "Formatter ecosystem and progress checks"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.formatter == 'true'
if: needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
@@ -337,9 +338,6 @@ jobs:
- name: "Formatter progress"
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: grep "similarity index" target/progress_projects_log.txt | sort > $GITHUB_STEP_SUMMARY
# CPython is not black formatted, so we run only the stability check
- name: "Clone CPython 3.10"
run: git clone --branch 3.10 --depth 1 https://github.com/python/cpython.git crates/ruff/resources/test/cpython
- name: "Check CPython stability"
run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython
run: cat target/progress_projects_stats.txt > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects

View File

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

View File

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

View File

@@ -1,5 +1,15 @@
# Breaking Changes
## 0.0.283 / 0.284
### The target Python version now defaults to 3.8 instead of 3.10 ([#6397](https://github.com/astral-sh/ruff/pull/6397))
Previously, when a target Python version was not specified, Ruff would use a default of Python 3.10. However, it is safer to default to an _older_ Python version to avoid assuming the availability of new features. We now default to the oldest supported Python version which is currently Python 3.8.
(We still support Python 3.7 but since [it has reached EOL](https://devguide.python.org/versions/#unsupported-versions) we've decided not to make it the default here.)
Note this change was announced in 0.0.283 but not active until 0.0.284.
## 0.0.277
### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

View File

@@ -131,7 +131,6 @@ At time of writing, the repository includes the following crates:
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
intermediate representation for each node, which `ruff_formatter` prints based on the configured
line length.
@@ -572,7 +571,7 @@ 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
flamegraph --perfdata perf.data --no-inline
```
#### Mac

33
Cargo.lock generated
View File

@@ -14,6 +14,18 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.20"
@@ -800,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.282"
version = "0.0.285"
dependencies = [
"anyhow",
"clap",
@@ -991,6 +1003,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "imara-diff"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
dependencies = [
"ahash",
"hashbrown 0.12.3",
]
[[package]]
name = "imperative"
version = "1.0.4"
@@ -2042,7 +2064,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.282"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2119,6 +2141,7 @@ dependencies = [
"ruff",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_index",
"ruff_python_parser",
"serde",
"serde_json",
@@ -2141,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.282"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2197,6 +2220,7 @@ dependencies = [
"anyhow",
"clap",
"ignore",
"imara-diff",
"indicatif",
"indoc",
"itertools",
@@ -2330,6 +2354,7 @@ dependencies = [
"similar",
"smallvec",
"thiserror",
"unicode-width",
]
[[package]]
@@ -2353,7 +2378,6 @@ dependencies = [
"is-macro",
"itertools",
"lexical-parse-float",
"num-bigint",
"num-traits",
"rand",
"unic-ucd-category",
@@ -2400,6 +2424,7 @@ dependencies = [
"num-traits",
"ruff_index",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_source_file",
"ruff_text_size",

View File

@@ -49,6 +49,7 @@ toml = { version = "0.7.2" }
tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-width = "0.1.10"
wsl = { version = "0.1.0" }
# v1.0.1

View File

@@ -30,7 +30,7 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [500 built-in rules](https://beta.ruff.rs/docs/rules/)
- 📏 Over [600 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.282
rev: v0.0.285
hooks:
- id: ruff
```
@@ -211,8 +211,8 @@ line-length = 88
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.10.
target-version = "py310"
# Assume Python 3.8
target-version = "py38"
[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
@@ -233,7 +233,7 @@ linting command.
<!-- Begin section: Rules -->
**Ruff supports over 500 lint rules**, many of which are inspired by popular tools like Flake8,
**Ruff supports over 600 lint rules**, many of which are inspired by popular tools like Flake8,
isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in
Rust as a first-party feature.

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.282"
version = "0.0.285"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -62,8 +62,6 @@ quick-junit = { version = "0.3.2" }
regex = { workspace = true }
result-like = { version = "0.4.6" }
rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true }
semver = { version = "1.0.16" }
serde = { workspace = true }
@@ -77,7 +75,7 @@ strum_macros = { workspace = true }
thiserror = { version = "1.0.43" }
toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { version = "0.1.10" }
unicode-width = { workspace = true }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
wsl = { version = "0.1.0" }

View File

@@ -14,3 +14,19 @@ with open("/dev/shm/unit/test", "w") as f:
# not ok by config
with open("/foo/bar", "w") as f:
f.write("def")
# Using `tempfile` module should be ok
import tempfile
from tempfile import TemporaryDirectory
with tempfile.NamedTemporaryFile(dir="/tmp") as f:
f.write(b"def")
with tempfile.NamedTemporaryFile(dir="/var/tmp") as f:
f.write(b"def")
with tempfile.TemporaryDirectory(dir="/dev/shm") as d:
pass
with TemporaryDirectory(dir="/tmp") as d:
pass

View File

@@ -69,6 +69,7 @@ g_action.set_enabled(True)
settings.set_enable_developer_extras(True)
foo.is_(True)
bar.is_not(False)
next(iter([]), False)
class Registry:
def __init__(self) -> None:

View File

@@ -68,6 +68,20 @@ def this_is_also_wrong(value={}):
...
class Foo:
@staticmethod
def this_is_also_wrong_and_more_indented(value={}):
pass
def multiline_arg_wrong(value={
}):
...
def single_line_func_wrong(value = {}): ...
def and_this(value=set()):
...
@@ -240,12 +254,16 @@ def foo(f=lambda x: print(x)):
from collections import abc
from typing import Annotated, Dict, Optional, Sequence, Union, Set
import typing_extensions
def immutable_annotations(
a: Sequence[int] | None = [],
b: Optional[abc.Mapping[int, int]] = {},
c: Annotated[Union[abc.Set[str], abc.Sized], "annotation"] = set(),
d: typing_extensions.Annotated[
Union[abc.Set[str], abc.Sized], "annotation"
] = set(),
):
pass
@@ -254,5 +272,35 @@ def mutable_annotations(
a: list[int] | None = [],
b: Optional[Dict[int, int]] = {},
c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
):
pass
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""
...
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""; ...
def single_line_func_wrong(value: dict[str, str] = {}):
"""Docstring"""; \
...
def single_line_func_wrong(value: dict[str, str] = {
# This is a comment
}):
"""Docstring"""
def single_line_func_wrong(value: dict[str, str] = {}) \
: \
"""Docstring"""

View File

@@ -74,3 +74,10 @@ try:
except (ValueError, binascii.Error):
# binascii.Error is a subclass of ValueError.
pass
# https://github.com/astral-sh/ruff/issues/6412
try:
pass
except (ValueError, ValueError, TypeError):
pass

View File

@@ -22,6 +22,10 @@ tuple(
"o"]
)
)
set(set())
set(list())
set(tuple())
sorted(reversed())
# Nested sorts with differing keyword arguments. Not flagged.
sorted(sorted(x, key=lambda y: y))

View File

@@ -1,22 +1,29 @@
import math # not checked
def not_checked():
import math
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import tkinter # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import tkinter as tkr # unconventional
def unconventional():
import altair
import matplotlib.pyplot
import numpy
import pandas
import seaborn
import tkinter
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional
import tkinter as tk # conventional
def unconventional_aliases():
import altair as altr
import matplotlib.pyplot as plot
import numpy as nmp
import pandas as pdas
import seaborn as sbrn
import tkinter as tkr
def conventional_aliases():
import altair as alt
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tkinter as tk

View File

@@ -3,3 +3,6 @@ import logging
name = "world"
logging.info(f"Hello {name}")
logging.log(logging.INFO, f"Hello {name}")
_LOGGER = logging.getLogger()
_LOGGER.info(f"{__name__}")

View File

@@ -0,0 +1,13 @@
# PIE808
range(0, 10)
# OK
range(x, 10)
range(-15, 10)
range(10)
range(0)
range(0, 10, x)
range(0, 10, 1)
range(0, 10, step=1)
range(start=0, stop=10)
range(0, stop=10)

View File

@@ -1,20 +1,19 @@
import typing
# Shouldn't affect non-union field types.
field1: str
# Should emit for duplicate field types.
field2: str | str # PYI016: Duplicate union member `str`
# Should emit for union types in arguments.
def func1(arg1: int | int): # PYI016: Duplicate union member `int`
print(arg1)
# Should emit for unions in return types.
def func2() -> str | str: # PYI016: Duplicate union member `str`
return "my string"
# Should emit in longer unions, even if not directly adjacent.
field3: str | str | int # PYI016: Duplicate union member `str`
field4: int | int | str # PYI016: Duplicate union member `int`
@@ -33,3 +32,55 @@ field10: (str | int) | str # PYI016: Duplicate union member `str`
# Should emit for nested unions.
field11: dict[int | int, str]
# Should emit for unions with more than two cases
field12: int | int | int # Error
field13: int | int | int | int # Error
# Should emit for unions with more than two cases, even if not directly adjacent
field14: int | int | str | int # Error
# Should emit for duplicate literal types; also covered by PYI030
field15: typing.Literal[1] | typing.Literal[1] # Error
# Shouldn't emit if in new parent type
field16: int | dict[int, str] # OK
# Shouldn't emit if not in a union parent
field17: dict[int, int] # OK
# Should emit in cases with newlines
field18: typing.Union[
set[
int # foo
],
set[
int # bar
],
] # Error, newline and comment will not be emitted in message
# Should emit in cases with `typing.Union` instead of `|`
field19: typing.Union[int, int] # Error
# Should emit in cases with nested `typing.Union`
field20: typing.Union[int, typing.Union[int, str]] # Error
# Should emit in cases with mixed `typing.Union` and `|`
field21: typing.Union[int, int | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@@ -74,3 +74,13 @@ field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@@ -14,6 +14,10 @@ class BadClass:
def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
@classmethod
def excluded_edge_case(cls: Type[_S], arg: int) -> _S: ... # Ok

View File

@@ -14,6 +14,10 @@ class BadClass:
def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
@classmethod
def excluded_edge_case(cls: Type[_S], arg: int) -> _S: ... # Ok

View File

@@ -1,7 +1,6 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
@@ -9,7 +8,9 @@ z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
def func(arg: type[int] | str | type[float]) -> None: ...
def func(arg: type[int] | str | type[float]) -> None:
...
# OK
x: type[int, str, float]
@@ -17,4 +18,14 @@ y: builtins.type[int, str, complex]
z: Union[float, complex]
def func(arg: type[int, float] | str) -> None: ...
def func(arg: type[int, float] | str) -> None:
...
# OK
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
def func():
# PYI055
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker

View File

@@ -1,14 +1,12 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
def func(arg: type[int] | str | type[float]) -> None: ...
# OK
@@ -16,5 +14,11 @@ x: type[int, str, float]
y: builtins.type[int, str, complex]
z: Union[float, complex]
def func(arg: type[int, float] | str) -> None: ...
# OK
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
def func():
# PYI055
item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker

View File

@@ -64,3 +64,8 @@ def test_implicit_str_concat_no_parens(param1, param2, param3):
@pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
...
@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
def test_csv_with_parens(param1, param2):
...

View File

@@ -80,3 +80,15 @@ class Test(unittest.TestCase):
def test_assert_not_regexp_matches(self):
self.assertNotRegex("abc", r"abc") # Error
def test_fail_if(self):
self.failIf("abc") # Error
def test_fail_unless(self):
self.failUnless("abc") # Error
def test_fail_unless_equal(self):
self.failUnlessEqual(1, 2) # Error
def test_fail_if_equal(self):
self.failIfEqual(1, 2) # Error

View File

@@ -1,3 +1,4 @@
from pickle import PicklingError, UnpicklingError
import socket
import pytest
@@ -20,6 +21,12 @@ def test_error_no_argument_given():
with pytest.raises(socket.error):
raise ValueError("Can't divide 1 by 0")
with pytest.raises(PicklingError):
raise PicklingError("Can't pickle")
with pytest.raises(UnpicklingError):
raise UnpicklingError("Can't unpickle")
def test_error_match_is_empty():
with pytest.raises(ValueError, match=None):

View File

@@ -0,0 +1,53 @@
import pytest
@pytest.mark.parametrize("x", [1, 1, 2])
def test_error_literal(x):
...
a = 1
b = 2
c = 3
@pytest.mark.parametrize("x", [a, a, b, b, b, c])
def test_error_expr_simple(x):
...
@pytest.mark.parametrize(
"x",
[
(a, b),
# comment
(a, b),
(b, c),
],
)
def test_error_expr_complex(x):
...
@pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
def test_error_parentheses(x):
...
@pytest.mark.parametrize(
"x",
[
a,
b,
(a),
c,
((a)),
],
)
def test_error_parentheses_trailing_comma(x):
...
@pytest.mark.parametrize("x", [1, 2])
def test_ok(x):
...

View File

@@ -0,0 +1,48 @@
import unittest
class Test(unittest.TestCase):
def test_errors(self):
with self.assertRaises(ValueError):
raise ValueError
with self.assertRaises(expected_exception=ValueError):
raise ValueError
with self.failUnlessRaises(ValueError):
raise ValueError
with self.assertRaisesRegex(ValueError, "test"):
raise ValueError("test")
with self.assertRaisesRegex(ValueError, expected_regex="test"):
raise ValueError("test")
with self.assertRaisesRegex(
expected_exception=ValueError, expected_regex="test"
):
raise ValueError("test")
with self.assertRaisesRegex(
expected_regex="test", expected_exception=ValueError
):
raise ValueError("test")
with self.assertRaisesRegexp(ValueError, "test"):
raise ValueError("test")
def test_unfixable_errors(self):
with self.assertRaises(ValueError, msg="msg"):
raise ValueError
with self.assertRaises(
# comment
ValueError
):
raise ValueError
with (
self
# comment
.assertRaises(ValueError)
):
raise ValueError

View File

@@ -0,0 +1,12 @@
import unittest
import pytest
class Test(unittest.TestCase):
def test_pytest_raises(self):
with pytest.raises(ValueError):
raise ValueError
def test_errors(self):
with self.assertRaises(ValueError):
raise ValueError

View File

@@ -19,11 +19,20 @@ raise TypeError ()
raise TypeError \
()
# RSE102
raise TypeError \
();
# RSE102
raise TypeError(
)
# RSE102
raise (TypeError) (
)
# RSE102
raise TypeError(
# Hello, world!
@@ -52,3 +61,10 @@ class Class:
# OK
raise Class.error()
import ctypes
# OK
raise ctypes.WinError(1)

View File

@@ -73,3 +73,7 @@ print(foo.__dict__)
print(foo.__str__())
print(foo().__class__)
print(foo._asdict())
import os
os._exit()

View File

@@ -31,6 +31,8 @@ for key in list(obj.keys()):
key in (obj or {}).keys() # SIM118
(key) in (obj or {}).keys() # SIM118
from typing import KeysView

View File

@@ -0,0 +1,31 @@
## Banned modules ##
import torch
from torch import *
from tensorflow import a, b, c
import torch as torch_wearing_a_trenchcoat
# this should count as module level
x = 1; import tensorflow
# banning a module also bans any submodules
import torch.foo.bar
from tensorflow.foo import bar
from torch.foo.bar import *
# unlike TID251, inline imports are *not* banned
def my_cool_function():
import tensorflow.foo.bar
def another_cool_function():
from torch.foo import bar
def import_alias():
from torch.foo import bar
if TYPE_CHECKING:
import torch

View File

@@ -0,0 +1,12 @@
from __future__ import annotations
from datetime import date
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Birthday(DeclarativeBase):
__tablename__ = "birthday"
id: Mapped[int] = mapped_column(primary_key=True)
day: Mapped[date]

View File

@@ -27,6 +27,8 @@ def f(cls, x):
###
lambda x: print("Hello, world!")
lambda: print("Hello, world!")
class C:
###
@@ -202,3 +204,14 @@ class C:
###
def f(x: None) -> None:
_ = cast(Any, _identity)(x=x)
###
# Unused arguments with `locals`.
###
def f(bar: str):
print(locals())
class C:
def __init__(self, x) -> None:
print(locals())

View File

@@ -0,0 +1,6 @@
#!/usr/bin/env python3
# A copyright notice could go here
# A linter directive could go here
x = 1

View File

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

View File

@@ -0,0 +1,52 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "eab4754a-d6df-4b41-8ee8-7e23aef440f9",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"import os\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2b0e2986-1b87-4bb6-9b1d-c11ca1decd87",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"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,51 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "cad32845-44f9-4a53-8b8c-a6b1bb3f3378",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d7b8e967-8b4a-493b-b6f7-d5cecfb3a5c3",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"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

@@ -25,6 +25,23 @@
"def foo():\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16214f6f-bb32-4594-81be-79fb27c6ec92",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import sys\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import math\n",
"import abc"
]
}
],
"metadata": {

View File

@@ -27,6 +27,23 @@
"def foo():\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d6c55c6-4a34-4662-914b-4ee11c9c24a5",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"from pathlib import Path\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import abc\n",
"import math"
]
}
],
"metadata": {

View File

@@ -0,0 +1,49 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "a0efffbc-85f1-4513-bf49-5387ec3a2a4e",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" foo1 = %matplotlib --list\n",
" foo2: list[str] = %matplotlib --list"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6e0b2b50-43f2-4f59-951d-9404dd560ae4",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" bar1 = !pwd\n",
" bar2: str = !pwd"
]
}
],
"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,49 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "24426ef2-046c-453e-b809-05b56e7355e0",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" %matplotlib --list\n",
" %matplotlib --list"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d98fdae-b86b-476e-b4db-9d3ce5562682",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" !pwd\n",
" !pwd"
]
}
],
"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

@@ -21,3 +21,29 @@ while i < 10:
print("error")
i += 1
# OK - no other way to write this
for i in range(10):
try:
print(f"{i}")
break
except:
print("error")
# OK - no other way to write this
for i in range(10):
try:
print(f"{i}")
continue
except:
print("error")
# OK - no other way to write this
for i in range(10):
try:
print(f"{i}")
if i > 0:
break
except:
print("error")

View File

@@ -28,3 +28,6 @@ mdtypes_template = {
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
}
#: Okay
a = (1,

View File

@@ -30,3 +30,10 @@ def foo() -> None:
if __name__ == "__main__":
import g
import h; import i
if __name__ == "__main__":
import j; \
import k

View File

@@ -1,11 +1,16 @@
a = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
a = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
# aaaa
# aaaaa
# a
# a
# aa
# aaa
# aaaa
# a
# aa
# aaa
b = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
b = """ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
c = """24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
c = """24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
d = """💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
d = """💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A67ß9💣24A6"""
if True: # noqa: E501
[12]
[12 ]
[1,2]
[1, 2]

View File

@@ -25,6 +25,12 @@ if (True) == TrueElement or x == TrueElement:
if res == True != False:
pass
if(True) == TrueElement or x == TrueElement:
pass
if (yield i) == True:
print("even")
#: Okay
if x not in y:
pass

View File

@@ -61,3 +61,30 @@ if x == types.X:
#: E721
assert type(res) is int
class Foo:
def asdf(self, value: str | None):
#: E721
if type(value) is str:
...
class Foo:
def type(self):
pass
def asdf(self, value: str | None):
#: E721
if type(value) is str:
...
class Foo:
def asdf(self, value: str | None):
def type():
pass
# Okay
if type(value) is str:
...

View File

@@ -133,3 +133,8 @@ def scope():
from collections.abc import Callable
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
class TemperatureScales(Enum):
CELSIUS = (lambda deg_c: deg_c)
FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32)

View File

@@ -634,3 +634,8 @@ def starts_with_this():
@expect('D404: First word of the docstring should not be "This"')
def starts_with_space_then_this():
""" This is a docstring that starts with a space.""" # noqa: D210
class SameLine: """This is a docstring on the same line"""
def same_line(): """This is a docstring on the same line"""

View File

@@ -92,3 +92,10 @@ match *0, 1, *2:
case 0,:
import x
import y
# Test: access a sub-importation via an alias.
import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))

View File

@@ -70,3 +70,13 @@ import requests_mock as rm
def requests_mock(requests_mock: rm.Mocker):
print(rm.ANY)
import sklearn.base
import mlflow.sklearn
def f():
import sklearn
mlflow

View File

@@ -145,3 +145,9 @@ def f() -> None:
obj = Foo()
obj.do_thing()
def f():
try:
pass
except Exception as _:
pass

View File

@@ -0,0 +1,46 @@
class Apples:
def _init_(self): # [bad-dunder-name]
pass
def __hello__(self): # [bad-dunder-name]
print("hello")
def __init_(self): # [bad-dunder-name]
# author likely unintentionally misspelled the correct init dunder.
pass
def _init_(self): # [bad-dunder-name]
# author likely unintentionally misspelled the correct init dunder.
pass
def ___neg__(self): # [bad-dunder-name]
# author likely accidentally added an additional `_`
pass
def __inv__(self): # [bad-dunder-name]
# author likely meant to call the invert dunder method
pass
def hello(self):
print("hello")
def __init__(self):
pass
def init(self):
# valid name even though someone could accidentally mean __init__
pass
def _protected_method(self):
print("Protected")
def __private_method(self):
print("Private")
@property
def __doc__(self):
return "Docstring"
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
...

View File

@@ -14,7 +14,12 @@
"{:s} {:y}".format("hello", "world") # [bad-format-character]
"{:*^30s}".format("centered")
"{:*^30s}".format("centered") # OK
"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
"{0:.{prec}g}".format(1.23, prec=15) # OK
"{0:.{foo}x{bar}y{foobar}g}".format(...) # OK (all nested replacements are consumed without considering in between chars)
"{0:.{foo}{bar}{foobar}y}".format(...) # [bad-format-character] (check value after replacements)
## f-strings

View File

@@ -13,6 +13,7 @@ print("foo %(foo)d bar %(bar)d" % {"foo": "1", "bar": "2"})
"%(key)d" % {"key": []}
print("%d" % ("%s" % ("nested",),))
"%d" % ((1, 2, 3),)
"%d" % (1 if x > 0 else [])
# False negatives
WORD = "abc"
@@ -55,3 +56,4 @@ r'\%03o' % (ord(c),)
"%d" % (len(foo),)
'(%r, %r, %r, %r)' % (hostname, address, username, '$PASSWORD')
'%r' % ({'server_school_roles': server_school_roles, 'is_school_multiserver_domain': is_school_multiserver_domain}, )
"%d" % (1 if x > 0 else 2)

View File

@@ -19,6 +19,10 @@ foo in foo
foo not in foo
id(foo) == id(foo)
len(foo) == len(foo)
# Non-errors.
"foo" == "foo" # This is flagged by `comparison-of-constant` instead.
@@ -43,3 +47,11 @@ foo is not bar
foo in bar
foo not in bar
x(foo) == y(foo)
id(foo) == id(bar)
id(foo, bar) == id(foo, bar)
id(foo, bar=1) == id(foo, bar=1)

View File

@@ -1,10 +1,11 @@
class Person:
class Person: # [eq-without-hash]
def __init__(self):
self.name = "monty"
def __eq__(self, other):
return isinstance(other, Person) and other.name == self.name
# OK
class Language:
def __init__(self):
self.name = "python"
@@ -14,3 +15,9 @@ class Language:
def __hash__(self):
return hash(self.name)
class MyClass:
def __eq__(self, other):
return True
__hash__ = None

View File

@@ -10,3 +10,4 @@ os.getenv("AA", "GOOD" + "BAD")
os.getenv("AA", "GOOD" + 1)
os.getenv("AA", "GOOD %s" % "BAD")
os.getenv("B", Z)

View File

@@ -10,6 +10,8 @@ os.getenv(key="foo", default="bar")
os.getenv(key=f"foo", default="bar")
os.getenv(key="foo" + "bar", default=1)
os.getenv(key=1 + "bar", default=1) # [invalid-envvar-value]
os.getenv("PATH_TEST" if using_clear_path else "PATH_ORIG")
os.getenv(1 if using_clear_path else "PATH_ORIG")
AA = "aa"
os.getenv(AA)

View File

@@ -19,6 +19,10 @@ logging.error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
# do not handle keyword arguments
logging.error("%(objects)d modifications: %(modifications)d errors: %(errors)d")
logging.info(msg="Hello %s")
logging.info(msg="Hello %s %s")
import warning
warning.warning("Hello %s %s", "World!")

View File

@@ -15,6 +15,10 @@ logging.error("Example log %s, %s", "foo", "bar", "baz", **kwargs)
# do not handle keyword arguments
logging.error("%(objects)d modifications: %(modifications)d errors: %(errors)d", {"objects": 1, "modifications": 1, "errors": 1})
logging.info(msg="Hello")
logging.info(msg="Hello", something="else")
import warning
warning.warning("Hello %s", "World!", "again")

View File

@@ -0,0 +1,62 @@
import abc
class Person:
def developer_greeting(self, name): # [no-self-use]
print(f"Greetings {name}!")
def greeting_1(self): # [no-self-use]
print("Hello!")
def greeting_2(self): # [no-self-use]
print("Hi!")
# OK
def developer_greeting():
print("Greetings developer!")
# OK
class Person:
name = "Paris"
def __init__(self):
pass
def __cmp__(self, other):
print(24)
def __repr__(self):
return "Person"
def func(self):
...
def greeting_1(self):
print(f"Hello from {self.name} !")
@staticmethod
def greeting_2():
print("Hi!")
class Base(abc.ABC):
"""abstract class"""
@abstractmethod
def abstract_method(self):
"""abstract method could not be a function"""
raise NotImplementedError
class Sub(Base):
@override
def abstract_method(self):
print("concret method")
class Prop:
@property
def count(self):
return 24

View File

@@ -32,3 +32,7 @@ foo not in {"a", "b", "c"} # Uses membership test already.
foo == "a" # Single comparison.
foo != "a" # Single comparison.
foo == "a" == "b" or foo == "c" # Multiple comparisons.
foo == bar == "b" or foo == "c" # Multiple comparisons.

View File

@@ -0,0 +1,13 @@
import subprocess
# Errors.
subprocess.run("ls")
subprocess.run("ls", shell=True)
# Non-errors.
subprocess.run("ls", check=True)
subprocess.run("ls", check=False)
subprocess.run("ls", shell=True, check=True)
subprocess.run("ls", shell=True, check=False)
foo.run("ls") # Not a subprocess.run call.
subprocess.bar("ls") # Not a subprocess.run call.

View File

@@ -0,0 +1,3 @@
from sys import *
exit(0)

View File

@@ -32,3 +32,30 @@ print(
)
'{' '0}'.format(1)
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)

View File

@@ -15,3 +15,17 @@ f"{0}".format(1)
print(f"{0}".format(1))
''.format(1)
'{1} {0}'.format(*args)
"{1}_{0}".format(*args, 1)
"{1}_{0}".format(*args, 1, 2)
"{1}_{0}".format(1, **kwargs)
"{1}_{0}".format(1, foo=2)
"{1}_{0}".format(1, 2, **kwargs)
"{1}_{0}".format(1, 2, foo=3, bar=4)

View File

@@ -1,28 +0,0 @@
# These SHOULD change
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{1}_{0}".format(*args)
"{1}_{0}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)

View File

@@ -106,3 +106,7 @@ print('Hello %(arg)s' % bar['bop'])
"""
% (x,)
)
"%s" % (
x, # comment
)

View File

@@ -198,3 +198,7 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
).format(a=1)
"{}".format(**c)
"{}".format(
1 # comment
)

View File

@@ -46,6 +46,35 @@ from typing import Callable, Match, Pattern, List, OrderedDict, AbstractSet, Con
if True: from collections import (
Mapping, Counter)
# Bad imports from PYI027 that are now handled by PYI022 (UP035)
from typing import ContextManager
from typing import OrderedDict
from typing_extensions import OrderedDict
from typing import Callable
from typing import ByteString
from typing import Container
from typing import Hashable
from typing import ItemsView
from typing import Iterable
from typing import Iterator
from typing import KeysView
from typing import Mapping
from typing import MappingView
from typing import MutableMapping
from typing import MutableSequence
from typing import MutableSet
from typing import Sequence
from typing import Sized
from typing import ValuesView
from typing import Awaitable
from typing import AsyncIterator
from typing import AsyncIterable
from typing import Coroutine
from typing import Collection
from typing import AsyncGenerator
from typing import Reversible
from typing import Generator
# OK
from a import b

View File

@@ -5,11 +5,42 @@ from typing import TypeAlias
x: typing.TypeAlias = int
x: TypeAlias = int
# UP040 with generics (todo)
# UP040 simple generic
T = typing.TypeVar["T"]
x: typing.TypeAlias = list[T]
# UP040 call style generic
T = typing.TypeVar("T")
x: typing.TypeAlias = list[T]
# UP040 bounded generic (todo)
T = typing.TypeVar("T", bound=int)
x: typing.TypeAlias = list[T]
T = typing.TypeVar("T", int, str)
x: typing.TypeAlias = list[T]
# UP040 contravariant generic (todo)
T = typing.TypeVar("T", contravariant=True)
x: typing.TypeAlias = list[T]
# UP040 covariant generic (todo)
T = typing.TypeVar("T", covariant=True)
x: typing.TypeAlias = list[T]
# UP040 in class scope
T = typing.TypeVar["T"]
class Foo:
# reference to global variable
x: typing.TypeAlias = list[T]
# reference to class variable
TCLS = typing.TypeVar["TCLS"]
y: typing.TypeAlias = list[TCLS]
# UP040 wont add generics in fix
T = typing.TypeVar(*args)
x: typing.TypeAlias = list[T]
# OK
x: TypeAlias

View File

@@ -0,0 +1,14 @@
x = [1, 2, 3]
y = [4, 5, 6]
# RUF017
sum([x, y], start=[])
sum([x, y], [])
sum([[1, 2, 3], [4, 5, 6]], start=[])
sum([[1, 2, 3], [4, 5, 6]], [])
sum([[1, 2, 3], [4, 5, 6]],
[])
# OK
sum([x, y])
sum([[1, 2, 3], [4, 5, 6]])

View File

@@ -0,0 +1,5 @@
# ruff: noqa: RUF100
import os # noqa: F401
print(os.sep)

View File

@@ -52,3 +52,7 @@ def good(a: int):
def another_good(a):
if a % 2 == 0:
raise GoodArgCantBeEven(a)
def another_good():
raise NotImplementedError("This is acceptable too")

View File

@@ -33,7 +33,7 @@ impl<'a, T: Codegen<'a>> CodegenStylist<'a> for T {
///
/// Returns `Ok(None)` if the statement is empty after removing the imports.
pub(crate) fn remove_imports<'a>(
imports: impl Iterator<Item = &'a str>,
member_names: impl Iterator<Item = &'a str>,
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
@@ -45,27 +45,20 @@ pub(crate) fn remove_imports<'a>(
bail!("Expected Statement::Simple");
};
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
names
} else if let ImportNames::Star(..) = &import_body.names {
// Special-case: if the import is a `from ... import *`, then we delete the
// entire statement.
let mut found_star = false;
for import in imports {
let qualified_name = match import_body.module.as_ref() {
Some(module_name) => format!("{}.*", compose_module_path(module_name)),
None => "*".to_string(),
};
if import == qualified_name {
for member in member_names {
if member == "*" {
found_star = true;
} else {
bail!("Expected \"*\" for unused import (got: \"{}\")", import);
bail!("Expected \"*\" for unused import (got: \"{}\")", member);
}
}
if !found_star {
@@ -82,30 +75,10 @@ pub(crate) fn remove_imports<'a>(
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
for import in imports {
let alias_index = aliases.iter().position(|alias| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == import
});
for member in member_names {
let alias_index = aliases
.iter()
.position(|alias| member == compose_module_path(&alias.name));
if let Some(index) = alias_index {
aliases.remove(index);
}
@@ -139,7 +112,7 @@ pub(crate) fn remove_imports<'a>(
///
/// Returns the modified import statement.
pub(crate) fn retain_imports(
imports: &[&str],
member_names: &[&str],
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
@@ -151,14 +124,11 @@ pub(crate) fn retain_imports(
bail!("Expected Statement::Simple");
};
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
names
} else {
bail!("Expected: ImportNames::Aliases");
}
@@ -170,28 +140,9 @@ pub(crate) fn retain_imports(
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
aliases.retain(|alias| {
imports.iter().any(|import| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == *import
})
member_names
.iter()
.any(|member| *member == compose_module_path(&alias.name))
});
// But avoid destroying any trailing comments.

View File

@@ -1,13 +1,15 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{bail, Result};
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::{lexer, Mode};
use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace};
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextRange, TextSize};
@@ -56,14 +58,14 @@ pub(crate) fn delete_stmt(
/// Generate a `Fix` to remove the specified imports from an `import` statement.
pub(crate) fn remove_unused_imports<'a>(
unused_imports: impl Iterator<Item = &'a str>,
member_names: impl Iterator<Item = &'a str>,
stmt: &Stmt,
parent: Option<&Stmt>,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
) -> Result<Edit> {
match codemods::remove_imports(unused_imports, stmt, locator, stylist)? {
match codemods::remove_imports(member_names, stmt, locator, stylist)? {
None => Ok(delete_stmt(stmt, parent, locator, indexer)),
Some(content) => Ok(Edit::range_replacement(content, stmt.range())),
}
@@ -87,77 +89,49 @@ pub(crate) fn remove_argument<T: Ranged>(
argument: &T,
arguments: &Arguments,
parentheses: Parentheses,
locator: &Locator,
source: &str,
) -> Result<Edit> {
// TODO(sbrugman): Preserve trailing comments.
if arguments.keywords.len() + arguments.args.len() > 1 {
let mut fix_start = None;
let mut fix_end = None;
// Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments
.args
.iter()
.map(Expr::range)
.chain(arguments.keywords.iter().map(Keyword::range))
.filter(|range| argument.range() != *range)
.partition(|range| range.start() < argument.start());
if arguments
.args
.iter()
.map(Expr::start)
.chain(arguments.keywords.iter().map(Keyword::start))
.any(|location| location > argument.start())
{
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
// argument to the end of the subsequent comma.
let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
Mode::Module,
arguments.start(),
)
.flatten()
{
if seen_comma {
if tok.is_non_logical_newline() {
// Also delete any non-logical newlines after the comma.
continue;
}
fix_end = Some(if tok.is_newline() {
range.end()
} else {
range.start()
});
break;
}
if range.start() == argument.start() {
fix_start = Some(range.start());
}
if fix_start.is_some() && tok.is_comma() {
seen_comma = true;
}
}
} else {
// Case 2: argument or keyword is the last node, so delete from the start of the
// previous comma to the end of the argument.
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
Mode::Module,
arguments.start(),
)
.flatten()
{
if range.start() == argument.start() {
fix_end = Some(argument.end());
break;
}
if tok.is_comma() {
fix_start = Some(range.start());
}
}
}
if !after.is_empty() {
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
// argument to the end of the subsequent comma.
let mut tokenizer = SimpleTokenizer::starts_at(argument.end(), source);
match (fix_start, fix_end) {
(Some(start), Some(end)) => Ok(Edit::deletion(start, end)),
_ => {
bail!("No fix could be constructed")
}
}
// Find the trailing comma.
tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
// Find the next non-whitespace token.
let next = tokenizer
.find(|token| {
token.kind != SimpleTokenKind::Whitespace && token.kind != SimpleTokenKind::Newline
})
.context("Unable to find next token")?;
Ok(Edit::deletion(argument.start(), next.start()))
} else if let Some(previous) = before.iter().map(Ranged::end).max() {
// Case 2: argument or keyword is the last node, so delete from the start of the
// previous comma to the end of the argument.
let mut tokenizer = SimpleTokenizer::starts_at(previous, source);
// Find the trailing comma.
let comma = tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
Ok(Edit::deletion(comma.start(), argument.end()))
} else {
// Only one argument; remove it (but preserve parentheses, if needed).
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
// parentheses, if needed).
Ok(match parentheses {
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
Parentheses::Preserve => {
@@ -176,16 +150,13 @@ fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
| Stmt::With(ast::StmtWith { body, .. }) => {
if is_only(body, child) {
return true;
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if is_only(body, child) || is_only(orelse, child) {
return true;
@@ -209,14 +180,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
..
}) => {
if is_only(body, child)
|| is_only(orelse, child)

View File

@@ -13,6 +13,7 @@ use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod snippet;
pub(crate) mod source_map;
pub(crate) struct FixResult {

View File

@@ -0,0 +1,36 @@
use unicode_width::UnicodeWidthStr;
/// A snippet of source code for user-facing display, as in a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SourceCodeSnippet(String);
impl SourceCodeSnippet {
pub(crate) fn new(source_code: String) -> Self {
Self(source_code)
}
/// Return the full snippet for user-facing display, or `None` if the snippet should be
/// truncated.
pub(crate) fn full_display(&self) -> Option<&str> {
if Self::should_truncate(&self.0) {
None
} else {
Some(&self.0)
}
}
/// Return a truncated snippet for user-facing display.
pub(crate) fn truncated_display(&self) -> &str {
if Self::should_truncate(&self.0) {
"..."
} else {
&self.0
}
}
/// Returns `true` if the source code should be truncated when included in a user-facing
/// diagnostic.
fn should_truncate(source_code: &str) -> bool {
source_code.width() > 50 || source_code.contains(['\r', '\n'])
}
}

View File

@@ -1,4 +1,5 @@
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_python_ast::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
@@ -16,14 +17,20 @@ pub(crate) fn bindings(checker: &mut Checker) {
return;
}
for binding in checker.semantic.bindings.iter() {
for binding in &*checker.semantic.bindings {
if checker.enabled(Rule::UnusedVariable) {
if binding.kind.is_bound_exception() && !binding.is_used() {
if binding.kind.is_bound_exception()
&& !binding.is_used()
&& !checker
.settings
.dummy_variable_rgx
.is_match(binding.name(checker.locator))
{
let mut diagnostic = Diagnostic::new(
pyflakes::rules::UnusedVariable {
name: binding.name(checker.locator).to_string(),
},
binding.range,
binding.range(),
);
if checker.patch(Rule::UnusedVariable) {
diagnostic.try_set_fix(|| {

View File

@@ -7,10 +7,6 @@ use crate::rules::flake8_simplify;
/// Run lint rules over a [`Comprehension`] syntax nodes.
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) {
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(
checker,
&comprehension.target,
&comprehension.iter,
);
flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension);
}
}

View File

@@ -1,8 +1,8 @@
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_ast::Stmt;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bugbear, perflint};
use crate::rules::{flake8_bugbear, perflint, pyupgrade};
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
@@ -11,21 +11,18 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
for snapshot in for_loops {
checker.semantic.restore(snapshot);
if let Stmt::For(ast::StmtFor {
target, iter, body, ..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target, iter, body, ..
}) = &checker.semantic.stmt()
{
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
} else {
unreachable!("Expected Expr::For | Expr::AsyncFor");
let Stmt::For(stmt_for) = checker.semantic.current_statement() else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, stmt_for);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, stmt_for);
}
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt_for);
}
}
}

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_ast::Ranged;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use crate::checkers::ast::Checker;
@@ -30,6 +30,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::NoSelfUse,
]) {
return;
}
@@ -37,7 +38,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let enforce_typing_imports = !checker.is_stub
let enforce_typing_imports = !checker.source_type.is_stub()
&& checker.any_enabled(&[
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
@@ -82,7 +83,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range,
binding.range(),
));
}
}
@@ -112,21 +113,21 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &checker.semantic.stmts)
checker.semantic.different_branches(left, right)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.range.start());
let line = checker.locator.compute_line_index(shadowed.start());
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
binding.range(),
));
}
}
@@ -168,16 +169,25 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(
checker.semantic.stmts[shadowed.source.unwrap()],
),
&checker.semantic,
)
{
let Some(node_id) = shadowed.source else {
continue;
};
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(node_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
&function.decorator_list,
&checker.semantic,
)
})
{
continue;
}
}
} else {
// Only enforce cross-scope shadowing for imports.
@@ -195,20 +205,20 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &checker.semantic.stmts)
checker.semantic.different_branches(left, right)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.range.start());
let line = checker.locator.compute_line_index(shadowed.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
line,
},
binding.range,
binding.range(),
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
@@ -231,10 +241,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Lambda(_)
) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
}
@@ -243,7 +250,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics);
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnusedClassMethodArgument,
Rule::UnusedFunctionArgument,
@@ -260,10 +267,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Module
) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if enforce_typing_imports {
let runtime_imports: Vec<&Binding> = checker
.semantic
@@ -299,6 +303,12 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
}
}
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
}

View File

@@ -30,7 +30,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
]);
let enforce_stubs = checker.is_stub && checker.enabled(Rule::DocstringInStub);
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
let enforce_docstrings = checker.any_enabled(&[
Rule::BlankLineAfterLastSection,
@@ -171,7 +171,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
expr.start(),
));
if pydocstyle::helpers::should_ignore_docstring(contents) {
if pydocstyle::helpers::should_ignore_docstring(expr) {
#[allow(deprecated)]
let location = checker.locator.compute_source_location(expr.start());
warn_user!(

View File

@@ -31,7 +31,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic)
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py310
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -44,7 +44,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP604Annotation) {
if checker.is_stub
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py310
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -59,7 +59,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
// Ex) list[...]
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39
&& !checker.semantic.future_annotations()
&& checker.semantic.in_annotation()
@@ -80,17 +80,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
]) {
// Avoid duplicate checks if the parent is an `Union[...]` since these rules
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
let is_unchecked_union = checker
.semantic
.expr_grandparent()
.and_then(Expr::as_subscript_expr)
.map_or(true, |parent| {
!checker.semantic.match_typing_expr(&parent.value, "Union")
});
if is_unchecked_union {
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
@@ -176,7 +168,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
typing::to_pep585_generic(expr, &checker.semantic)
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -187,7 +179,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.is_stub
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -206,11 +198,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
ExprContext::Store => {
if checker.enabled(Rule::NonLowercaseVariableInFunction) {
if checker.semantic.scope().kind.is_any_function() {
if checker.semantic.current_scope().kind.is_function() {
// Ignore globals.
if !checker.semantic.scope().get(id).is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
}) {
if !checker
.semantic
.current_scope()
.get(id)
.is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
})
{
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
@@ -219,7 +216,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
&checker.semantic.scope().kind
&checker.semantic.current_scope().kind
{
pep8_naming::rules::mixed_case_variable_in_class_scope(
checker,
@@ -230,7 +227,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::MixedCaseVariableInGlobalScope) {
if matches!(checker.semantic.scope().kind, ScopeKind::Module) {
if matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
pep8_naming::rules::mixed_case_variable_in_global_scope(
checker, expr, id,
);
@@ -243,7 +240,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
@@ -264,7 +261,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::load_before_global_declaration(checker, id, expr);
}
}
Expr::Attribute(ast::ExprAttribute { attr, value, .. }) => {
Expr::Attribute(attribute) => {
// Ex) typing.List[...]
if checker.any_enabled(&[
Rule::FutureRewritableTypeAnnotation,
@@ -272,7 +269,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
]) {
if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -285,7 +282,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.is_stub
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -326,7 +323,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(checker, expr);
}
pandas_vet::rules::attr(checker, attr, value, expr);
pandas_vet::rules::attr(checker, attribute);
}
Expr::Call(
call @ ast::ExprCall {
@@ -341,6 +338,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
},
) => {
if checker.any_enabled(&[
// pylint
Rule::BadStringFormatCharacter,
// pyflakes
Rule::StringDotFormatInvalidFormat,
Rule::StringDotFormatExtraNamedArguments,
@@ -403,7 +402,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, expr);
pyupgrade::rules::format_literals(checker, &summary, call);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
@@ -419,9 +418,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::call(
checker,
val.as_str(),
location,
checker, val, location,
);
}
}
@@ -435,7 +432,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::deprecated_unittest_alias(checker, func);
}
if checker.enabled(Rule::SuperCallWithParameters) {
pyupgrade::rules::super_call_with_parameters(checker, expr, func, args);
pyupgrade::rules::super_call_with_parameters(checker, call);
}
if checker.enabled(Rule::UnnecessaryEncodeUTF8) {
pyupgrade::rules::unnecessary_encode_utf8(checker, call);
@@ -534,6 +531,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
}
if checker.enabled(Rule::UnnecessaryRangeStart) {
flake8_pie::rules::unnecessary_range_start(checker, call);
}
if checker.enabled(Rule::ExecBuiltin) {
flake8_bandit::rules::exec_used(checker, func);
}
@@ -666,7 +666,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_comprehensions::rules::unnecessary_map(
checker,
expr,
checker.semantic.expr_parent(),
checker.semantic.current_expression_parent(),
func,
args,
);
@@ -676,10 +676,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker, expr, func, args, keywords,
);
}
if checker.enabled(Rule::BooleanPositionalValueInFunctionCall) {
flake8_boolean_trap::rules::check_boolean_positional_value_in_function_call(
checker, args, func,
);
if checker.enabled(Rule::BooleanPositionalValueInCall) {
flake8_boolean_trap::rules::boolean_positional_value_in_call(checker, args, func);
}
if checker.enabled(Rule::Debugger) {
flake8_debugger::rules::debugger_call(checker, expr, func);
@@ -761,9 +759,19 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::PytestUnittestRaisesAssertion) {
if let Some(diagnostic) =
flake8_pytest_style::rules::unittest_raises_assertion(checker, call)
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::SubprocessPopenPreexecFn) {
pylint::rules::subprocess_popen_preexec_fn(checker, call);
}
if checker.enabled(Rule::SubprocessRunWithoutCheck) {
pylint::rules::subprocess_run_without_check(checker, call);
}
if checker.any_enabled(&[
Rule::PytestRaisesWithoutException,
Rule::PytestRaisesTooBroad,
@@ -868,6 +876,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
}
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}
}
Expr::Dict(ast::ExprDict {
keys,
@@ -916,7 +927,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::await_outside_async(checker, expr);
}
}
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, .. }) => {
if checker.enabled(Rule::FStringMissingPlaceholders) {
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
}
@@ -943,7 +954,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
}) => {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = left.as_ref()
{
@@ -1064,7 +1075,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}) => {
// Ex) `str | None`
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py310
&& !checker.semantic.future_annotations()
&& checker.semantic.in_annotation()
@@ -1077,47 +1088,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
// Avoid duplicate checks if the parent is an `|` since these rules
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
let is_unchecked_union = !matches!(
checker.semantic.expr_parent(),
Some(Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
..
}))
);
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
&& is_unchecked_union
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
}
}
}
Expr::UnaryOp(ast::ExprUnaryOp {
op,
operand,
range: _,
}) => {
let check_not_in = checker.enabled(Rule::NotInTest);
let check_not_is = checker.enabled(Rule::NotIsTest);
if check_not_in || check_not_is {
pycodestyle::rules::not_tests(
checker,
expr,
*op,
operand,
check_not_in,
check_not_is,
);
Expr::UnaryOp(
unary_op @ ast::ExprUnaryOp {
op,
operand,
range: _,
},
) => {
if checker.any_enabled(&[Rule::NotInTest, Rule::NotIsTest]) {
pycodestyle::rules::not_tests(checker, unary_op);
}
if checker.enabled(Rule::UnaryPrefixIncrementDecrement) {
flake8_bugbear::rules::unary_prefix_increment_decrement(
@@ -1142,18 +1140,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
range: _,
},
) => {
let check_none_comparisons = checker.enabled(Rule::NoneComparison);
let check_true_false_comparisons = checker.enabled(Rule::TrueFalseComparison);
if check_none_comparisons || check_true_false_comparisons {
pycodestyle::rules::literal_comparisons(
checker,
expr,
left,
ops,
comparators,
check_none_comparisons,
check_true_false_comparisons,
);
if checker.any_enabled(&[Rule::NoneComparison, Rule::TrueFalseComparison]) {
pycodestyle::rules::literal_comparisons(checker, compare);
}
if checker.enabled(Rule::IsLiteral) {
pyflakes::rules::invalid_literal_comparison(checker, left, ops, comparators, expr);
@@ -1190,7 +1178,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::magic_value_comparison(checker, left, comparators);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_compare(checker, expr, left, ops, comparators);
flake8_simplify::rules::key_in_dict_compare(checker, compare);
}
if checker.enabled(Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(checker, expr, left, ops, comparators);
@@ -1210,7 +1198,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _,
range: _,
}) => {
if checker.is_stub && checker.enabled(Rule::NumericLiteralTooLong) {
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
flake8_pyi::rules::numeric_literal_too_long(checker, expr);
}
}
@@ -1219,7 +1207,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _,
range: _,
}) => {
if checker.is_stub && checker.enabled(Rule::StringOrBytesTooLong) {
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
}
@@ -1236,18 +1224,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::HardcodedTempFile) {
if let Some(diagnostic) = flake8_bandit::rules::hardcoded_tmp_directory(
expr,
value,
&checker.settings.flake8_bandit.hardcoded_tmp_directory,
) {
checker.diagnostics.push(diagnostic);
}
flake8_bandit::rules::hardcoded_tmp_directory(checker, expr, value);
}
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}

View File

@@ -6,16 +6,13 @@ use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
/// Run lint rules over a [`Parameters`] syntax node.
pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
if checker.enabled(Rule::MutableArgumentDefault) {
flake8_bugbear::rules::mutable_argument_default(checker, parameters);
}
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
flake8_bugbear::rules::function_call_in_argument_default(checker, parameters);
}
if checker.settings.rules.enabled(Rule::ImplicitOptional) {
ruff::rules::implicit_optional(checker, parameters);
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::TypedArgumentDefaultInStub) {
flake8_pyi::rules::typed_argument_simple_defaults(checker, parameters);
}

View File

@@ -53,7 +53,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::BreakOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::break_outside_loop(
stmt,
&mut checker.semantic.parents().skip(1),
&mut checker.semantic.current_statements().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
@@ -63,30 +63,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ContinueOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::continue_outside_loop(
stmt,
&mut checker.semantic.parents().skip(1),
&mut checker.semantic.current_statements().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
}
}
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
}) => {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
is_async,
name,
decorator_list,
returns,
parameters,
body,
type_params,
range: _,
},
) => {
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
}
@@ -113,7 +107,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
pep8_naming::rules::invalid_first_argument_name_for_class_method(
checker,
checker.semantic.scope(),
checker.semantic.current_scope(),
name,
decorator_list,
parameters,
@@ -125,7 +119,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::InvalidFirstArgumentNameForMethod) {
if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_method(
checker,
checker.semantic.scope(),
checker.semantic.current_scope(),
name,
decorator_list,
parameters,
@@ -133,7 +127,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
@@ -151,11 +145,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::non_self_return_type(
checker,
stmt,
*is_async,
name,
decorator_list,
returns.as_ref().map(AsRef::as_ref),
parameters,
stmt.is_async_function_def_stmt(),
);
}
if checker.enabled(Rule::CustomTypeVarReturnType) {
@@ -168,30 +162,27 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
type_params.as_ref(),
);
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
}
}
if checker.is_stub || checker.settings.target_version >= PythonVersion::Py311 {
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py311
{
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
flake8_pyi::rules::no_return_argument_annotation(checker, parameters);
}
}
if checker.enabled(Rule::BadExitAnnotation) {
flake8_pyi::rules::bad_exit_annotation(
checker,
stmt.is_async_function_def_stmt(),
name,
parameters,
);
flake8_pyi::rules::bad_exit_annotation(checker, *is_async, name, parameters);
}
if checker.enabled(Rule::RedundantNumericUnion) {
flake8_pyi::rules::redundant_numeric_union(checker, parameters);
}
if checker.enabled(Rule::DunderFunctionName) {
if let Some(diagnostic) = pep8_naming::rules::dunder_function_name(
checker.semantic.scope(),
checker.semantic.current_scope(),
stmt,
name,
&checker.settings.pep8_naming.ignore_names,
@@ -215,6 +206,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::CachedInstanceMethod) {
flake8_bugbear::rules::cached_instance_method(checker, decorator_list);
}
if checker.enabled(Rule::MutableArgumentDefault) {
flake8_bugbear::rules::mutable_argument_default(checker, function_def);
}
if checker.any_enabled(&[
Rule::UnnecessaryReturnNone,
Rule::ImplicitReturnValue,
@@ -306,6 +300,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.any_enabled(&[
Rule::PytestParametrizeNamesWrongType,
Rule::PytestParametrizeValuesWrongType,
Rule::PytestDuplicateParametrizeTestCases,
]) {
flake8_pytest_style::rules::parametrize(checker, decorator_list);
}
@@ -315,16 +310,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
]) {
flake8_pytest_style::rules::marks(checker, decorator_list);
}
if checker.enabled(Rule::BooleanPositionalArgInFunctionDefinition) {
flake8_boolean_trap::rules::check_positional_boolean_in_def(
if checker.enabled(Rule::BooleanTypeHintPositionalArgument) {
flake8_boolean_trap::rules::boolean_type_hint_positional_argument(
checker,
name,
decorator_list,
parameters,
);
}
if checker.enabled(Rule::BooleanDefaultValueInFunctionDefinition) {
flake8_boolean_trap::rules::check_boolean_default_value_in_function_definition(
if checker.enabled(Rule::BooleanDefaultValuePositionalArgument) {
flake8_boolean_trap::rules::boolean_default_value_positional_argument(
checker,
name,
decorator_list,
@@ -343,10 +338,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, body);
}
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt);
}
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
checker,
@@ -412,7 +404,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
body,
);
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.enabled(Rule::DjangoModelWithoutDunderStr) {
flake8_django::rules::model_without_dunder_str(checker, class_def);
}
@@ -453,7 +445,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::AbstractBaseClassWithoutAbstractMethod,
Rule::EmptyMethodWithoutAbstractDecorator,
@@ -467,22 +459,22 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
if checker.enabled(Rule::PassInClassBody) {
flake8_pyi::rules::pass_in_class_body(checker, stmt, body);
flake8_pyi::rules::pass_in_class_body(checker, class_def);
}
}
if checker.enabled(Rule::EllipsisInNonEmptyClassBody) {
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, stmt, body);
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, body);
}
if checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle) {
flake8_pytest_style::rules::marks(checker, decorator_list);
}
if checker.enabled(Rule::DuplicateClassFieldDefinition) {
flake8_pie::rules::duplicate_class_field_definition(checker, stmt, body);
flake8_pie::rules::duplicate_class_field_definition(checker, body);
}
if checker.enabled(Rule::NonUniqueEnums) {
flake8_pie::rules::non_unique_enums(checker, stmt, body);
@@ -517,17 +509,16 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::SingleStringSlots) {
pylint::rules::single_string_slots(checker, class_def);
}
if checker.enabled(Rule::BadDunderMethodName) {
pylint::rules::bad_dunder_method_name(checker, body);
}
}
Stmt::Import(ast::StmtImport { names, range: _ }) => {
if checker.enabled(Rule::MultipleImportsOnOneLine) {
pycodestyle::rules::multiple_imports_on_one_line(checker, stmt, names);
}
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(
checker,
stmt,
checker.locator,
);
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
@@ -563,13 +554,30 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::BannedApi) {
flake8_tidy_imports::rules::name_or_parent_is_banned(
flake8_tidy_imports::rules::banned_api(
checker,
&alias.name,
alias,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent {
module: &alias.name,
},
),
&alias,
);
}
if !checker.is_stub {
if checker.enabled(Rule::BannedModuleLevelImports) {
flake8_tidy_imports::rules::banned_module_level_imports(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent {
module: &alias.name,
},
),
&alias,
);
}
if !checker.source_type.is_stub() {
if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias);
}
@@ -683,11 +691,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
let module = module.as_deref();
let level = level.map(|level| level.to_u32());
if checker.enabled(Rule::ModuleImportNotAtTopOfFile) {
pycodestyle::rules::module_import_not_at_top_of_file(
checker,
stmt,
checker.locator,
);
pycodestyle::rules::module_import_not_at_top_of_file(checker, stmt);
}
if checker.enabled(Rule::GlobalStatement) {
for name in names {
@@ -723,16 +727,56 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(module) =
helpers::resolve_imported_module_path(level, module, checker.module_path)
{
flake8_tidy_imports::rules::name_or_parent_is_banned(checker, &module, stmt);
flake8_tidy_imports::rules::banned_api(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent { module: &module },
),
&stmt,
);
for alias in names {
if &alias.name == "*" {
continue;
}
flake8_tidy_imports::rules::name_is_banned(
flake8_tidy_imports::rules::banned_api(
checker,
format!("{module}.{}", alias.name),
alias,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchName(
flake8_tidy_imports::matchers::MatchName {
module: &module,
member: &alias.name,
},
),
&alias,
);
}
}
}
if checker.enabled(Rule::BannedModuleLevelImports) {
if let Some(module) =
helpers::resolve_imported_module_path(level, module, checker.module_path)
{
flake8_tidy_imports::rules::banned_module_level_imports(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchNameOrParent(
flake8_tidy_imports::matchers::MatchNameOrParent { module: &module },
),
&stmt,
);
for alias in names {
if &alias.name == "*" {
continue;
}
flake8_tidy_imports::rules::banned_module_level_imports(
checker,
&flake8_tidy_imports::matchers::NameMatchPolicy::MatchName(
flake8_tidy_imports::matchers::MatchName {
module: &module,
member: &alias.name,
},
),
&alias,
);
}
}
@@ -744,7 +788,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::FutureAnnotationsInStub) {
flake8_pyi::rules::from_future_import(checker, import_from);
}
@@ -764,7 +808,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
} else if &alias.name == "*" {
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.scope().kind, ScopeKind::Module) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
name: helpers::format_import_from(level, module),
@@ -889,7 +933,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias);
}
@@ -980,7 +1024,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_simplify::rules::nested_if_statements(
checker,
if_,
checker.semantic.stmt_parent(),
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::IfWithSameArms) {
@@ -1002,7 +1046,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
tryceratops::rules::type_check_without_type_error(
checker,
if_,
checker.semantic.stmt_parent(),
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::OutdatedVersionBlock) {
@@ -1013,7 +1057,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
Rule::PatchVersionComparison,
@@ -1095,8 +1139,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
}
}
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@@ -1106,9 +1149,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,
stmt,
body,
checker.semantic.stmt_parent(),
with_stmt,
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::RedefinedLoopName) {
@@ -1126,22 +1168,21 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
perflint::rules::try_except_in_loop(checker, body);
}
}
Stmt::For(ast::StmtFor {
target,
body,
iter,
orelse,
..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target,
body,
iter,
orelse,
..
}) => {
if checker.any_enabled(&[Rule::UnusedLoopControlVariable, Rule::IncorrectDictIterator])
{
Stmt::For(
for_stmt @ ast::StmtFor {
target,
body,
iter,
orelse,
is_async,
..
},
) => {
if checker.any_enabled(&[
Rule::UnusedLoopControlVariable,
Rule::IncorrectDictIterator,
Rule::YieldInForLoop,
]) {
checker.deferred.for_loops.push(checker.semantic.snapshot());
}
if checker.enabled(Rule::LoopVariableOverridesIterator) {
@@ -1162,17 +1203,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::IterationOverSet) {
pylint::rules::iteration_over_set(checker, iter);
}
if stmt.is_for_stmt() {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(checker, target, iter);
}
if checker.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
}
}
if checker.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, target, body);
}
@@ -1182,20 +1212,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(checker, for_stmt);
}
if checker.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
}
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
..
}) => {
if checker.enabled(Rule::JumpStatementInFinally) {
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
@@ -1325,7 +1359,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::TypeBivariance) {
pylint::rules::type_bivariance(checker, value);
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnprefixedTypeParam,
Rule::AssignmentDefaultInStub,
@@ -1336,8 +1370,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.scopes()
.any(|scope| scope.kind.is_any_function())
.current_scopes()
.any(|scope| scope.kind.is_function())
{
if checker.enabled(Rule::UnprefixedTypeParam) {
flake8_pyi::rules::prefix_type_params(checker, value, targets);
@@ -1395,14 +1429,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695TypeAlias) {
pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt);
}
if checker.is_stub {
if checker.source_type.is_stub() {
if let Some(value) = value {
if checker.enabled(Rule::AssignmentDefaultInStub) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.scopes()
.any(|scope| scope.kind.is_any_function())
.current_scopes()
.any(|scope| scope.kind.is_function())
{
flake8_pyi::rules::annotated_assignment_default_in_stub(
checker, target, value, annotation,

View File

@@ -32,18 +32,20 @@ use itertools::Itertools;
use log::error;
use ruff_python_ast::{
self as ast, Arguments, Comprehension, Constant, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt,
Suite, UnaryOp,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged,
Stmt, Suite, UnaryOp,
};
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, IsolationLevel};
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::helpers::{
collect_import_from_member, extract_handled_exceptions, to_module_path,
};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::str::trailing_quote;
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::{helpers, str, visitor};
use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Quote, Stylist};
use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
@@ -53,7 +55,6 @@ use ruff_python_semantic::{
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator;
use crate::checkers::ast::deferred::Deferred;
@@ -75,8 +76,8 @@ pub(crate) struct Checker<'a> {
package: Option<&'a Path>,
/// The module representation of the current file (e.g., `foo.bar`).
module_path: Option<&'a [String]>,
/// Whether the current file is a stub (`.pyi`) file.
is_stub: bool,
/// The [`PySourceType`] of the current file.
pub(crate) source_type: PySourceType,
/// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression
/// comments).
noqa: flags::Noqa,
@@ -118,6 +119,7 @@ impl<'a> Checker<'a> {
stylist: &'a Stylist,
indexer: &'a Indexer,
importer: Importer<'a>,
source_type: PySourceType,
) -> Checker<'a> {
Checker {
settings,
@@ -126,7 +128,7 @@ impl<'a> Checker<'a> {
path,
package,
module_path: module.path(),
is_stub: is_python_stub_file(path),
source_type,
locator,
stylist,
indexer,
@@ -174,13 +176,12 @@ impl<'a> Checker<'a> {
///
/// If the current expression in the context is not an f-string, returns ``None``.
pub(crate) fn f_string_quote_style(&self) -> Option<Quote> {
let model = &self.semantic;
if !model.in_f_string() {
if !self.semantic.in_f_string() {
return None;
}
// Find the quote character used to start the containing f-string.
let expr = model.expr()?;
let expr = self.semantic.current_expression()?;
let string_range = self.indexer.f_string_range(expr.start())?;
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
@@ -192,18 +193,22 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`IsolationLevel`] for fixes in the current context.
/// Returns the [`IsolationLevel`] to isolate fixes for the current statement.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel {
parent
.and_then(|stmt| self.semantic.stmts.node_id(stmt))
.map_or(IsolationLevel::default(), |node_id| {
IsolationLevel::Group(node_id.into())
})
pub(crate) fn statement_isolation(&self) -> IsolationLevel {
IsolationLevel::Group(self.semantic.current_statement_id().into())
}
/// Returns the [`IsolationLevel`] to isolate fixes in the current statement's parent.
pub(crate) fn parent_isolation(&self) -> IsolationLevel {
self.semantic
.current_statement_parent_id()
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
@@ -233,11 +238,6 @@ impl<'a> Checker<'a> {
&self.semantic
}
/// Return `true` if the current file is a stub file (`.pyi`).
pub(crate) const fn is_stub(&self) -> bool {
self.is_stub
}
/// The [`Path`] to the file under analysis.
pub(crate) const fn path(&self) -> &'a Path {
self.path
@@ -267,7 +267,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_stmt(stmt);
self.semantic.push_node(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -291,7 +291,7 @@ where
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !self.semantic.seen_import_boundary()
&& !helpers::is_assignment_to_a_dunder(stmt)
&& !helpers::in_nested_block(self.semantic.parents())
&& !helpers::in_nested_block(self.semantic.current_statements())
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
@@ -325,11 +325,11 @@ where
// Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be
// "foo.bar".
let name = alias.name.split('.').next().unwrap();
let qualified_name = &alias.name;
let call_path: Box<[&str]> = alias.name.split('.').collect();
self.add_binding(
name,
alias.identifier(),
BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }),
BindingKind::SubmoduleImport(SubmoduleImport { call_path }),
BindingFlags::EXTERNAL,
);
} else {
@@ -346,11 +346,11 @@ where
}
let name = alias.asname.as_ref().unwrap_or(&alias.name);
let qualified_name = &alias.name;
let call_path: Box<[&str]> = alias.name.split('.').collect();
self.add_binding(
name,
alias.identifier(),
BindingKind::Import(Import { qualified_name }),
BindingKind::Import(Import { call_path }),
flags,
);
}
@@ -375,7 +375,7 @@ where
);
} else if &alias.name == "*" {
self.semantic
.scope_mut()
.current_scope_mut()
.add_star_import(StarImport { level, module });
} else {
let mut flags = BindingFlags::EXTERNAL;
@@ -394,12 +394,16 @@ where
// be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz"
// and `qualified_name` would be "foo.bar".
let name = alias.asname.as_ref().unwrap_or(&alias.name);
let qualified_name =
helpers::format_import_from_member(level, module, &alias.name);
// Attempt to resolve any relative imports; but if we don't know the current
// module path, or the relative import extends beyond the package root,
// fallback to a literal representation (e.g., `[".", "foo"]`).
let call_path = collect_import_from_member(level, module, &alias.name)
.into_boxed_slice();
self.add_binding(
name,
alias.identifier(),
BindingKind::FromImport(FromImport { qualified_name }),
BindingKind::FromImport(FromImport { call_path }),
flags,
);
}
@@ -420,7 +424,7 @@ where
BindingKind::Global,
BindingFlags::GLOBAL,
);
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(name, binding_id);
}
}
@@ -443,7 +447,7 @@ where
BindingKind::Nonlocal(scope_id),
BindingFlags::NONLOCAL,
);
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(name, binding_id);
}
}
@@ -454,22 +458,16 @@ where
// Step 2: Traversal
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body,
parameters,
decorator_list,
type_params,
returns,
..
}) => {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
},
) => {
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
@@ -530,8 +528,7 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Function,
stmt,
ExtractionTarget::Function(function_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -539,8 +536,7 @@ where
self.semantic.push_scope(match &stmt {
Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt),
Stmt::AsyncFunctionDef(stmt) => ScopeKind::AsyncFunction(stmt),
_ => unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef"),
_ => unreachable!("Expected Stmt::FunctionDef"),
});
self.deferred.functions.push(self.semantic.snapshot());
@@ -574,8 +570,7 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Class,
stmt,
ExtractionTarget::Class(class_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -608,14 +603,7 @@ where
handlers,
orelse,
finalbody,
range: _,
})
| Stmt::TryStar(ast::StmtTryStar {
body,
handlers,
orelse,
finalbody,
range: _,
..
}) => {
let mut handled_exceptions = Exceptions::empty();
for type_ in extract_handled_exceptions(handlers) {
@@ -635,16 +623,28 @@ where
}
}
// Iterate over the `body`, then the `handlers`, then the `orelse`, then the
// `finalbody`, but treat the body and the `orelse` as a single branch for
// flow analysis purposes.
let branch = self.semantic.push_branch();
self.semantic.handled_exceptions.push(handled_exceptions);
self.visit_body(body);
self.semantic.handled_exceptions.pop();
self.semantic.pop_branch();
for except_handler in handlers {
self.semantic.push_branch();
self.visit_except_handler(except_handler);
self.semantic.pop_branch();
}
self.semantic.set_branch(branch);
self.visit_body(orelse);
self.semantic.pop_branch();
self.semantic.push_branch();
self.visit_body(finalbody);
self.semantic.pop_branch();
}
Stmt::AnnAssign(ast::StmtAnnAssign {
target,
@@ -656,7 +656,7 @@ where
// available at runtime.
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
let runtime_annotation = if self.semantic.future_annotations() {
if self.semantic.scope().kind.is_class() {
if self.semantic.current_scope().kind.is_class() {
let baseclasses = &self
.settings
.flake8_type_checking
@@ -675,7 +675,7 @@ where
}
} else {
matches!(
self.semantic.scope().kind,
self.semantic.current_scope().kind,
ScopeKind::Class(_) | ScopeKind::Module
)
};
@@ -724,6 +724,7 @@ where
) => {
self.visit_boolean_test(test);
self.semantic.push_branch();
if typing::is_type_checking_block(stmt_if, &self.semantic) {
if self.semantic.at_top_level() {
self.importer.visit_type_checking_block(stmt);
@@ -732,9 +733,12 @@ where
} else {
self.visit_body(body);
}
self.semantic.pop_branch();
for clause in elif_else_clauses {
self.semantic.push_branch();
self.visit_elif_else_clause(clause);
self.semantic.pop_branch();
}
}
_ => visitor::walk_stmt(self, stmt),
@@ -742,8 +746,7 @@ where
// Step 3: Clean-up
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
let scope_id = self.semantic.scope_id;
self.deferred.scopes.push(scope_id);
self.semantic.pop_scope(); // Function scope
@@ -776,7 +779,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_stmt();
self.semantic.pop_node();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -812,7 +815,7 @@ where
return;
}
self.semantic.push_expr(expr);
self.semantic.push_node(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -840,7 +843,7 @@ where
}) => {
if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() {
if id == "locals" && ctx.is_load() {
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.set_uses_locals();
}
}
@@ -891,18 +894,20 @@ where
},
) => {
// Visit the default arguments, but avoid the body, which will be deferred.
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
self.visit_expr(expr);
if let Some(parameters) = parameters {
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
self.visit_expr(expr);
}
}
}
@@ -1206,7 +1211,7 @@ where
));
}
}
Expr::JoinedStr(_) => {
Expr::FString(_) => {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
@@ -1230,7 +1235,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_expr();
self.semantic.pop_node();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
@@ -1285,7 +1290,7 @@ where
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match format_spec {
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, .. }) => {
for value in values {
self.visit_expr(value);
}
@@ -1368,6 +1373,17 @@ where
}
}
fn visit_match_case(&mut self, match_case: &'b MatchCase) {
self.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
self.visit_expr(expr);
}
self.semantic.push_branch();
self.visit_body(&match_case.body);
self.semantic.pop_branch();
}
fn visit_type_param(&mut self, type_param: &'b ast::TypeParam) {
// Step 1: Binding
match type_param {
@@ -1610,7 +1626,7 @@ impl<'a> Checker<'a> {
}
fn handle_node_store(&mut self, id: &'a str, expr: &Expr) {
let parent = self.semantic.stmt();
let parent = self.semantic.current_statement();
if matches!(
parent,
@@ -1625,7 +1641,7 @@ impl<'a> Checker<'a> {
return;
}
if matches!(parent, Stmt::For(_) | Stmt::AsyncFor(_)) {
if parent.is_for_stmt() {
self.add_binding(
id,
expr.range(),
@@ -1645,7 +1661,7 @@ impl<'a> Checker<'a> {
return;
}
let scope = self.semantic.scope();
let scope = self.semantic.current_scope();
if scope.kind.is_module()
&& match parent {
@@ -1697,8 +1713,8 @@ impl<'a> Checker<'a> {
if self
.semantic
.expr_ancestors()
.any(|expr| expr.is_named_expr_expr())
.current_expressions()
.any(Expr::is_named_expr_expr)
{
self.add_binding(
id,
@@ -1724,7 +1740,7 @@ impl<'a> Checker<'a> {
self.semantic.resolve_del(id, expr.range());
if helpers::on_conditional_branch(&mut self.semantic.parents()) {
if helpers::on_conditional_branch(&mut self.semantic.current_statements()) {
return;
}
@@ -1732,7 +1748,7 @@ impl<'a> Checker<'a> {
let binding_id =
self.semantic
.push_binding(expr.range(), BindingKind::Deletion, BindingFlags::empty());
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(id, binding_id);
}
@@ -1786,7 +1802,7 @@ impl<'a> Checker<'a> {
pyupgrade::rules::quoted_annotation(self, value, range);
}
}
if self.is_stub {
if self.source_type.is_stub() {
if self.enabled(Rule::QuotedAnnotationInStub) {
flake8_pyi::rules::quoted_annotation_in_stub(self, value, range);
}
@@ -1824,19 +1840,14 @@ impl<'a> Checker<'a> {
for snapshot in deferred_functions {
self.semantic.restore(snapshot);
match &self.semantic.stmt() {
Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body, parameters, ..
}) => {
self.visit_parameters(parameters);
self.visit_body(body);
}
_ => {
unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef")
}
if let Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
}) = self.semantic.current_statement()
{
self.visit_parameters(parameters);
self.visit_body(body);
} else {
unreachable!("Expected Stmt::FunctionDef")
}
}
}
@@ -1856,7 +1867,9 @@ impl<'a> Checker<'a> {
range: _,
}) = expr
{
self.visit_parameters(parameters);
if let Some(parameters) = parameters {
self.visit_parameters(parameters);
}
self.visit_expr(body);
} else {
unreachable!("Expected Expr::Lambda");
@@ -1877,7 +1890,7 @@ impl<'a> Checker<'a> {
.map(|binding_id| &self.semantic.bindings[binding_id])
.filter_map(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => {
Some(names.iter().map(|name| (*name, binding.range)))
Some(names.iter().map(|name| (*name, binding.range())))
}
_ => None,
})
@@ -1928,6 +1941,7 @@ pub(crate) fn check_ast(
noqa: flags::Noqa,
path: &Path,
package: Option<&Path>,
source_type: PySourceType,
) -> Vec<Diagnostic> {
let module_path = package.and_then(|package| to_module_path(package, path));
let module = Module {
@@ -1955,6 +1969,7 @@ pub(crate) fn check_ast(
stylist,
indexer,
Importer::new(python_ast, locator, stylist),
source_type,
);
checker.bind_builtins();

View File

@@ -2,7 +2,7 @@
use std::borrow::Cow;
use std::path::Path;
use ruff_python_ast::{self as ast, Ranged, Stmt, Suite};
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::helpers::to_module_path;
@@ -10,7 +10,7 @@ use ruff_python_ast::imports::{ImportMap, ModuleImport};
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator;
use crate::directives::IsortDirectives;
@@ -87,12 +87,12 @@ pub(crate) fn check_imports(
path: &Path,
package: Option<&Path>,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> (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, source_kind);
let mut tracker =
BlockBuilder::new(locator, directives, source_type.is_stub(), source_kind);
tracker.visit_body(python_ast);
tracker
};
@@ -104,7 +104,13 @@ pub(crate) fn check_imports(
for block in &blocks {
if !block.imports.is_empty() {
if let Some(diagnostic) = isort::rules::organize_imports(
block, locator, stylist, indexer, settings, package,
block,
locator,
stylist,
indexer,
settings,
package,
source_type,
) {
diagnostics.push(diagnostic);
}
@@ -113,7 +119,11 @@ pub(crate) fn check_imports(
}
if settings.rules.enabled(Rule::MissingRequiredImport) {
diagnostics.extend(isort::rules::add_required_imports(
python_ast, locator, stylist, settings, is_stub,
python_ast,
locator,
stylist,
settings,
source_type,
));
}

View File

@@ -1,10 +1,10 @@
use ruff_python_parser::lexer::LexResult;
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_python_ast::Ranged;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::logical_lines::{

View File

@@ -94,8 +94,15 @@ pub(crate) fn check_noqa(
}
}
// Enforce that the noqa directive was actually used (RUF100).
if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) {
// Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed.
if settings.rules.enabled(Rule::UnusedNOQA)
&& analyze_directives
&& !exemption.is_some_and(|exemption| match exemption {
FileExemption::All => true,
FileExemption::Codes(codes) => codes.contains(&Rule::UnusedNOQA.noqa_code()),
})
{
for line in noqa_directives.lines() {
match &line.directive {
Directive::All(directive) => {

View File

@@ -216,6 +216,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
@@ -226,8 +227,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "W0711") => (RuleGroup::Unspecified, rules::pylint::rules::BinaryOpException),
(Pylint, "W1508") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarDefault),
(Pylint, "W1509") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessPopenPreexecFn),
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
// flake8-async
@@ -309,6 +312,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// flake8-tidy-imports
(Flake8TidyImports, "251") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedApi),
(Flake8TidyImports, "252") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::RelativeImports),
(Flake8TidyImports, "253") => (RuleGroup::Unspecified, rules::flake8_tidy_imports::rules::BannedModuleLevelImports),
// flake8-return
(Flake8Return, "501") => (RuleGroup::Unspecified, rules::flake8_return::rules::UnnecessaryReturnNone),
@@ -565,9 +569,9 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "701") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::Jinja2AutoescapeFalse),
// flake8-boolean-trap
(Flake8BooleanTrap, "001") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalArgInFunctionDefinition),
(Flake8BooleanTrap, "002") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanDefaultValueInFunctionDefinition),
(Flake8BooleanTrap, "003") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalValueInFunctionCall),
(Flake8BooleanTrap, "001") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanTypeHintPositionalArgument),
(Flake8BooleanTrap, "002") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanDefaultValuePositionalArgument),
(Flake8BooleanTrap, "003") => (RuleGroup::Unspecified, rules::flake8_boolean_trap::rules::BooleanPositionalValueInCall),
// flake8-unused-arguments
(Flake8UnusedArguments, "001") => (RuleGroup::Unspecified, rules::flake8_unused_arguments::rules::UnusedFunctionArgument),
@@ -682,6 +686,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8PytestStyle, "011") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestRaisesTooBroad),
(Flake8PytestStyle, "012") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestRaisesWithMultipleStatements),
(Flake8PytestStyle, "013") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestIncorrectPytestImport),
(Flake8PytestStyle, "014") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestDuplicateParametrizeTestCases),
(Flake8PytestStyle, "015") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestAssertAlwaysFalse),
(Flake8PytestStyle, "016") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestFailWithoutMessage),
(Flake8PytestStyle, "017") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestAssertInExcept),
@@ -694,6 +699,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8PytestStyle, "024") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnnecessaryAsyncioMarkOnFixture),
(Flake8PytestStyle, "025") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestErroneousUseFixturesOnFixture),
(Flake8PytestStyle, "026") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUseFixturesWithoutParameters),
(Flake8PytestStyle, "027") => (RuleGroup::Unspecified, rules::flake8_pytest_style::rules::PytestUnittestRaisesAssertion),
// flake8-pie
(Flake8Pie, "790") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryPass),
@@ -702,6 +708,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pie, "800") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessarySpread),
(Flake8Pie, "804") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryDictKwargs),
(Flake8Pie, "807") => (RuleGroup::Unspecified, rules::flake8_pie::rules::ReimplementedListBuiltin),
(Flake8Pie, "808") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryRangeStart),
(Flake8Pie, "810") => (RuleGroup::Unspecified, rules::flake8_pie::rules::MultipleStartsEndsWith),
// flake8-commas
@@ -811,6 +818,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -405,7 +405,7 @@ y = 2
z = x + 1";
assert_eq!(
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22)),])
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22))])
);
let contents = "x = 1

View File

@@ -1,7 +1,6 @@
//! Extract docstrings from an AST.
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, MemberKind};
/// Extract a docstring from a function or class body.
@@ -28,63 +27,48 @@ pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
pub(crate) fn extract_docstring<'a>(definition: &'a Definition<'a>) -> Option<&'a Expr> {
match definition {
Definition::Module(module) => docstring_from(module.python_ast),
Definition::Member(member) => {
if let Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. }) = &member.stmt
{
docstring_from(body)
} else {
None
}
}
Definition::Member(member) => docstring_from(member.body()),
}
}
#[derive(Copy, Clone)]
pub(crate) enum ExtractionTarget {
Class,
Function,
pub(crate) enum ExtractionTarget<'a> {
Class(&'a ast::StmtClassDef),
Function(&'a ast::StmtFunctionDef),
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub(crate) fn extract_definition<'a>(
target: ExtractionTarget,
stmt: &'a Stmt,
target: ExtractionTarget<'a>,
parent: DefinitionId,
definitions: &Definitions<'a>,
) -> Member<'a> {
match target {
ExtractionTarget::Function => match &definitions[parent] {
ExtractionTarget::Function(function) => match &definitions[parent] {
Definition::Module(..) => Member {
parent,
kind: MemberKind::Function,
stmt,
kind: MemberKind::Function(function),
},
Definition::Member(Member {
kind: MemberKind::Class | MemberKind::NestedClass,
kind: MemberKind::Class(_) | MemberKind::NestedClass(_),
..
}) => Member {
parent,
kind: MemberKind::Method,
stmt,
kind: MemberKind::Method(function),
},
Definition::Member(..) => Member {
Definition::Member(_) => Member {
parent,
kind: MemberKind::NestedFunction,
stmt,
kind: MemberKind::NestedFunction(function),
},
},
ExtractionTarget::Class => match &definitions[parent] {
Definition::Module(..) => Member {
ExtractionTarget::Class(class) => match &definitions[parent] {
Definition::Module(_) => Member {
parent,
kind: MemberKind::Class,
stmt,
kind: MemberKind::Class(class),
},
Definition::Member(..) => Member {
Definition::Member(_) => Member {
parent,
kind: MemberKind::NestedClass,
stmt,
kind: MemberKind::NestedClass(class),
},
},
}

View File

@@ -2,9 +2,8 @@ use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::{TextRange, TextSize};
use ruff_python_semantic::Definition;
use ruff_text_size::TextRange;
pub(crate) mod extraction;
pub(crate) mod google;
@@ -28,43 +27,34 @@ impl<'a> Docstring<'a> {
DocstringBody { docstring: self }
}
pub(crate) fn start(&self) -> TextSize {
self.expr.start()
}
pub(crate) fn end(&self) -> TextSize {
self.expr.end()
}
pub(crate) fn range(&self) -> TextRange {
self.expr.range()
}
pub(crate) fn leading_quote(&self) -> &'a str {
&self.contents[TextRange::up_to(self.body_range.start())]
}
}
impl Ranged for Docstring<'_> {
fn range(&self) -> TextRange {
self.expr.range()
}
}
#[derive(Copy, Clone)]
pub(crate) struct DocstringBody<'a> {
docstring: &'a Docstring<'a>,
}
impl<'a> DocstringBody<'a> {
#[inline]
pub(crate) fn start(self) -> TextSize {
self.range().start()
}
pub(crate) fn range(self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
pub(crate) fn as_str(self) -> &'a str {
&self.docstring.contents[self.docstring.body_range]
}
}
impl Ranged for DocstringBody<'_> {
fn range(&self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
}
impl Deref for DocstringBody<'_> {
type Target = str;

View File

@@ -2,6 +2,7 @@ use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_python_ast::Ranged;
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
@@ -366,6 +367,12 @@ impl<'a> SectionContext<'a> {
}
}
impl Ranged for SectionContext<'_> {
fn range(&self) -> TextRange {
self.range()
}
}
impl Debug for SectionContext<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SectionContext")

View File

@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use globset::GlobMatcher;
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use path_absolutize::Absolutize;
use crate::registry::RuleSet;
@@ -61,7 +61,13 @@ pub fn normalize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root:
/// Convert an absolute path to be relative to the current working directory.
pub fn relativize_path<P: AsRef<Path>>(path: P) -> String {
let path = path.as_ref();
if let Ok(path) = path.strip_prefix(&*path_dedot::CWD) {
#[cfg(target_arch = "wasm32")]
let cwd = Path::new(".");
#[cfg(not(target_arch = "wasm32"))]
let cwd = path_absolutize::path_dedot::CWD.as_path();
if let Ok(path) = path.strip_prefix(cwd) {
return format!("{}", path.display());
}
format!("{}", path.display())

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