Compare commits

...

123 Commits

Author SHA1 Message Date
Charlie Marsh
0e268c91ab Update tests 2023-08-16 21:57:05 -04:00
Charlie Marsh
dbe62cc741 Merge branch 'main' into evanrittenhouse_5073 2023-08-16 21:54:49 -04:00
Charlie Marsh
e38e8c0a51 Use functools.reduce 2023-08-16 21:54:45 -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
Evan Rittenhouse
b6d786fb10 Implement reviewer comments 2023-08-11 12:11:40 -05: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
Evan Rittenhouse
a12a71a845 Initial implementation for RUF017 2023-08-10 23:22:12 -05: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
531 changed files with 62823 additions and 44540 deletions

View File

@@ -328,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"
@@ -338,6 +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
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@3.0.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@3.0.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -1,6 +1,6 @@
# Breaking Changes
## 0.0.283
## 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))
@@ -8,6 +8,8 @@ Previously, when a target Python version was not specified, Ruff would use a def
(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

@@ -571,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.283"
version = "0.0.284"
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.283"
version = "0.0.284"
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.283"
version = "0.0.284"
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.283
rev: v0.0.284
hooks:
- id: ruff
```
@@ -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.283"
version = "0.0.284"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.283"
version = "0.0.284"
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

@@ -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()):
...
@@ -261,3 +275,32 @@ def mutable_annotations(
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

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

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

@@ -0,0 +1,26 @@
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), (a, b), (b, c)])
def test_error_expr_complex(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

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

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

@@ -202,3 +202,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

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

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

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

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

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

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

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

@@ -209,14 +209,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

@@ -16,9 +16,15 @@ 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(),

View File

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

@@ -261,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,
@@ -323,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 {
@@ -418,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,
);
}
}
@@ -675,10 +673,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);
@@ -760,9 +756,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,
@@ -867,6 +873,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,
@@ -915,7 +924,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::await_outside_async(checker, expr);
}
}
Expr::FString(ast::ExprFString { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, .. }) => {
if checker.enabled(Rule::FStringMissingPlaceholders) {
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
}
@@ -942,7 +951,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()
{
@@ -1095,22 +1104,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
}
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(
@@ -1135,18 +1137,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);
@@ -1229,13 +1221,7 @@ 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());

View File

@@ -6,9 +6,6 @@ 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);
}

View File

@@ -69,16 +69,18 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::FunctionDef(ast::StmtFunctionDef {
is_async,
name,
decorator_list,
returns,
parameters,
body,
type_params,
range: _,
}) => {
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);
}
@@ -204,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,
@@ -295,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);
}
@@ -304,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,
@@ -506,17 +512,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 {
@@ -552,12 +557,29 @@ 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.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);
@@ -672,11 +694,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 {
@@ -712,16 +730,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,
);
}
}
@@ -1168,14 +1226,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
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);

View File

@@ -599,14 +599,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) {
@@ -1275,7 +1268,7 @@ where
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match format_spec {
Expr::FString(ast::ExprFString { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, .. }) => {
for value in values {
self.visit_expr(value);
}

View File

@@ -226,8 +226,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 +311,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 +568,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 +685,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 +698,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),
@@ -811,6 +816,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

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

View File

@@ -67,9 +67,13 @@ impl<'a> Insertion<'a> {
TextSize::default()
};
// Skip over commented lines.
// Skip over commented lines, with whitespace separation.
for line in UniversalNewlineIterator::with_offset(locator.after(location), location) {
if line.trim_whitespace_start().starts_with('#') {
let trimmed_line = line.trim_whitespace_start();
if trimmed_line.is_empty() {
continue;
}
if trimmed_line.starts_with('#') {
location = line.full_end();
} else {
break;

View File

@@ -1,7 +1,7 @@
use std::cmp::Ordering;
use std::fmt::Display;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::io::{BufReader, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
use std::iter;
use std::path::Path;
@@ -26,7 +26,7 @@ pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
/// Run round-trip source code generation on a given Jupyter notebook file path.
pub fn round_trip(path: &Path) -> anyhow::Result<String> {
let mut notebook = Notebook::read(path).map_err(|err| {
let mut notebook = Notebook::from_path(path).map_err(|err| {
anyhow::anyhow!(
"Failed to read notebook file `{}`: {:?}",
path.display(),
@@ -120,18 +120,30 @@ pub struct Notebook {
impl Notebook {
/// Read the Jupyter Notebook from the given [`Path`].
///
/// See also the black implementation
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
pub fn read(path: &Path) -> Result<Self, Box<Diagnostic>> {
let mut reader = BufReader::new(File::open(path).map_err(|err| {
pub fn from_path(path: &Path) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(BufReader::new(File::open(path).map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
},
TextRange::default(),
)
})?);
})?))
}
/// Read the Jupyter Notebook from its JSON string.
pub fn from_contents(contents: &str) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(Cursor::new(contents))
}
/// Read a Jupyter Notebook from a [`Read`] implementor.
///
/// See also the black implementation
/// <https://github.com/psf/black/blob/69ca0a4c7a365c5f5eea519a90980bab72cab764/src/black/__init__.py#L1017-L1046>
fn from_reader<R>(mut reader: R) -> Result<Self, Box<Diagnostic>>
where
R: Read + Seek,
{
let trailing_newline = reader.seek(SeekFrom::End(-1)).is_ok_and(|_| {
let mut buf = [0; 1];
reader.read_exact(&mut buf).is_ok_and(|_| buf[0] == b'\n')
@@ -144,7 +156,7 @@ impl Notebook {
TextRange::default(),
)
})?;
let raw_notebook: RawNotebook = match serde_json::from_reader(reader) {
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@@ -159,14 +171,19 @@ impl Notebook {
Category::Syntax | Category::Eof => {
// Maybe someone saved the python sources (those with the `# %%` separator)
// as jupyter notebook instead. Let's help them.
let contents = std::fs::read_to_string(path).map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
},
TextRange::default(),
)
})?;
let mut contents = String::new();
reader
.rewind()
.and_then(|_| reader.read_to_string(&mut contents))
.map_err(|err| {
Diagnostic::new(
IOError {
message: format!("{err}"),
},
TextRange::default(),
)
})?;
// Check if tokenizing was successful and the file is non-empty
if lex(&contents, Mode::Module).any(|result| result.is_err()) {
Diagnostic::new(
@@ -182,7 +199,7 @@ impl Notebook {
Diagnostic::new(
SyntaxError {
message: format!(
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT} extension), \
"Expected a Jupyter Notebook (.{JUPYTER_NOTEBOOK_EXT}), \
which must be internally stored as JSON, \
but found a Python source file: {err}"
),
@@ -484,22 +501,22 @@ mod tests {
fn test_invalid() {
let path = Path::new("resources/test/fixtures/jupyter/invalid_extension.ipynb");
assert_eq!(
Notebook::read(path).unwrap_err().kind.body,
"SyntaxError: Expected a Jupyter Notebook (.ipynb extension), \
Notebook::from_path(path).unwrap_err().kind.body,
"SyntaxError: Expected a Jupyter Notebook (.ipynb), \
which must be internally stored as JSON, \
but found a Python source file: \
expected value at line 1 column 1"
);
let path = Path::new("resources/test/fixtures/jupyter/not_json.ipynb");
assert_eq!(
Notebook::read(path).unwrap_err().kind.body,
Notebook::from_path(path).unwrap_err().kind.body,
"SyntaxError: A Jupyter Notebook (.ipynb) must internally be JSON, \
but this file isn't valid JSON: \
expected value at line 1 column 1"
);
let path = Path::new("resources/test/fixtures/jupyter/wrong_schema.ipynb");
assert_eq!(
Notebook::read(path).unwrap_err().kind.body,
Notebook::from_path(path).unwrap_err().kind.body,
"SyntaxError: This file does not match the schema expected of Jupyter Notebooks: \
missing field `cells` at line 1 column 2"
);
@@ -571,11 +588,11 @@ print("after empty cells")
}
#[test]
fn test_line_magics() -> Result<()> {
let path = "line_magics.ipynb".to_string();
fn test_ipy_escape_command() -> Result<()> {
let path = "ipy_escape_command.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("line_magics_expected.ipynb"),
Path::new("ipy_escape_command_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(diagnostics, path, source_kind);

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
line_magics.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
ipy_escape_command.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
3 | %matplotlib inline
4 |

View File

@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU8;
use unicode_width::UnicodeWidthChar;
use ruff_macros::CacheKey;
@@ -58,7 +59,7 @@ impl Eq for LineWidth {}
impl PartialOrd for LineWidth {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.width.partial_cmp(&other.width)
Some(self.cmp(other))
}
}
@@ -83,7 +84,7 @@ impl LineWidth {
}
fn update(mut self, chars: impl Iterator<Item = char>) -> Self {
let tab_size: usize = self.tab_size.into();
let tab_size: usize = self.tab_size.as_usize();
for c in chars {
match c {
'\t' => {
@@ -144,22 +145,22 @@ impl PartialOrd<LineLength> for LineWidth {
/// The size of a tab.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabSize(pub u8);
pub struct TabSize(NonZeroU8);
impl TabSize {
pub(crate) fn as_usize(self) -> usize {
self.0.get() as usize
}
}
impl Default for TabSize {
fn default() -> Self {
Self(4)
Self(NonZeroU8::new(4).unwrap())
}
}
impl From<u8> for TabSize {
fn from(tab_size: u8) -> Self {
impl From<NonZeroU8> for TabSize {
fn from(tab_size: NonZeroU8) -> Self {
Self(tab_size)
}
}
impl From<TabSize> for usize {
fn from(tab_size: TabSize) -> Self {
tab_size.0 as usize
}
}

View File

@@ -293,12 +293,10 @@ impl Display for MessageCodeFrame<'_> {
}
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
static TAB_SIZE: TabSize = TabSize(4); // TODO(jonathan): use `tab-size`
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidth::new(TAB_SIZE);
let mut line_width = LineWidth::new(TabSize::default());
for (index, c) in source.char_indices() {
let old_width = line_width.get();

View File

@@ -72,7 +72,7 @@ impl RuleSet {
/// let set_1 = RuleSet::from_rules(&[Rule::AmbiguousFunctionName, Rule::AnyType]);
/// let set_2 = RuleSet::from_rules(&[
/// Rule::BadQuotesInlineString,
/// Rule::BooleanPositionalValueInFunctionCall,
/// Rule::BooleanPositionalValueInCall,
/// ]);
///
/// let union = set_1.union(&set_2);
@@ -80,7 +80,7 @@ impl RuleSet {
/// assert!(union.contains(Rule::AmbiguousFunctionName));
/// assert!(union.contains(Rule::AnyType));
/// assert!(union.contains(Rule::BadQuotesInlineString));
/// assert!(union.contains(Rule::BooleanPositionalValueInFunctionCall));
/// assert!(union.contains(Rule::BooleanPositionalValueInCall));
/// ```
#[must_use]
pub const fn union(mut self, other: &Self) -> Self {
@@ -132,7 +132,7 @@ impl RuleSet {
/// ])));
///
/// assert!(!set_1.intersects(&RuleSet::from_rules(&[
/// Rule::BooleanPositionalValueInFunctionCall,
/// Rule::BooleanPositionalValueInCall,
/// Rule::BadQuotesInlineString
/// ])));
/// ```

View File

@@ -80,7 +80,7 @@ pub(crate) fn variable_name_task_id(
// If the keyword argument is not a string, we can't do anything.
let task_id = match &keyword.value {
Expr::Constant(constant) => match &constant.value {
Constant::Str(value) => value,
Constant::Str(ast::StringConstant { value, .. }) => value,
_ => return None,
},
_ => return None,

View File

@@ -1,8 +1,10 @@
use ruff_python_ast::{Expr, Ranged};
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the use of hardcoded temporary file or directory paths.
///
@@ -49,19 +51,33 @@ impl Violation for HardcodedTempFile {
}
/// S108
pub(crate) fn hardcoded_tmp_directory(
expr: &Expr,
value: &str,
prefixes: &[String],
) -> Option<Diagnostic> {
if prefixes.iter().any(|prefix| value.starts_with(prefix)) {
Some(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
))
} else {
None
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, expr: &Expr, value: &str) {
if !checker
.settings
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| value.starts_with(prefix))
{
return;
}
if let Some(Expr::Call(ast::ExprCall { func, .. })) =
checker.semantic().current_expression_parent()
{
if checker
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["tempfile", ..]))
{
return;
}
}
checker.diagnostics.push(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
));
}

View File

@@ -76,7 +76,7 @@ impl Violation for SuspiciousPickleUsage {
/// import marshal
///
/// with open("foo.marshal", "rb") as file:
/// foo = pickle.load(file)
/// foo = marshal.load(file)
/// ```
///
/// Use instead:

View File

@@ -1,8 +1,4 @@
use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use crate::checkers::ast::Checker;
use ruff_python_ast::{self as ast, Constant, Expr};
/// Returns `true` if a function call is allowed to use a boolean trap.
pub(super) fn is_allowed_func_call(name: &str) -> bool {
@@ -62,18 +58,13 @@ pub(super) fn allow_boolean_trap(func: &Expr) -> bool {
false
}
const fn is_boolean_arg(arg: &Expr) -> bool {
/// Returns `true` if an expression is a boolean literal.
pub(super) const fn is_boolean(expr: &Expr) -> bool {
matches!(
&arg,
&expr,
Expr::Constant(ast::ExprConstant {
value: Constant::Bool(_),
..
})
)
}
pub(super) fn add_if_boolean(checker: &mut Checker, arg: &Expr, kind: DiagnosticKind) {
if is_boolean_arg(arg) {
checker.diagnostics.push(Diagnostic::new(kind, arg.range()));
}
}

View File

@@ -13,9 +13,9 @@ mod tests {
use crate::test::test_path;
use crate::{assert_messages, settings};
#[test_case(Rule::BooleanPositionalArgInFunctionDefinition, Path::new("FBT.py"))]
#[test_case(Rule::BooleanDefaultValueInFunctionDefinition, Path::new("FBT.py"))]
#[test_case(Rule::BooleanPositionalValueInFunctionCall, Path::new("FBT.py"))]
#[test_case(Rule::BooleanTypeHintPositionalArgument, Path::new("FBT.py"))]
#[test_case(Rule::BooleanDefaultValuePositionalArgument, Path::new("FBT.py"))]
#[test_case(Rule::BooleanPositionalValueInCall, Path::new("FBT.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,126 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::{is_allowed_func_def, is_boolean};
/// ## What it does
/// Checks for the use of boolean positional arguments in function definitions,
/// as determined by the presence of a boolean default value.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
/// meaning of the boolean value is not clear to the caller and to future
/// readers of the code.
///
/// The use of a boolean will also limit the function to only two possible
/// behaviors, which makes the function difficult to extend in the future.
///
/// Instead, consider refactoring into separate implementations for the
/// `True` and `False` cases, using an `Enum`, or making the argument a
/// keyword-only argument, to force callers to be explicit when providing
/// the argument.
///
/// ## Example
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number, up=True):
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, True) # What does `True` mean?
/// round_number(1.5, False) # What does `False` mean?
/// ```
///
/// Instead, refactor into separate implementations:
/// ```python
/// from math import ceil, floor
///
///
/// def round_up(number):
/// return ceil(number)
///
///
/// def round_down(number):
/// return floor(number)
///
///
/// round_up(1.5)
/// round_down(1.5)
/// ```
///
/// Or, refactor to use an `Enum`:
/// ```python
/// from enum import Enum
///
///
/// class RoundingMethod(Enum):
/// UP = 1
/// DOWN = 2
///
///
/// def round_number(value, method):
/// ...
/// ```
///
/// Or, make the argument a keyword-only argument:
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number, *, up=True):
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, up=True)
/// round_number(1.5, up=False)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanDefaultValuePositionalArgument;
impl Violation for BooleanDefaultValuePositionalArgument {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean default positional argument in function definition")
}
}
pub(crate) fn boolean_default_value_positional_argument(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
parameters: &Parameters,
) {
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter,
default,
range: _,
} in parameters.posonlyargs.iter().chain(&parameters.args)
{
if default.as_ref().is_some_and(|default| is_boolean(default)) {
checker.diagnostics.push(Diagnostic::new(
BooleanDefaultValuePositionalArgument,
parameter.name.range(),
));
}
}
}

View File

@@ -1,11 +1,9 @@
use ruff_python_ast::Expr;
use ruff_diagnostics::Violation;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_trap};
use crate::rules::flake8_boolean_trap::helpers::{allow_boolean_trap, is_boolean};
/// ## What it does
/// Checks for boolean positional arguments in function calls.
@@ -17,44 +15,42 @@ use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, allow_boolean_t
///
/// ## Example
/// ```python
/// def foo(flag: bool) -> None:
/// def func(flag: bool) -> None:
/// ...
///
///
/// foo(True)
/// func(True)
/// ```
///
/// Use instead:
/// ```python
/// def foo(flag: bool) -> None:
/// def func(flag: bool) -> None:
/// ...
///
///
/// foo(flag=True)
/// func(flag=True)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanPositionalValueInFunctionCall;
pub struct BooleanPositionalValueInCall;
impl Violation for BooleanPositionalValueInFunctionCall {
impl Violation for BooleanPositionalValueInCall {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean positional value in function call")
}
}
pub(crate) fn check_boolean_positional_value_in_function_call(
checker: &mut Checker,
args: &[Expr],
func: &Expr,
) {
pub(crate) fn boolean_positional_value_in_call(checker: &mut Checker, args: &[Expr], func: &Expr) {
if allow_boolean_trap(func) {
return;
}
for arg in args {
add_if_boolean(checker, arg, BooleanPositionalValueInFunctionCall.into());
for arg in args.iter().filter(|arg| is_boolean(arg)) {
checker
.diagnostics
.push(Diagnostic::new(BooleanPositionalValueInCall, arg.range()));
}
}

View File

@@ -11,16 +11,22 @@ use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ## What it does
/// Checks for boolean positional arguments in function definitions.
/// Checks for the use of boolean positional arguments in function definitions,
/// as determined by the presence of a `bool` type hint.
///
/// ## Why is this bad?
/// Calling a function with boolean positional arguments is confusing as the
/// meaning of the boolean value is not clear to the caller, and to future
/// meaning of the boolean value is not clear to the caller and to future
/// readers of the code.
///
/// The use of a boolean will also limit the function to only two possible
/// behaviors, which makes the function difficult to extend in the future.
///
/// Instead, consider refactoring into separate implementations for the
/// `True` and `False` cases, using an `Enum`, or making the argument a
/// keyword-only argument, to force callers to be explicit when providing
/// the argument.
///
/// ## Example
/// ```python
/// from math import ceil, floor
@@ -65,20 +71,33 @@ use crate::rules::flake8_boolean_trap::helpers::is_allowed_func_def;
/// ...
/// ```
///
/// Or, make the argument a keyword-only argument:
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, up=True)
/// round_number(1.5, up=False)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanPositionalArgInFunctionDefinition;
pub struct BooleanTypeHintPositionalArgument;
impl Violation for BooleanPositionalArgInFunctionDefinition {
impl Violation for BooleanTypeHintPositionalArgument {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean positional arg in function definition")
format!("Boolean-typed positional argument in function definition")
}
}
pub(crate) fn check_positional_boolean_in_def(
pub(crate) fn boolean_type_hint_positional_argument(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
@@ -101,28 +120,25 @@ pub(crate) fn check_positional_boolean_in_def(
range: _,
} in parameters.posonlyargs.iter().chain(&parameters.args)
{
if parameter.annotation.is_none() {
continue;
}
let Some(expr) = &parameter.annotation else {
let Some(annotation) = parameter.annotation.as_ref() else {
continue;
};
// check for both bool (python class) and 'bool' (string annotation)
let hint = match expr.as_ref() {
let hint = match annotation.as_ref() {
Expr::Name(name) => &name.id == "bool",
Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) => value == "bool",
_ => false,
};
if !hint {
if !hint || !checker.semantic().is_builtin("bool") {
continue;
}
checker.diagnostics.push(Diagnostic::new(
BooleanPositionalArgInFunctionDefinition,
parameter.range(),
BooleanTypeHintPositionalArgument,
parameter.name.range(),
));
}
}

View File

@@ -1,90 +0,0 @@
use ruff_python_ast::{Decorator, ParameterWithDefault, Parameters};
use ruff_diagnostics::Violation;
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::collect_call_path;
use crate::checkers::ast::Checker;
use crate::rules::flake8_boolean_trap::helpers::{add_if_boolean, is_allowed_func_def};
/// ## What it does
/// Checks for the use of booleans as default values in function definitions.
///
/// ## Why is this bad?
/// Calling a function with boolean default means that the keyword argument
/// argument can be omitted, which makes the function call ambiguous.
///
/// Instead, define the relevant argument as keyword-only.
///
/// ## Example
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool = True) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5)
/// round_number(1.5, up=False)
/// ```
///
/// Use instead:
/// ```python
/// from math import ceil, floor
///
///
/// def round_number(number: float, *, up: bool) -> int:
/// return ceil(number) if up else floor(number)
///
///
/// round_number(1.5, up=True)
/// round_number(1.5, up=False)
/// ```
///
/// ## References
/// - [Python documentation: Calls](https://docs.python.org/3/reference/expressions.html#calls)
/// - [_How to Avoid “The Boolean Trap”_ by Adam Johnson](https://adamj.eu/tech/2021/07/10/python-type-hints-how-to-avoid-the-boolean-trap/)
#[violation]
pub struct BooleanDefaultValueInFunctionDefinition;
impl Violation for BooleanDefaultValueInFunctionDefinition {
#[derive_message_formats]
fn message(&self) -> String {
format!("Boolean default value in function definition")
}
}
pub(crate) fn check_boolean_default_value_in_function_definition(
checker: &mut Checker,
name: &str,
decorator_list: &[Decorator],
parameters: &Parameters,
) {
if is_allowed_func_def(name) {
return;
}
if decorator_list.iter().any(|decorator| {
collect_call_path(&decorator.expression)
.is_some_and(|call_path| call_path.as_slice() == [name, "setter"])
}) {
return;
}
for ParameterWithDefault {
parameter: _,
default,
range: _,
} in parameters.args.iter().chain(&parameters.posonlyargs)
{
let Some(default) = default else {
continue;
};
add_if_boolean(
checker,
default,
BooleanDefaultValueInFunctionDefinition.into(),
);
}
}

View File

@@ -1,7 +1,7 @@
pub(crate) use check_boolean_default_value_in_function_definition::*;
pub(crate) use check_boolean_positional_value_in_function_call::*;
pub(crate) use check_positional_boolean_in_def::*;
pub(crate) use boolean_default_value_positional_argument::*;
pub(crate) use boolean_positional_value_in_call::*;
pub(crate) use boolean_type_hint_positional_argument::*;
mod check_boolean_default_value_in_function_definition;
mod check_boolean_positional_value_in_function_call;
mod check_positional_boolean_in_def;
mod boolean_default_value_positional_argument;
mod boolean_positional_value_in_call;
mod boolean_type_hint_positional_argument;

View File

@@ -1,91 +1,91 @@
---
source: crates/ruff/src/rules/flake8_boolean_trap/mod.rs
---
FBT.py:4:5: FBT001 Boolean positional arg in function definition
FBT.py:4:5: FBT001 Boolean-typed positional argument in function definition
|
2 | posonly_nohint,
3 | posonly_nonboolhint: int,
4 | posonly_boolhint: bool,
| ^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^ FBT001
5 | posonly_boolstrhint: "bool",
6 | /,
|
FBT.py:5:5: FBT001 Boolean positional arg in function definition
FBT.py:5:5: FBT001 Boolean-typed positional argument in function definition
|
3 | posonly_nonboolhint: int,
4 | posonly_boolhint: bool,
5 | posonly_boolstrhint: "bool",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^ FBT001
6 | /,
7 | offset,
|
FBT.py:10:5: FBT001 Boolean positional arg in function definition
FBT.py:10:5: FBT001 Boolean-typed positional argument in function definition
|
8 | posorkw_nonvalued_nohint,
9 | posorkw_nonvalued_nonboolhint: int,
10 | posorkw_nonvalued_boolhint: bool,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
11 | posorkw_nonvalued_boolstrhint: "bool",
12 | posorkw_boolvalued_nohint=True,
|
FBT.py:11:5: FBT001 Boolean positional arg in function definition
FBT.py:11:5: FBT001 Boolean-typed positional argument in function definition
|
9 | posorkw_nonvalued_nonboolhint: int,
10 | posorkw_nonvalued_boolhint: bool,
11 | posorkw_nonvalued_boolstrhint: "bool",
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
|
FBT.py:14:5: FBT001 Boolean positional arg in function definition
FBT.py:14:5: FBT001 Boolean-typed positional argument in function definition
|
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
16 | posorkw_nonboolvalued_nohint=1,
|
FBT.py:15:5: FBT001 Boolean positional arg in function definition
FBT.py:15:5: FBT001 Boolean-typed positional argument in function definition
|
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|
FBT.py:18:5: FBT001 Boolean positional arg in function definition
FBT.py:18:5: FBT001 Boolean-typed positional argument in function definition
|
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
18 | posorkw_nonboolvalued_boolhint: bool = 3,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
20 | *,
|
FBT.py:19:5: FBT001 Boolean positional arg in function definition
FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
|
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
18 | posorkw_nonboolvalued_boolhint: bool = 3,
19 | posorkw_nonboolvalued_boolstrhint: "bool" = 4,
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT001
20 | *,
21 | kwonly_nonvalued_nohint,
|
FBT.py:86:19: FBT001 Boolean positional arg in function definition
FBT.py:86:19: FBT001 Boolean-typed positional argument in function definition
|
85 | # FBT001: Boolean positional arg in function definition
86 | def foo(self, value: bool) -> None:
| ^^^^^^^^^^^ FBT001
| ^^^^^ FBT001
87 | pass
|

View File

@@ -1,42 +1,42 @@
---
source: crates/ruff/src/rules/flake8_boolean_trap/mod.rs
---
FBT.py:12:31: FBT002 Boolean default value in function definition
FBT.py:12:5: FBT002 Boolean default positional argument in function definition
|
10 | posorkw_nonvalued_boolhint: bool,
11 | posorkw_nonvalued_boolstrhint: "bool",
12 | posorkw_boolvalued_nohint=True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
|
FBT.py:13:43: FBT002 Boolean default value in function definition
FBT.py:13:5: FBT002 Boolean default positional argument in function definition
|
11 | posorkw_nonvalued_boolstrhint: "bool",
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
14 | posorkw_boolvalued_boolhint: bool = True,
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
|
FBT.py:14:41: FBT002 Boolean default value in function definition
FBT.py:14:5: FBT002 Boolean default positional argument in function definition
|
12 | posorkw_boolvalued_nohint=True,
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
16 | posorkw_nonboolvalued_nohint=1,
|
FBT.py:15:46: FBT002 Boolean default value in function definition
FBT.py:15:5: FBT002 Boolean default positional argument in function definition
|
13 | posorkw_boolvalued_nonboolhint: int = True,
14 | posorkw_boolvalued_boolhint: bool = True,
15 | posorkw_boolvalued_boolstrhint: "bool" = True,
| ^^^^ FBT002
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FBT002
16 | posorkw_nonboolvalued_nohint=1,
17 | posorkw_nonboolvalued_nonboolhint: int = 2,
|

View File

@@ -49,7 +49,6 @@ mod tests {
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -60,6 +59,17 @@ mod tests {
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";
let diagnostics = test_path(
Path::new("flake8_bugbear").join(snapshot).as_path(),
&Settings::for_rule(Rule::ZipWithoutExplicitStrict),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
@@ -72,7 +82,7 @@ mod tests {
"fastapi.Query".to_string(),
],
},
..Settings::for_rules(vec![Rule::FunctionCallInDefaultArgument])
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)
},
)?;
assert_messages!(snapshot, diagnostics);

View File

@@ -64,7 +64,7 @@ pub(crate) fn getattr_with_constant(
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = arg
else {

View File

@@ -74,7 +74,6 @@ fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
}
Stmt::If(ast::StmtIf { body, .. })
| Stmt::Try(ast::StmtTry { body, .. })
| Stmt::TryStar(ast::StmtTryStar { body, .. })
| Stmt::With(ast::StmtWith { body, .. }) => {
walk_stmt(checker, body, f);
}

View File

@@ -1,10 +1,15 @@
use ruff_python_ast::{ParameterWithDefault, Parameters, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, Expr, Parameter, ParameterWithDefault, Ranged};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_index::Indexer;
use ruff_python_semantic::analyze::typing::{is_immutable_annotation, is_mutable_expr};
use ruff_python_trivia::{indentation_at_offset, textwrap};
use ruff_source_file::Locator;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of mutable objects as function argument defaults.
@@ -50,24 +55,30 @@ use crate::checkers::ast::Checker;
pub struct MutableArgumentDefault;
impl Violation for MutableArgumentDefault {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not use mutable data structures for argument defaults")
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `None`; initialize within function"))
}
}
/// B006
pub(crate) fn mutable_argument_default(checker: &mut Checker, parameters: &Parameters) {
pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
// Scan in reverse order to right-align zip().
for ParameterWithDefault {
parameter,
default,
range: _,
} in parameters
} in function_def
.parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.chain(&function_def.parameters.args)
.chain(&function_def.parameters.kwonlyargs)
{
let Some(default) = default else {
continue;
@@ -79,9 +90,84 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, parameters: &Param
.as_ref()
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(MutableArgumentDefault, default.range()));
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
// If the function body is on the same line as the function def, do not fix
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = move_initialization(
function_def,
parameter,
default,
checker.locator(),
checker.stylist(),
checker.indexer(),
checker.generator(),
) {
diagnostic.set_fix(fix);
}
}
checker.diagnostics.push(diagnostic);
}
}
}
/// Generate a [`Fix`] to move a mutable argument default initialization
/// into the function body.
fn move_initialization(
function_def: &ast::StmtFunctionDef,
parameter: &Parameter,
default: &Expr,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
generator: Generator,
) -> Option<Fix> {
let mut body = function_def.body.iter();
let statement = body.next()?;
if indexer.preceded_by_multi_statement_line(statement, locator) {
return None;
}
// Determine the indentation depth of the function body.
let indentation = indentation_at_offset(statement.start(), locator)?;
// Set the default argument value to `None`.
let default_edit = Edit::range_replacement("None".to_string(), default.range());
// Add an `if`, to set the argument to its original value if still `None`.
let mut content = String::new();
content.push_str(&format!("if {} is None:", parameter.name.as_str()));
content.push_str(stylist.line_ending().as_str());
content.push_str(stylist.indentation());
content.push_str(&format!(
"{} = {}",
parameter.name.as_str(),
generator.expr(default)
));
content.push_str(stylist.line_ending().as_str());
// Indent the edit to match the body indentation.
let content = textwrap::indent(&content, indentation).to_string();
let initialization_edit = if is_docstring_stmt(statement) {
// If the first statement in the function is a docstring, insert _after_ it.
if let Some(statement) = body.next() {
// If there's a second statement, insert _before_ it, but ensure this isn't a
// multi-statement line.
if indexer.preceded_by_multi_statement_line(statement, locator) {
return None;
}
Edit::insertion(content, locator.line_start(statement.start()))
} else {
// If the docstring is the only statement, insert _before_ it.
Edit::insertion(content, locator.full_line_end(statement.end()))
}
} else {
// Otherwise, insert before the first statement.
let at = locator.line_start(statement.start());
Edit::insertion(content, at)
};
Some(Fix::manual_edits(default_edit, [initialization_edit]))
}

View File

@@ -88,7 +88,7 @@ pub(crate) fn setattr_with_constant(
if !is_identifier(name) {
return;
}
if is_mangled_private(name.as_str()) {
if is_mangled_private(name) {
return;
}
// We can only replace a `setattr` call (which is an `Expr`) with an assignment

View File

@@ -68,13 +68,13 @@ pub(crate) fn unreliable_callable_check(
return;
};
let Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = attr
else {
return;
};
if string != "__call__" {
if value != "__call__" {
return;
}

View File

@@ -1,123 +1,479 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_B008.py:63:25: B006 Do not use mutable data structures for argument defaults
B006_B008.py:63:25: B006 [*] Do not use mutable data structures for argument defaults
|
63 | def this_is_wrong(value=[1, 2, 3]):
| ^^^^^^^^^ B006
64 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:67:30: B006 Do not use mutable data structures for argument defaults
Possible fix
60 60 | # Flag mutable literals/comprehensions
61 61 |
62 62 |
63 |-def this_is_wrong(value=[1, 2, 3]):
63 |+def this_is_wrong(value=None):
64 |+ if value is None:
65 |+ value = [1, 2, 3]
64 66 | ...
65 67 |
66 68 |
B006_B008.py:67:30: B006 [*] Do not use mutable data structures for argument defaults
|
67 | def this_is_also_wrong(value={}):
| ^^ B006
68 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:71:20: B006 Do not use mutable data structures for argument defaults
|
71 | def and_this(value=set()):
| ^^^^^ B006
72 | ...
|
Possible fix
64 64 | ...
65 65 |
66 66 |
67 |-def this_is_also_wrong(value={}):
67 |+def this_is_also_wrong(value=None):
68 |+ if value is None:
69 |+ value = {}
68 70 | ...
69 71 |
70 72 |
B006_B008.py:75:20: B006 Do not use mutable data structures for argument defaults
B006_B008.py:73:52: B006 [*] Do not use mutable data structures for argument defaults
|
75 | def this_too(value=collections.OrderedDict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
76 | ...
71 | class Foo:
72 | @staticmethod
73 | def this_is_also_wrong_and_more_indented(value={}):
| ^^ B006
74 | pass
|
= help: Replace with `None`; initialize within function
B006_B008.py:79:32: B006 Do not use mutable data structures for argument defaults
Possible fix
70 70 |
71 71 | class Foo:
72 72 | @staticmethod
73 |- def this_is_also_wrong_and_more_indented(value={}):
73 |+ def this_is_also_wrong_and_more_indented(value=None):
74 |+ if value is None:
75 |+ value = {}
74 76 | pass
75 77 |
76 78 |
B006_B008.py:77:31: B006 [*] Do not use mutable data structures for argument defaults
|
77 | def multiline_arg_wrong(value={
| _______________________________^
78 | |
79 | | }):
| |_^ B006
80 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
74 74 | pass
75 75 |
76 76 |
77 |-def multiline_arg_wrong(value={
78 |-
79 |-}):
77 |+def multiline_arg_wrong(value=None):
78 |+ if value is None:
79 |+ value = {}
80 80 | ...
81 81 |
82 82 | def single_line_func_wrong(value = {}): ...
B006_B008.py:82:36: B006 Do not use mutable data structures for argument defaults
|
79 | async def async_this_too(value=collections.defaultdict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
80 | ...
81 |
82 | def single_line_func_wrong(value = {}): ...
| ^^ B006
|
= help: Replace with `None`; initialize within function
B006_B008.py:83:26: B006 Do not use mutable data structures for argument defaults
B006_B008.py:85:20: B006 [*] Do not use mutable data structures for argument defaults
|
83 | def dont_forget_me(value=collections.deque()):
85 | def and_this(value=set()):
| ^^^^^ B006
86 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
82 82 | def single_line_func_wrong(value = {}): ...
83 83 |
84 84 |
85 |-def and_this(value=set()):
85 |+def and_this(value=None):
86 |+ if value is None:
87 |+ value = set()
86 88 | ...
87 89 |
88 90 |
B006_B008.py:89:20: B006 [*] Do not use mutable data structures for argument defaults
|
89 | def this_too(value=collections.OrderedDict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
90 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
86 86 | ...
87 87 |
88 88 |
89 |-def this_too(value=collections.OrderedDict()):
89 |+def this_too(value=None):
90 |+ if value is None:
91 |+ value = collections.OrderedDict()
90 92 | ...
91 93 |
92 94 |
B006_B008.py:93:32: B006 [*] Do not use mutable data structures for argument defaults
|
93 | async def async_this_too(value=collections.defaultdict()):
| ^^^^^^^^^^^^^^^^^^^^^^^^^ B006
94 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
90 90 | ...
91 91 |
92 92 |
93 |-async def async_this_too(value=collections.defaultdict()):
93 |+async def async_this_too(value=None):
94 |+ if value is None:
95 |+ value = collections.defaultdict()
94 96 | ...
95 97 |
96 98 |
B006_B008.py:97:26: B006 [*] Do not use mutable data structures for argument defaults
|
97 | def dont_forget_me(value=collections.deque()):
| ^^^^^^^^^^^^^^^^^^^ B006
84 | ...
98 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:88:46: B006 Do not use mutable data structures for argument defaults
|
87 | # N.B. we're also flagging the function call in the comprehension
88 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
89 | pass
|
Possible fix
94 94 | ...
95 95 |
96 96 |
97 |-def dont_forget_me(value=collections.deque()):
97 |+def dont_forget_me(value=None):
98 |+ if value is None:
99 |+ value = collections.deque()
98 100 | ...
99 101 |
100 102 |
B006_B008.py:92:46: B006 Do not use mutable data structures for argument defaults
|
92 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
93 | pass
|
B006_B008.py:96:45: B006 Do not use mutable data structures for argument defaults
|
96 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
97 | pass
|
B006_B008.py:100:33: B006 Do not use mutable data structures for argument defaults
B006_B008.py:102:46: B006 [*] Do not use mutable data structures for argument defaults
|
100 | def kwonlyargs_mutable(*, value=[]):
101 | # N.B. we're also flagging the function call in the comprehension
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
103 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
99 99 |
100 100 |
101 101 | # N.B. we're also flagging the function call in the comprehension
102 |-def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
102 |+def list_comprehension_also_not_okay(default=None):
103 |+ if default is None:
104 |+ default = [i ** 2 for i in range(3)]
103 105 | pass
104 106 |
105 107 |
B006_B008.py:106:46: B006 [*] Do not use mutable data structures for argument defaults
|
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
107 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
103 103 | pass
104 104 |
105 105 |
106 |-def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
106 |+def dict_comprehension_also_not_okay(default=None):
107 |+ if default is None:
108 |+ default = {i: i ** 2 for i in range(3)}
107 109 | pass
108 110 |
109 111 |
B006_B008.py:110:45: B006 [*] Do not use mutable data structures for argument defaults
|
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^^^^^^^^^^^^^^^^^ B006
111 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
107 107 | pass
108 108 |
109 109 |
110 |-def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
110 |+def set_comprehension_also_not_okay(default=None):
111 |+ if default is None:
112 |+ default = {i ** 2 for i in range(3)}
111 113 | pass
112 114 |
113 115 |
B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument defaults
|
114 | def kwonlyargs_mutable(*, value=[]):
| ^^ B006
101 | ...
115 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:221:20: B006 Do not use mutable data structures for argument defaults
Possible fix
111 111 | pass
112 112 |
113 113 |
114 |-def kwonlyargs_mutable(*, value=[]):
114 |+def kwonlyargs_mutable(*, value=None):
115 |+ if value is None:
116 |+ value = []
115 117 | ...
116 118 |
117 119 |
B006_B008.py:235:20: B006 [*] Do not use mutable data structures for argument defaults
|
219 | # B006 and B008
220 | # We should handle arbitrary nesting of these B008.
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
222 | pass
236 | pass
|
= help: Replace with `None`; initialize within function
B006_B008.py:258:27: B006 Do not use mutable data structures for argument defaults
Possible fix
232 232 |
233 233 | # B006 and B008
234 234 | # We should handle arbitrary nesting of these B008.
235 |-def nested_combo(a=[float(3), dt.datetime.now()]):
235 |+def nested_combo(a=None):
236 |+ if a is None:
237 |+ a = [float(3), dt.datetime.now()]
236 238 | pass
237 239 |
238 240 |
B006_B008.py:272:27: B006 [*] Do not use mutable data structures for argument defaults
|
257 | def mutable_annotations(
258 | a: list[int] | None = [],
271 | def mutable_annotations(
272 | a: list[int] | None = [],
| ^^ B006
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
B006_B008.py:259:35: B006 Do not use mutable data structures for argument defaults
Possible fix
269 269 |
270 270 |
271 271 | def mutable_annotations(
272 |- a: list[int] | None = [],
272 |+ a: list[int] | None = None,
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if a is None:
278 |+ a = []
277 279 | pass
278 280 |
279 281 |
B006_B008.py:273:35: B006 [*] Do not use mutable data structures for argument defaults
|
257 | def mutable_annotations(
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
271 | def mutable_annotations(
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
| ^^ B006
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
B006_B008.py:260:62: B006 Do not use mutable data structures for argument defaults
Possible fix
270 270 |
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 |- b: Optional[Dict[int, int]] = {},
273 |+ b: Optional[Dict[int, int]] = None,
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if b is None:
278 |+ b = {}
277 279 | pass
278 280 |
279 281 |
B006_B008.py:274:62: B006 [*] Do not use mutable data structures for argument defaults
|
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
262 | ):
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | ):
|
= help: Replace with `None`; initialize within function
B006_B008.py:261:80: B006 Do not use mutable data structures for argument defaults
Possible fix
271 271 | def mutable_annotations(
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
274 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
275 275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 276 | ):
277 |+ if c is None:
278 |+ c = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:275:80: B006 [*] Do not use mutable data structures for argument defaults
|
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
262 | ):
263 | pass
276 | ):
277 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
272 272 | a: list[int] | None = [],
273 273 | b: Optional[Dict[int, int]] = {},
274 274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
275 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
276 276 | ):
277 |+ if d is None:
278 |+ d = set()
277 279 | pass
278 280 |
279 281 |
B006_B008.py:280:52: B006 [*] Do not use mutable data structures for argument defaults
|
280 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
281 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
277 277 | pass
278 278 |
279 279 |
280 |-def single_line_func_wrong(value: dict[str, str] = {}):
280 |+def single_line_func_wrong(value: dict[str, str] = None):
281 281 | """Docstring"""
282 |+ if value is None:
283 |+ value = {}
282 284 |
283 285 |
284 286 | def single_line_func_wrong(value: dict[str, str] = {}):
B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument defaults
|
284 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
285 | """Docstring"""
286 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
281 281 | """Docstring"""
282 282 |
283 283 |
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
284 |+def single_line_func_wrong(value: dict[str, str] = None):
285 285 | """Docstring"""
286 |+ if value is None:
287 |+ value = {}
286 288 | ...
287 289 |
288 290 |
B006_B008.py:289:52: B006 Do not use mutable data structures for argument defaults
|
289 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
290 | """Docstring"""; ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:293:52: B006 Do not use mutable data structures for argument defaults
|
293 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
294 | """Docstring"""; \
295 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:298:52: B006 [*] Do not use mutable data structures for argument defaults
|
298 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
299 | | # This is a comment
300 | | }):
| |_^ B006
301 | """Docstring"""
|
= help: Replace with `None`; initialize within function
Possible fix
295 295 | ...
296 296 |
297 297 |
298 |-def single_line_func_wrong(value: dict[str, str] = {
299 |- # This is a comment
300 |-}):
298 |+def single_line_func_wrong(value: dict[str, str] = None):
301 299 | """Docstring"""
300 |+ if value is None:
301 |+ value = {}
302 302 |
303 303 |
304 304 | def single_line_func_wrong(value: dict[str, str] = {}) \
B006_B008.py:304:52: B006 Do not use mutable data structures for argument defaults
|
304 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^ B006
305 | : \
306 | """Docstring"""
|
= help: Replace with `None`; initialize within function

View File

@@ -1,83 +1,83 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_B008.py:88:61: B008 Do not perform function call `range` in argument defaults
|
87 | # N.B. we're also flagging the function call in the comprehension
88 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^ B008
89 | pass
|
B006_B008.py:92:64: B008 Do not perform function call `range` in argument defaults
|
92 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^ B008
93 | pass
|
B006_B008.py:96:60: B008 Do not perform function call `range` in argument defaults
|
96 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^ B008
97 | pass
|
B006_B008.py:112:39: B008 Do not perform function call `time.time` in argument defaults
B006_B008.py:102:61: B008 Do not perform function call `range` in argument defaults
|
110 | # B008
111 | # Flag function calls as default args (including if they are part of a sub-expression)
112 | def in_fact_all_calls_are_wrong(value=time.time()):
101 | # N.B. we're also flagging the function call in the comprehension
102 | def list_comprehension_also_not_okay(default=[i**2 for i in range(3)]):
| ^^^^^^^^ B008
103 | pass
|
B006_B008.py:106:64: B008 Do not perform function call `range` in argument defaults
|
106 | def dict_comprehension_also_not_okay(default={i: i**2 for i in range(3)}):
| ^^^^^^^^ B008
107 | pass
|
B006_B008.py:110:60: B008 Do not perform function call `range` in argument defaults
|
110 | def set_comprehension_also_not_okay(default={i**2 for i in range(3)}):
| ^^^^^^^^ B008
111 | pass
|
B006_B008.py:126:39: B008 Do not perform function call `time.time` in argument defaults
|
124 | # B008
125 | # Flag function calls as default args (including if they are part of a sub-expression)
126 | def in_fact_all_calls_are_wrong(value=time.time()):
| ^^^^^^^^^^^ B008
113 | ...
127 | ...
|
B006_B008.py:116:12: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:130:12: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
116 | def f(when=dt.datetime.now() + dt.timedelta(days=7)):
130 | def f(when=dt.datetime.now() + dt.timedelta(days=7)):
| ^^^^^^^^^^^^^^^^^ B008
117 | pass
131 | pass
|
B006_B008.py:120:30: B008 Do not perform function call in argument defaults
B006_B008.py:134:30: B008 Do not perform function call in argument defaults
|
120 | def can_even_catch_lambdas(a=(lambda x: x)()):
134 | def can_even_catch_lambdas(a=(lambda x: x)()):
| ^^^^^^^^^^^^^^^ B008
121 | ...
135 | ...
|
B006_B008.py:221:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:235:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
219 | # B006 and B008
220 | # We should handle arbitrary nesting of these B008.
221 | def nested_combo(a=[float(3), dt.datetime.now()]):
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ B008
222 | pass
236 | pass
|
B006_B008.py:227:22: B008 Do not perform function call `map` in argument defaults
B006_B008.py:241:22: B008 Do not perform function call `map` in argument defaults
|
225 | # Don't flag nested B006 since we can't guarantee that
226 | # it isn't made mutable by the outer operation.
227 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
239 | # Don't flag nested B006 since we can't guarantee that
240 | # it isn't made mutable by the outer operation.
241 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
228 | pass
242 | pass
|
B006_B008.py:232:19: B008 Do not perform function call `random.randint` in argument defaults
B006_B008.py:246:19: B008 Do not perform function call `random.randint` in argument defaults
|
231 | # B008-ception.
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
233 | pass
247 | pass
|
B006_B008.py:232:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:246:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
231 | # B008-ception.
232 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ B008
233 | pass
247 | pass
|

View File

@@ -1,12 +1,4 @@
use ruff_python_ast::{self as ast, Expr, Keyword};
pub(super) fn expr_name(func: &Expr) -> Option<&str> {
if let Expr::Name(ast::ExprName { id, .. }) = func {
Some(id)
} else {
None
}
}
use ruff_python_ast::{Expr, Keyword};
pub(super) fn exactly_one_argument_with_matching_function<'a>(
name: &str,
@@ -20,7 +12,8 @@ pub(super) fn exactly_one_argument_with_matching_function<'a>(
if !keywords.is_empty() {
return None;
}
if expr_name(func)? != name {
let func = func.as_name_expr()?;
if func.id != name {
return None;
}
Some(arg)
@@ -31,8 +24,8 @@ pub(super) fn first_argument_with_matching_function<'a>(
func: &Expr,
args: &'a [Expr],
) -> Option<&'a Expr> {
if expr_name(func)? == name {
Some(args.first()?)
if func.as_name_expr().is_some_and(|func| func.id == name) {
args.first()
} else {
None
}

View File

@@ -1,14 +1,11 @@
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
use super::helpers;
/// ## What it does
/// Checks for unnecessary `list` or `reversed` calls around `sorted`
/// calls.
@@ -57,10 +54,10 @@ pub(crate) fn unnecessary_call_around_sorted(
func: &Expr,
args: &[Expr],
) {
let Some(outer) = helpers::expr_name(func) else {
let Some(outer) = func.as_name_expr() else {
return;
};
if !(outer == "list" || outer == "reversed") {
if !matches!(outer.id.as_str(), "list" | "reversed") {
return;
}
let Some(arg) = args.first() else {
@@ -69,18 +66,18 @@ pub(crate) fn unnecessary_call_around_sorted(
let Expr::Call(ast::ExprCall { func, .. }) = arg else {
return;
};
let Some(inner) = helpers::expr_name(func) else {
let Some(inner) = func.as_name_expr() else {
return;
};
if inner != "sorted" {
if inner.id != "sorted" {
return;
}
if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) {
if !checker.semantic().is_builtin(&inner.id) || !checker.semantic().is_builtin(&outer.id) {
return;
}
let mut diagnostic = Diagnostic::new(
UnnecessaryCallAroundSorted {
func: outer.to_string(),
func: outer.id.to_string(),
},
expr.range(),
);
@@ -91,7 +88,7 @@ pub(crate) fn unnecessary_call_around_sorted(
checker.stylist(),
expr,
)?;
if outer == "reversed" {
if outer.id == "reversed" {
Ok(Fix::suggested(edit))
} else {
Ok(Fix::automatic(edit))

View File

@@ -1,15 +1,12 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
use crate::rules::flake8_comprehensions::settings::Settings;
use super::helpers;
/// ## What it does
/// Checks for unnecessary `dict`, `list` or `tuple` calls that can be
/// rewritten as empty literals.
@@ -63,10 +60,10 @@ pub(crate) fn unnecessary_collection_call(
if !args.is_empty() {
return;
}
let Some(id) = helpers::expr_name(func) else {
let Some(func) = func.as_name_expr() else {
return;
};
match id {
match func.id.as_str() {
"dict"
if keywords.is_empty()
|| (!settings.allow_dict_calls_with_keyword_arguments
@@ -79,12 +76,12 @@ pub(crate) fn unnecessary_collection_call(
}
_ => return,
};
if !checker.semantic().is_builtin(id) {
if !checker.semantic().is_builtin(func.id.as_str()) {
return;
}
let mut diagnostic = Diagnostic::new(
UnnecessaryCollectionCall {
obj_type: id.to_string(),
obj_type: func.id.to_string(),
},
expr.range(),
);

View File

@@ -1,14 +1,11 @@
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
use super::helpers;
/// ## What it does
/// Checks for unnecessary `dict`, `list`, and `set` comprehension.
///
@@ -88,28 +85,28 @@ pub(crate) fn unnecessary_dict_comprehension(
if !generator.ifs.is_empty() || generator.is_async {
return;
}
let Some(key_id) = helpers::expr_name(key) else {
let Some(key) = key.as_name_expr() else {
return;
};
let Some(value_id) = helpers::expr_name(value) else {
let Some(value) = value.as_name_expr() else {
return;
};
let Expr::Tuple(ast::ExprTuple { elts, .. }) = &generator.target else {
return;
};
if elts.len() != 2 {
return;
}
let Some(target_key_id) = helpers::expr_name(&elts[0]) else {
let [target_key, target_value] = elts.as_slice() else {
return;
};
if target_key_id != key_id {
return;
}
let Some(target_value_id) = helpers::expr_name(&elts[1]) else {
let Some(target_key) = target_key.as_name_expr() else {
return;
};
if target_value_id != value_id {
let Some(target_value) = target_value.as_name_expr() else {
return;
};
if target_key.id != key.id {
return;
}
if target_value.id != value.id {
return;
}
add_diagnostic(checker, expr);
@@ -128,13 +125,13 @@ pub(crate) fn unnecessary_list_set_comprehension(
if !generator.ifs.is_empty() || generator.is_async {
return;
}
let Some(elt_id) = helpers::expr_name(elt) else {
let Some(elt) = elt.as_name_expr() else {
return;
};
let Some(target_id) = helpers::expr_name(&generator.target) else {
let Some(target) = generator.target.as_name_expr() else {
return;
};
if elt_id != target_id {
if elt.id != target.id {
return;
}
add_diagnostic(checker, expr);

View File

@@ -1,15 +1,12 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableKeyword;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
use crate::rules::flake8_comprehensions::fixes;
use super::helpers;
/// ## What it does
/// Checks for unnecessary `list`, `reversed`, `set`, `sorted`, and `tuple`
/// call within `list`, `set`, `sorted`, and `tuple` calls.
@@ -72,15 +69,13 @@ pub(crate) fn unnecessary_double_cast_or_process(
args: &[Expr],
outer_kw: &[Keyword],
) {
let Some(outer) = helpers::expr_name(func) else {
let Some(outer) = func.as_name_expr() else {
return;
};
if !(outer == "list"
|| outer == "tuple"
|| outer == "set"
|| outer == "reversed"
|| outer == "sorted")
{
if !matches!(
outer.id.as_str(),
"list" | "tuple" | "set" | "reversed" | "sorted"
) {
return;
}
let Some(arg) = args.first() else {
@@ -96,16 +91,16 @@ pub(crate) fn unnecessary_double_cast_or_process(
else {
return;
};
let Some(inner) = helpers::expr_name(func) else {
let Some(inner) = func.as_name_expr() else {
return;
};
if !checker.semantic().is_builtin(inner) || !checker.semantic().is_builtin(outer) {
if !checker.semantic().is_builtin(&inner.id) || !checker.semantic().is_builtin(&outer.id) {
return;
}
// Avoid collapsing nested `sorted` calls with non-identical keyword arguments
// (i.e., `key`, `reverse`).
if inner == "sorted" && outer == "sorted" {
if inner.id == "sorted" && outer.id == "sorted" {
if inner_kw.len() != outer_kw.len() {
return;
}
@@ -118,18 +113,19 @@ pub(crate) fn unnecessary_double_cast_or_process(
}
}
// Ex) set(tuple(...))
// Ex) list(tuple(...))
// Ex) set(set(...))
if ((outer == "set" || outer == "sorted")
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted"))
|| (outer == "set" && inner == "set")
|| ((outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple"))
{
// Ex) `set(tuple(...))`
// Ex) `list(tuple(...))`
// Ex) `set(set(...))`
if matches!(
(outer.id.as_str(), inner.id.as_str()),
("set" | "sorted", "list" | "tuple" | "reversed" | "sorted")
| ("set", "set")
| ("list" | "tuple", "list" | "tuple")
) {
let mut diagnostic = Diagnostic::new(
UnnecessaryDoubleCastOrProcess {
inner: inner.to_string(),
outer: outer.to_string(),
inner: inner.id.to_string(),
outer: outer.id.to_string(),
},
expr.range(),
);

View File

@@ -70,7 +70,7 @@ pub(crate) fn unnecessary_literal_dict(
// Accept `dict((1, 2), ...))` `dict([(1, 2), ...])`.
if !elts
.iter()
.all(|elt| matches!(&elt, Expr::Tuple(ast::ExprTuple { elts, .. } )if elts.len() == 2))
.all(|elt| matches!(&elt, Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.len() == 2))
{
return;
}

View File

@@ -68,11 +68,11 @@ pub(crate) fn unnecessary_map(
func: &Expr,
args: &[Expr],
) {
let Some(id) = helpers::expr_name(func) else {
let Some(func) = func.as_name_expr() else {
return;
};
let object_type = match id {
let object_type = match func.id.as_str() {
"map" => ObjectType::Generator,
"list" => ObjectType::List,
"set" => ObjectType::Set,
@@ -80,20 +80,20 @@ pub(crate) fn unnecessary_map(
_ => return,
};
if !checker.semantic().is_builtin(id) {
if !checker.semantic().is_builtin(&func.id) {
return;
}
match object_type {
ObjectType::Generator => {
// Exclude the parent if already matched by other arms.
if let Some(Expr::Call(ast::ExprCall { func, .. })) = parent {
if let Some(name) = helpers::expr_name(func) {
if matches!(name, "list" | "set" | "dict") {
return;
}
}
};
if parent
.and_then(ruff_python_ast::Expr::as_call_expr)
.and_then(|call| call.func.as_name_expr())
.is_some_and(|name| matches!(name.id.as_str(), "list" | "set" | "dict"))
{
return;
}
// Only flag, e.g., `map(lambda x: x + 1, iterable)`.
let [Expr::Lambda(ast::ExprLambda {

View File

@@ -1,13 +1,11 @@
use num_bigint::BigInt;
use ruff_python_ast::{self as ast, Constant, Expr, Ranged, UnaryOp};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, Ranged, UnaryOp};
use crate::checkers::ast::Checker;
use super::helpers;
/// ## What it does
/// Checks for unnecessary subscript reversal of iterable.
///
@@ -52,13 +50,13 @@ pub(crate) fn unnecessary_subscript_reversal(
let Some(first_arg) = args.first() else {
return;
};
let Some(id) = helpers::expr_name(func) else {
let Some(func) = func.as_name_expr() else {
return;
};
if !(id == "set" || id == "sorted" || id == "reversed") {
if !matches!(func.id.as_str(), "reversed" | "set" | "sorted") {
return;
}
if !checker.semantic().is_builtin(id) {
if !checker.semantic().is_builtin(&func.id) {
return;
}
let Expr::Subscript(ast::ExprSubscript { slice, .. }) = first_arg else {
@@ -99,7 +97,7 @@ pub(crate) fn unnecessary_subscript_reversal(
};
checker.diagnostics.push(Diagnostic::new(
UnnecessarySubscriptReversal {
func: id.to_string(),
func: func.id.to_string(),
},
expr.range(),
));

View File

@@ -8,6 +8,44 @@ use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
use super::helpers;
/// ## What it does
/// Checks for usage of `datetime.datetime.fromtimestamp()` without a `tz`
/// argument.
///
/// ## Why is this bad?
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.datetime.fromtimestamp(ts)` returns a naive datetime object.
/// Instead, use `datetime.datetime.fromtimestamp(ts, tz=)` to return a
/// timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800)
/// ```
///
/// Use instead:
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[violation]
pub struct CallDatetimeFromtimestamp;
@@ -20,7 +58,6 @@ impl Violation for CallDatetimeFromtimestamp {
}
}
/// DTZ006
pub(crate) fn call_datetime_fromtimestamp(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()

View File

@@ -8,6 +8,42 @@ use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
use super::helpers;
/// ## What it does
/// Checks for usage of `datetime.datetime.now()` without a `tz` argument.
///
/// ## Why is this bad?
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.datetime.now()` returns a naive datetime object. Instead, use
/// `datetime.datetime.now(tz=)` to return a timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.now()
/// ```
///
/// Use instead:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[violation]
pub struct CallDatetimeNowWithoutTzinfo;
@@ -18,7 +54,6 @@ impl Violation for CallDatetimeNowWithoutTzinfo {
}
}
/// DTZ005
pub(crate) fn call_datetime_now_without_tzinfo(checker: &mut Checker, call: &ast::ExprCall) {
if !checker
.semantic()

View File

@@ -83,13 +83,13 @@ pub(crate) fn all_with_model_form(
continue;
};
match value {
Constant::Str(s) => {
if s == "__all__" {
Constant::Str(ast::StringConstant { value, .. }) => {
if value == "__all__" {
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
}
}
Constant::Bytes(b) => {
if b == "__all__".as_bytes() {
Constant::Bytes(ast::BytesConstant { value, .. }) => {
if value == "__all__".as_bytes() {
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
}
}

View File

@@ -20,6 +20,12 @@ use crate::checkers::ast::Checker;
/// annotations at evaluation time, making the code compatible with both past
/// and future Python versions.
///
/// This rule respects the [`target-version`] setting. For example, if your
/// project targets Python 3.10 and above, adding `from __future__ import annotations`
/// does not impact your ability to leverage PEP 604-style unions (e.g., to
/// convert `Optional[str]` to `str | None`). As such, this rule will only
/// flag such usages if your project targets Python 3.9 or below.
///
/// ## Example
/// ```python
/// def func(obj: dict[str, int | None]) -> None:
@@ -34,6 +40,9 @@ use crate::checkers::ast::Checker;
/// def func(obj: dict[str, int | None]) -> None:
/// ...
/// ```
///
/// ## Options
/// - `target-version`
#[violation]
pub struct FutureRequiredTypeAnnotation {
reason: Reason,

View File

@@ -13,14 +13,25 @@ use crate::checkers::ast::Checker;
///
/// ## Why is this bad?
/// PEP 563 enabled the use of a number of convenient type annotations, such as
/// `list[str]` instead of `List[str]`, or `str | None` instead of
/// `Optional[str]`. However, these annotations are only available on Python
/// 3.9 and higher, _unless_ the `from __future__ import annotations` import is present.
/// `list[str]` instead of `List[str]`. However, these annotations are only
/// available on Python 3.9 and higher, _unless_ the `from __future__ import annotations`
/// import is present.
///
/// Similarly, PEP 604 enabled the use of the `|` operator for unions, such as
/// `str | None` instead of `Optional[str]`. However, these annotations are only
/// available on Python 3.10 and higher, _unless_ the `from __future__ import annotations`
/// import is present.
///
/// By adding the `__future__` import, the pyupgrade rules can automatically
/// migrate existing code to use the new syntax, even for older Python versions.
/// This rule thus pairs well with pyupgrade and with Ruff's pyupgrade rules.
///
/// This rule respects the [`target-version`] setting. For example, if your
/// project targets Python 3.10 and above, adding `from __future__ import annotations`
/// does not impact your ability to leverage PEP 604-style unions (e.g., to
/// convert `Optional[str]` to `str | None`). As such, this rule will only
/// flag such usages if your project targets Python 3.9 or below.
///
/// ## Example
/// ```python
/// from typing import List, Dict, Optional

View File

@@ -43,6 +43,11 @@ impl Violation for UnnecessaryTypeUnion {
/// PYI055
pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr) {
// The `|` operator isn't always safe to allow to runtime-evaluated annotations.
if checker.semantic().execution_context().is_runtime() {
return;
}
let mut type_exprs = Vec::new();
// Check if `union` is a PEP604 union (e.g. `float | int`) or a `typing.Union[float, int]`

View File

@@ -123,7 +123,7 @@ pub(crate) fn unrecognized_platform(checker: &mut Checker, test: &Expr) {
}
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(value),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = right
{

View File

@@ -1,4 +1,25 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI050.py:13:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
13 | def foo_no_return(arg: NoReturn):
| ^^^^^^^^ PYI050
14 | ...
|
PYI050.py:23:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
23 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn):
| ^^^^^^^^ PYI050
24 | ...
|
PYI050.py:27:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
27 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn):
| ^^^^^^^^ PYI050
28 | ...
|

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI050.pyi:6:24: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for argument annotations
PYI050.pyi:6:24: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
4 | def foo(arg): ...
5 | def foo_int(arg: int): ...
@@ -11,7 +11,7 @@ PYI050.pyi:6:24: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for arg
8 | arg: typing_extensions.NoReturn,
|
PYI050.pyi:10:44: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for argument annotations
PYI050.pyi:10:44: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
8 | arg: typing_extensions.NoReturn,
9 | ): ... # Error: PYI050
@@ -21,7 +21,7 @@ PYI050.pyi:10:44: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for ar
12 | def foo_never(arg: Never): ...
|
PYI050.pyi:11:47: PYI050 Prefer `typing_extensions.Never` over `NoReturn` for argument annotations
PYI050.pyi:11:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
9 | ): ... # Error: PYI050
10 | def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050

View File

@@ -1,56 +1,12 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI055.py:5:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
6 | x: type[int] | type[str] | type[float]
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
PYI055.py:6:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | float]`.
|
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
6 | x: type[int] | type[str] | type[float]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
8 | z: Union[type[float], type[complex]]
|
PYI055.py:7:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
6 | x: type[int] | type[str] | type[float]
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
8 | z: Union[type[float], type[complex]]
9 | z: Union[type[float, int], type[complex]]
|
PYI055.py:8:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, complex]]`.
|
6 | x: type[int] | type[str] | type[float]
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
8 | z: Union[type[float], type[complex]]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
9 | z: Union[type[float, int], type[complex]]
|
PYI055.py:9:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`.
|
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
8 | z: Union[type[float], type[complex]]
9 | z: Union[type[float, int], type[complex]]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
PYI055.py:12:15: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | float]`.
PYI055.py:31:11: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[requests_mock.Mocker | httpretty]`.
|
12 | def func(arg: type[int] | str | type[float]) -> None: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
13 |
14 | # OK
29 | def func():
30 | # PYI055
31 | item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|

View File

@@ -1,56 +1,79 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
---
PYI055.pyi:5:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
PYI055.pyi:4:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
2 | from typing import Union
3 |
4 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
6 | x: type[int] | type[str] | type[float]
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
5 | x: type[int] | type[str] | type[float]
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
|
PYI055.pyi:6:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | float]`.
PYI055.pyi:5:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | float]`.
|
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
6 | x: type[int] | type[str] | type[float]
4 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
5 | x: type[int] | type[str] | type[float]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
8 | z: Union[type[float], type[complex]]
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
7 | z: Union[type[float], type[complex]]
|
PYI055.pyi:7:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
PYI055.pyi:6:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | str | complex]`.
|
5 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
6 | x: type[int] | type[str] | type[float]
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
4 | w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
5 | x: type[int] | type[str] | type[float]
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
8 | z: Union[type[float], type[complex]]
9 | z: Union[type[float, int], type[complex]]
7 | z: Union[type[float], type[complex]]
8 | z: Union[type[float, int], type[complex]]
|
PYI055.pyi:8:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, complex]]`.
PYI055.pyi:7:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, complex]]`.
|
6 | x: type[int] | type[str] | type[float]
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
8 | z: Union[type[float], type[complex]]
5 | x: type[int] | type[str] | type[float]
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
7 | z: Union[type[float], type[complex]]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
9 | z: Union[type[float, int], type[complex]]
8 | z: Union[type[float, int], type[complex]]
|
PYI055.pyi:9:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`.
|
7 | y: builtins.type[int] | type[str] | builtins.type[complex]
8 | z: Union[type[float], type[complex]]
9 | z: Union[type[float, int], type[complex]]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|
PYI055.pyi:12:15: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | float]`.
PYI055.pyi:8:4: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[Union[float, int, complex]]`.
|
12 | def func(arg: type[int] | str | type[float]) -> None: ...
6 | y: builtins.type[int] | type[str] | builtins.type[complex]
7 | z: Union[type[float], type[complex]]
8 | z: Union[type[float, int], type[complex]]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
9 |
10 | def func(arg: type[int] | str | type[float]) -> None: ...
|
PYI055.pyi:10:15: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[int | float]`.
|
8 | z: Union[type[float, int], type[complex]]
9 |
10 | def func(arg: type[int] | str | type[float]) -> None: ...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
13 |
14 | # OK
11 |
12 | # OK
|
PYI055.pyi:20:7: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[requests_mock.Mocker | httpretty]`.
|
19 | # OK
20 | item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
21 |
22 | def func():
|
PYI055.pyi:24:11: PYI055 Multiple `type` members in a union. Combine them into one, e.g., `type[requests_mock.Mocker | httpretty]`.
|
22 | def func():
23 | # PYI055
24 | item: type[requests_mock.Mocker] | type[httpretty] = requests_mock.Mocker
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
|

View File

@@ -169,6 +169,12 @@ mod tests {
Settings::default(),
"PT013"
)]
#[test_case(
Rule::PytestDuplicateParametrizeTestCases,
Path::new("PT014.py"),
Settings::default(),
"PT014"
)]
#[test_case(
Rule::PytestAssertAlwaysFalse,
Path::new("PT015.py"),
@@ -250,6 +256,18 @@ mod tests {
Settings::default(),
"PT026"
)]
#[test_case(
Rule::PytestUnittestRaisesAssertion,
Path::new("PT027_0.py"),
Settings::default(),
"PT027_0"
)]
#[test_case(
Rule::PytestUnittestRaisesAssertion,
Path::new("PT027_1.py"),
Settings::default(),
"PT027_1"
)]
fn test_pytest_style(
rule_code: Rule,
path: &Path,

View File

@@ -7,12 +7,14 @@ use libcst_native::{
ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement,
TrailingWhitespace, UnaryOperation,
};
use ruff_python_ast::{self as ast, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{
self as ast, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Ranged, Stmt, UnaryOp,
};
use ruff_python_ast::{visitor, whitespace};
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
@@ -21,6 +23,7 @@ use crate::autofix::codemods::CodegenStylist;
use crate::checkers::ast::Checker;
use crate::cst::matchers::match_indented_block;
use crate::cst::matchers::match_module;
use crate::importer::ImportRequest;
use crate::registry::AsRule;
use super::unittest_assert::UnittestAssert;
@@ -89,6 +92,9 @@ impl Violation for PytestCompositeAssertion {
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// def test_foo():
/// with pytest.raises(ZeroDivisionError) as exc_info:
/// 1 / 0
@@ -127,6 +133,9 @@ impl Violation for PytestAssertInExcept {
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// def test_foo():
/// if some_condition:
/// pytest.fail("some_condition was True")
@@ -145,6 +154,36 @@ impl Violation for PytestAssertAlwaysFalse {
}
}
/// ## What it does
/// Checks for uses of assertion methods from the `unittest` module.
///
/// ## Why is this bad?
/// To make use of `pytest`'s assertion rewriting, a regular `assert` statement
/// is preferred over `unittest`'s assertion methods.
///
/// ## Example
/// ```python
/// import unittest
///
///
/// class TestFoo(unittest.TestCase):
/// def test_foo(self):
/// self.assertEqual(a, b)
/// ```
///
/// Use instead:
/// ```python
/// import unittest
///
///
/// class TestFoo(unittest.TestCase):
/// def test_foo(self):
/// assert a == b
/// ```
///
/// ## References
/// - [`pytest` documentation: Assertion introspection details](https://docs.pytest.org/en/7.1.x/how-to/assert.html#assertion-introspection-details)
#[violation]
pub struct PytestUnittestAssertion {
assertion: String,
@@ -267,6 +306,186 @@ pub(crate) fn unittest_assertion(
}
}
/// ## What it does
/// Checks for uses of exception-related assertion methods from the `unittest`
/// module.
///
/// ## Why is this bad?
/// To enforce the assertion style recommended by `pytest`, `pytest.raises` is
/// preferred over the exception-related assertion methods in `unittest`, like
/// `assertRaises`.
///
/// ## Example
/// ```python
/// import unittest
///
///
/// class TestFoo(unittest.TestCase):
/// def test_foo(self):
/// with self.assertRaises(ValueError):
/// raise ValueError("foo")
/// ```
///
/// Use instead:
/// ```python
/// import unittest
/// import pytest
///
///
/// class TestFoo(unittest.TestCase):
/// def test_foo(self):
/// with pytest.raises(ValueError):
/// raise ValueError("foo")
/// ```
///
/// ## References
/// - [`pytest` documentation: Assertions about expected exceptions](https://docs.pytest.org/en/latest/how-to/assert.html#assertions-about-expected-exceptions)
#[violation]
pub struct PytestUnittestRaisesAssertion {
assertion: String,
}
impl Violation for PytestUnittestRaisesAssertion {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let PytestUnittestRaisesAssertion { assertion } = self;
format!("Use `pytest.raises` instead of unittest-style `{assertion}`")
}
fn autofix_title(&self) -> Option<String> {
let PytestUnittestRaisesAssertion { assertion } = self;
Some(format!("Replace `{assertion}` with `pytest.raises`"))
}
}
/// PT027
pub(crate) fn unittest_raises_assertion(
checker: &Checker,
call: &ast::ExprCall,
) -> Option<Diagnostic> {
let Expr::Attribute(ast::ExprAttribute { attr, .. }) = call.func.as_ref() else {
return None;
};
if !matches!(
attr.as_str(),
"assertRaises" | "failUnlessRaises" | "assertRaisesRegex" | "assertRaisesRegexp"
) {
return None;
}
let mut diagnostic = Diagnostic::new(
PytestUnittestRaisesAssertion {
assertion: attr.to_string(),
},
call.func.range(),
);
if checker.patch(diagnostic.kind.rule())
&& !checker.indexer().has_comments(call, checker.locator())
{
if let Some(args) = to_pytest_raises_args(checker, attr.as_str(), &call.arguments) {
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pytest", "raises"),
call.func.start(),
checker.semantic(),
)?;
let edit = Edit::range_replacement(format!("{binding}({args})"), call.range());
Ok(Fix::suggested_edits(import_edit, [edit]))
});
}
}
Some(diagnostic)
}
fn to_pytest_raises_args<'a>(
checker: &Checker<'a>,
attr: &str,
arguments: &Arguments,
) -> Option<Cow<'a, str>> {
let args = match attr {
"assertRaises" | "failUnlessRaises" => {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
// Ex) `assertRaises(Exception)`
([arg], []) => Cow::Borrowed(checker.locator().slice(arg.range())),
// Ex) `assertRaises(expected_exception=Exception)`
([], [kwarg])
if kwarg
.arg
.as_ref()
.is_some_and(|id| id.as_str() == "expected_exception") =>
{
Cow::Borrowed(checker.locator().slice(kwarg.value.range()))
}
_ => return None,
}
}
"assertRaisesRegex" | "assertRaisesRegexp" => {
match (arguments.args.as_slice(), arguments.keywords.as_slice()) {
// Ex) `assertRaisesRegex(Exception, regex)`
([arg1, arg2], []) => Cow::Owned(format!(
"{}, match={}",
checker.locator().slice(arg1.range()),
checker.locator().slice(arg2.range())
)),
// Ex) `assertRaisesRegex(Exception, expected_regex=regex)`
([arg], [kwarg])
if kwarg
.arg
.as_ref()
.is_some_and(|arg| arg.as_str() == "expected_regex") =>
{
Cow::Owned(format!(
"{}, match={}",
checker.locator().slice(arg.range()),
checker.locator().slice(kwarg.value.range())
))
}
// Ex) `assertRaisesRegex(expected_exception=Exception, expected_regex=regex)`
([], [kwarg1, kwarg2])
if kwarg1
.arg
.as_ref()
.is_some_and(|id| id.as_str() == "expected_exception")
&& kwarg2
.arg
.as_ref()
.is_some_and(|id| id.as_str() == "expected_regex") =>
{
Cow::Owned(format!(
"{}, match={}",
checker.locator().slice(kwarg1.value.range()),
checker.locator().slice(kwarg2.value.range())
))
}
// Ex) `assertRaisesRegex(expected_regex=regex, expected_exception=Exception)`
([], [kwarg1, kwarg2])
if kwarg1
.arg
.as_ref()
.is_some_and(|id| id.as_str() == "expected_regex")
&& kwarg2
.arg
.as_ref()
.is_some_and(|id| id.as_str() == "expected_exception") =>
{
Cow::Owned(format!(
"{}, match={}",
checker.locator().slice(kwarg2.value.range()),
checker.locator().slice(kwarg1.value.range())
))
}
_ => return None,
}
}
_ => return None,
};
Some(args)
}
/// PT015
pub(crate) fn assert_falsy(checker: &mut Checker, stmt: &Stmt, test: &Expr) {
if Truthiness::from_expr(test, |id| checker.semantic().is_builtin(id)).is_falsey() {

View File

@@ -35,6 +35,9 @@ use super::helpers::{
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture
/// def my_fixture():
/// ...
@@ -42,6 +45,9 @@ use super::helpers::{
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture():
/// ...
@@ -74,6 +80,35 @@ impl AlwaysAutofixableViolation for PytestFixtureIncorrectParenthesesStyle {
}
}
/// ## What it does
/// Checks for `pytest.fixture` calls with positional arguments.
///
/// ## Why is this bad?
/// For clarity and consistency, prefer using keyword arguments to specify
/// fixture configuration.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture("module")
/// def my_fixture():
/// ...
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture(scope="module")
/// def my_fixture():
/// ...
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[violation]
pub struct PytestFixturePositionalArgs {
function: String,
@@ -87,6 +122,34 @@ impl Violation for PytestFixturePositionalArgs {
}
}
/// ## What it does
/// Checks for `pytest.fixture` calls with `scope="function"`.
///
/// ## Why is this bad?
/// `scope="function"` can be omitted, as it is the default.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture(scope="function")
/// def my_fixture():
/// ...
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture():
/// ...
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[violation]
pub struct PytestExtraneousScopeFunction;
@@ -101,6 +164,50 @@ impl AlwaysAutofixableViolation for PytestExtraneousScopeFunction {
}
}
/// ## What it does
/// Checks for `pytest` fixtures that do not return a value, but are not named
/// with a leading underscore.
///
/// ## Why is this bad?
/// By convention, fixtures that don't return a value should be named with a
/// leading underscore, while fixtures that do return a value should not.
///
/// This rule ignores abstract fixtures and generators.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def patch_something(mocker):
/// mocker.patch("module.object")
///
///
/// @pytest.fixture()
/// def use_context():
/// with create_context():
/// yield
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def _patch_something(mocker):
/// mocker.patch("module.object")
///
///
/// @pytest.fixture()
/// def _use_context():
/// with create_context():
/// yield
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[violation]
pub struct PytestMissingFixtureNameUnderscore {
function: String,
@@ -114,6 +221,52 @@ impl Violation for PytestMissingFixtureNameUnderscore {
}
}
/// ## What it does
/// Checks for `pytest` fixtures that return a value, but are named with a
/// leading underscore.
///
/// ## Why is this bad?
/// By convention, fixtures that don't return a value should be named with a
/// leading underscore, while fixtures that do return a value should not.
///
/// This rule ignores abstract fixtures.
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def _some_object():
/// return SomeClass()
///
///
/// @pytest.fixture()
/// def _some_object_with_cleanup():
/// obj = SomeClass()
/// yield obj
/// obj.cleanup()
/// ```
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def some_object():
/// return SomeClass()
///
///
/// @pytest.fixture()
/// def some_object_with_cleanup():
/// obj = SomeClass()
/// yield obj
/// obj.cleanup()
/// ```
///
/// ## References
/// - [`pytest` documentation: `@pytest.fixture` functions](https://docs.pytest.org/en/latest/reference/reference.html#pytest-fixture)
#[violation]
pub struct PytestIncorrectFixtureNameUnderscore {
function: String,
@@ -145,6 +298,9 @@ impl Violation for PytestIncorrectFixtureNameUnderscore {
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture
/// def _patch_something():
/// ...
@@ -156,6 +312,9 @@ impl Violation for PytestIncorrectFixtureNameUnderscore {
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture
/// def _patch_something():
/// ...
@@ -239,6 +398,9 @@ impl Violation for PytestDeprecatedYieldFixture {
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture(request):
/// resource = acquire_resource()
@@ -248,6 +410,9 @@ impl Violation for PytestDeprecatedYieldFixture {
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture():
/// resource = acquire_resource()
@@ -288,6 +453,9 @@ impl Violation for PytestFixtureFinalizerCallback {
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture():
/// resource = acquire_resource()
@@ -296,6 +464,9 @@ impl Violation for PytestFixtureFinalizerCallback {
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def my_fixture_with_teardown():
/// resource = acquire_resource()
@@ -337,6 +508,9 @@ impl AlwaysAutofixableViolation for PytestUselessYieldFixture {
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def a():
/// pass
@@ -350,6 +524,9 @@ impl AlwaysAutofixableViolation for PytestUselessYieldFixture {
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// def a():
/// pass
@@ -384,6 +561,9 @@ impl AlwaysAutofixableViolation for PytestErroneousUseFixturesOnFixture {
///
/// ## Example
/// ```python
/// import pytest
///
///
/// @pytest.mark.asyncio()
/// @pytest.fixture()
/// async def my_fixture():
@@ -392,6 +572,9 @@ impl AlwaysAutofixableViolation for PytestErroneousUseFixturesOnFixture {
///
/// Use instead:
/// ```python
/// import pytest
///
///
/// @pytest.fixture()
/// async def my_fixture():
/// return 0

View File

@@ -46,11 +46,11 @@ pub(super) fn is_pytest_parametrize(decorator: &Decorator, semantic: &SemanticMo
pub(super) fn keyword_is_literal(keyword: &Keyword, literal: &str) -> bool {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Str(string),
value: Constant::Str(ast::StringConstant { value, .. }),
..
}) = &keyword.value
{
string == literal
value == literal
} else {
false
}
@@ -63,7 +63,7 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool {
..
}) => string.is_empty(),
Expr::Constant(constant) if constant.value.is_none() => true,
Expr::FString(ast::ExprFString { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, .. }) => {
values.iter().all(is_empty_or_null_string)
}
_ => false,

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