Compare commits

...

59 Commits

Author SHA1 Message Date
Charlie Marsh
91880b8273 Bump version to 0.0.286 (#6876) 2023-08-25 14:59:26 -04:00
Zanie Blue
100904adb9 Avoid parsing other parts of a format specification if replacements are present (#6858)
Closes #6767
Replaces https://github.com/astral-sh/ruff/pull/6773 (this cherry-picks
some parts from there)
Alternative to the approach introduced in #6616 which added support for
placeholders in format specifications while retaining parsing of other
format specification parts.

The idea is that if there are placeholders in a format specification we
will not attempt to glean semantic meaning from the other parts of the
format specification we'll just extract all of the placeholders ignoring
other characters. The dynamic content of placeholders can drastically
change the meaning of the format specification in ways unknowable by
static analysis. This change prevents false analysis and will ensure
safety if we build other rules on top of this at the cost of missing
detection of some bad specifications.

Minor note: I've use "replacements" and "placeholders" interchangeably
but am trying to go with "placeholder" as I think it's a better term for
the static analysis concept here
2023-08-25 17:42:57 +00:00
Zanie Blue
0bac7bd114 Update RUF100 test to reflect applicability (#6877)
Broken on merge of #6822 due to new test cases conflicting with the
addition of an applicability to RUF100 in #6827
2023-08-25 16:57:59 +00:00
Zanie Blue
f2eb7bcacf Update ERA100 to apply to commented dictionary items with trailing comments (#6822)
Closes https://github.com/astral-sh/ruff/issues/6821

ERA100 was not raising on commented parts of dictionaries if it included
another comment (such as a noqa clause). In cases where this comment was
a noqa clause, RUF100 to be emitted since the noqa would have no effect.
Here, we update ERA100 to raise even when there are trailing comments.
This resolves the linked issue _and_ increases the scope of ERA100. We
could narrow the regular expression to only apply to noqa comments if we
do not want to expand ERA100 however I think this change is within the
spirit of the rule.
2023-08-25 11:02:31 -05:00
Micha Reiser
29a0c1003b Use BestFit layout even for attributes with a short name (#6872) 2023-08-25 17:47:02 +02:00
Micha Reiser
15b7525464 Rename parser goal 'All' to 'all' (#6867) 2023-08-25 12:00:57 +00:00
konsti
0b6dab5e3f Add jupyter notebook cell ids in 4.5+ if missing (#6853)
**Summary** See
https://github.com/astral-sh/ruff/issues/6834#issuecomment-1691202417

**Test Plan** Added a new notebook
2023-08-25 08:34:42 +00:00
David Szotten
1c66bb80b7 fix is_raw_string for multiple prefixes (#6865)
fix `is_raw_string` in the presence of other prefixes (like `rb"foo"`)

fixes #6864
2023-08-25 09:58:26 +02:00
Dhruv Manilawala
d1f07008f7 Rename Notebook related symbols (#6862)
This PR renames the following symbols:

* `PySourceType::Jupyter` -> `PySourceType::Ipynb`
* `SourceKind::Jupyter` -> `SourceKind::IpyNotebook`
* `JupyterIndex` -> `NotebookIndex`
2023-08-25 11:40:54 +05:30
Micha Reiser
61b2ffa8e8 Add assert test cases (#6855) 2023-08-25 07:51:55 +02:00
Charlie Marsh
1044d66c1c Add support for PatternMatchMapping formatting (#6836)
<!--
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

Adds support for `PatternMatchMapping` -- i.e., cases like:

```python
match foo:
    case {"a": 1, "b": 2, **rest}:
        pass
```

Unfortunately, this node has _three_ kinds of dangling comments:

```python
{  # "open parenthesis comment"
   key: pattern,
   **  # end-of-line "double star comment"
   # own-line "double star comment"
   rest  # end-of-line "after rest comment"
   # own-line "after rest comment"
}
```

Some of the complexity comes from the fact that in `**rest`, `rest` is
an _identifier_, not a node, so we have to handle comments _after_ it as
dangling on the enclosing node, rather than trailing on `**rest`. (We
could change the AST to use `PatternMatchAs` there, which would be more
permissive than the grammar but not totally crazy -- `PatternMatchAs` is
used elsewhere to mean "a single identifier".)

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

## Test Plan

`cargo test`
2023-08-25 04:33:34 +00:00
Charlie Marsh
813d7da7ec Respect own-line leading comments before parenthesized nodes (#6820)
## Summary

This PR ensures that if an expression has an own-line leading comment
_before_ its open parentheses, we render it as such.

For example, given:

```python
[ # foo
    # bar
    ( # baz
        1
    )
]
```

On `main`, we format as:

```python
[  # foo
    (
        # bar
        # baz
        1
    )
]
```

As of this PR, we format as:

```python
[  # foo
    # bar
    (  # baz
        1
    )
]
```

## Test Plan

`cargo test`
2023-08-25 00:18:05 -04:00
Charlie Marsh
59e70896c0 Fix formatting of comments between function and arguments (#6826)
## Summary

We now format comments between a function and its arguments as dangling.
Like with other strange placements, I've biased towards preserving the
existing formatting, rather than attempting to reorder the comments.

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

## Test Plan

`cargo test`

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.76050          |
| django       | 0.99820          |
| transformers | 0.99800          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99615          |
| zulip        | 0.99729          |

After:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.76050          |
| django       | 0.99820          |
| transformers | 0.99800          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99615          |
| zulip        | 0.99729          |
2023-08-25 04:06:56 +00:00
Charlie Marsh
f754ad5898 Handle bracketed comments on sequence patterns (#6801)
## Summary

This PR ensures that we handle bracketed comments on sequences, like `#
comment` here:

```python
match x:
    case [ # comment
        1, 2
    ]:
        pass
```

The handling is very similar to other, similar nodes, except that we do
need some special logic to determine whether the sequence is
parenthesized, similar to our logic for tuples.

## Test Plan

`cargo test`
2023-08-25 04:03:27 +00:00
Charlie Marsh
474e8fbcd4 Format all attribute dot comments manually (#6825)
## Summary

This PR modifies our formatting of comments around the `.` in an
attribute. Specifically, the goal here is to avoid _reordering_
comments, and the net effect is that we generally leave comments
where-they-are when dealing with comments between around the dot (which
you can also think of as comments between attributes).

All comments around the dot are now treated as dangling and formatted
manually, with the exception of end-of-line or parenthesized comments on
the value, like those marked as trailing here, which remain trailing:

```python
(
    (
        a # trailing end-of-line
        # trailing own-line
    ) # dangling before dot end-of-line
    .b # trailing end-of-line
)
```

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

## Test Plan

`cargo test`

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.76050          |
| django       | 0.99820          |
| transformers | 0.99800          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99615          |
| zulip        | 0.99729          |

After:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.76050          |
| django       | 0.99820   |
| transformers | 0.99800          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99615          |
| zulip        | 0.99729          |
2023-08-25 03:50:56 +00:00
Charlie Marsh
6f23469e00 Handle pattern parentheses in FormatPattern (#6800)
## Summary

This PR fixes the duplicate-parenthesis problem that's visible in the
tests from https://github.com/astral-sh/ruff/pull/6799. The issue is
that we might have parentheses around the entire match-case pattern,
like in `(1)` here:

```python
match foo:
    case (1):
        y = 0
```

In this case, the inner expression (`1`) will _think_ it's
parenthesized, but we'll _also_ detect the parentheses at the case level
-- so they get rendered by the case, then again by the expression.
Instead, if we detect parentheses at the case level, we can force-off
the parentheses for the pattern using a design similar to the way we
handle parentheses on expressions.

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

## Test Plan

`cargo test`
2023-08-25 03:45:49 +00:00
Charlie Marsh
281ce56dc1 Make isort's detect-same-package behavior configurable (#6833)
## Summary

Our first-party import detection uses a heuristic that doesn't exist in
isort: if an import appears to be from within the same package as the
containing file, we mark it as first-party. For example, if you have a
directory `./foo/__init__.py`, and you import `from foo import bar` in
`./foo/baz.py`, we'll mark that as first-party. (See:
https://github.com/astral-sh/ruff/pull/1266.)

This is often unnecessary, and arguably should be removed (though it
does have some important use-cases that are otherwise unserved -- I
believe Dagster uses it to ensure that all packages mark imports from
within the same package as first-party, but not imports _across_
different first-party packages)... but it does exist, and it does help
in cases in which the `src` field is not properly configured.

This PR adds an option to turn off this behavior:

```toml
[tool.ruff.isort]
detect-same-package = false
```

This is being introduced to help codebases migrating over from isort
that may want more consistent behavior with their current sorting.

## Test Plan

`cargo test`
2023-08-24 14:09:26 -04:00
Zanie Blue
3bd199cdf6 Update function-call-in-argument-default (B008) to ignore arguments with immutable annotations (#6784)
Extends #6781 
Part of https://github.com/astral-sh/ruff/issues/3762
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

<!-- What's the purpose of the change? What does it do, and why? -->
Allows calls in argument defaults if the argument is annotated as an
immutable type to avoid false positives.

## Test Plan

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

Snapshots
2023-08-24 11:12:59 -05:00
Harutaka Kawamura
948cd29b23 Skip serializing cell ID if it's None (#6851)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

Fix #6834 

## Test Plan

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

Need tests?

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2023-08-24 13:20:25 +00:00
Micha Reiser
1e7d1968b1 Printer: Slice based queue and stack (#6819) 2023-08-24 14:49:27 +02:00
Micha Reiser
8b46b71038 Fix parenthesizing of implicit strings (#6852) 2023-08-24 12:31:02 +00:00
Micha Reiser
1cd7790a8a Use BestFits for non-fluent attribute chains (#6817) 2023-08-24 14:09:25 +02:00
konsti
d376cb4c2a Improve formatter contributor docs (#6776)
The docs were out of date, and the new version incorporates some
feedback.

I tried to keep the language concise and the information ordered by how
early you need it, so people can get the relevant information quickly
before jumping into the code.

I did some minor format_dev changes for consistency in the docs.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-08-24 10:45:08 +00:00
Micha Reiser
04a9a8dd03 Maybe parenthesize long constants and names (#6816) 2023-08-24 09:47:57 +00:00
Micha Reiser
e4c13846e3 Fix Printer group modes (#6815) 2023-08-24 08:42:59 +02:00
Harutaka Kawamura
205d234856 Format PatternMatchStar (#6653) 2023-08-24 01:58:05 +00:00
Charlie Marsh
4889b84338 Document logger-objects setting in flake8-logging-format rules (#6832)
Closes https://github.com/astral-sh/ruff/issues/6764.
2023-08-24 00:18:51 +00:00
Charlie Marsh
847432cacf Avoid attempting to fix PT018 in multi-statement lines (#6829)
## Summary

These fixes will _always_ fail, so we should avoid trying to construct
them in the first place.

Closes https://github.com/astral-sh/ruff/issues/6812.
2023-08-23 19:09:34 -04:00
Charlie Marsh
9b6e008cf1 Remove remaining usages of try_set_fix (#6828)
There are just a few remaining usages of this deprecated method.
2023-08-23 22:36:09 +00:00
Charlie Marsh
39c6665ff9 Remove remaining usage of set_fix_from_edit (#6827)
This method is deprecated; we have one last usage, so removing it.
2023-08-23 18:25:39 -04:00
konsti
19a87c220a Document ecosystem_all_check.sh input json (#6824)
Document the origin of `github_search.jsonl` in `ecosystem_all_check.sh`
2023-08-23 20:08:52 +00:00
Zanie Blue
0688883404 Fix uncessary-coding-comment fix when there's leading content (#6775)
Closes https://github.com/astral-sh/ruff/issues/6756

Including whitespace, code, and continuations.
2023-08-23 12:00:06 -05:00
Zanie Blue
3bb638875f Fix native-literals handling of int literal with attribute access (#6792)
Closes https://github.com/astral-sh/ruff/issues/6788 by special casing
integer literals with attribute access — either retaining parenthesis
for literals with values (e.g. `int(7).denominator` to
`(7).denominator)` or leaving calls without values (e.g.
`int().denominator`) unchanged.
2023-08-23 11:22:05 -05:00
Charlie Marsh
26e63ab137 Remove lexing from flake8-pytest-style (#6795)
## Summary

Another drive-by change to remove unnecessary custom lexing. We just
need to know the parenthesized range, so we can use...
`parenthesized_range`. I've also updated `parenthesized_range` to
support nested parentheses.

## Test Plan

`cargo test`
2023-08-23 15:54:11 +00:00
Zanie Blue
417a1d0717 Update mutable-argument-default (B006) to use extend-immutable-calls when determining if annotations are immutable (#6781)
Part of https://github.com/astral-sh/ruff/issues/3762
2023-08-23 15:44:35 +00:00
Micha Reiser
34b2ae73b4 Extend BestFitting with mode (#6814) 2023-08-23 17:23:45 +02:00
Charlie Marsh
71c25e4f9d Implement FormatPatternMatchValue (#6799)
## Summary

This is effectively #6608, but with additional tests.

We aren't properly handling parenthesized patterns, but that needs to be
dealt with separately as it's somewhat involved.

Closes #6555
2023-08-23 14:01:14 +00:00
Micha Reiser
4bdd99f882 Fix: Re-add missing node start positions (#6780) 2023-08-23 09:59:36 +02:00
Charlie Marsh
1e6d1182bf Improve comment handling around PatternMatchAs (#6797)
## Summary

Follows up on
https://github.com/astral-sh/ruff/pull/6652#discussion_r1300871033 with
some modifications to the `PatternMatchAs` comment handling.
Specifically, any comments between the `as` and the end are now
formatted as dangling, and we now insert some newlines in the
appropriate places.

## Test Plan

`cargo test`
2023-08-23 04:48:20 +00:00
Charlie Marsh
d08f697a04 Remove lexing for colon-matching use cases (#6803)
It's much simpler to just search ahead for the first colon.
2023-08-23 04:44:51 +00:00
Charlie Marsh
4bc5eddf91 Handle open-parenthesis comments on match case (#6798)
## Summary

Ensures that we retain the open-parenthesis comment in cases like:
```python
match pattern_comments:
    case (  # leading
        only_leading
    ):
        ...
```

Previously, this was treated as a leading comment on `only_leading`.

## Test Plan

`cargo test`
2023-08-23 00:40:18 -04:00
Charlie Marsh
5f5de52aba Confine repeated-equality-comparison-target to names and attributes (#6802)
Empirically, Pylint does this, so seems reasonable to follow.
2023-08-23 03:56:37 +00:00
Tom Kuson
1cb1bd731c Extend repeated-equality-comparison-target to check for mixed orderings and Yoda conditions. (#6691) 2023-08-23 03:45:30 +00:00
Dhruv Manilawala
db2e548f4f Simplify ANN204 autofix to use Parameters range (#6793)
## Summary

This PR fixes the bug where the decorator parentheses weren't being considered
when computing the autofix for `ANN204`. The existing logic would only look
for balanced parentheses and not multiple pairs of parentheses.

The solution is to remove the logic to generate the autofix and use the
`Parameters` end range directly which includes the parentheses as well.

## Test Plan

Add test case for `ANN204` with decorator being called

fixes: #6790
2023-08-23 09:05:46 +05:30
Harutaka Kawamura
94f5f18ddb Format PatternMatchSequence (#6676) 2023-08-23 00:44:33 +00:00
Luc Khai Hai
c34a342ab4 Format PatternMatchAs (#6652)
## Summary

Add formatting for `PatternMatchAs`.

This closes #6641.

## Test Plan

Add tests for comments.
2023-08-22 23:58:15 +00:00
Charlie Marsh
42ff833d00 Remove comment lexing from isort (#6794)
## Summary

No need to lex to find comments -- we already know their locations via
`Indexer`.
2023-08-22 21:26:38 +00:00
Konrad Listwan-Ciesielski
e1f4438498 Add docs for DTZ007 (#6757) 2023-08-22 21:12:50 +00:00
Charlie Marsh
1acdec3e29 Add a note on __future__ imports and keep-runtime-typing to pyupgrade rules (#6746)
Closes https://github.com/astral-sh/ruff/issues/6740.
2023-08-22 17:04:00 -04:00
Charlie Marsh
ca2bb20063 Fallback to end-of-file if ends in trailing continuation (#6789)
## Summary

Given:

```python
def end_of_file():
    if False:
        return 1
    x = 2 \

```

Then when searching for the end of the `x = 2` statement, we'd reach a
panic as we'd hit the last line (`\\`) and abort, since the universal
iterator doesn't return trailing newlines. Instead, we should just use
the end of the file as the fallback.

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

## Test Plan

`cargo test`
2023-08-22 15:12:26 -04:00
Dhruv Manilawala
2e00983762 Avoid C417 for lambda with default and variadic parameters (#6752)
## Summary

Avoid `C417` for `lambda` with default and variadic parameters.

## Test Plan

`cargo test` and checking if it generates any autofix errors as test
cases
for `lambda` with default parameters already exists.

fixes: #6715
2023-08-23 00:38:08 +05:30
Dhruv Manilawala
fb7caf43c8 Update lexer tests to use snapshots (#6658)
## Summary

This PR updates the lexer tests to use the snapshot testing framework.
It also
makes the following changes:
* Remove the use of macros in the lexer tests
* Use `test_case` for EOL tests

## Test Plan

```
cargo test --package ruff_python_parser --lib --all-features -- lexer::tests --no-capture
```
2023-08-22 18:23:19 +00:00
konsti
e53bf25616 Update formatter ecosystem checks revisions (#6770)
With https://github.com/django/django/pull/17181 merged, this removes an
odd edge case (tuple expression statements aka bogus trailing commas
after statements that turn them into a tuple without you noticing) that
we don't want to care about because the input code is ~wrong from the
similarity index. I've took this opportunity to update the revisions of
all projects we test.

main

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75477          |
| django       | 0.99814          |
| transformers | 0.99621          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

this PR

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75996          |
| django       | 0.99819          |
| transformers | 0.99622          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99607          |
| zulip        | 0.99729          |
2023-08-22 14:19:10 -04:00
Charlie Marsh
214eb707a6 Parenthesize expressions prior to LibCST parsing (#6742)
<!--
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 a utility for transforming expressions via LibCST that
automatically wraps the expression in parentheses, applies a
user-provided transformation, then strips the parentheses from the
generated code. LibCST can't parse arbitrary expression ranges, since
some expressions may require parenthesization in order to be parsed
properly. For example:

```python
option = (
    '{name}={value}'
    .format(nam=name, value=value)
)
```

In this case, the expression range is:

```python
'{name}={value}'
    .format(nam=name, value=value)
```

Which isn't valid on its own. So, instead, we add "fake" parentheses
around the expression.

We were already doing this in a few places, so this is mostly
formalizing and DRYing up that pattern.

Closes https://github.com/astral-sh/ruff/issues/6720.
2023-08-22 17:45:05 +00:00
Zanie Blue
5c1f7fd5dd Add networkx to conventional aliases (#6778)
Closes https://github.com/astral-sh/ruff/issues/6763
2023-08-22 11:49:04 -05:00
Charlie Marsh
cc278c24e2 Allow up to two empty lines after top-level imports (#6777)
## Summary

For imports, we enforce that there's _at least_ one empty line after an
import (assuming the next statement is _not_ an import), but allow up to
two at the module level.

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

## Test Plan

`cargo test`
2023-08-22 12:27:40 -04:00
Charlie Marsh
558b56f8a8 Avoid fixing D200 for docstrings that end in escapes (#6779)
Appease the fuzzers! Closes
https://github.com/astral-sh/ruff/issues/6755.
2023-08-22 16:25:37 +00:00
Charlie Marsh
749da6589a Fix isolation groups for unused imports (#6774)
## Summary

The isolation group for unused imports was relying on
`checker.semantic().current_statement()`, which isn't valid for that
rule, since it runs over the _scope_, not the statement. Instead, we
need to lookup the isolation group based on the `NodeId` of the
statement.

Our tests didn't catch this, because we mostly have cases that look like
this:

```python
if TYPE_CHECKING:
    import shelve
    import importlib
```

In this case, the two fixes to remove the two unused imports are
considered overlapping (since we delete the _full_ line, and the two
_full_ lines touch, and we consider exactly-adjacent fixes to be
overlapping), and so they don't run in a single pass due to the
non-overlapping-fixes requirement. That is: the isolation groups aren't
required for this case. They are, however, required for cases like:

```python
if TYPE_CHECKING:
    import shelve

    import importlib
```

...where the fixes don't overlap.

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

## Test Plan

`cargo test`
2023-08-22 11:55:27 -04:00
Charlie Marsh
d2eace3377 Prefer range_* edit methods (#6751) 2023-08-22 15:46:04 +00:00
255 changed files with 38159 additions and 39754 deletions

32
Cargo.lock generated
View File

@@ -812,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.285"
version = "0.0.286"
dependencies = [
"anyhow",
"clap",
@@ -876,8 +876,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -2064,7 +2066,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.285"
version = "0.0.286"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2128,6 +2130,7 @@ dependencies = [
"typed-arena",
"unicode-width",
"unicode_names2",
"uuid",
"wsl",
]
@@ -2164,7 +2167,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.285"
version = "0.0.286"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2340,6 +2343,7 @@ dependencies = [
"insta",
"is-macro",
"itertools",
"memchr",
"once_cell",
"ruff_formatter",
"ruff_python_ast",
@@ -2399,6 +2403,7 @@ dependencies = [
"ruff_text_size",
"rustc-hash",
"static_assertions",
"test-case",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",
@@ -3344,9 +3349,26 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.4.0"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be"
checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
dependencies = [
"getrandom",
"rand",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.23",
]
[[package]]
name = "valuable"

View File

@@ -50,6 +50,7 @@ tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-width = "0.1.10"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
# v1.0.1

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.285
rev: v0.0.286
hooks:
- id: ruff
```

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.285"
version = "0.0.286"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -77,6 +77,7 @@ toml = { workspace = true }
typed-arena = { version = "2.0.2" }
unicode-width = { workspace = true }
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
uuid = { workspace = true, features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
[dev-dependencies]

View File

@@ -19,3 +19,12 @@ def foo(x, y, z):
class A():
pass
# b = c
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456,
# "key3": 789, # test
}
#import os # noqa

View File

@@ -152,3 +152,9 @@ def f(a: Union[str, bytes, Any]) -> None: ...
def f(a: Optional[Any]) -> None: ...
def f(a: Annotated[Any, ...]) -> None: ...
def f(a: "Union[str, bytes, Any]") -> None: ...
class Foo:
@decorator()
def __init__(self: "Foo", foo: int):
...

View File

@@ -230,6 +230,10 @@ def timedelta_okay(value=dt.timedelta(hours=1)):
def path_okay(value=Path(".")):
pass
# B008 allow arbitrary call with immutable annotation
def immutable_annotation_call(value: Sequence[int] = foo()):
pass
# B006 and B008
# We should handle arbitrary nesting of these B008.
def nested_combo(a=[float(3), dt.datetime.now()]):

View File

@@ -0,0 +1,18 @@
import custom
from custom import ImmutableTypeB
def okay(foo: ImmutableTypeB = []):
...
def okay(foo: custom.ImmutableTypeA = []):
...
def okay(foo: custom.ImmutableTypeB = []):
...
def error_due_to_missing_import(foo: ImmutableTypeA = []):
...

View File

@@ -1,6 +1,7 @@
from typing import List
import fastapi
import custom
from fastapi import Query
@@ -16,5 +17,9 @@ def okay(data: List[str] = Query(None)):
...
def okay(data: custom.ImmutableTypeA = foo()):
...
def error_due_to_missing_import(data: List[str] = Depends(None)):
...

View File

@@ -15,11 +15,6 @@ filter(func, map(lambda v: v, nums))
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
_ = f"{dict(map(lambda v: (v, v**2), nums))}"
# Error, but unfixable.
# For simple expressions, this could be: `(x if x else 1 for x in nums)`.
# For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
map(lambda x=1: x, nums)
# False negatives.
map(lambda x=2, y=1: x + y, nums, nums)
set(map(lambda x, y: x, nums, nums))
@@ -37,3 +32,8 @@ map(lambda x: lambda: x, range(4))
# Error: the `x` is overridden by the inner lambda.
map(lambda x: lambda x: x, range(4))
# Ok because of the default parameters, and variadic arguments.
map(lambda x=1: x, nums)
map(lambda *args: len(args), range(4))
map(lambda **kwargs: len(kwargs), range(4))

View File

@@ -9,6 +9,7 @@ def unconventional():
import pandas
import seaborn
import tkinter
import networkx
def unconventional_aliases():
@@ -18,7 +19,7 @@ def unconventional_aliases():
import pandas as pdas
import seaborn as sbrn
import tkinter as tkr
import networkx as nxy
def conventional_aliases():
import altair as alt
@@ -27,3 +28,4 @@ def conventional_aliases():
import pandas as pd
import seaborn as sns
import tkinter as tk
import networkx as nx

View File

@@ -43,3 +43,12 @@ message
assert something # OK
assert something and something_else # Error
assert something and something_else and something_third # Error
def test_multiline():
assert something and something_else; x = 1
x = 1; assert something and something_else
x = 1; \
assert something and something_else

View File

@@ -320,3 +320,9 @@ def end_of_statement():
if True:
return "" \
; # type: ignore
def end_of_file():
if False:
return 1
x = 2 \

View File

@@ -0,0 +1,3 @@
import os
import pandas
import foo.baz

View File

@@ -0,0 +1,2 @@
[tool.ruff]
line-length = 88

View File

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

View File

@@ -0,0 +1,37 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"import os\n",
"\n",
"math.pi"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}

View File

@@ -0,0 +1,13 @@
def func():
"""\
"""
def func():
"""\\
"""
def func():
"""\ \
"""

View File

@@ -99,3 +99,16 @@ import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))
# Test: isolated deletions.
if TYPE_CHECKING:
import a1
import a2
match *0, 1, *2:
case 0,:
import b1
import b2

View File

@@ -2,6 +2,5 @@
"{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
# Not fixable
(''
.format(x=2))
.format(x=2)) # F522

View File

@@ -28,6 +28,6 @@
"{1}{3}".format(1, 2, 3, 4) # F523, # F524
"{1} {8}".format(0, 1) # F523, # F524
# Not fixable
# Multiline
(''
.format(2))

View File

@@ -154,3 +154,14 @@ def f() -> None:
print("hello")
except A as e :
print("oh no!")
def f():
x = 1
y = 2
def f():
x = 1
y = 2

View File

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

View File

@@ -9,13 +9,22 @@ foo != "a" and foo != "b" and foo != "c"
foo == a or foo == "b" or foo == 3 # Mixed types.
# False negatives (the current implementation doesn't support Yoda conditions).
"a" == foo or "b" == foo or "c" == foo
"a" != foo and "b" != foo and "c" != foo
"a" == foo or foo == "b" or "c" == foo
foo == bar or baz == foo or qux == foo
foo == "a" or "b" == foo or foo == "c"
foo != "a" and "b" != foo and foo != "c"
foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
foo.bar == "a" or foo.bar == "b" # Attributes.
# OK
foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
@@ -36,3 +45,9 @@ foo != "a" # Single comparison.
foo == "a" == "b" or foo == "c" # Multiple comparisons.
foo == bar == "b" or foo == "c" # Multiple comparisons.
foo == foo or foo == bar # Self-comparison.
foo[0] == "a" or foo[0] == "b" # Subscripts.
foo() == "a" or foo() == "b" # Calls.

View File

@@ -0,0 +1,7 @@
"""
# coding=utf8""" # empty comment
"""
Invalid coding declaration since it is nested inside a docstring
The following empty comment tests for false positives as our implementation visits comments
"""

View File

@@ -0,0 +1,7 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading space must be removed to prevent invalid syntax.
"""

View File

@@ -0,0 +1,7 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading tab must be removed to prevent invalid syntax.
"""

View File

@@ -0,0 +1,6 @@
print("foo") # coding=utf8
print("Hello world")
"""
Invalid coding declaration due to a statement before the comment
"""

View File

@@ -0,0 +1,7 @@
x = 1 \
# coding=utf8
x = 2
"""
Invalid coding declaration due to continuation on preceding line
"""

View File

@@ -31,6 +31,7 @@ bool("foo")
bool("")
bool(b"")
bool(1.0)
int().denominator
# These become string or byte literals
str()
@@ -49,3 +50,6 @@ float(1.0)
bool()
bool(True)
bool(False)
# These become a literal but retain parentheses
int(1).denominator

View File

@@ -0,0 +1,11 @@
#import os # noqa
#import os # noqa: ERA001
dictionary = {
# "key1": 123, # noqa: ERA001
# "key2": 456, # noqa
# "key3": 789,
}
#import os # noqa: E501

View File

@@ -11,7 +11,7 @@ use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_text_size::{TextLen, TextSize};
use crate::autofix::codemods;
@@ -48,7 +48,7 @@ pub(crate) fn delete_stmt(
} else if has_leading_content(stmt.start(), locator) {
Edit::range_deletion(stmt.range())
} else if let Some(start) = indexer.preceded_by_continuations(stmt.start(), locator) {
Edit::range_deletion(TextRange::new(start, stmt.end()))
Edit::deletion(start, stmt.end())
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
@@ -133,10 +133,8 @@ pub(crate) fn remove_argument<T: Ranged>(
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
// parentheses, if needed).
Ok(match parentheses {
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
Parentheses::Preserve => {
Edit::replacement("()".to_string(), arguments.start(), arguments.end())
}
Parentheses::Remove => Edit::range_deletion(arguments.range()),
Parentheses::Preserve => Edit::range_replacement("()".to_string(), arguments.range()),
})
}
}
@@ -228,25 +226,25 @@ fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option<TextSize> {
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let start_location = semicolon + TextSize::from(1);
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
for line in
NewlineWithTrailingNewline::with_offset(locator.after(start_location), start_location)
{
let trimmed = line.trim_whitespace();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return start_location
+ if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
return if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
locator.line_end(start_location)

View File

@@ -381,34 +381,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Ok(summary) => {
if checker.enabled(Rule::StringDotFormatExtraNamedArguments) {
pyflakes::rules::string_dot_format_extra_named_arguments(
checker, &summary, keywords, location,
checker, call, &summary, keywords,
);
}
if checker
.enabled(Rule::StringDotFormatExtraPositionalArguments)
{
pyflakes::rules::string_dot_format_extra_positional_arguments(
checker, &summary, args, location,
checker, call, &summary, args,
);
}
if checker.enabled(Rule::StringDotFormatMissingArguments) {
pyflakes::rules::string_dot_format_missing_argument(
checker, &summary, args, keywords, location,
checker, call, &summary, args, keywords,
);
}
if checker.enabled(Rule::StringDotFormatMixingAutomatic) {
pyflakes::rules::string_dot_format_mixing_automatic(
checker, &summary, location,
checker, call, &summary,
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, call);
pyupgrade::rules::format_literals(checker, call, &summary);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
checker,
call,
&summary,
expr,
value,
checker.settings.line_length,
);
@@ -441,7 +441,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::redundant_open_modes(checker, call);
}
if checker.enabled(Rule::NativeLiterals) {
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
pyupgrade::rules::native_literals(
checker,
call,
checker.semantic().current_expression_parent(),
);
}
if checker.enabled(Rule::OpenAlias) {
pyupgrade::rules::open_alias(checker, expr, func);

View File

@@ -52,7 +52,8 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::analyze::{typing, visibility};
use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_source_file::Locator;
@@ -193,24 +194,6 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`IsolationLevel`] to isolate fixes for the current statement.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn statement_isolation(&self) -> IsolationLevel {
IsolationLevel::Group(self.semantic.current_statement_id().into())
}
/// Returns the [`IsolationLevel`] to isolate fixes in the current statement's parent.
pub(crate) fn parent_isolation(&self) -> IsolationLevel {
self.semantic
.current_statement_parent_id()
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
@@ -259,6 +242,18 @@ impl<'a> Checker<'a> {
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
self.settings.rules.any_enabled(rules)
}
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(node_id: Option<NodeId>) -> IsolationLevel {
node_id
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>

View File

@@ -110,8 +110,8 @@ pub(crate) fn check_noqa(
let mut diagnostic =
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
if settings.rules.should_fix(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator));
diagnostic
.set_fix(Fix::automatic(delete_noqa(directive.range(), locator)));
}
diagnostics.push(diagnostic);
}
@@ -175,12 +175,12 @@ pub(crate) fn check_noqa(
);
if settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
#[allow(deprecated)]
diagnostic
.set_fix_from_edit(delete_noqa(directive.range(), locator));
diagnostic.set_fix(Fix::automatic(delete_noqa(
directive.range(),
locator,
)));
} else {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));

View File

@@ -1,9 +1,11 @@
use crate::autofix::codemods::CodegenStylist;
use anyhow::{bail, Result};
use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
};
use ruff_python_codegen::Stylist;
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -12,13 +14,6 @@ pub(crate) fn match_module(module_text: &str) -> Result<Module> {
}
}
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
match libcst_native::parse_statement(statement_text) {
Ok(statement) => Ok(statement),
@@ -205,3 +200,59 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
bail!("Expected Statement::Compound")
}
}
/// Given the source code for an expression, return the parsed [`Expression`].
///
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
/// Run a transformation function over an expression.
///
/// Passing an expression to [`match_expression`] directly can lead to parse errors if the
/// expression is not a valid standalone expression (e.g., it was parenthesized in the original
/// source). This method instead wraps the expression in "fake" parentheses, runs the
/// transformation, then removes the "fake" parentheses.
pub(crate) fn transform_expression(
source_code: &str,
stylist: &Stylist,
func: impl FnOnce(Expression) -> Result<Expression>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
let expression = match_expression(&source_code)?;
// Run the function on the expression.
let expression = func(expression)?;
// Codegen the expression.
let mut source_code = expression.codegen_stylist(stylist);
// Drop the outer parentheses.
source_code.drain(0..1);
source_code.drain(source_code.len() - 1..source_code.len());
Ok(source_code)
}
/// Like [`transform_expression`], but operates on the source code of the expression, rather than
/// the parsed [`Expression`]. This _shouldn't_ exist, but does to accommodate lifetime issues.
pub(crate) fn transform_expression_text(
source_code: &str,
func: impl FnOnce(String) -> Result<String>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
// Run the function on the expression.
let mut transformed = func(source_code)?;
// Drop the outer parentheses.
transformed.drain(0..1);
transformed.drain(transformed.len() - 1..transformed.len());
Ok(transformed)
}

View File

@@ -3,14 +3,14 @@
/// When we lint a jupyter notebook, we have to translate the row/column based on
/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JupyterIndex {
pub struct NotebookIndex {
/// Enter a row (1-based), get back the cell (1-based)
pub(super) row_to_cell: Vec<u32>,
/// Enter a row (1-based), get back the row in cell (1-based)
pub(super) row_to_row_in_cell: Vec<u32>,
}
impl JupyterIndex {
impl NotebookIndex {
/// Returns the cell number (1-based) for the given row (1-based).
pub fn cell(&self, row: usize) -> Option<u32> {
self.row_to_cell.get(row).copied()

View File

@@ -9,6 +9,7 @@ use itertools::Itertools;
use once_cell::sync::OnceCell;
use serde::Serialize;
use serde_json::error::Category;
use uuid::Uuid;
use ruff_diagnostics::Diagnostic;
use ruff_python_parser::lexer::lex;
@@ -17,7 +18,7 @@ use ruff_source_file::{NewlineWithTrailingNewline, UniversalNewlineIterator};
use ruff_text_size::{TextRange, TextSize};
use crate::autofix::source_map::{SourceMap, SourceMarker};
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::schema::{Cell, RawNotebook, SortAlphabetically, SourceValue};
use crate::rules::pycodestyle::rules::SyntaxError;
use crate::IOError;
@@ -82,8 +83,8 @@ impl Cell {
Cell::Code(cell) => &cell.source,
_ => return false,
};
// Ignore cells containing cell magic. This is different from line magic
// which is allowed and ignored by the parser.
// Ignore cells containing cell magic as they act on the entire cell
// as compared to line magic which acts on a single line.
!match source {
SourceValue::String(string) => string
.lines()
@@ -106,7 +107,7 @@ pub struct Notebook {
source_code: String,
/// The index of the notebook. This is used to map between the concatenated
/// source code and the original notebook.
index: OnceCell<JupyterIndex>,
index: OnceCell<NotebookIndex>,
/// The raw notebook i.e., the deserialized version of JSON string.
raw: RawNotebook,
/// The offsets of each cell in the concatenated source code. This includes
@@ -156,7 +157,7 @@ impl Notebook {
TextRange::default(),
)
})?;
let raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
let mut raw_notebook: RawNotebook = match serde_json::from_reader(reader.by_ref()) {
Ok(notebook) => notebook,
Err(err) => {
// Translate the error into a diagnostic
@@ -262,6 +263,23 @@ impl Notebook {
cell_offsets.push(current_offset);
}
// Add cell ids to 4.5+ notebooks if they are missing
// https://github.com/astral-sh/ruff/issues/6834
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#required-field
if raw_notebook.nbformat == 4 && raw_notebook.nbformat_minor >= 5 {
for cell in &mut raw_notebook.cells {
let id = match cell {
Cell::Code(cell) => &mut cell.id,
Cell::Markdown(cell) => &mut cell.id,
Cell::Raw(cell) => &mut cell.id,
};
if id.is_none() {
// https://github.com/jupyter/enhancement-proposals/blob/master/62-cell-id/cell-id.md#questions
*id = Some(Uuid::new_v4().to_string());
}
}
}
Ok(Self {
raw: raw_notebook,
index: OnceCell::new(),
@@ -368,7 +386,7 @@ impl Notebook {
///
/// The index building is expensive as it needs to go through the content of
/// every valid code cell.
fn build_index(&self) -> JupyterIndex {
fn build_index(&self) -> NotebookIndex {
let mut row_to_cell = vec![0];
let mut row_to_row_in_cell = vec![0];
@@ -395,7 +413,7 @@ impl Notebook {
row_to_row_in_cell.extend(1..=line_count);
}
JupyterIndex {
NotebookIndex {
row_to_cell,
row_to_row_in_cell,
}
@@ -413,7 +431,7 @@ impl Notebook {
/// The index is built only once when required. This is only used to
/// report diagnostics, so by that time all of the autofixes must have
/// been applied if `--fix` was passed.
pub(crate) fn index(&self) -> &JupyterIndex {
pub(crate) fn index(&self) -> &NotebookIndex {
self.index.get_or_init(|| self.build_index())
}
@@ -473,12 +491,14 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::jupyter::index::JupyterIndex;
use crate::jupyter::index::NotebookIndex;
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
use crate::source_kind::SourceKind;
use crate::test::{
read_jupyter_notebook, test_notebook_path, test_resource_path, TestedNotebook,
read_jupyter_notebook, test_contents, test_notebook_path, test_resource_path,
TestedNotebook,
};
use crate::{assert_messages, settings};
@@ -559,7 +579,7 @@ print("after empty cells")
);
assert_eq!(
notebook.index(),
&JupyterIndex {
&NotebookIndex {
row_to_cell: vec![0, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 5, 7, 7, 8],
row_to_row_in_cell: vec![0, 1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 1, 1, 2, 1],
}
@@ -659,4 +679,28 @@ print("after empty cells")
Ok(())
}
// Version <4.5, don't emit cell ids
#[test_case(Path::new("no_cell_id.ipynb"), false; "no_cell_id")]
// Version 4.5, cell ids are missing and need to be added
#[test_case(Path::new("add_missing_cell_id.ipynb"), true; "add_missing_cell_id")]
fn test_cell_id(path: &Path, has_id: bool) -> Result<()> {
let source_notebook = read_jupyter_notebook(path)?;
let source_kind = SourceKind::IpyNotebook(source_notebook);
let (_, transformed) = test_contents(
&source_kind,
path,
&settings::Settings::for_rule(Rule::UnusedImport),
);
let linted_notebook = transformed.into_owned().expect_ipy_notebook();
let mut writer = Vec::new();
linted_notebook.write_inner(&mut writer)?;
let actual = String::from_utf8(writer)?;
if has_id {
assert!(actual.contains(r#""id": ""#));
} else {
assert!(!actual.contains(r#""id":"#));
}
Ok(())
}
}

View File

@@ -150,6 +150,7 @@ pub struct CodeCell {
/// Technically, id isn't required (it's not even present) in schema v4.0 through v4.4, but
/// it's required in v4.5. Main issue is that pycharm creates notebooks without an id
/// <https://youtrack.jetbrains.com/issue/PY-59438/Jupyter-notebooks-created-with-PyCharm-are-missing-the-id-field-in-cells-in-the-.ipynb-json>
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
/// Cell-level metadata.
pub metadata: Value,

View File

@@ -147,7 +147,7 @@ pub fn check_path(
match ruff_python_parser::parse_program_tokens(
tokens,
&path.to_string_lossy(),
source_type.is_jupyter(),
source_type.is_ipynb(),
) {
Ok(python_ast) => {
if use_ast {

View File

@@ -7,7 +7,7 @@ use colored::Colorize;
use ruff_source_file::OneIndexed;
use crate::fs::relativize_path;
use crate::jupyter::{JupyterIndex, Notebook};
use crate::jupyter::{Notebook, NotebookIndex};
use crate::message::diff::calculate_print_width;
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
@@ -92,7 +92,7 @@ struct DisplayGroupedMessage<'a> {
show_source: bool,
row_length: NonZeroUsize,
column_length: NonZeroUsize,
jupyter_index: Option<&'a JupyterIndex>,
jupyter_index: Option<&'a NotebookIndex>,
}
impl Display for DisplayGroupedMessage<'_> {

View File

@@ -11,7 +11,7 @@ use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
use crate::fs::relativize_path;
use crate::jupyter::{JupyterIndex, Notebook};
use crate::jupyter::{Notebook, NotebookIndex};
use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
@@ -161,7 +161,7 @@ impl Display for RuleCodeAndBody<'_> {
pub(super) struct MessageCodeFrame<'a> {
pub(crate) message: &'a Message,
pub(crate) jupyter_index: Option<&'a JupyterIndex>,
pub(crate) jupyter_index: Option<&'a NotebookIndex>,
}
impl Display for MessageCodeFrame<'_> {

View File

@@ -562,7 +562,7 @@ fn add_noqa_inner(
let mut prev_end = TextSize::default();
for (offset, (rules, directive)) in matches_by_line {
output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]);
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
let line = locator.full_line(offset);
@@ -619,7 +619,7 @@ fn add_noqa_inner(
prev_end = offset + line.text_len();
}
output.push_str(&locator.contents()[usize::from(prev_end)..]);
output.push_str(locator.after(prev_end));
(count, output)
}

View File

@@ -28,7 +28,7 @@ static HASH_NUMBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"#\d").unwrap());
static MULTILINE_ASSIGNMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^\s*([(\[]\s*)?(\w+\s*,\s*)*\w+\s*([)\]]\s*)?=.*[(\[{]$").unwrap());
static PARTIAL_DICTIONARY_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*$"#).unwrap());
Lazy::new(|| Regex::new(r#"^\s*['"]\w+['"]\s*:.+[,{]\s*(#.*)?$"#).unwrap());
static PRINT_RETURN_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^(print|return)\b\s*").unwrap());
/// Returns `true` if a comment contains Python code.

View File

@@ -105,5 +105,47 @@ ERA001.py:21:5: ERA001 [*] Found commented-out code
19 19 | class A():
20 20 | pass
21 |- # b = c
22 21 |
23 22 |
24 23 | dictionary = {
ERA001.py:26:5: ERA001 [*] Found commented-out code
|
24 | dictionary = {
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,
| ^^^^^^^^^^^^^^ ERA001
27 | # "key3": 789, # test
28 | }
|
= help: Remove commented-out code
Possible fix
23 23 |
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
26 |- # "key2": 456,
27 26 | # "key3": 789, # test
28 27 | }
29 28 |
ERA001.py:27:5: ERA001 [*] Found commented-out code
|
25 | # "key1": 123, # noqa: ERA001
26 | # "key2": 456,
27 | # "key3": 789, # test
| ^^^^^^^^^^^^^^^^^^^^^^ ERA001
28 | }
|
= help: Remove commented-out code
Possible fix
24 24 | dictionary = {
25 25 | # "key1": 123, # noqa: ERA001
26 26 | # "key2": 456,
27 |- # "key3": 789, # test
28 27 | }
29 28 |
30 29 | #import os # noqa

View File

@@ -1,45 +0,0 @@
use anyhow::{bail, Result};
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_diagnostics::Edit;
use ruff_source_file::Locator;
/// ANN204
pub(crate) fn add_return_annotation<T: Ranged>(
statement: &T,
annotation: &str,
source_type: PySourceType,
locator: &Locator,
) -> Result<Edit> {
let contents = &locator.contents()[statement.range()];
// Find the colon (following the `def` keyword).
let mut seen_lpar = false;
let mut seen_rpar = false;
let mut count = 0u32;
for (tok, range) in
lexer::lex_starts_at(contents, source_type.as_mode(), statement.start()).flatten()
{
if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) {
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));
}
}
if matches!(tok, Tok::Lpar) {
if count == 0 {
seen_lpar = true;
}
count = count.saturating_add(1);
}
if matches!(tok, Tok::Rpar) {
count = count.saturating_sub(1);
if count == 0 {
seen_rpar = true;
}
}
}
bail!("Unable to locate colon in function definition");
}

View File

@@ -1,5 +1,4 @@
//! Rules from [flake8-annotations](https://pypi.org/project/flake8-annotations/).
mod fixes;
pub(crate) mod helpers;
pub(crate) mod rules;
pub mod settings;

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
@@ -11,7 +11,6 @@ use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_annotations::fixes;
use crate::rules::ruff::typing::type_hint_resolves_to_any;
/// ## What it does
@@ -704,15 +703,10 @@ pub(crate) fn definition(
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
"None",
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
diagnostic.set_fix(Fix::suggested(Edit::insertion(
" -> None".to_string(),
function.parameters.range().end(),
)));
}
diagnostics.push(diagnostic);
}
@@ -727,15 +721,10 @@ pub(crate) fn definition(
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
return_type,
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
diagnostic.set_fix(Fix::suggested(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
}
}
diagnostics.push(diagnostic);

View File

@@ -242,4 +242,22 @@ annotation_presence.py:154:10: ANN401 Dynamically typed expressions (typing.Any)
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|
annotation_presence.py:159:9: ANN204 [*] Missing return type annotation for special method `__init__`
|
157 | class Foo:
158 | @decorator()
159 | def __init__(self: "Foo", foo: int):
| ^^^^^^^^ ANN204
160 | ...
|
= help: Add `None` return type
Suggested fix
156 156 |
157 157 | class Foo:
158 158 | @decorator()
159 |- def __init__(self: "Foo", foo: int):
159 |+ def __init__(self: "Foo", foo: int) -> None:
160 160 | ...

View File

@@ -71,8 +71,27 @@ mod tests {
}
#[test]
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
fn extend_immutable_calls_arg_annotation() -> Result<()> {
let snapshot = "extend_immutable_calls_arg_annotation".to_string();
let diagnostics = test_path(
Path::new("flake8_bugbear/B006_extended.py"),
&Settings {
flake8_bugbear: super::settings::Settings {
extend_immutable_calls: vec![
"custom.ImmutableTypeA".to_string(),
"custom.ImmutableTypeB".to_string(),
],
},
..Settings::for_rule(Rule::MutableArgumentDefault)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_immutable_calls_arg_default() -> Result<()> {
let snapshot = "extend_immutable_calls_arg_default".to_string();
let diagnostics = test_path(
Path::new("flake8_bugbear/B008_extended.py"),
&Settings {
@@ -80,6 +99,7 @@ mod tests {
extend_immutable_calls: vec![
"fastapi.Depends".to_string(),
"fastapi.Query".to_string(),
"custom.ImmutableTypeA".to_string(),
],
},
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)

View File

@@ -7,7 +7,9 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::call_path::{compose_call_path, from_qualified_name, CallPath};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_func};
use ruff_python_semantic::analyze::typing::{
is_immutable_annotation, is_immutable_func, is_mutable_func,
};
use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
@@ -20,6 +22,13 @@ use crate::checkers::ast::Checker;
/// once, at definition time. The returned value will then be reused by all
/// calls to the function, which can lead to unexpected behaviour.
///
/// Calls can be marked as an exception to this rule with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
///
/// Arguments with immutable type annotations will be ignored by this rule.
/// Types outside of the standard library can be marked as immutable with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option as well.
///
/// ## Example
/// ```python
/// def create_list() -> list[int]:
@@ -60,14 +69,14 @@ impl Violation for FunctionCallInDefaultArgument {
}
}
struct ArgumentDefaultVisitor<'a> {
semantic: &'a SemanticModel<'a>,
extend_immutable_calls: Vec<CallPath<'a>>,
struct ArgumentDefaultVisitor<'a, 'b> {
semantic: &'a SemanticModel<'b>,
extend_immutable_calls: &'a [CallPath<'b>],
diagnostics: Vec<(DiagnosticKind, TextRange)>,
}
impl<'a> ArgumentDefaultVisitor<'a> {
fn new(semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
impl<'a, 'b> ArgumentDefaultVisitor<'a, 'b> {
fn new(semantic: &'a SemanticModel<'b>, extend_immutable_calls: &'a [CallPath<'b>]) -> Self {
Self {
semantic,
extend_immutable_calls,
@@ -76,15 +85,12 @@ impl<'a> ArgumentDefaultVisitor<'a> {
}
}
impl<'a, 'b> Visitor<'b> for ArgumentDefaultVisitor<'b>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
impl Visitor<'_> for ArgumentDefaultVisitor<'_, '_> {
fn visit_expr(&mut self, expr: &Expr) {
match expr {
Expr::Call(ast::ExprCall { func, .. }) => {
if !is_mutable_func(func, self.semantic)
&& !is_immutable_func(func, self.semantic, &self.extend_immutable_calls)
&& !is_immutable_func(func, self.semantic, self.extend_immutable_calls)
{
self.diagnostics.push((
FunctionCallInDefaultArgument {
@@ -114,25 +120,28 @@ pub(crate) fn function_call_in_argument_default(checker: &mut Checker, parameter
.iter()
.map(|target| from_qualified_name(target))
.collect();
let diagnostics = {
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), extend_immutable_calls);
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
let mut visitor = ArgumentDefaultVisitor::new(checker.semantic(), &extend_immutable_calls);
for ParameterWithDefault {
default,
parameter,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
if !parameter.annotation.as_ref().is_some_and(|expr| {
is_immutable_annotation(expr, checker.semantic(), &extend_immutable_calls)
}) {
visitor.visit_expr(expr);
}
}
visitor.diagnostics
};
for (check, range) in diagnostics {
}
for (check, range) in visitor.diagnostics {
checker.diagnostics.push(Diagnostic::new(check, range));
}
}

View File

@@ -1,3 +1,4 @@
use ast::call_path::{from_qualified_name, CallPath};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
@@ -25,6 +26,10 @@ use crate::registry::AsRule;
/// default, and initialize a new mutable object inside the function body
/// for each call.
///
/// Arguments with immutable type annotations will be ignored by this rule.
/// Types outside of the standard library can be marked as immutable with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
///
/// ## Example
/// ```python
/// def add_to_list(item, some_list=[]):
@@ -49,6 +54,9 @@ use crate::registry::AsRule;
/// l2 = add_to_list(1) # [1]
/// ```
///
/// ## Options
/// - `flake8-bugbear.extend-immutable-calls`
///
/// ## References
/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
#[violation]
@@ -84,11 +92,18 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast
continue;
};
let extend_immutable_calls: Vec<CallPath> = checker
.settings
.flake8_bugbear
.extend_immutable_calls
.iter()
.map(|target| from_qualified_name(target))
.collect();
if is_mutable_expr(default, checker.semantic())
&& !parameter
.annotation
.as_ref()
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
&& !parameter.annotation.as_ref().is_some_and(|expr| {
is_immutable_annotation(expr, checker.semantic(), extend_immutable_calls.as_slice())
})
{
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
@@ -125,7 +140,7 @@ fn move_initialization(
let mut body = function_def.body.iter();
let statement = body.next()?;
if indexer.preceded_by_multi_statement_line(statement, locator) {
if indexer.in_multi_statement_line(statement, locator) {
return None;
}
@@ -155,7 +170,7 @@ fn move_initialization(
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) {
if indexer.in_multi_statement_line(statement, locator) {
return None;
}
Edit::insertion(content, locator.line_start(statement.start()))

View File

@@ -79,7 +79,6 @@ pub(crate) fn redundant_tuple_in_exception_handler(
type_.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.generator().expr(elt),
type_.range(),

View File

@@ -258,161 +258,139 @@ B006_B008.py:114:33: B006 [*] Do not use mutable data structures for argument de
116 118 |
117 119 |
B006_B008.py:235:20: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:239:20: B006 [*] Do not use mutable data structures for argument defaults
|
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B006
236 | pass
240 | pass
|
= help: Replace with `None`; initialize within function
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 |
236 236 |
237 237 | # B006 and B008
238 238 | # We should handle arbitrary nesting of these B008.
239 |-def nested_combo(a=[float(3), dt.datetime.now()]):
239 |+def nested_combo(a=None):
240 |+ if a is None:
241 |+ a = [float(3), dt.datetime.now()]
240 242 | pass
241 243 |
242 244 |
B006_B008.py:272:27: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:276:27: B006 [*] Do not use mutable data structures for argument defaults
|
271 | def mutable_annotations(
272 | a: list[int] | None = [],
275 | def mutable_annotations(
276 | a: list[int] | None = [],
| ^^ B006
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
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
|
271 | def mutable_annotations(
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
| ^^ B006
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
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
|
272 | a: list[int] | None = [],
273 | b: Optional[Dict[int, int]] = {},
274 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
275 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
276 | ):
|
= help: Replace with `None`; initialize within function
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
|
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
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 = {}
273 273 |
274 274 |
275 275 | def mutable_annotations(
276 |- a: list[int] | None = [],
276 |+ a: list[int] | None = None,
277 277 | b: Optional[Dict[int, int]] = {},
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if a is None:
282 |+ a = []
281 283 | pass
282 284 |
283 285 |
B006_B008.py:277:35: B006 [*] Do not use mutable data structures for argument defaults
|
275 | def mutable_annotations(
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
| ^^ B006
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
= help: Replace with `None`; initialize within function
Possible fix
274 274 |
275 275 | def mutable_annotations(
276 276 | a: list[int] | None = [],
277 |- b: Optional[Dict[int, int]] = {},
277 |+ b: Optional[Dict[int, int]] = None,
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if b is None:
282 |+ b = {}
281 283 | pass
282 284 |
283 285 |
B006_B008.py:278:62: B006 [*] Do not use mutable data structures for argument defaults
|
276 | a: list[int] | None = [],
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 | ):
|
= help: Replace with `None`; initialize within function
Possible fix
275 275 | def mutable_annotations(
276 276 | a: list[int] | None = [],
277 277 | b: Optional[Dict[int, int]] = {},
278 |- c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
278 |+ c: Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
279 279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
280 280 | ):
281 |+ if c is None:
282 |+ c = set()
281 283 | pass
282 284 |
283 285 |
B006_B008.py:279:80: B006 [*] Do not use mutable data structures for argument defaults
|
277 | b: Optional[Dict[int, int]] = {},
278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
280 | ):
281 | pass
|
= help: Replace with `None`; initialize within function
Possible fix
276 276 | a: list[int] | None = [],
277 277 | b: Optional[Dict[int, int]] = {},
278 278 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 |- d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
279 |+ d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = None,
280 280 | ):
281 |+ if d is None:
282 |+ d = set()
281 283 | pass
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"""
281 281 | pass
282 282 |
283 283 |
284 |-def single_line_func_wrong(value: dict[str, str] = {}):
@@ -420,59 +398,81 @@ B006_B008.py:284:52: B006 [*] Do not use mutable data structures for argument de
285 285 | """Docstring"""
286 |+ if value is None:
287 |+ value = {}
286 288 | ...
286 288 |
287 289 |
288 290 |
288 290 | def single_line_func_wrong(value: dict[str, str] = {}):
B006_B008.py:289:52: B006 Do not use mutable data structures for argument defaults
B006_B008.py:288:52: B006 [*] Do not use mutable data structures for argument defaults
|
289 | def single_line_func_wrong(value: dict[str, str] = {}):
288 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
290 | """Docstring"""; ...
289 | """Docstring"""
290 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
285 285 | """Docstring"""
286 286 |
287 287 |
288 |-def single_line_func_wrong(value: dict[str, str] = {}):
288 |+def single_line_func_wrong(value: dict[str, str] = None):
289 289 | """Docstring"""
290 |+ if value is None:
291 |+ value = {}
290 292 | ...
291 293 |
292 294 |
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 | ...
294 | """Docstring"""; ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:298:52: B006 [*] Do not use mutable data structures for argument defaults
B006_B008.py:297:52: B006 Do not use mutable data structures for argument defaults
|
298 | def single_line_func_wrong(value: dict[str, str] = {
297 | def single_line_func_wrong(value: dict[str, str] = {}):
| ^^ B006
298 | """Docstring"""; \
299 | ...
|
= help: Replace with `None`; initialize within function
B006_B008.py:302:52: B006 [*] Do not use mutable data structures for argument defaults
|
302 | def single_line_func_wrong(value: dict[str, str] = {
| ____________________________________________________^
299 | | # This is a comment
300 | | }):
303 | | # This is a comment
304 | | }):
| |_^ B006
301 | """Docstring"""
305 | """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] = {}) \
299 299 | ...
300 300 |
301 301 |
302 |-def single_line_func_wrong(value: dict[str, str] = {
303 |- # This is a comment
304 |-}):
302 |+def single_line_func_wrong(value: dict[str, str] = None):
305 303 | """Docstring"""
304 |+ if value is None:
305 |+ value = {}
306 306 |
307 307 |
308 308 | def single_line_func_wrong(value: dict[str, str] = {}) \
B006_B008.py:304:52: B006 Do not use mutable data structures for argument defaults
B006_B008.py:308:52: B006 Do not use mutable data structures for argument defaults
|
304 | def single_line_func_wrong(value: dict[str, str] = {}) \
308 | def single_line_func_wrong(value: dict[str, str] = {}) \
| ^^ B006
305 | : \
306 | """Docstring"""
309 | : \
310 | """Docstring"""
|
= help: Replace with `None`; initialize within function

View File

@@ -46,38 +46,38 @@ B006_B008.py:134:30: B008 Do not perform function call in argument defaults
135 | ...
|
B006_B008.py:235:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:239:31: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
233 | # B006 and B008
234 | # We should handle arbitrary nesting of these B008.
235 | def nested_combo(a=[float(3), dt.datetime.now()]):
237 | # B006 and B008
238 | # We should handle arbitrary nesting of these B008.
239 | def nested_combo(a=[float(3), dt.datetime.now()]):
| ^^^^^^^^^^^^^^^^^ B008
236 | pass
240 | pass
|
B006_B008.py:241:22: B008 Do not perform function call `map` in argument defaults
B006_B008.py:245:22: B008 Do not perform function call `map` in argument defaults
|
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"])):
243 | # Don't flag nested B006 since we can't guarantee that
244 | # it isn't made mutable by the outer operation.
245 | def no_nested_b006(a=map(lambda s: s.upper(), ["a", "b", "c"])):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
242 | pass
246 | pass
|
B006_B008.py:246:19: B008 Do not perform function call `random.randint` in argument defaults
B006_B008.py:250:19: B008 Do not perform function call `random.randint` in argument defaults
|
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B008
247 | pass
251 | pass
|
B006_B008.py:246:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
B006_B008.py:250:37: B008 Do not perform function call `dt.datetime.now` in argument defaults
|
245 | # B008-ception.
246 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
249 | # B008-ception.
250 | def nested_b008(a=random.randint(0, dt.datetime.now().year)):
| ^^^^^^^^^^^^^^^^^ B008
247 | pass
251 | pass
|

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_extended.py:17:55: B006 [*] Do not use mutable data structures for argument defaults
|
17 | def error_due_to_missing_import(foo: ImmutableTypeA = []):
| ^^ B006
18 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
14 14 | ...
15 15 |
16 16 |
17 |-def error_due_to_missing_import(foo: ImmutableTypeA = []):
17 |+def error_due_to_missing_import(foo: ImmutableTypeA = None):
18 |+ if foo is None:
19 |+ foo = []
18 20 | ...

View File

@@ -1,11 +1,11 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B008_extended.py:19:51: B008 Do not perform function call `Depends` in argument defaults
B008_extended.py:24:51: B008 Do not perform function call `Depends` in argument defaults
|
19 | def error_due_to_missing_import(data: List[str] = Depends(None)):
24 | def error_due_to_missing_import(data: List[str] = Depends(None)):
| ^^^^^^^^^^^^^ B008
20 | ...
25 | ...
|

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword, Ranged};
@@ -86,8 +86,9 @@ pub(crate) fn unnecessary_collection_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_collection_call(checker, expr));
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
@@ -63,9 +63,9 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) {
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableKeyword;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
@@ -130,13 +130,13 @@ pub(crate) fn unnecessary_double_cast_or_process(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_double_cast_or_process(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -59,9 +58,8 @@ pub(crate) fn unnecessary_generator_dict(
Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.len() == 2 => {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_generator_dict(checker, expr)
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_dict(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_list(
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_list(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_set(
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic
.try_set_fix_from_edit(|| fixes::fix_unnecessary_generator_set(checker, expr));
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_set(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -56,9 +56,9 @@ pub(crate) fn unnecessary_list_call(
}
let mut diagnostic = Diagnostic::new(UnnecessaryListCall, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_call(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -66,9 +65,8 @@ pub(crate) fn unnecessary_list_comprehension_dict(
}
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_comprehension_dict(checker, expr)
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_dict(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -58,9 +58,8 @@ pub(crate) fn unnecessary_list_comprehension_set(
if argument.is_list_comp_expr() {
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_comprehension_set(checker, expr)
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_set(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -81,8 +80,8 @@ pub(crate) fn unnecessary_literal_dict(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_dict(checker, expr));
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_dict(checker, expr).map(Fix::suggested));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -75,8 +75,8 @@ pub(crate) fn unnecessary_literal_set(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_set(checker, expr));
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_set(checker, expr).map(Fix::suggested));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -2,7 +2,7 @@ use std::fmt;
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -91,13 +91,13 @@ pub(crate) fn unnecessary_literal_within_dict_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_dict_call(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -94,13 +93,13 @@ pub(crate) fn unnecessary_literal_within_list_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_list_call(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -95,13 +95,13 @@ pub(crate) fn unnecessary_literal_within_tuple_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_tuple_call(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -103,10 +103,17 @@ pub(crate) fn unnecessary_map(
return;
};
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
if parameters.as_ref().is_some_and(|parameters| {
late_binding(parameters, body)
|| parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.any(|param| param.default.is_some())
|| parameters.vararg.is_some()
|| parameters.kwarg.is_some()
}) {
return;
}
}
@@ -137,10 +144,17 @@ pub(crate) fn unnecessary_map(
return;
};
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
if parameters.as_ref().is_some_and(|parameters| {
late_binding(parameters, body)
|| parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.any(|param| param.default.is_some())
|| parameters.vararg.is_some()
|| parameters.kwarg.is_some()
}) {
return;
}
}
@@ -177,14 +191,21 @@ pub(crate) fn unnecessary_map(
return;
}
if parameters
.as_ref()
.is_some_and(|parameters| late_binding(parameters, body))
{
if parameters.as_ref().is_some_and(|parameters| {
late_binding(parameters, body)
|| parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.any(|param| param.default.is_some())
|| parameters.vararg.is_some()
|| parameters.kwarg.is_some()
}) {
return;
}
}
}
};
let mut diagnostic = Diagnostic::new(UnnecessaryMap { object_type }, expr.range());
if checker.patch(diagnostic.kind.rule()) {

View File

@@ -226,7 +226,7 @@ C417.py:15:8: C417 [*] Unnecessary `map` usage (rewrite using a `set` comprehens
15 |+_ = f"{ {x % 2 == 0 for x in nums} }"
16 16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
17 17 |
18 18 | # Error, but unfixable.
18 18 | # False negatives.
C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehension)
|
@@ -235,7 +235,7 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen
16 | _ = f"{dict(map(lambda v: (v, v**2), nums))}"
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
17 |
18 | # Error, but unfixable.
18 | # False negatives.
|
= help: Replace `map` with a `dict` comprehension
@@ -246,33 +246,27 @@ C417.py:16:8: C417 [*] Unnecessary `map` usage (rewrite using a `dict` comprehen
16 |-_ = f"{dict(map(lambda v: (v, v**2), nums))}"
16 |+_ = f"{ {v: v**2 for v in nums} }"
17 17 |
18 18 | # Error, but unfixable.
19 19 | # For simple expressions, this could be: `(x if x else 1 for x in nums)`.
18 18 | # False negatives.
19 19 | map(lambda x=2, y=1: x + y, nums, nums)
C417.py:21:1: C417 Unnecessary `map` usage (rewrite using a generator expression)
C417.py:34:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
19 | # For simple expressions, this could be: `(x if x else 1 for x in nums)`.
20 | # For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
21 | map(lambda x=1: x, nums)
| ^^^^^^^^^^^^^^^^^^^^^^^^ C417
22 |
23 | # False negatives.
|
= help: Replace `map` with a generator expression
C417.py:39:1: C417 [*] Unnecessary `map` usage (rewrite using a generator expression)
|
38 | # Error: the `x` is overridden by the inner lambda.
39 | map(lambda x: lambda x: x, range(4))
33 | # Error: the `x` is overridden by the inner lambda.
34 | map(lambda x: lambda x: x, range(4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C417
35 |
36 | # Ok because of the default parameters, and variadic arguments.
|
= help: Replace `map` with a generator expression
Suggested fix
36 36 | map(lambda x: lambda: x, range(4))
37 37 |
38 38 | # Error: the `x` is overridden by the inner lambda.
39 |-map(lambda x: lambda x: x, range(4))
39 |+(lambda x: x for x in range(4))
31 31 | map(lambda x: lambda: x, range(4))
32 32 |
33 33 | # Error: the `x` is overridden by the inner lambda.
34 |-map(lambda x: lambda x: x, range(4))
34 |+(lambda x: x for x in range(4))
35 35 |
36 36 | # Ok because of the default parameters, and variadic arguments.
37 37 | map(lambda x=1: x, nums)

View File

@@ -5,6 +5,49 @@ use ruff_python_ast::{self as ast, Constant, Expr, Ranged};
use crate::checkers::ast::Checker;
use crate::rules::flake8_datetimez::rules::helpers::has_non_none_keyword;
/// ## What it does
/// Checks for uses of `datetime.datetime.strptime()` that lead to naive
/// datetime objects.
///
/// ## 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.strptime()` without `%z` returns a naive datetime
/// object. Follow it with `.replace(tzinfo=)` or `.astimezone()`.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d")
/// ```
///
/// Instead, use `.replace(tzinfo=)`:
/// ```python
/// import datetime
///
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d").replace(
/// tzinfo=datetime.timezone.utc
/// )
/// ```
///
/// Or, use `.astimezone()`:
/// ```python
/// import datetime
///
/// datetime.datetime.strptime("2022/01/31", "%Y/%m/%d").astimezone(datetime.timezone.utc)
/// ```
///
/// On Python 3.11 and later, `datetime.timezone.utc` can be replaced with
/// `datetime.UTC`.
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
/// - [Python documentation: `strftime()` and `strptime()` Behavior](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior)
#[violation]
pub struct CallDatetimeStrptimeWithoutZone;

View File

@@ -126,8 +126,8 @@ pub(crate) fn implicit(
}
fn concatenate_strings(a_range: TextRange, b_range: TextRange, locator: &Locator) -> Option<Fix> {
let a_text = &locator.contents()[a_range];
let b_text = &locator.contents()[b_range];
let a_text = locator.slice(a_range);
let b_text = locator.slice(b_range);
let a_leading_quote = leading_quote(a_text)?;
let b_leading_quote = leading_quote(b_text)?;

View File

@@ -9,6 +9,7 @@ const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
("altair", "alt"),
("matplotlib", "mpl"),
("matplotlib.pyplot", "plt"),
("networkx", "nx"),
("numpy", "np"),
("pandas", "pd"),
("seaborn", "sns"),

View File

@@ -72,7 +72,7 @@ defaults.py:9:12: ICN001 [*] `pandas` should be imported as `pd`
9 |+ import pandas as pd
10 10 | import seaborn
11 11 | import tkinter
12 12 |
12 12 | import networkx
defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
|
@@ -81,6 +81,7 @@ defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
10 | import seaborn
| ^^^^^^^ ICN001
11 | import tkinter
12 | import networkx
|
= help: Alias `seaborn` to `sns`
@@ -91,7 +92,7 @@ defaults.py:10:12: ICN001 [*] `seaborn` should be imported as `sns`
10 |- import seaborn
10 |+ import seaborn as sns
11 11 | import tkinter
12 12 |
12 12 | import networkx
13 13 |
defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
@@ -100,6 +101,7 @@ defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
10 | import seaborn
11 | import tkinter
| ^^^^^^^ ICN001
12 | import networkx
|
= help: Alias `tkinter` to `tk`
@@ -109,130 +111,172 @@ defaults.py:11:12: ICN001 [*] `tkinter` should be imported as `tk`
10 10 | import seaborn
11 |- import tkinter
11 |+ import tkinter as tk
12 12 |
12 12 | import networkx
13 13 |
14 14 | def unconventional_aliases():
14 14 |
defaults.py:15:22: ICN001 [*] `altair` should be imported as `alt`
defaults.py:12:12: ICN001 [*] `networkx` should be imported as `nx`
|
14 | def unconventional_aliases():
15 | import altair as altr
10 | import seaborn
11 | import tkinter
12 | import networkx
| ^^^^^^^^ ICN001
|
= help: Alias `networkx` to `nx`
Suggested fix
9 9 | import pandas
10 10 | import seaborn
11 11 | import tkinter
12 |- import networkx
12 |+ import networkx as nx
13 13 |
14 14 |
15 15 | def unconventional_aliases():
defaults.py:16:22: ICN001 [*] `altair` should be imported as `alt`
|
15 | def unconventional_aliases():
16 | import altair as altr
| ^^^^ ICN001
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
17 | import matplotlib.pyplot as plot
18 | import numpy as nmp
|
= help: Alias `altair` to `alt`
Suggested fix
12 12 |
13 13 |
14 14 | def unconventional_aliases():
15 |- import altair as altr
15 |+ import altair as alt
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
14 14 |
15 15 | def unconventional_aliases():
16 |- import altair as altr
16 |+ import altair as alt
17 17 | import matplotlib.pyplot as plot
18 18 | import numpy as nmp
19 19 | import pandas as pdas
defaults.py:16:33: ICN001 [*] `matplotlib.pyplot` should be imported as `plt`
defaults.py:17:33: ICN001 [*] `matplotlib.pyplot` should be imported as `plt`
|
14 | def unconventional_aliases():
15 | import altair as altr
16 | import matplotlib.pyplot as plot
15 | def unconventional_aliases():
16 | import altair as altr
17 | import matplotlib.pyplot as plot
| ^^^^ ICN001
17 | import numpy as nmp
18 | import pandas as pdas
18 | import numpy as nmp
19 | import pandas as pdas
|
= help: Alias `matplotlib.pyplot` to `plt`
Suggested fix
13 13 |
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 |- import matplotlib.pyplot as plot
16 |+ import matplotlib.pyplot as plt
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
14 14 |
15 15 | def unconventional_aliases():
16 16 | import altair as altr
17 |- import matplotlib.pyplot as plot
17 |+ import matplotlib.pyplot as plt
18 18 | import numpy as nmp
19 19 | import pandas as pdas
20 20 | import seaborn as sbrn
defaults.py:17:21: ICN001 [*] `numpy` should be imported as `np`
defaults.py:18:21: ICN001 [*] `numpy` should be imported as `np`
|
15 | import altair as altr
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
16 | import altair as altr
17 | import matplotlib.pyplot as plot
18 | import numpy as nmp
| ^^^ ICN001
18 | import pandas as pdas
19 | import seaborn as sbrn
19 | import pandas as pdas
20 | import seaborn as sbrn
|
= help: Alias `numpy` to `np`
Suggested fix
14 14 | def unconventional_aliases():
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 |- import numpy as nmp
17 |+ import numpy as np
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
15 15 | def unconventional_aliases():
16 16 | import altair as altr
17 17 | import matplotlib.pyplot as plot
18 |- import numpy as nmp
18 |+ import numpy as np
19 19 | import pandas as pdas
20 20 | import seaborn as sbrn
21 21 | import tkinter as tkr
defaults.py:18:22: ICN001 [*] `pandas` should be imported as `pd`
defaults.py:19:22: ICN001 [*] `pandas` should be imported as `pd`
|
16 | import matplotlib.pyplot as plot
17 | import numpy as nmp
18 | import pandas as pdas
17 | import matplotlib.pyplot as plot
18 | import numpy as nmp
19 | import pandas as pdas
| ^^^^ ICN001
19 | import seaborn as sbrn
20 | import tkinter as tkr
20 | import seaborn as sbrn
21 | import tkinter as tkr
|
= help: Alias `pandas` to `pd`
Suggested fix
15 15 | import altair as altr
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 |- import pandas as pdas
18 |+ import pandas as pd
19 19 | import seaborn as sbrn
20 20 | import tkinter as tkr
21 21 |
16 16 | import altair as altr
17 17 | import matplotlib.pyplot as plot
18 18 | import numpy as nmp
19 |- import pandas as pdas
19 |+ import pandas as pd
20 20 | import seaborn as sbrn
21 21 | import tkinter as tkr
22 22 | import networkx as nxy
defaults.py:19:23: ICN001 [*] `seaborn` should be imported as `sns`
defaults.py:20:23: ICN001 [*] `seaborn` should be imported as `sns`
|
17 | import numpy as nmp
18 | import pandas as pdas
19 | import seaborn as sbrn
18 | import numpy as nmp
19 | import pandas as pdas
20 | import seaborn as sbrn
| ^^^^ ICN001
20 | import tkinter as tkr
21 | import tkinter as tkr
22 | import networkx as nxy
|
= help: Alias `seaborn` to `sns`
Suggested fix
16 16 | import matplotlib.pyplot as plot
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 |- import seaborn as sbrn
19 |+ import seaborn as sns
20 20 | import tkinter as tkr
21 21 |
22 22 |
17 17 | import matplotlib.pyplot as plot
18 18 | import numpy as nmp
19 19 | import pandas as pdas
20 |- import seaborn as sbrn
20 |+ import seaborn as sns
21 21 | import tkinter as tkr
22 22 | import networkx as nxy
23 23 |
defaults.py:20:23: ICN001 [*] `tkinter` should be imported as `tk`
defaults.py:21:23: ICN001 [*] `tkinter` should be imported as `tk`
|
18 | import pandas as pdas
19 | import seaborn as sbrn
20 | import tkinter as tkr
19 | import pandas as pdas
20 | import seaborn as sbrn
21 | import tkinter as tkr
| ^^^ ICN001
22 | import networkx as nxy
|
= help: Alias `tkinter` to `tk`
Suggested fix
17 17 | import numpy as nmp
18 18 | import pandas as pdas
19 19 | import seaborn as sbrn
20 |- import tkinter as tkr
20 |+ import tkinter as tk
21 21 |
22 22 |
23 23 | def conventional_aliases():
18 18 | import numpy as nmp
19 19 | import pandas as pdas
20 20 | import seaborn as sbrn
21 |- import tkinter as tkr
21 |+ import tkinter as tk
22 22 | import networkx as nxy
23 23 |
24 24 | def conventional_aliases():
defaults.py:22:24: ICN001 [*] `networkx` should be imported as `nx`
|
20 | import seaborn as sbrn
21 | import tkinter as tkr
22 | import networkx as nxy
| ^^^ ICN001
23 |
24 | def conventional_aliases():
|
= help: Alias `networkx` to `nx`
Suggested fix
19 19 | import pandas as pdas
20 20 | import seaborn as sbrn
21 21 | import tkinter as tkr
22 |- import networkx as nxy
22 |+ import networkx as nx
23 23 |
24 24 | def conventional_aliases():
25 25 | import altair as alt

View File

@@ -21,6 +21,19 @@ use ruff_macros::{derive_message_formats, violation};
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -54,6 +67,9 @@ use ruff_macros::{derive_message_formats, violation};
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -89,6 +105,19 @@ impl Violation for LoggingStringFormat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -122,6 +151,9 @@ impl Violation for LoggingStringFormat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -156,6 +188,19 @@ impl Violation for LoggingPercentFormat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -189,6 +234,9 @@ impl Violation for LoggingPercentFormat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -222,6 +270,19 @@ impl Violation for LoggingStringConcat {
/// As an alternative to `extra`, passing values as arguments to the logging
/// method can also be used to defer string formatting until required.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -255,6 +316,9 @@ impl Violation for LoggingStringConcat {
/// logging.info("%s - Something happened", user)
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging`](https://docs.python.org/3/library/logging.html)
/// - [Python documentation: Optimization](https://docs.python.org/3/howto/logging.html#optimization)
@@ -276,6 +340,19 @@ impl Violation for LoggingFString {
/// `logging.warning` and `logging.Logger.warning`, which are functionally
/// equivalent.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -290,6 +367,9 @@ impl Violation for LoggingFString {
/// logging.warning("Something happened")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.warning`](https://docs.python.org/3/library/logging.html#logging.warning)
/// - [Python documentation: `logging.Logger.warning`](https://docs.python.org/3/library/logging.html#logging.Logger.warning)
@@ -320,6 +400,19 @@ impl AlwaysAutofixableViolation for LoggingWarn {
/// the `LogRecord` constructor will raise a `KeyError` when the `LogRecord` is
/// constructed.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -342,6 +435,9 @@ impl AlwaysAutofixableViolation for LoggingWarn {
/// logging.info("Something happened", extra=dict(user=username))
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: LogRecord attributes](https://docs.python.org/3/library/logging.html#logrecord-attributes)
#[violation]
@@ -365,6 +461,19 @@ impl Violation for LoggingExtraAttrClash {
/// `logging.exception`. Using `logging.exception` is more concise, more
/// readable, and conveys the intent of the logging statement more clearly.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -385,6 +494,9 @@ impl Violation for LoggingExtraAttrClash {
/// logging.exception("Exception occurred")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)
@@ -410,6 +522,19 @@ impl Violation for LoggingExcInfo {
/// Passing `exc_info=True` to `logging.exception` calls is redundant, as is
/// passing `exc_info=False` to `logging.error` calls.
///
/// ## Known problems
///
/// This rule detects uses of the `logging` module via a heuristic.
/// Specifically, it matches against:
///
/// - Uses of the `logging` module itself (e.g., `import logging; logging.info(...)`).
/// - Uses of `flask.current_app.logger` (e.g., `from flask import current_app; current_app.logger.info(...)`).
/// - Objects whose name starts with `log` or ends with `logger` or `logging`,
/// when used in the same file in which they are defined (e.g., `logger = logging.getLogger(); logger.info(...)`).
/// - Imported objects marked as loggers via the [`logger-objects`] setting, which can be
/// used to enforce these rules against shared logger objects (e.g., `from module import logger; logger.info(...)`,
/// when [`logger-objects`] is set to `["module.logger"]`).
///
/// ## Example
/// ```python
/// import logging
@@ -430,6 +555,9 @@ impl Violation for LoggingExcInfo {
/// logging.exception("Exception occurred")
/// ```
///
/// ## Options
/// - `logger-objects`
///
/// ## References
/// - [Python documentation: `logging.exception`](https://docs.python.org/3/library/logging.html#logging.exception)
/// - [Python documentation: `exception`](https://docs.python.org/3/library/logging.html#logging.Logger.exception)

View File

@@ -85,7 +85,9 @@ pub(crate) fn duplicate_class_field_definition(checker: &mut Checker, body: &[St
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::suggested(edit).isolate(checker.statement_isolation()));
diagnostic.set_fix(Fix::suggested(edit).isolate(Checker::isolation(Some(
checker.semantic().current_statement_id(),
))));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -50,12 +50,12 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
// parent without the duplicate.
// If the parent node is not a `BinOp` we will not perform a fix
if let Some(Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
if let Some(parent @ Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
// Replace the parent with its non-duplicate child.
let child = if expr == left.as_ref() { right } else { left };
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.locator().slice(child.range()).to_string(),
parent.unwrap().range(),
parent.range(),
)));
}
}

View File

@@ -69,7 +69,9 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &mut Checker, body: &[St
checker.locator(),
checker.indexer(),
);
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(Some(
checker.semantic().current_statement_id(),
))));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -36,7 +36,9 @@ pub(crate) fn pass_in_class_body(checker: &mut Checker, class_def: &ast::StmtCla
if checker.patch(diagnostic.kind.rule()) {
let edit =
autofix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.statement_isolation()));
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(Some(
checker.semantic().current_statement_id(),
))));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -99,7 +99,9 @@ pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) {
let stmt = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let edit = delete_stmt(stmt, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.parent_isolation()));
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,7 +1,7 @@
use std::borrow::Cow;
use anyhow::bail;
use anyhow::Result;
use anyhow::{bail, Context};
use libcst_native::{
self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace,
ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement,
@@ -635,9 +635,8 @@ fn parenthesize<'a>(expression: Expression<'a>, parent: &Expression<'a>) -> Expr
/// `assert a == "hello"` and `assert b == "world"`.
fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Result<Edit> {
// Infer the indentation of the outer block.
let Some(outer_indent) = whitespace::indentation(locator, stmt) else {
bail!("Unable to fix multiline statement");
};
let outer_indent =
whitespace::indentation(locator, stmt).context("Unable to fix multiline statement")?;
// Extract the module text.
let contents = locator.lines(stmt.range());
@@ -672,11 +671,11 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) ->
&mut indented_block.body
};
let [Statement::Simple(simple_statement_line)] = &statements[..] else {
let [Statement::Simple(simple_statement_line)] = statements.as_slice() else {
bail!("Expected one simple statement")
};
let [SmallStatement::Assert(assert_statement)] = &simple_statement_line.body[..] else {
let [SmallStatement::Assert(assert_statement)] = simple_statement_line.body.as_slice() else {
bail!("Expected simple statement to be an assert")
};
@@ -754,10 +753,13 @@ pub(crate) fn composite_condition(
if matches!(composite, CompositionKind::Simple)
&& msg.is_none()
&& !checker.indexer().comment_ranges().intersects(stmt.range())
&& !checker
.indexer()
.in_multi_statement_line(stmt, checker.locator())
{
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fix_composite_condition(stmt, checker.locator(), checker.stylist())
.map(Fix::suggested)
});
}
}

View File

@@ -1,18 +1,16 @@
use rustc_hash::FxHashMap;
use std::hash::BuildHasherDefault;
use ruff_python_ast::{
self as ast, Arguments, Constant, Decorator, Expr, ExprContext, PySourceType, Ranged,
};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::node::AstNode;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, Constant, Decorator, Expr, ExprContext, Ranged};
use ruff_python_codegen::Generator;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use ruff_text_size::{TextRange, TextSize};
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
@@ -74,7 +72,7 @@ use super::helpers::{is_pytest_parametrize, split_names};
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
#[violation]
pub struct PytestParametrizeNamesWrongType {
pub expected: types::ParametrizeNameType,
expected: types::ParametrizeNameType,
}
impl Violation for PytestParametrizeNamesWrongType {
@@ -159,8 +157,8 @@ impl Violation for PytestParametrizeNamesWrongType {
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
#[violation]
pub struct PytestParametrizeValuesWrongType {
pub values: types::ParametrizeValuesType,
pub row: types::ParametrizeValuesRowType,
values: types::ParametrizeValuesType,
row: types::ParametrizeValuesRowType,
}
impl Violation for PytestParametrizeValuesWrongType {
@@ -283,34 +281,12 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
fn get_parametrize_name_range(
decorator: &Decorator,
expr: &Expr,
locator: &Locator,
source_type: PySourceType,
) -> TextRange {
let mut locations = Vec::new();
let mut name_range = None;
// The parenthesis are not part of the AST, so we need to tokenize the
// decorator to find them.
for (tok, range) in lexer::lex_starts_at(
locator.slice(decorator.range()),
source_type.as_mode(),
decorator.start(),
)
.flatten()
{
match tok {
Tok::Lpar => locations.push(range.start()),
Tok::Rpar => {
if let Some(start) = locations.pop() {
name_range = Some(TextRange::new(start, range.end()));
}
}
// Stop after the first argument.
Tok::Comma => break,
_ => (),
}
}
name_range.unwrap_or_else(|| expr.range())
source: &str,
) -> Option<TextRange> {
decorator
.expression
.as_call_expr()
.and_then(|call| parenthesized_range(expr.into(), call.arguments.as_any_node_ref(), source))
}
/// PT006
@@ -329,9 +305,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.locator(),
checker.source_type,
);
checker.locator().contents(),
)
.unwrap_or(expr.range());
let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType {
expected: names_type,
@@ -364,9 +340,9 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.locator(),
checker.source_type,
);
checker.locator().contents(),
)
.unwrap_or(expr.range());
let mut diagnostic = Diagnostic::new(
PytestParametrizeNamesWrongType {
expected: names_type,

View File

@@ -299,6 +299,8 @@ PT018.py:44:1: PT018 [*] Assertion should be broken down into multiple parts
44 |+assert something
45 |+assert something_else
45 46 | assert something and something_else and something_third # Error
46 47 |
47 48 |
PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts
|
@@ -316,5 +318,37 @@ PT018.py:45:1: PT018 [*] Assertion should be broken down into multiple parts
45 |-assert something and something_else and something_third # Error
45 |+assert something and something_else
46 |+assert something_third
46 47 |
47 48 |
48 49 | def test_multiline():
PT018.py:49:5: PT018 Assertion should be broken down into multiple parts
|
48 | def test_multiline():
49 | assert something and something_else; x = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
50 |
51 | x = 1; assert something and something_else
|
= help: Break down assertion into multiple parts
PT018.py:51:12: PT018 Assertion should be broken down into multiple parts
|
49 | assert something and something_else; x = 1
50 |
51 | x = 1; assert something and something_else
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
52 |
53 | x = 1; \
|
= help: Break down assertion into multiple parts
PT018.py:54:9: PT018 Assertion should be broken down into multiple parts
|
53 | x = 1; \
54 | assert something and something_else
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PT018
|
= help: Break down assertion into multiple parts

View File

@@ -25,19 +25,11 @@ pub(super) fn result_exists(returns: &[&ast::StmtReturn]) -> bool {
/// This method assumes that the statement is the last statement in its body; specifically, that
/// the statement isn't followed by a semicolon, followed by a multi-line statement.
pub(super) fn end_of_last_statement(stmt: &Stmt, locator: &Locator) -> TextSize {
if stmt.end() == locator.text_len() {
// End-of-file, so just return the end of the statement.
stmt.end()
} else {
// Otherwise, find the end of the last line that's "part of" the statement.
let contents = locator.after(stmt.end());
for line in contents.universal_newlines() {
if !line.ends_with('\\') {
return stmt.end() + line.end();
}
// Find the end of the last line that's "part of" the statement.
for line in locator.after(stmt.end()).universal_newlines() {
if !line.ends_with('\\') {
return stmt.end() + line.end();
}
unreachable!("Expected to find end-of-statement")
}
locator.text_len()
}

View File

@@ -380,5 +380,24 @@ RET503.py:320:9: RET503 [*] Missing explicit `return` at the end of function abl
321 321 | return "" \
322 322 | ; # type: ignore
323 |+ return None
323 324 |
324 325 |
325 326 | def end_of_file():
RET503.py:328:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
326 | if False:
327 | return 1
328 | x = 2 \
| ^^^^^ RET503
|
= help: Add explicit `return` statement
Suggested fix
326 326 | if False:
327 327 | return 1
328 328 | x = 2 \
329 |+
330 |+ return None

View File

@@ -11,8 +11,8 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::{ComparableConstant, ComparableExpr, ComparableStmt};
use ruff_python_ast::helpers::{any_over_expr, contains_effect};
use ruff_python_ast::stmt_if::{if_elif_branches, IfElifBranch};
use ruff_python_parser::first_colon_range;
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::{Locator, UniversalNewlines};
use crate::checkers::ast::Checker;
@@ -369,16 +369,10 @@ pub(crate) fn nested_if_statements(
};
// Find the deepest nested if-statement, to inform the range.
let Some((test, first_stmt)) = find_last_nested_if(body) else {
let Some((test, _first_stmt)) = find_last_nested_if(body) else {
return;
};
let colon = first_colon_range(
TextRange::new(test.end(), first_stmt.start()),
checker.locator().contents(),
checker.source_type.is_jupyter(),
);
// Check if the parent is already emitting a larger diagnostic including this if statement
if let Some(Stmt::If(stmt_if)) = parent {
if let Some((body, _range, _is_elif)) = nested_if_body(stmt_if) {
@@ -392,10 +386,14 @@ pub(crate) fn nested_if_statements(
}
}
let mut diagnostic = Diagnostic::new(
CollapsibleIf,
colon.map_or(range, |colon| TextRange::new(range.start(), colon.end())),
);
let Some(colon) = SimpleTokenizer::starts_at(test.end(), checker.locator().contents())
.skip_trivia()
.find(|token| token.kind == SimpleTokenKind::Colon)
else {
return;
};
let mut diagnostic = Diagnostic::new(CollapsibleIf, TextRange::new(range.start(), colon.end()));
if checker.patch(diagnostic.kind.rule()) {
// The fixer preserves comments in the nested body, but removes comments between
// the outer and inner if statements.
@@ -721,10 +719,16 @@ pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_i
// ...and the same comments
let first_comments = checker
.indexer()
.comments_in_range(body_range(&current_branch, locator), locator);
.comment_ranges()
.comments_in_range(body_range(&current_branch, locator))
.iter()
.map(|range| locator.slice(*range));
let second_comments = checker
.indexer()
.comments_in_range(body_range(following_branch, locator), locator);
.comment_ranges()
.comments_in_range(body_range(following_branch, locator))
.iter()
.map(|range| locator.slice(*range));
if !first_comments.eq(second_comments) {
continue;
}

View File

@@ -1,12 +1,12 @@
use log::error;
use ruff_python_ast::{self as ast, Ranged, Stmt, WithItem};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Violation};
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_parser::first_colon_range;
use ruff_python_ast::{self as ast, Ranged, Stmt, WithItem};
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::UniversalNewlines;
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
use crate::line_width::LineWidth;
@@ -106,32 +106,24 @@ pub(crate) fn multiple_with_statements(
}
}
if let Some((is_async, items, body)) = next_with(&with_stmt.body) {
if let Some((is_async, items, _body)) = next_with(&with_stmt.body) {
if is_async != with_stmt.is_async {
// One of the statements is an async with, while the other is not,
// we can't merge those statements.
return;
}
let last_item = items.last().expect("Expected items to be non-empty");
let colon = first_colon_range(
TextRange::new(
last_item
.optional_vars
.as_ref()
.map_or(last_item.context_expr.end(), |v| v.end()),
body.first().expect("Expected body to be non-empty").start(),
),
checker.locator().contents(),
checker.source_type.is_jupyter(),
);
let Some(colon) = items.last().and_then(|item| {
SimpleTokenizer::starts_at(item.end(), checker.locator().contents())
.skip_trivia()
.find(|token| token.kind == SimpleTokenKind::Colon)
}) else {
return;
};
let mut diagnostic = Diagnostic::new(
MultipleWithStatements,
colon.map_or_else(
|| with_stmt.range(),
|colon| TextRange::new(with_stmt.start(), colon.end()),
),
TextRange::new(with_stmt.start(), colon.end()),
);
if checker.patch(diagnostic.kind.rule()) {
if !checker

View File

@@ -8,10 +8,9 @@ use ruff_python_codegen::Stylist;
use ruff_python_stdlib::str::{self};
use ruff_source_file::Locator;
use crate::autofix::codemods::CodegenStylist;
use crate::autofix::snippet::SourceCodeSnippet;
use crate::checkers::ast::Checker;
use crate::cst::matchers::{match_comparison, match_expression};
use crate::cst::matchers::{match_comparison, transform_expression};
use crate::registry::AsRule;
/// ## What it does
@@ -96,68 +95,69 @@ fn is_constant_like(expr: &Expr) -> bool {
/// Generate a fix to reverse a comparison.
fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Result<String> {
let range = expr.range();
let contents = locator.slice(range);
let source_code = locator.slice(range);
let mut expression = match_expression(contents)?;
let comparison = match_comparison(&mut expression)?;
transform_expression(source_code, stylist, |mut expression| {
let comparison = match_comparison(&mut expression)?;
let left = (*comparison.left).clone();
let left = (*comparison.left).clone();
// Copy the right side to the left side.
comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
// Copy the right side to the left side.
comparison.left = Box::new(comparison.comparisons[0].comparator.clone());
// Copy the left side to the right side.
comparison.comparisons[0].comparator = left;
// Copy the left side to the right side.
comparison.comparisons[0].comparator = left;
// Reverse the operator.
let op = comparison.comparisons[0].operator.clone();
comparison.comparisons[0].operator = match op {
CompOp::LessThan {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThan {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThan {
whitespace_before,
whitespace_after,
} => CompOp::LessThan {
whitespace_before,
whitespace_after,
},
CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::Equal {
whitespace_before,
whitespace_after,
} => CompOp::Equal {
whitespace_before,
whitespace_after,
},
CompOp::NotEqual {
whitespace_before,
whitespace_after,
} => CompOp::NotEqual {
whitespace_before,
whitespace_after,
},
_ => panic!("Expected comparison operator"),
};
// Reverse the operator.
let op = comparison.comparisons[0].operator.clone();
comparison.comparisons[0].operator = match op {
CompOp::LessThan {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThan {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThan {
whitespace_before,
whitespace_after,
} => CompOp::LessThan {
whitespace_before,
whitespace_after,
},
CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::GreaterThanEqual {
whitespace_before,
whitespace_after,
} => CompOp::LessThanEqual {
whitespace_before,
whitespace_after,
},
CompOp::Equal {
whitespace_before,
whitespace_after,
} => CompOp::Equal {
whitespace_before,
whitespace_after,
},
CompOp::NotEqual {
whitespace_before,
whitespace_after,
} => CompOp::NotEqual {
whitespace_before,
whitespace_after,
},
_ => panic!("Expected comparison operator"),
};
Ok(expression.codegen_stylist(stylist))
Ok(expression)
})
}
/// SIM300

View File

@@ -61,7 +61,9 @@ pub(crate) fn empty_type_checking_block(checker: &mut Checker, stmt: &ast::StmtI
let stmt = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let edit = autofix::edits::delete_stmt(stmt, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::automatic(edit).isolate(checker.parent_isolation()));
diagnostic.set_fix(Fix::automatic(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -236,7 +236,8 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
)?;
Ok(
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits())
.isolate(checker.parent_isolation()),
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
Checker::isolation(checker.semantic().parent_statement_id(node_id)),
),
)
}

View File

@@ -283,6 +283,7 @@ pub(crate) fn typing_only_runtime_import(
None,
&checker.settings.src,
checker.package(),
checker.settings.isort.detect_same_package,
&checker.settings.isort.known_modules,
checker.settings.target_version,
) {
@@ -486,7 +487,8 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
)?;
Ok(
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits())
.isolate(checker.parent_isolation()),
Fix::suggested_edits(remove_import_edit, add_import_edit.into_edits()).isolate(
Checker::isolation(checker.semantic().parent_statement_id(node_id)),
),
)
}

View File

@@ -69,6 +69,7 @@ pub(crate) fn categorize<'a>(
level: Option<u32>,
src: &[PathBuf],
package: Option<&Path>,
detect_same_package: bool,
known_modules: &'a KnownModules,
target_version: PythonVersion,
) -> &'a ImportSection {
@@ -88,7 +89,7 @@ pub(crate) fn categorize<'a>(
&ImportSection::Known(ImportType::StandardLibrary),
Reason::KnownStandardLibrary,
)
} else if same_package(package, module_base) {
} else if detect_same_package && same_package(package, module_base) {
(
&ImportSection::Known(ImportType::FirstParty),
Reason::SamePackage,
@@ -137,6 +138,7 @@ pub(crate) fn categorize_imports<'a>(
block: ImportBlock<'a>,
src: &[PathBuf],
package: Option<&Path>,
detect_same_package: bool,
known_modules: &'a KnownModules,
target_version: PythonVersion,
) -> BTreeMap<&'a ImportSection, ImportBlock<'a>> {
@@ -148,6 +150,7 @@ pub(crate) fn categorize_imports<'a>(
None,
src,
package,
detect_same_package,
known_modules,
target_version,
);
@@ -164,6 +167,7 @@ pub(crate) fn categorize_imports<'a>(
import_from.level,
src,
package,
detect_same_package,
known_modules,
target_version,
);
@@ -180,6 +184,7 @@ pub(crate) fn categorize_imports<'a>(
import_from.level,
src,
package,
detect_same_package,
known_modules,
target_version,
);
@@ -196,6 +201,7 @@ pub(crate) fn categorize_imports<'a>(
import_from.level,
src,
package,
detect_same_package,
known_modules,
target_version,
);

View File

@@ -1,7 +1,7 @@
use std::borrow::Cow;
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_python_ast::Ranged;
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
@@ -21,20 +21,15 @@ impl Ranged for Comment<'_> {
pub(crate) fn collect_comments<'a>(
range: TextRange,
locator: &'a Locator,
source_type: PySourceType,
indexer: &'a Indexer,
) -> Vec<Comment<'a>> {
let contents = locator.slice(range);
lexer::lex_starts_at(contents, source_type.as_mode(), range.start())
.flatten()
.filter_map(|(tok, range)| {
if let Tok::Comment(value) = tok {
Some(Comment {
value: value.into(),
range,
})
} else {
None
}
indexer
.comment_ranges()
.comments_in_range(range)
.iter()
.map(|range| Comment {
value: locator.slice(*range).into(),
range: *range,
})
.collect()
}

View File

@@ -82,6 +82,7 @@ pub(crate) fn format_imports(
force_to_top: &BTreeSet<String>,
known_modules: &KnownModules,
order_by_type: bool,
detect_same_package: bool,
relative_imports_order: RelativeImportsOrder,
single_line_exclusions: &BTreeSet<String>,
split_on_trailing_comma: bool,
@@ -129,6 +130,7 @@ pub(crate) fn format_imports(
force_to_top,
known_modules,
order_by_type,
detect_same_package,
relative_imports_order,
split_on_trailing_comma,
classes,
@@ -187,6 +189,7 @@ fn format_import_block(
force_to_top: &BTreeSet<String>,
known_modules: &KnownModules,
order_by_type: bool,
detect_same_package: bool,
relative_imports_order: RelativeImportsOrder,
split_on_trailing_comma: bool,
classes: &BTreeSet<String>,
@@ -198,7 +201,14 @@ fn format_import_block(
section_order: &[ImportSection],
) -> String {
// Categorize by type (e.g., first-party vs. third-party).
let mut block_by_type = categorize_imports(block, src, package, known_modules, target_version);
let mut block_by_type = categorize_imports(
block,
src,
package,
detect_same_package,
known_modules,
target_version,
);
let mut output = String::new();
@@ -1084,4 +1094,38 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn detect_same_package() -> Result<()> {
let diagnostics = test_path(
Path::new("isort/detect_same_package/foo/bar.py"),
&Settings {
src: vec![],
isort: super::settings::Settings {
detect_same_package: true,
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn no_detect_same_package() -> Result<()> {
let diagnostics = test_path(
Path::new("isort/detect_same_package/foo/bar.py"),
&Settings {
src: vec![],
isort: super::settings::Settings {
detect_same_package: false,
..super::settings::Settings::default()
},
..Settings::for_rule(Rule::UnsortedImports)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View File

@@ -1,16 +1,16 @@
use std::path::Path;
use itertools::{EitherOrBoth, Itertools};
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_text_size::TextRange;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::whitespace::{followed_by_multi_statement_line, trailing_lines_end};
use ruff_python_ast::whitespace::trailing_lines_end;
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{leading_indentation, textwrap::indent, PythonWhitespace};
use ruff_source_file::{Locator, UniversalNewlines};
use ruff_text_size::TextRange;
use crate::line_width::LineWidth;
use crate::registry::AsRule;
@@ -97,7 +97,7 @@ pub(crate) fn organize_imports(
// Special-cases: there's leading or trailing content in the import block. These
// are too hard to get right, and relatively rare, so flag but don't fix.
if indexer.preceded_by_multi_statement_line(block.imports.first().unwrap(), locator)
|| followed_by_multi_statement_line(block.imports.last().unwrap(), locator)
|| indexer.followed_by_multi_statement_line(block.imports.last().unwrap(), locator)
{
return Some(Diagnostic::new(UnsortedImports, range));
}
@@ -106,7 +106,7 @@ pub(crate) fn organize_imports(
let comments = comments::collect_comments(
TextRange::new(range.start(), locator.full_line_end(range.end())),
locator,
source_type,
indexer,
);
let trailing_line_end = if block.trailer.is_none() {
@@ -134,6 +134,7 @@ pub(crate) fn organize_imports(
&settings.isort.force_to_top,
&settings.isort.known_modules,
settings.isort.order_by_type,
settings.isort.detect_same_package,
settings.isort.relative_imports_order,
&settings.isort.single_line_exclusions,
settings.isort.split_on_trailing_comma,

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