Compare commits

..

49 Commits

Author SHA1 Message Date
konstin
6bd83d36ee Replace num_bigint with malachite
[malachite](https://www.malachite.rs/) is an arbitrary precision crate that unlike num_bigint implement as small integer optimization. This avoids allocating a `Vec` for every single number parse and instead only allocates for rare unusually large numbers.

This is also a correctness improvement since we'd previously just ignore the higher parts of unreasonably large numbers.

The disadvantage is that malachite is slow to compile.
2023-09-19 12:02:32 +02:00
konsti
94b68f201b Fix stylist indentation with a formfeed (#7489)
**Summary** In python, a formfeed is technically undefined behaviour
(https://docs.python.org/3/reference/lexical_analysis.html#indentation):
> A formfeed character may be present at the start of the line; it will
be ignored for
> the indentation calculations above. Formfeed characters occurring
elsewhere in the
> leading whitespace have an undefined effect (for instance, they may
reset the space
> count to zero).

In practice, they just reset the indentation:


df8b3a46a7/Parser/tokenizer.c (L1819-L1821)
a41bb2733f/crates/ruff_python_parser/src/lexer.rs (L664-L667)

The stylist didn't handle formfeeds previously and would produce invalid
indents. The remedy is to cut everything before a form feed.

Checks box for
https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458825

**Test Plan** Unit test for the stylist and a regression test for the
rule
2023-09-19 12:01:16 +02:00
konsti
ef34c5cbec Update itertools to 0.11 (#7513)
Preparation for #7469.

Changelog:
https://github.com/rust-itertools/itertools/blob/master/CHANGELOG.md#0110
2023-09-19 09:53:14 +00:00
dependabot[bot]
fdbefd777c Bump syn from 2.0.33 to 2.0.37 (#7512)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-19 11:08:56 +02:00
dependabot[bot]
078547adbb Bump clap from 4.4.3 to 4.4.4 (#7511)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-19 11:08:39 +02:00
dependabot[bot]
42a0bec146 Bump schemars from 0.8.13 to 0.8.15 (#7510)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-19 11:08:22 +02:00
Micha Reiser
37b7d0f921 fix: Compiler warning about unused map_or (#7508) 2023-09-19 08:10:01 +00:00
Micha Reiser
6a4dbd622b Add optimized best_fit_parenthesize IR (#7475) 2023-09-19 06:29:05 +00:00
Charlie Marsh
28b48ab902 Avoid flagging starred expressions in UP007 (#7505)
## Summary

These can't be fixed, because fixing them would lead to invalid syntax.
So flagging them also feels misleading.

Closes https://github.com/astral-sh/ruff/issues/7452.
2023-09-19 03:37:38 +00:00
Valeriy Savchenko
4123d074bd [refurb] Implement reimplemented-starmap rule (FURB140) (#7253)
## Summary

This PR is part of a bigger effort of re-implementing `refurb` rules
#1348. It adds support for
[FURB140](https://github.com/dosisod/refurb/blob/master/refurb/checks/itertools/use_starmap.py)

## Test Plan

I included a new test + checked that all other tests pass.
2023-09-19 02:18:54 +00:00
Mathieu Kniewallner
c6ba7dfbc6 feat(rules): implement flake8-bandit S201 (flask_debug_true) (#7503)
Part of #1646.

## Summary

Implement `S201`
([`flask_debug_true`](https://bandit.readthedocs.io/en/latest/plugins/b201_flask_debug_true.html))
rule from `bandit`.

I am fairly new to Rust and Ruff's codebase, so there might be better
ways to implement the rule or write the code.

## Test Plan

Snapshot test from
https://github.com/PyCQA/bandit/blob/1.7.5/examples/flask_debug.py, with
a few additions in the "unrelated" part to test a bit more cases.
2023-09-19 00:43:28 +00:00
Micael Jarniac
40f6456add Use MkDocs' not_in_nav (#5498)
Closes #5497
Needs MkDocs 1.5 to be released.
- [x] https://github.com/mkdocs/mkdocs/milestone/15

## Summary
Uses MkDocs' `not_in_nav` config to hide spam about files in
`docs/rules/` not being in nav.
2023-09-19 00:01:43 +00:00
Micha Reiser
3e1dffab20 refactor: Use OnceCell in Memoized (#7500) 2023-09-18 19:58:55 +00:00
Micha Reiser
3336d23f48 perf: Reduce best fitting allocations (#7411) 2023-09-18 19:49:44 +00:00
Jonathan Plasse
2421805033 Avoid N802 violations for @overload methods (#7498)
Close #7479

The `@override` was already implemented

## Test Plan

Tested the code in the issue. After removing all the noqa's, only one
occurrence of `BadName()` raised a violation.
Added a fixture
2023-09-18 14:32:40 -04:00
Tom Kuson
359f50e6dc Ignore pass-statement-stub-body documentation formatting (#7497)
## Summary

Fix CI (broken in #7496).

The code snippet was formatted as Black would format a stub file, but
the CI script doesn't know that (it assumes all code snippets are
non-stub files). Easier to ignore.

Sorry for breaking CI!

## Test Plan

`python scripts/check_docs_formatted.py`
2023-09-18 14:27:20 -04:00
qdegraaf
bccba5d73f [flake8-logging] Implement LOG007: ExceptionWithoutExcInfo (#7410)
## Summary

This PR implements a new rule for `flake8-logging` plugin that checks
for uses of `logging.exception()` with `exc_info` set to `False` or a
falsy value. It suggests using `logging.error` in these cases instead.

I am unsure about the name. Open to suggestions there, went with the
most explicit name I could think of in the meantime.

Refer https://github.com/astral-sh/ruff/issues/7248

## Test Plan

Added a new fixture cases and ran `cargo test`
2023-09-18 17:46:12 +00:00
Tom Kuson
0bfdb15ecf Add documentation to pass-statement-stub-body (#7496)
## Summary

Add documentation to `pass-statement-stub-body` (`PYI009`) rule. Related
to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-09-18 17:33:15 +00:00
dependabot[bot]
a902d14c31 Bump chrono from 0.4.30 to 0.4.31 (#7481) 2023-09-18 13:11:51 -04:00
Charlie Marsh
728539291f Move FormatExprDict to top of expr_dict.rs (#7494)
Put the node itself up top, and internal structs down below.
2023-09-18 11:55:18 -04:00
Dhruv Manilawala
c2bd8af59a Remove triple-quoted string ranges computation (#7476)
## Summary

This is a follow-up PR for #7435 to remove the now unused triple-quoted
string ranges from the indexer.
2023-09-18 20:57:49 +05:30
Jaap Roes
c946bf157e Extend bad-dunder-method-name to permit __html__ (#7492)
## Summary

Fixes #7478

## Test Plan

`cargo test`
2023-09-18 15:16:22 +00:00
Charlie Marsh
8ab2519717 Respect parentheses for precedence in await (#7468)
## Summary

We were using `Parenthesize::IfBreaks` universally for `await`, but
dropping parentheses can change the AST due to precedence. It turns out
that Black's rules aren't _exactly_ the same as operator precedence
(e.g., they leave parentheses around `await ([1, 2, 3])`, although they
aren't strictly required).

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

## Test Plan

`cargo test`

No change in similarity.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 398 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 398 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |
2023-09-18 09:56:41 -04:00
konsti
c4d85d6fb6 Fix ''' ""''' formatting (#7485)
## Summary

`''' ""'''` is an edge case that was previously incorrectly formatted as
`""" """""`.

Fixes #7460

## Test Plan

Added regression test
2023-09-18 10:28:15 +00:00
dependabot[bot]
70ea49bf72 Bump test-case from 3.1.0 to 3.2.1 (#7484)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 11:31:41 +02:00
dependabot[bot]
8e255974bc Bump unicode-ident from 1.0.11 to 1.0.12 (#7482)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 11:31:06 +02:00
dependabot[bot]
d358604464 Bump serde_json from 1.0.106 to 1.0.107 (#7480)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 11:30:06 +02:00
dependabot[bot]
8243db74fe Bump indoc from 2.0.3 to 2.0.4 (#7483)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-18 11:29:50 +02:00
Micha Reiser
0346e781d4 Fix handling of newlines in empty files (#7473) 2023-09-18 06:08:10 +00:00
Tom Kuson
b66bfa6570 Extend bad-dunder-method-name to permit attrs dunders (#7472)
## Summary

Closes #7451.

## Test Plan

`cargo test`
2023-09-17 16:13:33 -04:00
Charlie Marsh
28273eb00b Avoid flagging starred elements in C402 (#7466)
## Summary

Rewriting these is not valid syntax.

Part of https://github.com/astral-sh/ruff/issues/7455.
2023-09-17 15:23:27 +00:00
Charlie Marsh
12acd191e1 Remove parentheses when rewriting assert calls to statements (#7464) 2023-09-17 15:18:56 +00:00
Charlie Marsh
64b929bc29 Add padding to prevent some autofix errors (#7461)
## Summary

We should really be doing this anywhere we _replace_ an expression.

See: https://github.com/astral-sh/ruff/issues/7455.
2023-09-17 15:14:16 +00:00
Micha Reiser
26ae0a6e8d Fix dangling module comments (#7456) 2023-09-17 14:56:41 +00:00
Dhruv Manilawala
959338d39d Refactor tab-indentation as a token-based rule (#7435)
## Summary

This PR updates the `W191` (`tab-indentation`) rule from a line-based to
a token-based rule.

Earlier, the rule used the `triple_quoted_string_ranges` from the
indexer to skip over any lines _inside_ a triple-quoted string. This was the only
use of the ranges. These ranges were extracted through the tokens, so instead
we can directly use the newline tokens to perform the check.

This would also mean that we can remove the `triple_quoted_string_ranges` from
the indexer but I'll hold that off until we have a better idea on #7326
but I don't think it would be a problem to remove it.

This will also fix #7379 once PEP 701 changes are merged.

## Test Plan

`cargo test`
2023-09-16 20:25:20 +00:00
Charlie Marsh
422ff82f4a Avoid extra parentheses in yield expressions (#7444)
## Summary

This is equivalent to https://github.com/astral-sh/ruff/pull/7424, but
for `yield` and `yield from` expressions. Specifically, we want to avoid
adding unnecessary extra parentheses for `yield expr` when `expr` itself
does not require parentheses.

## Test Plan

`cargo test`

No change in any of the similarity metrics.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99929 | 648 | 16 |
| zulip | 0.99962 | 1437 | 22 |
2023-09-16 14:46:56 -04:00
Charlie Marsh
8d0a5e01bd Modify comment_ranges slice in BackwardsTokenizer (#7432)
## Summary

I was kinda curious to understand this issue
(https://github.com/astral-sh/ruff/issues/7426) and just ended up
attempting to address it.

## Test Plan

`cargo test`
2023-09-16 14:04:45 -04:00
Charlie Marsh
aae02cf275 Fix broken is_expression_parenthesized call from rebase (#7442) 2023-09-16 17:22:16 +00:00
Charlie Marsh
7e2eba2592 Avoiding grouping comprehension targets separately from conditions (#7429)
## Summary

Black seems to treat comprehension targets and conditions as if they're
in a single group -- so if the comprehension expands, all conditions are
rendered on their own line, etc.

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

## Test Plan

`cargo test`

No change in any of the similarity metrics.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99923 | 648 | 18 |
| zulip | 0.99962 | 1437 | 22 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99923 | 648 | 18 |
| zulip | 0.99962 | 1437 | 22 |
2023-09-16 17:19:34 +00:00
Charlie Marsh
22770fb4be Avoid extra parentheses in await expressions (#7424)
## Summary

This PR aligns the await parenthesizing with the unary case, which is:
if the value is already parenthesized, avoid parenthesizing; otherwise,
only parenthesize if the _value_ needs parenthesizing.

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

## Test Plan

`cargo test`

No change in similarity.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99923 | 648 | 18 |
| zulip | 0.99962 | 1437 | 22 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99923 | 648 | 18 |
| zulip | 0.99962 | 1437 | 22 |
2023-09-16 13:10:35 -04:00
Charlie Marsh
1880cceac1 Avoid extra parentheses in unary expressions (#7428)
## Summary

This PR applies a similar fix to unary expressions as in
https://github.com/astral-sh/ruff/pull/7424. Specifically, we only need
to parenthesize the entire operator if the operand itself doesn't have
parentheses, and requires parentheses.

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

## Test Plan

`cargo test`

No change in similarity.

Before:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99923 | 648 | 18 |
| zulip | 0.99962 | 1437 | 22 |

After:

| project | similarity index | total files | changed files |

|--------------|------------------:|------------------:|------------------:|
| cpython | 0.76083 | 1789 | 1632 |
| django | 0.99982 | 2760 | 37 |
| transformers | 0.99957 | 2587 | 399 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99983 | 3496 | 18 |
| warehouse | 0.99923 | 648 | 18 |
| zulip | 0.99962 | 1437 | 22 |
2023-09-16 13:07:38 -04:00
Dhruv Manilawala
0d1fb823d6 [flake8-logging] Implement LOG002: invalid-get-logger-argument (#7399)
## Summary

This PR implements a new rule for `flake8-logging` plugin that checks
for
`logging.getLogger` calls with either `__file__` or `__cached__` as the
first
argument and generates a suggested fix to use `__name__` instead.

Refer: #7248

## Test Plan

Add test cases and `cargo test`
2023-09-16 12:21:30 -04:00
Micha Reiser
c907317199 Fix build (#7437) 2023-09-16 14:50:36 +00:00
Micha Reiser
916dd5b7fa fix: Use BestFit layout for subscript (#7409) 2023-09-16 16:21:45 +02:00
konsti
2cbe1733c8 Use CommentRanges in backwards lexing (#7360)
## Summary

The tokenizer was split into a forward and a backwards tokenizer. The
backwards tokenizer uses the same names as the forwards ones (e.g.
`next_token`). The backwards tokenizer gets the comment ranges that we
already built to skip comments.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-09-16 03:21:45 +00:00
Charlie Marsh
1f6e1485f9 Change playground to use pages deploy (#7430)
Fixes a deprecation warning.
2023-09-16 03:11:34 +00:00
Charlie Marsh
9b43162cc4 Move documentation to docs.astral.sh/ruff (#7419)
## Summary

We're planning to move the documentation from
[https://beta.ruff.rs/docs](https://beta.ruff.rs/docs) to
[https://docs.astral.sh/ruff](https://docs.astral.sh/ruff), for a few
reasons:

1. We want to remove the `beta` from the domain, as Ruff is no longer
considered beta software.
2. We want to migrate to a structure that could accommodate multiple
future tools living under one domain.

The docs are actually already live at
[https://docs.astral.sh/ruff](https://docs.astral.sh/ruff), but later
today, I'll add a permanent redirect from the previous to the new
domain. **All existing links will continue to work, now and in
perpetuity.**

This PR contains the code changes necessary for the updated
documentation. As part of this effort, I moved the playground and
documentation from my personal Cloudflare account to our team Cloudflare
account (hence the new `--project-name` references). After merging, I'll
also update the secrets on this repo.
2023-09-15 22:49:42 -04:00
Charlie Marsh
cc9e84c144 Format trailing operator comments as dangling (#7427)
## Summary

Given a trailing operator comment in a unary expression, like:

```python
if (
  not  # comment
  a):
    ...
```

We were attaching these to the operand (`a`), but formatting them in the
unary operator via special handling. Parents shouldn't format the
comments of their children, so this instead attaches them as dangling
comments on the unary expression. (No intended change in formatting.)
2023-09-15 20:34:09 -04:00
Zanie Blue
f4d50a2aec Fix release validation step (#7417)
Proof of concept at #7416
Fixes `main` branch check added in #7279 (see
[failure](https://github.com/astral-sh/ruff/actions/runs/6201772425/job/16839150669))
Removes the meaningless "SHA consistency" check (since we literally
check out the SHA now)
2023-09-15 20:18:25 +00:00
208 changed files with 4267 additions and 1717 deletions

View File

@@ -366,31 +366,3 @@ jobs:
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
tmp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: main # We checkout the main branch to check for the commit
- name: Check main branch
run: |
# Fetch the main branch since a shallow checkout is used by default
git fetch origin main --unshallow
if ! git branch --contains 0c030b5bf31e425cb6070db7386243eca6dbd8f1 | grep -E '(^|\s)main$'; then
echo "The specified sha is not on the main branch" >&2
exit 1
fi
- name: Check tag consistency
run: |
# Switch to the commit we want to release
git checkout 0c030b5bf31e425cb6070db7386243eca6dbd8f1
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "0.0.290" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "0.0.290" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi

View File

@@ -52,4 +52,4 @@ jobs:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
command: pages deploy site --project-name=ruff-docs --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}
command: pages deploy site --project-name=astral-docs --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}

View File

@@ -44,4 +44,4 @@ jobs:
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}
command: pages publish playground/dist --project-name=ruff --branch ${GITHUB_HEAD_REF} --commit-hash ${GITHUB_SHA}
command: pages deploy playground/dist --project-name=ruff-playground --branch ${GITHUB_HEAD_REF} --commit-hash ${GITHUB_SHA}

View File

@@ -419,18 +419,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- name: Check tag consistency
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${version}"
fi
ref: main # We checkout the main branch to check for the commit
- name: Check main branch
if: ${{ inputs.sha }}
run: |
@@ -440,17 +429,18 @@ jobs:
echo "The specified sha is not on the main branch" >&2
exit 1
fi
- name: Check SHA consistency
if: ${{ inputs.sha }}
- name: Check tag consistency
run: |
git_sha=$(git rev-parse HEAD)
if [ "${{ inputs.sha }}" != "${git_sha}" ]; then
echo "The specified sha does not match the git checkout" >&2
echo "${{ inputs.sha }}" >&2
echo "${git_sha}" >&2
# Switch to the commit we want to release
git checkout ${{ inputs.sha }}
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
echo "The input tag does not match the version from pyproject.toml:" >&2
echo "${{ inputs.tag }}" >&2
echo "${version}" >&2
exit 1
else
echo "Releasing ${git_sha}"
echo "Releasing ${version}"
fi
upload-release:

View File

@@ -299,4 +299,4 @@ default.
`pyproject.toml` files are now resolved hierarchically, such that for each Python file, we find
the first `pyproject.toml` file in its path, and use that to determine its lint settings.
See the [documentation](https://beta.ruff.rs/docs/configuration/#python-file-discovery) for more.
See the [documentation](https://docs.astral.sh/ruff/configuration/#python-file-discovery) for more.

View File

@@ -719,8 +719,8 @@ Module {
- `cargo dev generate-cli-help`, `cargo dev generate-docs` and `cargo dev generate-json-schema`:
Update just `docs/configuration.md`, `docs/rules` and `ruff.schema.json` respectively.
- `cargo dev generate-options`: Generate a markdown-compatible table of all `pyproject.toml`
options. Used for <https://beta.ruff.rs/docs/settings/>
- `cargo dev generate-rules-table`: Generate a markdown-compatible table of all rules. Used for <https://beta.ruff.rs/docs/rules/>
options. Used for <https://docs.astral.sh/ruff/settings/>.
- `cargo dev generate-rules-table`: Generate a markdown-compatible table of all rules. Used for <https://docs.astral.sh/ruff/rules/>.
- `cargo dev round-trip <python file or jupyter notebook>`: Read a Python file or Jupyter Notebook,
parse it, serialize the parsed representation and write it back. Used to check how good our
representation is so that fixes don't rewrite irrelevant parts of a file.
@@ -778,7 +778,7 @@ To understand Ruff's import categorization system, we first need to define two c
- "Package root": The top-most directory defining the Python package that includes a given Python
file. To find the package root for a given Python file, traverse up its parent directories until
you reach a parent directory that doesn't contain an `__init__.py` file (and isn't marked as
a [namespace package](https://beta.ruff.rs/docs/settings/#namespace-packages)); take the directory
a [namespace package](https://docs.astral.sh/ruff/settings/#namespace-packages)); take the directory
just before that, i.e., the first directory in the package.
For example, given:
@@ -867,7 +867,7 @@ There are three ways in which an import can be categorized as "first-party":
package (e.g., `from foo import bar` or `import foo.bar`), they'll be classified as first-party
automatically. This check is as simple as comparing the first segment of the current file's
module path to the first segment of the import.
1. **Source roots**: Ruff supports a `[src](https://beta.ruff.rs/docs/settings/#src)` setting, which
1. **Source roots**: Ruff supports a `[src](https://docs.astral.sh/ruff/settings/#src)` setting, which
sets the directories to scan when identifying first-party imports. The algorithm is
straightforward: given an import, like `import foo`, iterate over the directories enumerated in
the `src` setting and, for each directory, check for the existence of a subdirectory `foo` or a

285
Cargo.lock generated
View File

@@ -172,6 +172,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.3"
@@ -214,6 +220,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bstr"
version = "1.6.2"
@@ -272,9 +287,9 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.30"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
dependencies = [
"android-tzdata",
"iana-time-zone",
@@ -313,9 +328,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.4.3"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84ed82781cea27b43c9b106a979fe450a13a31aab0500595fb3fc06616de08e6"
checksum = "b1d7b8d5ec32af0fadc644bf1fd509a688c2103b185644bb1e29d164e0703136"
dependencies = [
"clap_builder",
"clap_derive",
@@ -323,9 +338,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.4.2"
version = "4.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08"
checksum = "5179bb514e4d7c2051749d8fcefa2ed6d06a9f4e6d69faf3805f5d80b8cf8d56"
dependencies = [
"anstream",
"anstyle",
@@ -383,7 +398,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -495,6 +510,15 @@ version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
[[package]]
name = "cpufeatures"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -516,7 +540,7 @@ dependencies = [
"clap",
"criterion-plot",
"is-terminal",
"itertools",
"itertools 0.10.5",
"num-traits",
"once_cell",
"oorandom",
@@ -535,7 +559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
dependencies = [
"cast",
"itertools",
"itertools 0.10.5",
]
[[package]]
@@ -587,6 +611,16 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "darling"
version = "0.20.3"
@@ -608,7 +642,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -619,7 +653,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -634,6 +668,16 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "dirs"
version = "4.0.0"
@@ -720,6 +764,18 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "embed-doc-image"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af36f591236d9d822425cb6896595658fa558fcebf5ee8accac1d4b92c47166e"
dependencies = [
"base64 0.13.1",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "ena"
version = "0.14.2"
@@ -816,7 +872,7 @@ dependencies = [
"clap",
"colored",
"configparser",
"itertools",
"itertools 0.11.0",
"log",
"once_cell",
"pep440_rs",
@@ -872,6 +928,16 @@ dependencies = [
"libc",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.2.10"
@@ -1049,9 +1115,9 @@ dependencies = [
[[package]]
name = "indoc"
version = "2.0.3"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c785eefb63ebd0e33416dfcb8d6da0bf27ce752843a45632a67bf10d4d4b5c4"
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
[[package]]
name = "inotify"
@@ -1120,7 +1186,7 @@ dependencies = [
"pmutil 0.6.1",
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -1143,6 +1209,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.9"
@@ -1158,6 +1233,15 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "keccak"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940"
dependencies = [
"cpufeatures",
]
[[package]]
name = "kqueue"
version = "1.0.8"
@@ -1189,7 +1273,7 @@ dependencies = [
"diff",
"ena",
"is-terminal",
"itertools",
"itertools 0.10.5",
"lalrpop-util",
"petgraph",
"regex",
@@ -1311,6 +1395,41 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "malachite"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9fa232412d927f518cd873073911726943f432bac1bbc1288316d240635dc51"
dependencies = [
"malachite-base",
"malachite-nz",
]
[[package]]
name = "malachite-base"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "606a61b226dc58b8b283399b74754460433c193b193f26eaaad92f7966abd72b"
dependencies = [
"getrandom",
"itertools 0.11.0",
"rand",
"rand_chacha",
"ryu",
"sha3",
]
[[package]]
name = "malachite-nz"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "863d06218c83cc2269c425186cc15094d6284b1333a1184d0890aecfddb8916b"
dependencies = [
"embed-doc-image",
"itertools 0.11.0",
"malachite-base",
]
[[package]]
name = "matchers"
version = "0.1.0"
@@ -1445,27 +1564,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "num-bigint"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.16"
@@ -1720,7 +1818,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -1749,7 +1847,7 @@ checksum = "09963355b9f467184c04017ced4a2ba2d75cbcb4e7462690d388233253d4b1a9"
dependencies = [
"anstyle",
"difflib",
"itertools",
"itertools 0.10.5",
"predicates-core",
]
@@ -2035,12 +2133,12 @@ dependencies = [
"imperative",
"insta",
"is-macro",
"itertools",
"itertools 0.11.0",
"libcst",
"log",
"malachite",
"memchr",
"natord",
"num-bigint",
"num-traits",
"once_cell",
"path-absolutize",
@@ -2112,7 +2210,7 @@ dependencies = [
"filetime",
"glob",
"globset",
"itertools",
"itertools 0.11.0",
"regex",
"ruff_macros",
]
@@ -2139,7 +2237,7 @@ dependencies = [
"insta",
"insta-cmd",
"is-macro",
"itertools",
"itertools 0.11.0",
"itoa",
"log",
"mimalloc",
@@ -2186,7 +2284,7 @@ dependencies = [
"imara-diff",
"indicatif",
"indoc",
"itertools",
"itertools 0.11.0",
"libcst",
"once_cell",
"pretty_assertions",
@@ -2255,11 +2353,11 @@ dependencies = [
name = "ruff_macros"
version = "0.0.0"
dependencies = [
"itertools",
"itertools 0.11.0",
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -2268,7 +2366,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"insta",
"itertools",
"itertools 0.11.0",
"once_cell",
"ruff_diagnostics",
"ruff_source_file",
@@ -2288,10 +2386,9 @@ dependencies = [
"bitflags 2.4.0",
"insta",
"is-macro",
"itertools",
"itertools 0.11.0",
"malachite",
"memchr",
"num-bigint",
"num-traits",
"once_cell",
"ruff_python_parser",
"ruff_python_trivia",
@@ -2323,7 +2420,7 @@ dependencies = [
"clap",
"countme",
"insta",
"itertools",
"itertools 0.11.0",
"memchr",
"once_cell",
"ruff_formatter",
@@ -2348,7 +2445,7 @@ dependencies = [
name = "ruff_python_index"
version = "0.0.0"
dependencies = [
"itertools",
"itertools 0.11.0",
"ruff_python_ast",
"ruff_python_parser",
"ruff_python_trivia",
@@ -2363,7 +2460,7 @@ dependencies = [
"bitflags 2.4.0",
"hexf-parse",
"is-macro",
"itertools",
"itertools 0.11.0",
"lexical-parse-float",
"num-traits",
"rand",
@@ -2377,11 +2474,10 @@ dependencies = [
"anyhow",
"insta",
"is-macro",
"itertools",
"itertools 0.11.0",
"lalrpop",
"lalrpop-util",
"num-bigint",
"num-traits",
"malachite",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash",
@@ -2407,7 +2503,6 @@ version = "0.0.0"
dependencies = [
"bitflags 2.4.0",
"is-macro",
"num-traits",
"ruff_index",
"ruff_python_ast",
"ruff_python_parser",
@@ -2430,12 +2525,11 @@ name = "ruff_python_trivia"
version = "0.0.0"
dependencies = [
"insta",
"memchr",
"itertools 0.11.0",
"ruff_python_ast",
"ruff_python_parser",
"ruff_source_file",
"ruff_text_size",
"smallvec",
"unicode-ident",
]
@@ -2492,6 +2586,7 @@ dependencies = [
"ruff_python_formatter",
"ruff_python_index",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
@@ -2511,7 +2606,7 @@ dependencies = [
"glob",
"globset",
"ignore",
"itertools",
"itertools 0.11.0",
"log",
"path-absolutize",
"pep440_rs",
@@ -2612,9 +2707,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.13"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -2624,9 +2719,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.13"
version = "0.8.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
dependencies = [
"proc-macro2",
"quote",
@@ -2690,7 +2785,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -2706,9 +2801,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.106"
version = "1.0.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
dependencies = [
"itoa",
"ryu",
@@ -2752,7 +2847,17 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
name = "sha3"
version = "0.10.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60"
dependencies = [
"digest",
"keccak",
]
[[package]]
@@ -2847,7 +2952,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -2863,9 +2968,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.33"
version = "2.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668"
checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
dependencies = [
"proc-macro2",
"quote",
@@ -2935,36 +3040,36 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "test-case"
version = "3.1.0"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a1d6e7bde536b0412f20765b76e921028059adfd1b90d8974d33fd3c91b25df"
checksum = "c8f1e820b7f1d95a0cdbf97a5df9de10e1be731983ab943e56703ac1b8e9d425"
dependencies = [
"test-case-macros",
]
[[package]]
name = "test-case-core"
version = "3.1.0"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d10394d5d1e27794f772b6fc854c7e91a2dc26e2cbf807ad523370c2a59c0cee"
checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462"
dependencies = [
"cfg-if",
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.37",
]
[[package]]
name = "test-case-macros"
version = "3.1.0"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeb9a44b1c6a54c1ba58b152797739dba2a83ca74e18168a68c980eb142f9404"
checksum = "37cfd7bbc88a0104e304229fba519bdc45501a30b760fb72240342f1289ad257"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.37",
"test-case-core",
]
@@ -2985,7 +3090,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -3107,7 +3212,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -3167,6 +3272,12 @@ version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unic-char-property"
version = "0.9.0"
@@ -3217,9 +3328,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.11"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "unicode-normalization"
@@ -3262,7 +3373,7 @@ version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b11c96ac7ee530603dcdf68ed1557050f374ce55a5a07193ebf8cbc9f8927e9"
dependencies = [
"base64",
"base64 0.21.3",
"flate2",
"log",
"once_cell",
@@ -3310,7 +3421,7 @@ checksum = "f7e1ba1f333bd65ce3c9f27de592fcbc256dafe3af2717f56d7c87761fbaccf4"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
]
[[package]]
@@ -3404,7 +3515,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
"wasm-bindgen-shared",
]
@@ -3438,7 +3549,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.33",
"syn 2.0.37",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]

View File

@@ -5,8 +5,8 @@ resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.71"
homepage = "https://beta.ruff.rs/docs"
documentation = "https://beta.ruff.rs/docs"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
@@ -14,8 +14,8 @@ license = "MIT"
[workspace.dependencies]
anyhow = { version = "1.0.69" }
bitflags = { version = "2.3.1" }
chrono = { version = "0.4.30", default-features = false, features = ["clock"] }
clap = { version = "4.4.3", features = ["derive"] }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.4", features = ["derive"] }
colored = { version = "2.0.0" }
filetime = { version = "0.2.20" }
glob = { version = "0.3.1" }
@@ -23,10 +23,10 @@ globset = { version = "0.4.10" }
ignore = { version = "0.4.20" }
insta = { version = "1.31.0", feature = ["filters", "glob"] }
is-macro = { version = "0.3.0" }
itertools = { version = "0.10.5" }
itertools = { version = "0.11.0" }
log = { version = "0.4.17" }
malachite = { version = "0.4.0", default-features = false, features = ["naturals_and_integers"] }
memchr = "2.6.3"
num-bigint = { version = "0.4.3" }
num-traits = { version = "0.2.15" }
once_cell = { version = "1.17.1" }
path-absolutize = { version = "3.1.1" }
@@ -34,23 +34,23 @@ proc-macro2 = { version = "1.0.67" }
quote = { version = "1.0.23" }
regex = { version = "1.9.5" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.12" }
schemars = { version = "0.8.15" }
serde = { version = "1.0.152", features = ["derive"] }
serde_json = { version = "1.0.106" }
serde_json = { version = "1.0.107" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1", features = ["inline"] }
smallvec = { version = "1.10.0" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.2" }
syn = { version = "2.0.33" }
test-case = { version = "3.0.0" }
syn = { version = "2.0.37" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.48" }
toml = { version = "0.7.8" }
tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
unicode-ident = "1.0.11"
unicode-ident = "1.0.12"
unicode-width = "0.1.10"
uuid = { version = "1.4.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }

View File

@@ -8,7 +8,7 @@
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions)
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://beta.ruff.rs/docs/) | [**Playground**](https://play.ruff.rs/)
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
An extremely fast Python linter, written in Rust.
@@ -30,13 +30,13 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.11 compatibility
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- 📏 Over [600 built-in rules](https://beta.ruff.rs/docs/rules/)
- ⚖️ [Near-parity](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) with the
- 📏 Over [600 built-in rules](https://docs.astral.sh/ruff/rules/)
- ⚖️ [Near-parity](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) with the
built-in Flake8 rule set
- 🔌 Native re-implementations of dozens of Flake8 plugins, like flake8-bugbear
- ⌨️ First-party [editor integrations](https://beta.ruff.rs/docs/editor-integrations/) for
- ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/editor-integrations/) for
[VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://github.com/astral-sh/ruff-lsp)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://beta.ruff.rs/docs/configuration/#pyprojecttoml-discovery)
- 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#pyprojecttoml-discovery)
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
functionality behind a single, common interface.
@@ -98,7 +98,7 @@ developer of [Zulip](https://github.com/zulip/zulip):
## Table of Contents
For more, see the [documentation](https://beta.ruff.rs/docs/).
For more, see the [documentation](https://docs.astral.sh/ruff/).
1. [Getting Started](#getting-started)
1. [Configuration](#configuration)
@@ -111,7 +111,7 @@ For more, see the [documentation](https://beta.ruff.rs/docs/).
## Getting Started
For more, see the [documentation](https://beta.ruff.rs/docs/).
For more, see the [documentation](https://docs.astral.sh/ruff/).
### Installation
@@ -122,7 +122,7 @@ pip install ruff
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
and with [a variety of other package managers](https://beta.ruff.rs/docs/installation/).
and with [a variety of other package managers](https://docs.astral.sh/ruff/installation/).
### Usage
@@ -165,7 +165,7 @@ jobs:
### Configuration
Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file (see:
[_Configuration_](https://beta.ruff.rs/docs/configuration/), or [_Settings_](https://beta.ruff.rs/docs/settings/)
[_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/)
for a complete list of all configuration options).
If left unspecified, the default configuration is equivalent to:
@@ -238,7 +238,7 @@ isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implement
Rust as a first-party feature.
By default, Ruff enables Flake8's `E` and `F` rules. Ruff supports all rules from the `F` category,
and a [subset](https://beta.ruff.rs/docs/rules/#error-e) of the `E` category, omitting those
and a [subset](https://docs.astral.sh/ruff/rules/#error-e) of the `E` category, omitting those
stylistic rules made obsolete by the use of an autoformatter, like
[Black](https://github.com/psf/black).
@@ -304,12 +304,12 @@ quality tools, including:
- [tryceratops](https://pypi.org/project/tryceratops/)
- [yesqa](https://pypi.org/project/yesqa/)
For a complete enumeration of the supported rules, see [_Rules_](https://beta.ruff.rs/docs/rules/).
For a complete enumeration of the supported rules, see [_Rules_](https://docs.astral.sh/ruff/rules/).
## Contributing
Contributions are welcome and highly appreciated. To get started, check out the
[**contributing guidelines**](https://beta.ruff.rs/docs/contributing/).
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).

View File

@@ -86,7 +86,7 @@ flake8-to-ruff path/to/.flake8 --plugin flake8-builtins --plugin flake8-quotes
configuration options that don't exist in Flake8.)
1. Ruff will omit any rule codes that are unimplemented or unsupported by Ruff, including rule
codes from unsupported plugins. (See the
[documentation](https://beta.ruff.rs/docs/faq/#how-does-ruff-compare-to-flake8) for the complete
[documentation](https://docs.astral.sh/ruff/faq/#how-does-ruff-compare-to-flake8) for the complete
list of supported plugins.)
## License

View File

@@ -45,9 +45,9 @@ is-macro = { workspace = true }
itertools = { workspace = true }
libcst = { workspace = true }
log = { workspace = true }
malachite = { workspace = true }
memchr = { workspace = true }
natord = { version = "1.0.9" }
num-bigint = { workspace = true }
num-traits = { workspace = true }
once_cell = { workspace = true }
path-absolutize = { workspace = true, features = [

View File

@@ -0,0 +1,22 @@
from flask import Flask
app = Flask(__name__)
@app.route('/')
def main():
raise
# OK
app.run(debug=True)
# Errors
app.run()
app.run(debug=False)
# Unrelated
run()
run(debug=True)
run(debug)
foo.run(debug=True)
app = 1
app.run(debug=True)

View File

@@ -0,0 +1,9 @@
# formfeed indent
# https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458825
# This is technically a stylist bug (and has a test there), but it surfaced in B006
class FormFeedIndent:
def __init__(self, a=[]):
print(a)

View File

@@ -53,3 +53,6 @@ setattr(foo, "__123abc__", None)
setattr(foo, "abc123", None)
setattr(foo, r"abc123", None)
setattr(foo.bar, r"baz", None)
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
assert getattr(func, '_rpc')is True

View File

@@ -19,3 +19,6 @@ print(f'Hello {dict((x,f(x)) for x in "abc")} World')
# Regression test for: https://github.com/astral-sh/ruff/issues/7086
dict((k,v)for k,v in d.iteritems() if k in only_args)
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458940
dict((*v, k) for k, v in enumerate(calendar.month_abbr))

View File

@@ -0,0 +1,24 @@
import logging
from logging import getLogger
# Ok
logging.getLogger(__name__)
logging.getLogger(name=__name__)
logging.getLogger("custom")
logging.getLogger(name="custom")
# LOG002
getLogger(__file__)
logging.getLogger(name=__file__)
logging.getLogger(__cached__)
getLogger(name=__cached__)
# Override `logging.getLogger`
class logging:
def getLogger(self):
pass
logging.getLogger(__file__)

View File

@@ -0,0 +1,16 @@
import logging
logger = logging.getLogger(__name__)
logging.exception("foo") # OK
logging.exception("foo", exc_info=False) # LOG007
logging.exception("foo", exc_info=[]) # LOG007
logger.exception("foo") # OK
logger.exception("foo", exc_info=False) # LOG007
logger.exception("foo", exc_info=[]) # LOG007
from logging import exception
exception("foo", exc_info=False) # LOG007
exception("foo", exc_info=True) # OK

View File

@@ -92,3 +92,9 @@ class Test(unittest.TestCase):
def test_fail_if_equal(self):
self.failIfEqual(1, 2) # Error
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459517
(self.assertTrue(
"piAx_piAy_beta[r][x][y] = {17}".format(
self.model.piAx_piAy_beta[r][x][y])))

View File

@@ -41,9 +41,13 @@ class Test(unittest.TestCase):
assert True
from typing import override
from typing import override, overload
@override
def BAD_FUNC():
pass
@overload
def BAD_FUNC():
pass

View File

@@ -37,3 +37,4 @@ assert (not foo) in bar
assert {"x": not foo} in bar
assert [42, not foo] in bar
assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None)
assert not('name' in request)or not request['name']

View File

@@ -1,3 +1,6 @@
'''File starts with a tab
multiline string with tab in it'''
#: W191
if False:
print # indented with 1 tab

View File

@@ -49,6 +49,20 @@ class Apples:
def __doc__(self):
return "Docstring"
# Allow dunder methods recommended by attrs.
def __attrs_post_init__(self):
pass
def __attrs_pre_init__(self):
pass
def __attrs_init__(self):
pass
# Allow __html__, used by Jinja2 and Django.
def __html__(self):
pass
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
...

View File

@@ -36,3 +36,8 @@ def isinstances():
result = isinstance(var[6], int) or isinstance(var[7], int)
result = isinstance(var[6], (float, int)) or False
return result
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
if(isinstance(self.k, int)) or (isinstance(self.k, float)):
...

View File

@@ -10,6 +10,5 @@ type(arg)(" ")
# OK
y = x.dtype.type(0.0)
# OK
type = lambda *args, **kwargs: None
type("")
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459841
assert isinstance(fullname, type("")is not True)

View File

@@ -108,3 +108,9 @@ class ServiceRefOrValue:
# Regression test for: https://github.com/astral-sh/ruff/issues/7201
class ServiceRefOrValue:
service_specification: Optional[str]is not True = None
# Regression test for: https://github.com/astral-sh/ruff/issues/7452
class Collection(Protocol[*_B0]):
def __iter__(self) -> Iterator[Union[*_B0]]:
...

View File

@@ -75,3 +75,8 @@ print("foo".encode()) # print(b"foo")
(f"foo{bar}").encode(encoding="utf-8")
("unicode text©").encode("utf-8")
("unicode text©").encode(encoding="utf-8")
# Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459882
def _match_ignore(line):
input=stdin and'\n'.encode()or None

View File

@@ -0,0 +1,43 @@
def zipped():
return zip([1, 2, 3], "ABC")
# Errors.
# FURB140
[print(x, y) for x, y in zipped()]
# FURB140
(print(x, y) for x, y in zipped())
# FURB140
{print(x, y) for x, y in zipped()}
from itertools import starmap as sm
# FURB140
[print(x, y) for x, y in zipped()]
# FURB140
(print(x, y) for x, y in zipped())
# FURB140
{print(x, y) for x, y in zipped()}
# Non-errors.
[print(x, int) for x, _ in zipped()]
[print(x, *y) for x, y in zipped()]
[print(x, y, 1) for x, y in zipped()]
[print(y, x) for x, y in zipped()]
[print(x + 1, y) for x, y in zipped()]
[print(x) for x in range(100)]
[print() for x, y in zipped()]
[print(x, end=y) for x, y in zipped()]

View File

@@ -586,6 +586,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::LoggingConfigInsecureListen) {
flake8_bandit::rules::logging_config_insecure_listen(checker, call);
}
if checker.enabled(Rule::FlaskDebugTrue) {
flake8_bandit::rules::flask_debug_true(checker, call);
}
if checker.any_enabled(&[
Rule::SubprocessWithoutShellEqualsTrue,
Rule::SubprocessPopenWithShellEqualsTrue,
@@ -895,6 +898,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::DirectLoggerInstantiation) {
flake8_logging::rules::direct_logger_instantiation(checker, call);
}
if checker.enabled(Rule::InvalidGetLoggerArgument) {
flake8_logging::rules::invalid_get_logger_argument(checker, call);
}
if checker.enabled(Rule::ExceptionWithoutExcInfo) {
flake8_logging::rules::exception_without_exc_info(checker, call);
}
}
Expr::Dict(ast::ExprDict {
keys,
@@ -1270,16 +1279,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_simplify::rules::twisted_arms_in_ifexpr(checker, expr, test, body, orelse);
}
}
Expr::ListComp(ast::ExprListComp {
elt,
generators,
range: _,
})
| Expr::SetComp(ast::ExprSetComp {
elt,
generators,
range: _,
}) => {
Expr::ListComp(
comp @ ast::ExprListComp {
elt,
generators,
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1293,6 +1299,33 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::iteration_over_set(checker, &generator.iter);
}
}
if checker.enabled(Rule::ReimplementedStarmap) {
refurb::rules::reimplemented_starmap(checker, &comp.into());
}
}
Expr::SetComp(
comp @ ast::ExprSetComp {
elt,
generators,
range: _,
},
) => {
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}
if checker.enabled(Rule::IterationOverSet) {
for generator in generators {
pylint::rules::iteration_over_set(checker, &generator.iter);
}
}
if checker.enabled(Rule::ReimplementedStarmap) {
refurb::rules::reimplemented_starmap(checker, &comp.into());
}
}
Expr::DictComp(ast::ExprDictComp {
key,
@@ -1317,11 +1350,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
ruff::rules::static_key_dict_comprehension(checker, key);
}
}
Expr::GeneratorExp(ast::ExprGeneratorExp {
generators,
elt: _,
range: _,
}) => {
Expr::GeneratorExp(
generator @ ast::ExprGeneratorExp {
generators,
elt: _,
range: _,
},
) => {
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}
@@ -1330,6 +1365,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::iteration_over_set(checker, &generator.iter);
}
}
if checker.enabled(Rule::ReimplementedStarmap) {
refurb::rules::reimplemented_starmap(checker, &generator.into());
}
}
Expr::BoolOp(
bool_op @ ast::ExprBoolOp {

View File

@@ -6,6 +6,7 @@ use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::Locator;
use crate::noqa;
@@ -19,7 +20,7 @@ pub(crate) fn check_noqa(
diagnostics: &mut Vec<Diagnostic>,
path: &Path,
locator: &Locator,
comment_ranges: &[TextRange],
comment_ranges: &CommentRanges,
noqa_line_for: &NoqaMapping,
analyze_directives: bool,
settings: &Settings,

View File

@@ -9,7 +9,7 @@ use crate::registry::Rule;
use crate::rules::flake8_copyright::rules::missing_copyright_notice;
use crate::rules::pycodestyle::rules::{
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
tab_indentation, trailing_whitespace,
trailing_whitespace,
};
use crate::rules::pylint;
use crate::settings::Settings;
@@ -31,7 +31,6 @@ pub(crate) fn check_physical_lines(
let enforce_trailing_whitespace = settings.rules.enabled(Rule::TrailingWhitespace);
let enforce_blank_line_contains_whitespace =
settings.rules.enabled(Rule::BlankLineWithWhitespace);
let enforce_tab_indentation = settings.rules.enabled(Rule::TabIndentation);
let enforce_copyright_notice = settings.rules.enabled(Rule::MissingCopyrightNotice);
let mut doc_lines_iter = doc_lines.iter().peekable();
@@ -69,12 +68,6 @@ pub(crate) fn check_physical_lines(
diagnostics.push(diagnostic);
}
}
if enforce_tab_indentation {
if let Some(diagnostic) = tab_indentation(&line, indexer) {
diagnostics.push(diagnostic);
}
}
}
if enforce_no_newline_at_end_of_file {

View File

@@ -86,6 +86,10 @@ pub(crate) fn check_tokens(
}
}
if settings.rules.enabled(Rule::TabIndentation) {
pycodestyle::rules::tab_indentation(&mut diagnostics, tokens, locator, indexer);
}
if settings.rules.any_enabled(&[
Rule::InvalidCharacterBackspace,
Rule::InvalidCharacterSub,

View File

@@ -573,6 +573,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Bandit, "110") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::TryExceptPass),
(Flake8Bandit, "112") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::TryExceptContinue),
(Flake8Bandit, "113") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::RequestWithoutTimeout),
(Flake8Bandit, "201") => (RuleGroup::Preview, rules::flake8_bandit::rules::FlaskDebugTrue),
(Flake8Bandit, "301") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::SuspiciousPickleUsage),
(Flake8Bandit, "302") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::SuspiciousMarshalUsage),
(Flake8Bandit, "303") => (RuleGroup::Unspecified, rules::flake8_bandit::rules::SuspiciousInsecureHashUsage),
@@ -918,10 +919,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
(Refurb, "140") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedStarmap),
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
// flake8-logging
(Flake8Logging, "001") => (RuleGroup::Preview, rules::flake8_logging::rules::DirectLoggerInstantiation),
(Flake8Logging, "002") => (RuleGroup::Preview, rules::flake8_logging::rules::InvalidGetLoggerArgument),
(Flake8Logging, "007") => (RuleGroup::Preview, rules::flake8_logging::rules::ExceptionWithoutExcInfo),
(Flake8Logging, "009") => (RuleGroup::Preview, rules::flake8_logging::rules::UndocumentedWarn),
_ => return None,

View File

@@ -33,7 +33,7 @@ expression: content
},
"message": "`os` imported but unused",
"noqa_row": 1,
"url": "https://beta.ruff.rs/docs/rules/unused-import"
"url": "https://docs.astral.sh/ruff/rules/unused-import"
},
{
"code": "F841",
@@ -65,7 +65,7 @@ expression: content
},
"message": "Local variable `x` is assigned to but never used",
"noqa_row": 6,
"url": "https://beta.ruff.rs/docs/rules/unused-variable"
"url": "https://docs.astral.sh/ruff/rules/unused-variable"
},
{
"code": "F821",
@@ -81,6 +81,6 @@ expression: content
},
"message": "Undefined name `a`",
"noqa_row": 1,
"url": "https://beta.ruff.rs/docs/rules/undefined-name"
"url": "https://docs.astral.sh/ruff/rules/undefined-name"
}
]

View File

@@ -2,7 +2,7 @@
source: crates/ruff/src/message/json_lines.rs
expression: content
---
{"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://beta.ruff.rs/docs/rules/unused-import"}
{"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://beta.ruff.rs/docs/rules/unused-variable"}
{"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1,"url":"https://beta.ruff.rs/docs/rules/undefined-name"}
{"code":"F401","end_location":{"column":10,"row":1},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":1,"row":2},"location":{"column":1,"row":1}}],"message":"Remove unused import: `os`"},"location":{"column":8,"row":1},"message":"`os` imported but unused","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/unused-import"}
{"code":"F841","end_location":{"column":6,"row":6},"filename":"fib.py","fix":{"applicability":"Suggested","edits":[{"content":"","end_location":{"column":10,"row":6},"location":{"column":5,"row":6}}],"message":"Remove assignment to unused variable `x`"},"location":{"column":5,"row":6},"message":"Local variable `x` is assigned to but never used","noqa_row":6,"url":"https://docs.astral.sh/ruff/rules/unused-variable"}
{"code":"F821","end_location":{"column":5,"row":1},"filename":"undef.py","fix":null,"location":{"column":4,"row":1},"message":"Undefined name `a`","noqa_row":1,"url":"https://docs.astral.sh/ruff/rules/undefined-name"}

View File

@@ -11,7 +11,7 @@ use log::warn;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_python_trivia::indentation_at_offset;
use ruff_python_trivia::{indentation_at_offset, CommentRanges};
use ruff_source_file::{LineEnding, Locator};
use crate::codes::NoqaCode;
@@ -234,7 +234,7 @@ impl FileExemption {
/// globally ignored within the file.
pub(crate) fn try_extract(
contents: &str,
comment_ranges: &[TextRange],
comment_ranges: &CommentRanges,
path: &Path,
locator: &Locator,
) -> Option<Self> {
@@ -457,7 +457,7 @@ pub(crate) fn add_noqa(
path: &Path,
diagnostics: &[Diagnostic],
locator: &Locator,
commented_lines: &[TextRange],
comment_ranges: &CommentRanges,
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
) -> Result<usize> {
@@ -465,7 +465,7 @@ pub(crate) fn add_noqa(
path,
diagnostics,
locator,
commented_lines,
comment_ranges,
noqa_line_for,
line_ending,
);
@@ -477,7 +477,7 @@ fn add_noqa_inner(
path: &Path,
diagnostics: &[Diagnostic],
locator: &Locator,
commented_ranges: &[TextRange],
comment_ranges: &CommentRanges,
noqa_line_for: &NoqaMapping,
line_ending: LineEnding,
) -> (usize, String) {
@@ -487,8 +487,8 @@ fn add_noqa_inner(
// Whether the file is exempted from all checks.
// Codes that are globally exempted (within the current file).
let exemption = FileExemption::try_extract(locator.contents(), commented_ranges, path, locator);
let directives = NoqaDirectives::from_commented_ranges(commented_ranges, path, locator);
let exemption = FileExemption::try_extract(locator.contents(), comment_ranges, path, locator);
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, path, locator);
// Mark any non-ignored diagnostics.
for diagnostic in diagnostics {
@@ -658,7 +658,7 @@ pub(crate) struct NoqaDirectives<'a> {
impl<'a> NoqaDirectives<'a> {
pub(crate) fn from_commented_ranges(
comment_ranges: &[TextRange],
comment_ranges: &CommentRanges,
path: &Path,
locator: &'a Locator<'a>,
) -> Self {
@@ -800,6 +800,7 @@ mod tests {
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::Diagnostic;
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{LineEnding, Locator};
use crate::noqa::{add_noqa_inner, Directive, NoqaMapping, ParsedFileExemption};
@@ -997,7 +998,7 @@ mod tests {
path,
&[],
&Locator::new(contents),
&[],
&CommentRanges::default(),
&noqa_line_for,
LineEnding::Lf,
);
@@ -1017,7 +1018,7 @@ mod tests {
path,
&diagnostics,
&Locator::new(contents),
&[],
&CommentRanges::default(),
&noqa_line_for,
LineEnding::Lf,
);
@@ -1038,11 +1039,13 @@ mod tests {
];
let contents = "x = 1 # noqa: E741\n";
let noqa_line_for = NoqaMapping::default();
let comment_ranges =
CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(19))]);
let (count, output) = add_noqa_inner(
path,
&diagnostics,
&Locator::new(contents),
&[TextRange::new(TextSize::from(7), TextSize::from(19))],
&comment_ranges,
&noqa_line_for,
LineEnding::Lf,
);
@@ -1063,11 +1066,13 @@ mod tests {
];
let contents = "x = 1 # noqa";
let noqa_line_for = NoqaMapping::default();
let comment_ranges =
CommentRanges::new(vec![TextRange::new(TextSize::from(7), TextSize::from(13))]);
let (count, output) = add_noqa_inner(
path,
&diagnostics,
&Locator::new(contents),
&[TextRange::new(TextSize::from(7), TextSize::from(13))],
&comment_ranges,
&noqa_line_for,
LineEnding::Lf,
);

View File

@@ -251,7 +251,6 @@ impl Rule {
| Rule::MissingCopyrightNotice
| Rule::MissingNewlineAtEndOfFile
| Rule::MixedSpacesAndTabs
| Rule::TabIndentation
| Rule::TrailingWhitespace => LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
@@ -292,6 +291,7 @@ impl Rule {
| Rule::ShebangNotExecutable
| Rule::ShebangNotFirstLine
| Rule::SingleLineImplicitStringConcatenation
| Rule::TabIndentation
| Rule::TrailingCommaOnBareTuple
| Rule::TypeCommentInStub
| Rule::UselessSemicolon

View File

@@ -1,8 +1,6 @@
use num_bigint::BigInt;
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -237,7 +235,7 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
..
}) = slice.as_ref()
{
if *i == BigInt::from(0) {
if *i == 0 {
if let (
[CmpOp::Eq | CmpOp::NotEq],
[Expr::Constant(ast::ExprConstant {
@@ -246,13 +244,13 @@ pub(crate) fn compare(checker: &mut Checker, left: &Expr, ops: &[CmpOp], compara
})],
) = (ops, comparators)
{
if *n == BigInt::from(3) && checker.enabled(Rule::SysVersionInfo0Eq3) {
if *n == 3 && checker.enabled(Rule::SysVersionInfo0Eq3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionInfo0Eq3, left.range()));
}
}
} else if *i == BigInt::from(1) {
} else if *i == 1 {
if let (
[CmpOp::Lt | CmpOp::LtE | CmpOp::Gt | CmpOp::GtE],
[Expr::Constant(ast::ExprConstant {

View File

@@ -1,4 +1,3 @@
use num_bigint::BigInt;
use ruff_python_ast::{self as ast, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
@@ -184,11 +183,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
..
}) = upper.as_ref()
{
if *i == BigInt::from(1) && checker.enabled(Rule::SysVersionSlice1) {
if *i == 1 && checker.enabled(Rule::SysVersionSlice1) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice1, value.range()));
} else if *i == BigInt::from(3) && checker.enabled(Rule::SysVersionSlice3) {
} else if *i == 3 && checker.enabled(Rule::SysVersionSlice3) {
checker
.diagnostics
.push(Diagnostic::new(SysVersionSlice3, value.range()));
@@ -200,11 +199,11 @@ pub(crate) fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
value: Constant::Int(i),
..
}) => {
if *i == BigInt::from(2) && checker.enabled(Rule::SysVersion2) {
if *i == 2 && checker.enabled(Rule::SysVersion2) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion2, value.range()));
} else if *i == BigInt::from(0) && checker.enabled(Rule::SysVersion0) {
} else if *i == 0 && checker.enabled(Rule::SysVersion0) {
checker
.diagnostics
.push(Diagnostic::new(SysVersion0, value.range()));

View File

@@ -19,6 +19,7 @@ mod tests {
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"))]
#[test_case(Rule::CallWithShellEqualsTrue, Path::new("S604.py"))]
#[test_case(Rule::ExecBuiltin, Path::new("S102.py"))]
#[test_case(Rule::FlaskDebugTrue, Path::new("S201.py"))]
#[test_case(Rule::HardcodedBindAllInterfaces, Path::new("S104.py"))]
#[test_case(Rule::HardcodedPasswordDefault, Path::new("S107.py"))]
#[test_case(Rule::HardcodedPasswordFuncArg, Path::new("S106.py"))]

View File

@@ -1,4 +1,4 @@
use num_traits::ToPrimitive;
use malachite::Integer;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -36,7 +36,7 @@ use crate::checkers::ast::Checker;
/// - [Common Weakness Enumeration: CWE-732](https://cwe.mitre.org/data/definitions/732.html)
#[violation]
pub struct BadFilePermissions {
mask: u16,
mask: Integer,
}
impl Violation for BadFilePermissions {
@@ -56,7 +56,9 @@ pub(crate) fn bad_file_permissions(checker: &mut Checker, call: &ast::ExprCall)
{
if let Some(mode_arg) = call.arguments.find_argument("mode", 1) {
if let Some(int_value) = int_value(mode_arg, checker.semantic()) {
if (int_value & WRITE_WORLD > 0) || (int_value & EXECUTE_GROUP > 0) {
if (int_value.clone() & Integer::from(WRITE_WORLD) > 0)
|| (int_value.clone() & Integer::from(EXECUTE_GROUP) > 0)
{
checker.diagnostics.push(Diagnostic::new(
BadFilePermissions { mask: int_value },
mode_arg.range(),
@@ -113,13 +115,17 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
}
}
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<u16> {
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<Integer> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) => value.to_u16(),
Expr::Attribute(_) => semantic.resolve_call_path(expr).as_ref().and_then(py_stat),
}) => Some(value.clone()),
Expr::Attribute(_) => semantic
.resolve_call_path(expr)
.as_ref()
.and_then(py_stat)
.map(Integer::from),
Expr::BinOp(ast::ExprBinOp {
left,
op,

View File

@@ -0,0 +1,89 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::{Expr, ExprAttribute, ExprCall, Stmt, StmtAssign};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `debug=True` in Flask.
///
/// ## Why is this bad?
/// Enabling debug mode shows an interactive debugger in the browser if an
/// error occurs, and allows running arbitrary Python code from the browser.
/// This could leak sensitive information, or allow an attacker to run
/// arbitrary code.
///
/// ## Example
/// ```python
/// import flask
///
/// app = Flask()
///
/// app.run(debug=True)
/// ```
///
/// Use instead:
/// ```python
/// import flask
///
/// app = Flask()
///
/// app.run(debug=os.environ["ENV"] == "dev")
/// ```
///
/// ## References
/// - [Flask documentation: Debug Mode](https://flask.palletsprojects.com/en/latest/quickstart/#debug-mode)
#[violation]
pub struct FlaskDebugTrue;
impl Violation for FlaskDebugTrue {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of `debug=True` in Flask app detected")
}
}
/// S201
pub(crate) fn flask_debug_true(checker: &mut Checker, call: &ExprCall) {
let Expr::Attribute(ExprAttribute { attr, value, .. }) = call.func.as_ref() else {
return;
};
if attr.as_str() != "run" {
return;
}
let Some(debug_argument) = call.arguments.find_keyword("debug") else {
return;
};
if !is_const_true(&debug_argument.value) {
return;
}
let Expr::Name(name) = value.as_ref() else {
return;
};
if let Some(binding_id) = checker.semantic().resolve_name(name) {
if let Some(Stmt::Assign(StmtAssign { value, .. })) = checker
.semantic()
.binding(binding_id)
.statement(checker.semantic())
{
if let Expr::Call(ExprCall { func, .. }) = value.as_ref() {
if checker
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["flask", "Flask"]))
{
checker
.diagnostics
.push(Diagnostic::new(FlaskDebugTrue, debug_argument.range()));
}
}
}
};
}

View File

@@ -1,6 +1,7 @@
pub(crate) use assert_used::*;
pub(crate) use bad_file_permissions::*;
pub(crate) use exec_used::*;
pub(crate) use flask_debug_true::*;
pub(crate) use hardcoded_bind_all_interfaces::*;
pub(crate) use hardcoded_password_default::*;
pub(crate) use hardcoded_password_func_arg::*;
@@ -24,6 +25,7 @@ pub(crate) use unsafe_yaml_load::*;
mod assert_used;
mod bad_file_permissions;
mod exec_used;
mod flask_debug_true;
mod hardcoded_bind_all_interfaces;
mod hardcoded_password_default;
mod hardcoded_password_func_arg;

View File

@@ -1,5 +1,3 @@
use num_traits::{One, Zero};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr};
@@ -57,7 +55,7 @@ pub(crate) fn snmp_insecure_version(checker: &mut Checker, call: &ast::ExprCall)
..
}) = &keyword.value
{
if value.is_zero() || value.is_one() {
if *value == 0 || *value == 1 {
checker
.diagnostics
.push(Diagnostic::new(SnmpInsecureVersion, keyword.range()));

View File

@@ -0,0 +1,13 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
---
S201.py:10:9: S201 Use of `debug=True` in Flask app detected
|
9 | # OK
10 | app.run(debug=True)
| ^^^^^^^^^^ S201
11 |
12 | # Errors
|

View File

@@ -36,6 +36,7 @@ mod tests {
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_1.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_2.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_3.py"))]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_4.py"))]
#[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))]
#[test_case(Rule::RaiseLiteral, Path::new("B016.py"))]
#[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))]

View File

@@ -1,3 +1,4 @@
use crate::autofix::edits::pad;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr};
@@ -80,17 +81,21 @@ pub(crate) fn getattr_with_constant(
let mut diagnostic = Diagnostic::new(GetAttrWithConstant, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
if matches!(
obj,
Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) | Expr::Call(_)
) {
format!("{}.{}", checker.locator().slice(obj), value)
} else {
// Defensively parenthesize any other expressions. For example, attribute accesses
// on `int` literals must be parenthesized, e.g., `getattr(1, "real")` becomes
// `(1).real`. The same is true for named expressions and others.
format!("({}).{}", checker.locator().slice(obj), value)
},
pad(
if matches!(
obj,
Expr::Name(_) | Expr::Attribute(_) | Expr::Subscript(_) | Expr::Call(_)
) {
format!("{}.{}", checker.locator().slice(obj), value)
} else {
// Defensively parenthesize any other expressions. For example, attribute accesses
// on `int` literals must be parenthesized, e.g., `getattr(1, "real")` becomes
// `(1).real`. The same is true for named expressions and others.
format!("({}).{}", checker.locator().slice(obj), value)
},
expr.range(),
checker.locator(),
),
expr.range(),
)));
}

View File

@@ -0,0 +1,24 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_4.py:7:26: B006 [*] Do not use mutable data structures for argument defaults
|
6 | class FormFeedIndent:
7 | def __init__(self, a=[]):
| ^^ B006
8 | print(a)
|
= help: Replace with `None`; initialize within function
Possible fix
4 4 |
5 5 |
6 6 | class FormFeedIndent:
7 |- def __init__(self, a=[]):
7 |+ def __init__(self, a=None):
8 |+ if a is None:
9 |+ a = []
8 10 | print(a)
9 11 |

View File

@@ -316,4 +316,19 @@ B009_B010.py:34:1: B009 [*] Do not call `getattr` with a constant attribute valu
37 37 |
38 38 | # Valid setattr usage
B009_B010.py:58:8: B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
|
57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
58 | assert getattr(func, '_rpc')is True
| ^^^^^^^^^^^^^^^^^^^^^ B009
|
= help: Replace `getattr` with attribute access
Suggested fix
55 55 | setattr(foo.bar, r"baz", None)
56 56 |
57 57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
58 |-assert getattr(func, '_rpc')is True
58 |+assert func._rpc is True

View File

@@ -82,6 +82,7 @@ B009_B010.py:53:1: B010 [*] Do not call `setattr` with a constant attribute valu
53 |+foo.abc123 = None
54 54 | setattr(foo, r"abc123", None)
55 55 | setattr(foo.bar, r"baz", None)
56 56 |
B009_B010.py:54:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
@@ -100,6 +101,8 @@ B009_B010.py:54:1: B010 [*] Do not call `setattr` with a constant attribute valu
54 |-setattr(foo, r"abc123", None)
54 |+foo.abc123 = None
55 55 | setattr(foo.bar, r"baz", None)
56 56 |
57 57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
B009_B010.py:55:1: B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
|
@@ -107,6 +110,8 @@ B009_B010.py:55:1: B010 [*] Do not call `setattr` with a constant attribute valu
54 | setattr(foo, r"abc123", None)
55 | setattr(foo.bar, r"baz", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B010
56 |
57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
|
= help: Replace `setattr` with assignment
@@ -116,5 +121,8 @@ B009_B010.py:55:1: B010 [*] Do not call `setattr` with a constant attribute valu
54 54 | setattr(foo, r"abc123", None)
55 |-setattr(foo.bar, r"baz", None)
55 |+foo.bar.baz = None
56 56 |
57 57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
58 58 | assert getattr(func, '_rpc')is True

View File

@@ -54,18 +54,23 @@ pub(crate) fn unnecessary_generator_dict(
else {
return;
};
if let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument {
match elt.as_ref() {
Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.len() == 2 => {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_dict(expr, checker).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);
}
_ => {}
}
let Expr::GeneratorExp(ast::ExprGeneratorExp { elt, .. }) = argument else {
return;
};
let Expr::Tuple(ast::ExprTuple { elts, .. }) = elt.as_ref() else {
return;
};
if elts.len() != 2 {
return;
}
if elts.iter().any(Expr::is_starred_expr) {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_dict(expr, checker).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,5 +1,3 @@
use num_bigint::BigInt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
@@ -93,7 +91,7 @@ pub(crate) fn unnecessary_subscript_reversal(
else {
return;
};
if *val != BigInt::from(1) {
if *val != 1 {
return;
};
checker.diagnostics.push(Diagnostic::new(

View File

@@ -251,6 +251,8 @@ C402.py:21:1: C402 [*] Unnecessary generator (rewrite as a `dict` comprehension)
20 | # Regression test for: https://github.com/astral-sh/ruff/issues/7086
21 | dict((k,v)for k,v in d.iteritems() if k in only_args)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C402
22 |
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458940
|
= help: Rewrite as a `dict` comprehension
@@ -260,5 +262,8 @@ C402.py:21:1: C402 [*] Unnecessary generator (rewrite as a `dict` comprehension)
20 20 | # Regression test for: https://github.com/astral-sh/ruff/issues/7086
21 |-dict((k,v)for k,v in d.iteritems() if k in only_args)
21 |+{k: v for k,v in d.iteritems() if k in only_args}
22 22 |
23 23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458940
24 24 | dict((*v, k) for k, v in enumerate(calendar.month_abbr))

View File

@@ -14,6 +14,8 @@ mod tests {
use crate::test::test_path;
#[test_case(Rule::DirectLoggerInstantiation, Path::new("LOG001.py"))]
#[test_case(Rule::InvalidGetLoggerArgument, Path::new("LOG002.py"))]
#[test_case(Rule::ExceptionWithoutExcInfo, Path::new("LOG007.py"))]
#[test_case(Rule::UndocumentedWarn, Path::new("LOG009.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View File

@@ -0,0 +1,65 @@
use ruff_python_ast::ExprCall;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_semantic::analyze::logging::is_logger_candidate;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for uses of `logging.exception()` with `exc_info` set to `False`.
///
/// ## Why is this bad?
/// The `logging.exception()` method captures the exception automatically, but
/// accepts an optional `exc_info` argument to override this behavior. Setting
/// `exc_info` to `False` disables the automatic capture of the exception and
/// stack trace.
///
/// Instead of setting `exc_info` to `False`, prefer `logging.error()`, which
/// has equivalent behavior to `logging.exception()` with `exc_info` set to
/// `False`, but is clearer in intent.
///
/// ## Example
/// ```python
/// logging.exception("...", exc_info=False)
/// ```
///
/// Use instead:
/// ```python
/// logging.error("...")
/// ```
#[violation]
pub struct ExceptionWithoutExcInfo;
impl Violation for ExceptionWithoutExcInfo {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of `logging.exception` with falsy `exc_info`")
}
}
/// LOG007
pub(crate) fn exception_without_exc_info(checker: &mut Checker, call: &ExprCall) {
if !is_logger_candidate(
call.func.as_ref(),
checker.semantic(),
&["exception".to_string()],
) {
return;
}
if call
.arguments
.find_keyword("exc_info")
.map(|keyword| &keyword.value)
.is_some_and(|value| {
Truthiness::from_expr(value, |id| checker.semantic().is_builtin(id)).is_falsey()
})
{
checker
.diagnostics
.push(Diagnostic::new(ExceptionWithoutExcInfo, call.range()));
}
}

View File

@@ -0,0 +1,92 @@
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for any usage of `__cached__` and `__file__` as an argument to
/// `logging.getLogger()`.
///
/// ## Why is this bad?
/// The [logging documentation] recommends this pattern:
///
/// ```python
/// logging.getLogger(__name__)
/// ```
///
/// Here, `__name__` is the fully qualified module name, such as `foo.bar`,
/// which is the intended format for logger names.
///
/// This rule detects probably-mistaken usage of similar module-level dunder constants:
///
/// * `__cached__` - the pathname of the module's compiled version, such as `foo/__pycache__/bar.cpython-311.pyc`.
/// * `__file__` - the pathname of the module, such as `foo/bar.py`.
///
/// ## Example
/// ```python
/// import logging
///
/// logger = logging.getLogger(__file__)
/// ```
///
/// Use instead:
/// ```python
/// import logging
///
/// logger = logging.getLogger(__name__)
/// ```
///
/// [logging documentation]: https://docs.python.org/3/library/logging.html#logger-objects
#[violation]
pub struct InvalidGetLoggerArgument;
impl Violation for InvalidGetLoggerArgument {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `__name__` with `logging.getLogger()`")
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `__name__`"))
}
}
/// LOG002
pub(crate) fn invalid_get_logger_argument(checker: &mut Checker, call: &ast::ExprCall) {
let Some(Expr::Name(expr @ ast::ExprName { id, .. })) = call.arguments.find_argument("name", 0)
else {
return;
};
if !matches!(id.as_ref(), "__file__" | "__cached__") {
return;
}
if !checker.semantic().is_builtin(id) {
return;
}
if !checker
.semantic()
.resolve_call_path(call.func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "getLogger"]))
{
return;
}
let mut diagnostic = Diagnostic::new(InvalidGetLoggerArgument, expr.range());
if checker.patch(diagnostic.kind.rule()) {
if checker.semantic().is_builtin("__name__") {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
"__name__".to_string(),
expr.range(),
)));
}
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,5 +1,9 @@
pub(crate) use direct_logger_instantiation::*;
pub(crate) use exception_without_exc_info::*;
pub(crate) use invalid_get_logger_argument::*;
pub(crate) use undocumented_warn::*;
mod direct_logger_instantiation;
mod exception_without_exc_info;
mod invalid_get_logger_argument;
mod undocumented_warn;

View File

@@ -0,0 +1,82 @@
---
source: crates/ruff/src/rules/flake8_logging/mod.rs
---
LOG002.py:11:11: LOG002 [*] Use `__name__` with `logging.getLogger()`
|
10 | # LOG002
11 | getLogger(__file__)
| ^^^^^^^^ LOG002
12 | logging.getLogger(name=__file__)
|
= help: Replace with `name`
Suggested fix
8 8 | logging.getLogger(name="custom")
9 9 |
10 10 | # LOG002
11 |-getLogger(__file__)
11 |+getLogger(__name__)
12 12 | logging.getLogger(name=__file__)
13 13 |
14 14 | logging.getLogger(__cached__)
LOG002.py:12:24: LOG002 [*] Use `__name__` with `logging.getLogger()`
|
10 | # LOG002
11 | getLogger(__file__)
12 | logging.getLogger(name=__file__)
| ^^^^^^^^ LOG002
13 |
14 | logging.getLogger(__cached__)
|
= help: Replace with `name`
Suggested fix
9 9 |
10 10 | # LOG002
11 11 | getLogger(__file__)
12 |-logging.getLogger(name=__file__)
12 |+logging.getLogger(name=__name__)
13 13 |
14 14 | logging.getLogger(__cached__)
15 15 | getLogger(name=__cached__)
LOG002.py:14:19: LOG002 [*] Use `__name__` with `logging.getLogger()`
|
12 | logging.getLogger(name=__file__)
13 |
14 | logging.getLogger(__cached__)
| ^^^^^^^^^^ LOG002
15 | getLogger(name=__cached__)
|
= help: Replace with `name`
Suggested fix
11 11 | getLogger(__file__)
12 12 | logging.getLogger(name=__file__)
13 13 |
14 |-logging.getLogger(__cached__)
14 |+logging.getLogger(__name__)
15 15 | getLogger(name=__cached__)
16 16 |
17 17 |
LOG002.py:15:16: LOG002 [*] Use `__name__` with `logging.getLogger()`
|
14 | logging.getLogger(__cached__)
15 | getLogger(name=__cached__)
| ^^^^^^^^^^ LOG002
|
= help: Replace with `name`
Suggested fix
12 12 | logging.getLogger(name=__file__)
13 13 |
14 14 | logging.getLogger(__cached__)
15 |-getLogger(name=__cached__)
15 |+getLogger(name=__name__)
16 16 |
17 17 |
18 18 | # Override `logging.getLogger`

View File

@@ -0,0 +1,40 @@
---
source: crates/ruff/src/rules/flake8_logging/mod.rs
---
LOG007.py:6:1: LOG007 Use of `logging.exception` with falsy `exc_info`
|
5 | logging.exception("foo") # OK
6 | logging.exception("foo", exc_info=False) # LOG007
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LOG007
7 | logging.exception("foo", exc_info=[]) # LOG007
8 | logger.exception("foo") # OK
|
LOG007.py:7:1: LOG007 Use of `logging.exception` with falsy `exc_info`
|
5 | logging.exception("foo") # OK
6 | logging.exception("foo", exc_info=False) # LOG007
7 | logging.exception("foo", exc_info=[]) # LOG007
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LOG007
8 | logger.exception("foo") # OK
9 | logger.exception("foo", exc_info=False) # LOG007
|
LOG007.py:9:1: LOG007 Use of `logging.exception` with falsy `exc_info`
|
7 | logging.exception("foo", exc_info=[]) # LOG007
8 | logger.exception("foo") # OK
9 | logger.exception("foo", exc_info=False) # LOG007
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LOG007
10 | logger.exception("foo", exc_info=[]) # LOG007
|
LOG007.py:10:1: LOG007 Use of `logging.exception` with falsy `exc_info`
|
8 | logger.exception("foo") # OK
9 | logger.exception("foo", exc_info=False) # LOG007
10 | logger.exception("foo", exc_info=[]) # LOG007
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ LOG007
|

View File

@@ -1,5 +1,3 @@
use num_bigint::BigInt;
use ruff_diagnostics::Diagnostic;
use ruff_diagnostics::{AlwaysAutofixableViolation, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -75,7 +73,7 @@ pub(crate) fn unnecessary_range_start(checker: &mut Checker, call: &ast::ExprCal
else {
return;
};
if *value != BigInt::from(0) {
if *value != 0 {
return;
};

View File

@@ -6,6 +6,26 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::registry::Rule;
/// ## What it does
/// Checks for `pass` statements in empty stub bodies.
///
/// ## Why is this bad?
/// For consistency, empty stub bodies should contain `...` instead of `pass`.
///
/// Additionally, an ellipsis better conveys the intent of the stub body (that
/// the body has been implemented, but has been intentionally left blank to
/// document the interface).
///
/// ## Example
/// ```python
/// def foo(bar: int) -> list[int]:
/// pass
/// ```
///
/// Use instead:
/// ```python
/// def foo(bar: int) -> list[int]: ...
/// ```
#[violation]
pub struct PassStatementStubBody;

View File

@@ -1,5 +1,3 @@
use num_bigint::BigInt;
use num_traits::{One, Zero};
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_diagnostics::{Diagnostic, Violation};
@@ -249,10 +247,10 @@ impl ExpectedComparator {
..
}) = upper.as_ref()
{
if *upper == BigInt::one() {
if *upper == 1 {
return Some(ExpectedComparator::MajorTuple);
}
if *upper == BigInt::from(2) {
if *upper == 2 {
return Some(ExpectedComparator::MajorMinorTuple);
}
}
@@ -260,7 +258,7 @@ impl ExpectedComparator {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(n),
..
}) if n.is_zero() => {
}) if *n == 0 => {
return Some(ExpectedComparator::MajorDigit);
}
_ => (),

View File

@@ -10,6 +10,7 @@ use libcst_native::{
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::Truthiness;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{
self as ast, Arguments, BoolOp, ExceptHandler, Expr, Keyword, Stmt, UnaryOp,
@@ -293,7 +294,13 @@ pub(crate) fn unittest_assertion(
if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) {
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().stmt(&stmt),
expr.range(),
parenthesized_range(
expr.into(),
checker.semantic().current_statement().into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range()),
)));
}
}

View File

@@ -9,6 +9,7 @@ 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};
use ruff_python_codegen::Generator;
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -298,12 +299,17 @@ fn elts_to_csv(elts: &[Expr], generator: Generator) -> Option<String> {
fn get_parametrize_name_range(
decorator: &Decorator,
expr: &Expr,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<TextRange> {
decorator
.expression
.as_call_expr()
.and_then(|call| parenthesized_range(expr.into(), call.arguments.as_any_node_ref(), source))
decorator.expression.as_call_expr().and_then(|call| {
parenthesized_range(
expr.into(),
call.arguments.as_any_node_ref(),
comment_ranges,
source,
)
})
}
/// PT006
@@ -322,6 +328,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range());
@@ -356,6 +363,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
let name_range = get_parametrize_name_range(
decorator,
expr,
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(expr.range());

View File

@@ -637,5 +637,27 @@ PT009.py:94:9: PT009 [*] Use a regular `assert` instead of unittest-style `failI
93 93 | def test_fail_if_equal(self):
94 |- self.failIfEqual(1, 2) # Error
94 |+ assert 1 != 2 # Error
95 95 |
96 96 |
97 97 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459517
PT009.py:98:2: PT009 [*] Use a regular `assert` instead of unittest-style `assertTrue`
|
97 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459517
98 | (self.assertTrue(
| ^^^^^^^^^^^^^^^ PT009
99 | "piAx_piAy_beta[r][x][y] = {17}".format(
100 | self.model.piAx_piAy_beta[r][x][y])))
|
= help: Replace `assertTrue(...)` with `assert ...`
Suggested fix
95 95 |
96 96 |
97 97 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459517
98 |-(self.assertTrue(
99 |- "piAx_piAy_beta[r][x][y] = {17}".format(
100 |- self.model.piAx_piAy_beta[r][x][y])))
98 |+assert "piAx_piAy_beta[r][x][y] = {17}".format(self.model.piAx_piAy_beta[r][x][y])

View File

@@ -773,9 +773,14 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit(
value,
TextRange::new(
parenthesized_range(furthest.into(), expr.into(), checker.locator().contents())
.unwrap_or(furthest.range())
.start(),
parenthesized_range(
furthest.into(),
expr.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range())
.start(),
expr.end(),
),
short_circuit_truthiness,
@@ -796,9 +801,14 @@ fn is_short_circuit(
edit = Some(get_short_circuit_edit(
next_value,
TextRange::new(
parenthesized_range(furthest.into(), expr.into(), checker.locator().contents())
.unwrap_or(furthest.range())
.start(),
parenthesized_range(
furthest.into(),
expr.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(furthest.range())
.start(),
expr.end(),
),
short_circuit_truthiness,

View File

@@ -163,8 +163,13 @@ pub(crate) fn if_expr_with_true_false(
checker
.locator()
.slice(
parenthesized_range(test.into(), expr.into(), checker.locator().contents())
.unwrap_or(test.range()),
parenthesized_range(
test.into(),
expr.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(test.range()),
)
.to_string(),
expr.range(),

View File

@@ -88,10 +88,20 @@ fn key_in_dict(
}
// Extract the exact range of the left and right expressions.
let left_range = parenthesized_range(left.into(), parent, checker.locator().contents())
.unwrap_or(left.range());
let right_range = parenthesized_range(right.into(), parent, checker.locator().contents())
.unwrap_or(right.range());
let left_range = parenthesized_range(
left.into(),
parent,
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(left.range());
let right_range = parenthesized_range(
right.into(),
parent,
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(right.range());
let mut diagnostic = Diagnostic::new(
InDictKeys {

View File

@@ -3,6 +3,7 @@ use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Keyword, Stmt};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
@@ -85,6 +86,7 @@ pub(crate) fn inplace_argument(checker: &mut Checker, call: &ast::ExprCall) {
call,
keyword,
statement,
checker.indexer().comment_ranges(),
checker.locator(),
) {
diagnostic.set_fix(fix);
@@ -107,15 +109,21 @@ fn convert_inplace_argument_to_assignment(
call: &ast::ExprCall,
keyword: &Keyword,
statement: &Stmt,
comment_ranges: &CommentRanges,
locator: &Locator,
) -> Option<Fix> {
// Add the assignment.
let attr = call.func.as_attribute_expr()?;
let insert_assignment = Edit::insertion(
format!("{name} = ", name = locator.slice(attr.value.range())),
parenthesized_range(call.into(), statement.into(), locator.contents())
.unwrap_or(call.range())
.start(),
parenthesized_range(
call.into(),
statement.into(),
comment_ranges,
locator.contents(),
)
.unwrap_or(call.range())
.start(),
);
// Remove the `inplace` argument.

View File

@@ -1,4 +1,3 @@
use num_traits::One;
use ruff_python_ast::{self as ast, CmpOp, Constant, Expr};
use ruff_diagnostics::Diagnostic;
@@ -115,7 +114,7 @@ pub(crate) fn nunique_constant_series_check(
fn is_constant_one(expr: &Expr) -> bool {
match expr {
Expr::Constant(constant) => match &constant.value {
Constant::Int(int) => int.is_one(),
Constant::Int(int) => *int == 1,
_ => false,
},
_ => false,

View File

@@ -70,9 +70,12 @@ pub(crate) fn invalid_function_name(
return None;
}
// Ignore any functions that are explicitly `@override`. These are defined elsewhere,
// so if they're first-party, we'll flag them at the definition site.
if visibility::is_override(decorator_list, semantic) {
// Ignore any functions that are explicitly `@override` or `@overload`.
// These are defined elsewhere, so if they're first-party,
// we'll flag them at the definition site.
if visibility::is_override(decorator_list, semantic)
|| visibility::is_overload(decorator_list, semantic)
{
return None;
}

View File

@@ -3,6 +3,7 @@ use unicode_width::UnicodeWidthStr;
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{CmpOp, Expr};
use ruff_python_trivia::CommentRanges;
use ruff_source_file::{Line, Locator};
use ruff_text_size::{Ranged, TextLen, TextRange};
@@ -17,6 +18,7 @@ pub(super) fn generate_comparison(
ops: &[CmpOp],
comparators: &[Expr],
parent: AnyNodeRef,
comment_ranges: &CommentRanges,
locator: &Locator,
) -> String {
let start = left.start();
@@ -24,9 +26,12 @@ pub(super) fn generate_comparison(
let mut contents = String::with_capacity(usize::from(end - start));
// Add the left side of the comparison.
contents.push_str(locator.slice(
parenthesized_range(left.into(), parent, locator.contents()).unwrap_or(left.range()),
));
contents.push_str(
locator.slice(
parenthesized_range(left.into(), parent, comment_ranges, locator.contents())
.unwrap_or(left.range()),
),
);
for (op, comparator) in ops.iter().zip(comparators) {
// Add the operator.
@@ -46,8 +51,13 @@ pub(super) fn generate_comparison(
// Add the right side of the comparison.
contents.push_str(
locator.slice(
parenthesized_range(comparator.into(), parent, locator.contents())
.unwrap_or(comparator.range()),
parenthesized_range(
comparator.into(),
parent,
comment_ranges,
locator.contents(),
)
.unwrap_or(comparator.range()),
),
);
}

View File

@@ -283,6 +283,7 @@ pub(crate) fn literal_comparisons(checker: &mut Checker, compare: &ast::ExprComp
&ops,
&compare.comparators,
compare.into(),
checker.indexer().comment_ranges(),
checker.locator(),
);
for diagnostic in &mut diagnostics {

View File

@@ -1,3 +1,4 @@
use crate::autofix::edits::pad;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, CmpOp, Expr};
@@ -95,11 +96,16 @@ pub(crate) fn not_tests(checker: &mut Checker, unary_op: &ast::ExprUnaryOp) {
let mut diagnostic = Diagnostic::new(NotInTest, unary_op.operand.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
generate_comparison(
left,
&[CmpOp::NotIn],
comparators,
unary_op.into(),
pad(
generate_comparison(
left,
&[CmpOp::NotIn],
comparators,
unary_op.into(),
checker.indexer().comment_ranges(),
checker.locator(),
),
unary_op.range(),
checker.locator(),
),
unary_op.range(),
@@ -113,11 +119,16 @@ pub(crate) fn not_tests(checker: &mut Checker, unary_op: &ast::ExprUnaryOp) {
let mut diagnostic = Diagnostic::new(NotIsTest, unary_op.operand.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
generate_comparison(
left,
&[CmpOp::IsNot],
comparators,
unary_op.into(),
pad(
generate_comparison(
left,
&[CmpOp::IsNot],
comparators,
unary_op.into(),
checker.indexer().comment_ranges(),
checker.locator(),
),
unary_op.range(),
checker.locator(),
),
unary_op.range(),

View File

@@ -1,10 +1,11 @@
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Tok;
use ruff_python_trivia::leading_indentation;
use ruff_source_file::Line;
use ruff_source_file::Locator;
use ruff_text_size::{TextLen, TextRange, TextSize};
/// ## What it does
/// Checks for indentation that uses tabs.
@@ -37,17 +38,46 @@ impl Violation for TabIndentation {
}
/// W191
pub(crate) fn tab_indentation(line: &Line, indexer: &Indexer) -> Option<Diagnostic> {
let indent = leading_indentation(line);
if let Some(tab_index) = indent.find('\t') {
// If the tab character is within a multi-line string, abort.
let tab_offset = line.start() + TextSize::try_from(tab_index).unwrap();
if indexer.triple_quoted_string_range(tab_offset).is_none() {
return Some(Diagnostic::new(
TabIndentation,
TextRange::at(line.start(), indent.text_len()),
));
pub(crate) fn tab_indentation(
diagnostics: &mut Vec<Diagnostic>,
tokens: &[LexResult],
locator: &Locator,
indexer: &Indexer,
) {
// Always check the first line for tab indentation as there's no newline
// token before it.
tab_indentation_at_line_start(diagnostics, locator, TextSize::default());
for (tok, range) in tokens.iter().flatten() {
if matches!(tok, Tok::Newline | Tok::NonLogicalNewline) {
tab_indentation_at_line_start(diagnostics, locator, range.end());
}
}
None
// The lexer doesn't emit `Newline` / `NonLogicalNewline` for a line
// continuation character (`\`), so we need to manually check for tab
// indentation for lines that follow a line continuation character.
for continuation_line in indexer.continuation_line_starts() {
tab_indentation_at_line_start(
diagnostics,
locator,
locator.full_line_end(*continuation_line),
);
}
}
/// Checks for indentation that uses tabs for a line starting at
/// the given [`TextSize`].
fn tab_indentation_at_line_start(
diagnostics: &mut Vec<Diagnostic>,
locator: &Locator,
line_start: TextSize,
) {
let indent = leading_indentation(locator.after(line_start));
if indent.find('\t').is_some() {
diagnostics.push(Diagnostic::new(
TabIndentation,
TextRange::at(line_start, indent.text_len()),
));
}
}

View File

@@ -102,4 +102,20 @@ E713.py:14:9: E713 [*] Test for membership should be `not in`
16 16 |
17 17 | #: Okay
E713.py:40:12: E713 [*] Test for membership should be `not in`
|
38 | assert [42, not foo] in bar
39 | assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None)
40 | assert not('name' in request)or not request['name']
| ^^^^^^^^^^^^^^^^^ E713
|
= help: Convert to `not in`
Fix
37 37 | assert {"x": not foo} in bar
38 38 | assert [42, not foo] in bar
39 39 | assert not (re.search(r"^.:\\Users\\[^\\]*\\Downloads\\.*") is None)
40 |-assert not('name' in request)or not request['name']
40 |+assert 'name' not in request or not request['name']

View File

@@ -1,345 +1,352 @@
---
source: crates/ruff/src/rules/pycodestyle/mod.rs
---
W19.py:3:1: W191 Indentation contains tabs
W19.py:1:1: W191 Indentation contains tabs
|
1 | #: W191
2 | if False:
3 | print # indented with 1 tab
1 | '''File starts with a tab
| ^^^^ W191
4 | #:
2 | multiline string with tab in it'''
|
W19.py:9:1: W191 Indentation contains tabs
W19.py:6:1: W191 Indentation contains tabs
|
4 | #: W191
5 | if False:
6 | print # indented with 1 tab
| ^^^^ W191
7 | #:
|
W19.py:12:1: W191 Indentation contains tabs
|
7 | #: W191
8 | y = x == 2 \
9 | or x == 3
10 | #: W191
11 | y = x == 2 \
12 | or x == 3
| ^^^^ W191
10 | #: E101 W191 W504
11 | if (
13 | #: E101 W191 W504
14 | if (
|
W19.py:16:1: W191 Indentation contains tabs
W19.py:19:1: W191 Indentation contains tabs
|
14 | ) or
15 | y == 4):
16 | pass
17 | ) or
18 | y == 4):
19 | pass
| ^^^^ W191
17 | #: E101 W191
18 | if x == 2 \
20 | #: E101 W191
21 | if x == 2 \
|
W19.py:21:1: W191 Indentation contains tabs
W19.py:24:1: W191 Indentation contains tabs
|
19 | or y > 1 \
20 | or x == 3:
21 | pass
22 | or y > 1 \
23 | or x == 3:
24 | pass
| ^^^^ W191
22 | #: E101 W191
23 | if x == 2 \
25 | #: E101 W191
26 | if x == 2 \
|
W19.py:26:1: W191 Indentation contains tabs
W19.py:29:1: W191 Indentation contains tabs
|
24 | or y > 1 \
25 | or x == 3:
26 | pass
27 | or y > 1 \
28 | or x == 3:
29 | pass
| ^^^^ W191
27 | #:
30 | #:
|
W19.py:32:1: W191 Indentation contains tabs
W19.py:35:1: W191 Indentation contains tabs
|
30 | if (foo == bar and
31 | baz == bop):
32 | pass
33 | if (foo == bar and
34 | baz == bop):
35 | pass
| ^^^^ W191
33 | #: E101 W191 W504
34 | if (
36 | #: E101 W191 W504
37 | if (
|
W19.py:38:1: W191 Indentation contains tabs
W19.py:41:1: W191 Indentation contains tabs
|
36 | baz == bop
37 | ):
38 | pass
39 | baz == bop
40 | ):
41 | pass
| ^^^^ W191
39 | #:
42 | #:
|
W19.py:44:1: W191 Indentation contains tabs
W19.py:47:1: W191 Indentation contains tabs
|
42 | if start[1] > end_col and not (
43 | over_indent == 4 and indent_next):
44 | return (0, "E121 continuation line over-"
45 | if start[1] > end_col and not (
46 | over_indent == 4 and indent_next):
47 | return (0, "E121 continuation line over-"
| ^^^^ W191
45 | "indented for visual indent")
46 | #:
48 | "indented for visual indent")
49 | #:
|
W19.py:45:1: W191 Indentation contains tabs
W19.py:48:1: W191 Indentation contains tabs
|
43 | over_indent == 4 and indent_next):
44 | return (0, "E121 continuation line over-"
45 | "indented for visual indent")
46 | over_indent == 4 and indent_next):
47 | return (0, "E121 continuation line over-"
48 | "indented for visual indent")
| ^^^^^^^^^^^^ W191
46 | #:
49 | #:
|
W19.py:54:1: W191 Indentation contains tabs
W19.py:57:1: W191 Indentation contains tabs
|
52 | var_one, var_two, var_three,
53 | var_four):
54 | print(var_one)
55 | var_one, var_two, var_three,
56 | var_four):
57 | print(var_one)
| ^^^^ W191
55 | #: E101 W191 W504
56 | if ((row < 0 or self.moduleCount <= row or
|
W19.py:58:1: W191 Indentation contains tabs
|
56 | if ((row < 0 or self.moduleCount <= row or
57 | col < 0 or self.moduleCount <= col)):
58 | raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
| ^^^^ W191
59 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
60 | if bar:
58 | #: E101 W191 W504
59 | if ((row < 0 or self.moduleCount <= row or
|
W19.py:61:1: W191 Indentation contains tabs
|
59 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
60 | if bar:
61 | return (
59 | if ((row < 0 or self.moduleCount <= row or
60 | col < 0 or self.moduleCount <= col)):
61 | raise Exception("%s,%s - %s" % (row, col, self.moduleCount))
| ^^^^ W191
62 | start, 'E121 lines starting with a '
63 | 'closing bracket should be indented '
|
W19.py:62:1: W191 Indentation contains tabs
|
60 | if bar:
61 | return (
62 | start, 'E121 lines starting with a '
| ^^^^^^^^ W191
63 | 'closing bracket should be indented '
64 | "to match that of the opening "
|
W19.py:63:1: W191 Indentation contains tabs
|
61 | return (
62 | start, 'E121 lines starting with a '
63 | 'closing bracket should be indented '
| ^^^^^^^^ W191
64 | "to match that of the opening "
65 | "bracket's line"
62 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
63 | if bar:
|
W19.py:64:1: W191 Indentation contains tabs
|
62 | start, 'E121 lines starting with a '
63 | 'closing bracket should be indented '
64 | "to match that of the opening "
| ^^^^^^^^ W191
65 | "bracket's line"
66 | )
62 | #: E101 E101 E101 E101 W191 W191 W191 W191 W191 W191
63 | if bar:
64 | return (
| ^^^^ W191
65 | start, 'E121 lines starting with a '
66 | 'closing bracket should be indented '
|
W19.py:65:1: W191 Indentation contains tabs
|
63 | 'closing bracket should be indented '
64 | "to match that of the opening "
65 | "bracket's line"
63 | if bar:
64 | return (
65 | start, 'E121 lines starting with a '
| ^^^^^^^^ W191
66 | )
67 | #
66 | 'closing bracket should be indented '
67 | "to match that of the opening "
|
W19.py:66:1: W191 Indentation contains tabs
|
64 | "to match that of the opening "
65 | "bracket's line"
66 | )
| ^^^^ W191
67 | #
68 | #: E101 W191 W504
64 | return (
65 | start, 'E121 lines starting with a '
66 | 'closing bracket should be indented '
| ^^^^^^^^ W191
67 | "to match that of the opening "
68 | "bracket's line"
|
W19.py:73:1: W191 Indentation contains tabs
W19.py:67:1: W191 Indentation contains tabs
|
71 | foo.bar("bop")
72 | )):
73 | print "yes"
| ^^^^ W191
74 | #: E101 W191 W504
75 | # also ok, but starting to look like LISP
65 | start, 'E121 lines starting with a '
66 | 'closing bracket should be indented '
67 | "to match that of the opening "
| ^^^^^^^^ W191
68 | "bracket's line"
69 | )
|
W19.py:78:1: W191 Indentation contains tabs
W19.py:68:1: W191 Indentation contains tabs
|
76 | if ((foo.bar("baz") and
77 | foo.bar("bop"))):
78 | print "yes"
| ^^^^ W191
79 | #: E101 W191 W504
80 | if (a == 2 or
66 | 'closing bracket should be indented '
67 | "to match that of the opening "
68 | "bracket's line"
| ^^^^^^^^ W191
69 | )
70 | #
|
W19.py:83:1: W191 Indentation contains tabs
W19.py:69:1: W191 Indentation contains tabs
|
81 | b == "abc def ghi"
82 | "jkl mno"):
83 | return True
67 | "to match that of the opening "
68 | "bracket's line"
69 | )
| ^^^^ W191
84 | #: E101 W191 W504
85 | if (a == 2 or
70 | #
71 | #: E101 W191 W504
|
W19.py:88:1: W191 Indentation contains tabs
W19.py:76:1: W191 Indentation contains tabs
|
86 | b == """abc def ghi
87 | jkl mno"""):
88 | return True
74 | foo.bar("bop")
75 | )):
76 | print "yes"
| ^^^^ W191
89 | #: W191:2:1 W191:3:1 E101:3:2
90 | if length > options.max_line_length:
77 | #: E101 W191 W504
78 | # also ok, but starting to look like LISP
|
W19.py:81:1: W191 Indentation contains tabs
|
79 | if ((foo.bar("baz") and
80 | foo.bar("bop"))):
81 | print "yes"
| ^^^^ W191
82 | #: E101 W191 W504
83 | if (a == 2 or
|
W19.py:86:1: W191 Indentation contains tabs
|
84 | b == "abc def ghi"
85 | "jkl mno"):
86 | return True
| ^^^^ W191
87 | #: E101 W191 W504
88 | if (a == 2 or
|
W19.py:91:1: W191 Indentation contains tabs
|
89 | #: W191:2:1 W191:3:1 E101:3:2
90 | if length > options.max_line_length:
91 | return options.max_line_length, \
89 | b == """abc def ghi
90 | jkl mno"""):
91 | return True
| ^^^^ W191
92 | "E501 line too long (%d characters)" % length
92 | #: W191:2:1 W191:3:1 E101:3:2
93 | if length > options.max_line_length:
|
W19.py:92:1: W191 Indentation contains tabs
W19.py:94:1: W191 Indentation contains tabs
|
90 | if length > options.max_line_length:
91 | return options.max_line_length, \
92 | "E501 line too long (%d characters)" % length
92 | #: W191:2:1 W191:3:1 E101:3:2
93 | if length > options.max_line_length:
94 | return options.max_line_length, \
| ^^^^ W191
95 | "E501 line too long (%d characters)" % length
|
W19.py:95:1: W191 Indentation contains tabs
|
93 | if length > options.max_line_length:
94 | return options.max_line_length, \
95 | "E501 line too long (%d characters)" % length
| ^^^^^^^^ W191
|
W19.py:98:1: W191 Indentation contains tabs
W19.py:101:1: W191 Indentation contains tabs
|
96 | #: E101 W191 W191 W504
97 | if os.path.exists(os.path.join(path, PEP8_BIN)):
98 | cmd = ([os.path.join(path, PEP8_BIN)] +
99 | #: E101 W191 W191 W504
100 | if os.path.exists(os.path.join(path, PEP8_BIN)):
101 | cmd = ([os.path.join(path, PEP8_BIN)] +
| ^^^^ W191
99 | self._pep8_options(targetfile))
100 | #: W191 - okay
102 | self._pep8_options(targetfile))
103 | #: W191 - okay
|
W19.py:99:1: W191 Indentation contains tabs
W19.py:102:1: W191 Indentation contains tabs
|
97 | if os.path.exists(os.path.join(path, PEP8_BIN)):
98 | cmd = ([os.path.join(path, PEP8_BIN)] +
99 | self._pep8_options(targetfile))
100 | if os.path.exists(os.path.join(path, PEP8_BIN)):
101 | cmd = ([os.path.join(path, PEP8_BIN)] +
102 | self._pep8_options(targetfile))
| ^^^^^^^^^^^ W191
100 | #: W191 - okay
101 | '''
103 | #: W191 - okay
104 | '''
|
W19.py:125:1: W191 Indentation contains tabs
W19.py:128:1: W191 Indentation contains tabs
|
123 | if foo is None and bar is "bop" and \
124 | blah == 'yeah':
125 | blah = 'yeahnah'
126 | if foo is None and bar is "bop" and \
127 | blah == 'yeah':
128 | blah = 'yeahnah'
| ^^^^ W191
|
W19.py:131:1: W191 Indentation contains tabs
W19.py:134:1: W191 Indentation contains tabs
|
129 | #: W191 W191 W191
130 | if True:
131 | foo(
132 | #: W191 W191 W191
133 | if True:
134 | foo(
| ^^^^ W191
132 | 1,
133 | 2)
135 | 1,
136 | 2)
|
W19.py:132:1: W191 Indentation contains tabs
W19.py:135:1: W191 Indentation contains tabs
|
130 | if True:
131 | foo(
132 | 1,
133 | if True:
134 | foo(
135 | 1,
| ^^^^^^^^ W191
133 | 2)
134 | #: W191 W191 W191 W191 W191
|
W19.py:133:1: W191 Indentation contains tabs
|
131 | foo(
132 | 1,
133 | 2)
| ^^^^^^^^ W191
134 | #: W191 W191 W191 W191 W191
135 | def test_keys(self):
136 | 2)
137 | #: W191 W191 W191 W191 W191
|
W19.py:136:1: W191 Indentation contains tabs
|
134 | #: W191 W191 W191 W191 W191
135 | def test_keys(self):
136 | """areas.json - All regions are accounted for."""
| ^^^^ W191
137 | expected = set([
138 | u'Norrbotten',
|
W19.py:137:1: W191 Indentation contains tabs
|
135 | def test_keys(self):
136 | """areas.json - All regions are accounted for."""
137 | expected = set([
| ^^^^ W191
138 | u'Norrbotten',
139 | u'V\xe4sterbotten',
|
W19.py:138:1: W191 Indentation contains tabs
|
136 | """areas.json - All regions are accounted for."""
137 | expected = set([
138 | u'Norrbotten',
134 | foo(
135 | 1,
136 | 2)
| ^^^^^^^^ W191
139 | u'V\xe4sterbotten',
140 | ])
137 | #: W191 W191 W191 W191 W191
138 | def test_keys(self):
|
W19.py:139:1: W191 Indentation contains tabs
|
137 | expected = set([
138 | u'Norrbotten',
139 | u'V\xe4sterbotten',
| ^^^^^^^^ W191
140 | ])
141 | #: W191
137 | #: W191 W191 W191 W191 W191
138 | def test_keys(self):
139 | """areas.json - All regions are accounted for."""
| ^^^^ W191
140 | expected = set([
141 | u'Norrbotten',
|
W19.py:140:1: W191 Indentation contains tabs
|
138 | u'Norrbotten',
139 | u'V\xe4sterbotten',
140 | ])
138 | def test_keys(self):
139 | """areas.json - All regions are accounted for."""
140 | expected = set([
| ^^^^ W191
141 | #: W191
142 | x = [
141 | u'Norrbotten',
142 | u'V\xe4sterbotten',
|
W19.py:141:1: W191 Indentation contains tabs
|
139 | """areas.json - All regions are accounted for."""
140 | expected = set([
141 | u'Norrbotten',
| ^^^^^^^^ W191
142 | u'V\xe4sterbotten',
143 | ])
|
W19.py:142:1: W191 Indentation contains tabs
|
140 | expected = set([
141 | u'Norrbotten',
142 | u'V\xe4sterbotten',
| ^^^^^^^^ W191
143 | ])
144 | #: W191
|
W19.py:143:1: W191 Indentation contains tabs
|
141 | #: W191
142 | x = [
143 | 'abc'
141 | u'Norrbotten',
142 | u'V\xe4sterbotten',
143 | ])
| ^^^^ W191
144 | ]
145 | #: W191 - okay
144 | #: W191
145 | x = [
|
W19.py:146:1: W191 Indentation contains tabs
|
144 | #: W191
145 | x = [
146 | 'abc'
| ^^^^ W191
147 | ]
148 | #: W191 - okay
|

View File

@@ -39,7 +39,7 @@ use crate::registry::{AsRule, Rule};
/// ## Options
/// - `pydocstyle.convention`
///
/// [D211]: https://beta.ruff.rs/docs/rules/blank-line-before-class
/// [D211]: https://docs.astral.sh/ruff/rules/blank-line-before-class
#[violation]
pub struct OneBlankLineBeforeClass;
@@ -136,7 +136,7 @@ impl AlwaysAutofixableViolation for OneBlankLineAfterClass {
/// ## Options
/// - `pydocstyle.convention`
///
/// [D203]: https://beta.ruff.rs/docs/rules/one-blank-line-before-class
/// [D203]: https://docs.astral.sh/ruff/rules/one-blank-line-before-class
#[violation]
pub struct BlankLineBeforeClass;

View File

@@ -48,7 +48,7 @@ use crate::registry::{AsRule, Rule};
/// """
/// ```
///
/// [D213]: https://beta.ruff.rs/docs/rules/multi-line-summary-second-line
/// [D213]: https://docs.astral.sh/ruff/rules/multi-line-summary-second-line
#[violation]
pub struct MultiLineSummaryFirstLine;
@@ -102,7 +102,7 @@ impl AlwaysAutofixableViolation for MultiLineSummaryFirstLine {
/// """
/// ```
///
/// [D212]: https://beta.ruff.rs/docs/rules/multi-line-summary-first-line
/// [D212]: https://docs.astral.sh/ruff/rules/multi-line-summary-first-line
#[violation]
pub struct MultiLineSummarySecondLine;

View File

@@ -4,7 +4,7 @@ use ruff_diagnostics::Edit;
use ruff_python_ast as ast;
use ruff_python_codegen::Stylist;
use ruff_python_semantic::Binding;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_python_trivia::{BackwardsTokenizer, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
@@ -91,20 +91,27 @@ pub(crate) fn remove_exception_handler_assignment(
bound_exception: &Binding,
locator: &Locator,
) -> Result<Edit> {
// Lex backwards, to the token just before the `as`.
// Find the position just after the exception name. This is a late pass so we only have the
// binding and can't look its parent in the AST up anymore.
// ```
// except ZeroDivisionError as err:
// ^^^ This is the bound_exception range
// ^^^^ lex this range
// ^ preceding_end (we want to remove from here)
// ```
// There can't be any comments in that range.
let mut tokenizer =
SimpleTokenizer::up_to_without_back_comment(bound_exception.start(), locator.contents())
.skip_trivia();
BackwardsTokenizer::up_to(bound_exception.start(), locator.contents(), &[]).skip_trivia();
// Eat the `as` token.
let preceding = tokenizer
.next_back()
.next()
.context("expected the exception name to be preceded by `as`")?;
debug_assert!(matches!(preceding.kind, SimpleTokenKind::As));
// Lex to the end of the preceding token, which should be the exception value.
let preceding = tokenizer
.next_back()
.next()
.context("expected the exception name to be preceded by a token")?;
// Lex forwards, to the `:` token.

View File

@@ -224,6 +224,7 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option<Fix> {
let start = parenthesized_range(
target.into(),
statement.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(target.range())

View File

@@ -28,7 +28,7 @@ use crate::noqa::Directive;
/// ```
///
/// ## References
/// - [Ruff documentation](https://beta.ruff.rs/docs/configuration/#error-suppression)
/// - [Ruff documentation](https://docs.astral.sh/ruff/configuration/#error-suppression)
#[violation]
pub struct BlanketNOQA;

View File

@@ -83,6 +83,9 @@ fn is_known_dunder_method(method: &str) -> bool {
| "__aiter__"
| "__and__"
| "__anext__"
| "__attrs_init__"
| "__attrs_post_init__"
| "__attrs_pre_init__"
| "__await__"
| "__bool__"
| "__bytes__"
@@ -120,6 +123,7 @@ fn is_known_dunder_method(method: &str) -> bool {
| "__getstate__"
| "__gt__"
| "__hash__"
| "__html__"
| "__iadd__"
| "__iand__"
| "__ifloordiv__"

View File

@@ -2,6 +2,7 @@ use itertools::Itertools;
use ruff_python_ast::{self as ast, Arguments, BoolOp, Expr};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::autofix::edits::pad;
use crate::autofix::snippet::SourceCodeSnippet;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -133,7 +134,10 @@ pub(crate) fn repeated_isinstance_calls(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(call, expr.range())));
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
pad(call, expr.range(), checker.locator()),
expr.range(),
)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -124,4 +124,21 @@ repeated_isinstance_calls.py:30:14: PLR1701 [*] Merge `isinstance` calls: `isins
32 32 | # not merged but valid
33 33 | result = isinstance(var[5], int) and var[5] * 14 or isinstance(var[5], float) and var[5] * 14.4
repeated_isinstance_calls.py:42:3: PLR1701 [*] Merge `isinstance` calls: `isinstance(self.k, float | int)`
|
41 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
42 | if(isinstance(self.k, int)) or (isinstance(self.k, float)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1701
43 | ...
|
= help: Replace with `isinstance(self.k, float | int)`
Fix
39 39 |
40 40 |
41 41 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
42 |-if(isinstance(self.k, int)) or (isinstance(self.k, float)):
42 |+if isinstance(self.k, float | int):
43 43 | ...

View File

@@ -124,4 +124,21 @@ repeated_isinstance_calls.py:30:14: PLR1701 [*] Merge `isinstance` calls: `isins
32 32 | # not merged but valid
33 33 | result = isinstance(var[5], int) and var[5] * 14 or isinstance(var[5], float) and var[5] * 14.4
repeated_isinstance_calls.py:42:3: PLR1701 [*] Merge `isinstance` calls: `isinstance(self.k, (float, int))`
|
41 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
42 | if(isinstance(self.k, int)) or (isinstance(self.k, float)):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLR1701
43 | ...
|
= help: Replace with `isinstance(self.k, (float, int))`
Fix
39 39 |
40 40 |
41 41 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722460483
42 |-if(isinstance(self.k, int)) or (isinstance(self.k, float)):
42 |+if isinstance(self.k, (float, int)):
43 43 | ...

View File

@@ -1,8 +1,7 @@
use malachite::Integer;
use std::fmt;
use std::str::FromStr;
use num_bigint::BigInt;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Constant, Expr};
@@ -47,7 +46,7 @@ impl From<LiteralType> for Constant {
value: Vec::new(),
implicit_concatenated: false,
}),
LiteralType::Int => Constant::Int(BigInt::from(0)),
LiteralType::Int => Constant::Int(Integer::from(0)),
LiteralType::Float => Constant::Float(0.0),
LiteralType::Bool => Constant::Bool(false),
}

View File

@@ -1,6 +1,6 @@
use std::cmp::Ordering;
use num_bigint::{BigInt, Sign};
use malachite::Integer;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -122,31 +122,24 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
..
}) => {
if op == &CmpOp::Eq {
match bigint_to_u32(number) {
2 => {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) =
fix_always_false_branch(checker, stmt_if, &branch)
{
diagnostic.set_fix(fix);
}
if *number == 2 {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = fix_always_false_branch(checker, stmt_if, &branch) {
diagnostic.set_fix(fix);
}
checker.diagnostics.push(diagnostic);
}
3 => {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch)
{
diagnostic.set_fix(fix);
}
checker.diagnostics.push(diagnostic);
} else if *number == 3 {
let mut diagnostic =
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
if checker.patch(diagnostic.kind.rule()) {
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch) {
diagnostic.set_fix(fix);
}
checker.diagnostics.push(diagnostic);
}
_ => {}
checker.diagnostics.push(diagnostic);
}
}
}
@@ -156,7 +149,7 @@ pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
}
/// Returns true if the `target_version` is always less than the [`PythonVersion`].
fn compare_version(target_version: &[u32], py_version: PythonVersion, or_equal: bool) -> bool {
fn compare_version(target_version: &[Integer], py_version: PythonVersion, or_equal: bool) -> bool {
let mut target_version_iter = target_version.iter();
let Some(if_major) = target_version_iter.next() else {
@@ -165,7 +158,7 @@ fn compare_version(target_version: &[u32], py_version: PythonVersion, or_equal:
let (py_major, py_minor) = py_version.as_tuple();
match if_major.cmp(&py_major) {
match if_major.cmp(&Integer::from(py_major)) {
Ordering::Less => true,
Ordering::Greater => false,
Ordering::Equal => {
@@ -353,26 +346,16 @@ fn fix_always_true_branch(
}
}
/// Converts a `BigInt` to a `u32`. If the number is negative, it will return 0.
fn bigint_to_u32(number: &BigInt) -> u32 {
let the_number = number.to_u32_digits();
match the_number.0 {
Sign::Minus | Sign::NoSign => 0,
Sign::Plus => *the_number.1.first().unwrap(),
}
}
/// Gets the version from the tuple
fn extract_version(elts: &[Expr]) -> Vec<u32> {
let mut version: Vec<u32> = vec![];
fn extract_version(elts: &[Expr]) -> Vec<Integer> {
let mut version = Vec::new();
for elt in elts {
if let Expr::Constant(ast::ExprConstant {
value: Constant::Int(item),
value: Constant::Int(number),
..
}) = &elt
{
let number = bigint_to_u32(item);
version.push(number);
version.push(number.clone());
} else {
return version;
}
@@ -403,6 +386,13 @@ mod tests {
or_equal: bool,
expected: bool,
) {
assert_eq!(compare_version(version_vec, version, or_equal), expected);
let version_integers = version_vec
.iter()
.map(|x| Integer::from(*x))
.collect::<Vec<_>>();
assert_eq!(
compare_version(&version_integers, version, or_equal),
expected
);
}
}

View File

@@ -1,5 +1,6 @@
use ruff_python_ast::{self as ast, Expr};
use crate::autofix::edits::pad;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
@@ -76,7 +77,7 @@ pub(crate) fn type_of_primitive(checker: &mut Checker, expr: &Expr, func: &Expr,
let builtin = primitive.builtin();
if checker.semantic().is_builtin(&builtin) {
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
primitive.builtin(),
pad(primitive.builtin(), expr.range(), checker.locator()),
expr.range(),
)));
}

View File

@@ -5,7 +5,7 @@ use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::autofix::edits::{remove_argument, Parentheses};
use crate::autofix::edits::{pad, remove_argument, Parentheses};
use crate::checkers::ast::Checker;
use crate::registry::Rule;
@@ -151,7 +151,10 @@ fn replace_with_bytes_literal(
prev = range.end();
}
Fix::automatic(Edit::range_replacement(replacement, call.range()))
Fix::automatic(Edit::range_replacement(
pad(replacement, call.range(), locator),
call.range(),
))
}
/// UP012

View File

@@ -108,6 +108,7 @@ pub(crate) fn yield_in_for_loop(checker: &mut Checker, stmt_for: &ast::StmtFor)
parenthesized_range(
iter.as_ref().into(),
stmt_for.into(),
checker.indexer().comment_ranges(),
checker.locator().contents(),
)
.unwrap_or(iter.range()),

View File

@@ -96,4 +96,19 @@ UP003.py:5:1: UP003 [*] Use `complex` instead of `type(...)`
7 7 | # OK
8 8 | type(arg)(" ")
UP003.py:14:29: UP003 [*] Use `str` instead of `type(...)`
|
13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459841
14 | assert isinstance(fullname, type("")is not True)
| ^^^^^^^^ UP003
|
= help: Replace `type(...)` with `str`
Fix
11 11 | y = x.dtype.type(0.0)
12 12 |
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459841
14 |-assert isinstance(fullname, type("")is not True)
14 |+assert isinstance(fullname, str is not True)

View File

@@ -410,5 +410,8 @@ UP007.py:110:28: UP007 [*] Use `X | Y` for type annotations
109 109 | class ServiceRefOrValue:
110 |- service_specification: Optional[str]is not True = None
110 |+ service_specification: str | None is not True = None
111 111 |
112 112 |
113 113 | # Regression test for: https://github.com/astral-sh/ruff/issues/7452

View File

@@ -510,6 +510,7 @@ UP012.py:75:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
75 |+(f"foo{bar}").encode()
76 76 | ("unicode text©").encode("utf-8")
77 77 | ("unicode text©").encode(encoding="utf-8")
78 78 |
UP012.py:76:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
@@ -528,6 +529,8 @@ UP012.py:76:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
76 |-("unicode text©").encode("utf-8")
76 |+("unicode text©").encode()
77 77 | ("unicode text©").encode(encoding="utf-8")
78 78 |
79 79 |
UP012.py:77:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
|
@@ -544,5 +547,24 @@ UP012.py:77:1: UP012 [*] Unnecessary UTF-8 `encoding` argument to `encode`
76 76 | ("unicode text©").encode("utf-8")
77 |-("unicode text©").encode(encoding="utf-8")
77 |+("unicode text©").encode()
78 78 |
79 79 |
80 80 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459882
UP012.py:82:17: UP012 [*] Unnecessary call to `encode` as UTF-8
|
80 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459882
81 | def _match_ignore(line):
82 | input=stdin and'\n'.encode()or None
| ^^^^^^^^^^^^^ UP012
|
= help: Rewrite as bytes literal
Fix
79 79 |
80 80 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722459882
81 81 | def _match_ignore(line):
82 |- input=stdin and'\n'.encode()or None
82 |+ input=stdin and b'\n' or None

View File

@@ -17,6 +17,7 @@ mod tests {
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
#[test_case(Rule::DeleteFullSlice, Path::new("FURB131.py"))]
#[test_case(Rule::CheckAndRemoveFromSet, Path::new("FURB132.py"))]
#[test_case(Rule::ReimplementedStarmap, Path::new("FURB140.py"))]
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View File

@@ -1,9 +1,11 @@
pub(crate) use check_and_remove_from_set::*;
pub(crate) use delete_full_slice::*;
pub(crate) use reimplemented_starmap::*;
pub(crate) use repeated_append::*;
pub(crate) use slice_copy::*;
mod check_and_remove_from_set;
mod delete_full_slice;
mod reimplemented_starmap;
mod repeated_append;
mod slice_copy;

View File

@@ -0,0 +1,332 @@
use anyhow::{bail, Result};
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::{self as ast, Expr};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::registry::AsRule;
/// ## What it does
/// Checks for generator expressions, list and set comprehensions that can
/// be replaced with `itertools.starmap`.
///
/// ## Why is this bad?
/// When unpacking values from iterators to pass them directly to
/// a function, prefer `itertools.starmap`.
///
/// Using `itertools.starmap` is more concise and readable.
///
/// ## Example
/// ```python
/// scores = [85, 100, 60]
/// passing_scores = [60, 80, 70]
///
///
/// def passed_test(score: int, passing_score: int) -> bool:
/// return score >= passing_score
///
///
/// passed_all_tests = all(
/// passed_test(score, passing_score)
/// for score, passing_score in zip(scores, passing_scores)
/// )
/// ```
///
/// Use instead:
/// ```python
/// from itertools import starmap
///
///
/// scores = [85, 100, 60]
/// passing_scores = [60, 80, 70]
///
///
/// def passed_test(score: int, passing_score: int) -> bool:
/// return score >= passing_score
///
///
/// passed_all_tests = all(starmap(passed_test, zip(scores, passing_scores)))
/// ```
///
/// ## References
/// - [Python documentation: `itertools.starmap`](https://docs.python.org/3/library/itertools.html#itertools.starmap)
#[violation]
pub struct ReimplementedStarmap;
impl Violation for ReimplementedStarmap {
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `itertools.starmap` instead of the generator")
}
fn autofix_title(&self) -> Option<String> {
Some(format!("Replace with `itertools.starmap`"))
}
}
/// FURB140
pub(crate) fn reimplemented_starmap(checker: &mut Checker, target: &StarmapCandidate) {
// Generator should have exactly one comprehension.
let [comprehension @ ast::Comprehension { .. }] = target.generators() else {
return;
};
// This comprehension should have a form:
// ```python
// (x, y, z, ...) in iter
// ```
//
// `x, y, z, ...` are what we call `elts` for short.
let Some((elts, iter)) = match_comprehension(comprehension) else {
return;
};
// Generator should produce one element that should look like:
// ```python
// func(a, b, c, ...)
// ```
//
// here we refer to `a, b, c, ...` as `args`.
//
// NOTE: `func` is not necessarily just a function name, it can be an attribute access,
// or even a call itself.
let Some((args, func)) = match_call(target.element()) else {
return;
};
// Here we want to check that `args` and `elts` are the same (same length, same elements,
// same order).
if elts.len() != args.len()
|| !std::iter::zip(elts, args)
.all(|(x, y)| ComparableExpr::from(x) == ComparableExpr::from(y))
{
return;
}
let mut diagnostic = Diagnostic::new(ReimplementedStarmap, target.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
// Try importing `starmap` from `itertools`.
//
// It is not required to be `itertools.starmap`, though. The user might've already
// imported it. Maybe even under a different name. So, we should use that name
// for fix construction.
let (import_edit, starmap_name) = checker.importer().get_or_import_symbol(
&ImportRequest::import_from("itertools", "starmap"),
target.start(),
checker.semantic(),
)?;
// The actual fix suggestion depends on what type of expression we were looking at.
//
// - For generator expressions, we use `starmap` call directly.
// - For list and set comprehensions, we'd want to wrap it with `list` and `set`
// correspondingly.
let main_edit = Edit::range_replacement(
target.try_make_suggestion(starmap_name, iter, func, checker)?,
target.range(),
);
Ok(Fix::suggested_edits(import_edit, [main_edit]))
});
}
checker.diagnostics.push(diagnostic);
}
/// An enum for a node that can be considered a candidate for replacement with `starmap`.
#[derive(Debug)]
pub(crate) enum StarmapCandidate<'a> {
Generator(&'a ast::ExprGeneratorExp),
ListComp(&'a ast::ExprListComp),
SetComp(&'a ast::ExprSetComp),
}
impl<'a> From<&'a ast::ExprGeneratorExp> for StarmapCandidate<'a> {
fn from(generator: &'a ast::ExprGeneratorExp) -> Self {
Self::Generator(generator)
}
}
impl<'a> From<&'a ast::ExprListComp> for StarmapCandidate<'a> {
fn from(list_comp: &'a ast::ExprListComp) -> Self {
Self::ListComp(list_comp)
}
}
impl<'a> From<&'a ast::ExprSetComp> for StarmapCandidate<'a> {
fn from(set_comp: &'a ast::ExprSetComp) -> Self {
Self::SetComp(set_comp)
}
}
impl Ranged for StarmapCandidate<'_> {
fn range(&self) -> TextRange {
match self {
Self::Generator(generator) => generator.range(),
Self::ListComp(list_comp) => list_comp.range(),
Self::SetComp(set_comp) => set_comp.range(),
}
}
}
impl StarmapCandidate<'_> {
/// Return the generated element for the candidate.
pub(crate) fn element(&self) -> &Expr {
match self {
Self::Generator(generator) => generator.elt.as_ref(),
Self::ListComp(list_comp) => list_comp.elt.as_ref(),
Self::SetComp(set_comp) => set_comp.elt.as_ref(),
}
}
/// Return the generator comprehensions for the candidate.
pub(crate) fn generators(&self) -> &[ast::Comprehension] {
match self {
Self::Generator(generator) => generator.generators.as_slice(),
Self::ListComp(list_comp) => list_comp.generators.as_slice(),
Self::SetComp(set_comp) => set_comp.generators.as_slice(),
}
}
/// Try to produce a fix suggestion transforming this node into a call to `starmap`.
pub(crate) fn try_make_suggestion(
&self,
name: String,
iter: &Expr,
func: &Expr,
checker: &Checker,
) -> Result<String> {
match self {
Self::Generator(_) => {
// For generator expressions, we replace:
// ```python
// (foo(...) for ... in iter)
// ```
//
// with:
// ```python
// itertools.starmap(foo, iter)
// ```
let call = construct_starmap_call(name, iter, func);
Ok(checker.generator().expr(&call.into()))
}
Self::ListComp(_) => {
// For list comprehensions, we replace:
// ```python
// [foo(...) for ... in iter]
// ```
//
// with:
// ```python
// list(itertools.starmap(foo, iter))
// ```
try_construct_call(name, iter, func, "list", checker)
}
Self::SetComp(_) => {
// For set comprehensions, we replace:
// ```python
// {foo(...) for ... in iter}
// ```
//
// with:
// ```python
// set(itertools.starmap(foo, iter))
// ```
try_construct_call(name, iter, func, "set", checker)
}
}
}
}
/// Try constructing the call to `itertools.starmap` and wrapping it with the given builtin.
fn try_construct_call(
name: String,
iter: &Expr,
func: &Expr,
builtin: &str,
checker: &Checker,
) -> Result<String> {
// We can only do our fix if `builtin` identifier is still bound to
// the built-in type.
if !checker.semantic().is_builtin(builtin) {
bail!(format!("Can't use built-in `{builtin}` constructor"))
}
// In general, we replace:
// ```python
// foo(...) for ... in iter
// ```
//
// with:
// ```python
// builtin(itertools.starmap(foo, iter))
// ```
// where `builtin` is a constructor for a target collection.
let call = construct_starmap_call(name, iter, func);
let wrapped = wrap_with_call_to(call, builtin);
Ok(checker.generator().expr(&wrapped.into()))
}
/// Construct the call to `itertools.starmap` for suggestion.
fn construct_starmap_call(starmap_binding: String, iter: &Expr, func: &Expr) -> ast::ExprCall {
let starmap = ast::ExprName {
id: starmap_binding,
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
ast::ExprCall {
func: Box::new(starmap.into()),
arguments: ast::Arguments {
args: vec![func.clone(), iter.clone()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
}
}
/// Wrap given function call with yet another call.
fn wrap_with_call_to(call: ast::ExprCall, func_name: &str) -> ast::ExprCall {
let name = ast::ExprName {
id: func_name.to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
ast::ExprCall {
func: Box::new(name.into()),
arguments: ast::Arguments {
args: vec![call.into()],
keywords: vec![],
range: TextRange::default(),
},
range: TextRange::default(),
}
}
/// Match that the given comprehension is `(x, y, z, ...) in iter`.
fn match_comprehension(comprehension: &ast::Comprehension) -> Option<(&[Expr], &Expr)> {
if comprehension.is_async || !comprehension.ifs.is_empty() {
return None;
}
let ast::ExprTuple { elts, .. } = comprehension.target.as_tuple_expr()?;
Some((elts, &comprehension.iter))
}
/// Match that the given expression is `func(x, y, z, ...)`.
fn match_call(element: &Expr) -> Option<(&[Expr], &Expr)> {
let ast::ExprCall {
func,
arguments: ast::Arguments { args, keywords, .. },
..
} = element.as_call_expr()?;
if !keywords.is_empty() {
return None;
}
Some((args, func))
}

View File

@@ -0,0 +1,136 @@
---
source: crates/ruff/src/rules/refurb/mod.rs
---
FURB140.py:7:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
6 | # FURB140
7 | [print(x, y) for x, y in zipped()]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
8 |
9 | # FURB140
|
= help: Replace with `itertools.starmap`
Suggested fix
1 |+from itertools import starmap
1 2 | def zipped():
2 3 | return zip([1, 2, 3], "ABC")
3 4 |
4 5 | # Errors.
5 6 |
6 7 | # FURB140
7 |-[print(x, y) for x, y in zipped()]
8 |+list(starmap(print, zipped()))
8 9 |
9 10 | # FURB140
10 11 | (print(x, y) for x, y in zipped())
FURB140.py:10:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
9 | # FURB140
10 | (print(x, y) for x, y in zipped())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
11 |
12 | # FURB140
|
= help: Replace with `itertools.starmap`
Suggested fix
1 |+from itertools import starmap
1 2 | def zipped():
2 3 | return zip([1, 2, 3], "ABC")
3 4 |
--------------------------------------------------------------------------------
7 8 | [print(x, y) for x, y in zipped()]
8 9 |
9 10 | # FURB140
10 |-(print(x, y) for x, y in zipped())
11 |+starmap(print, zipped())
11 12 |
12 13 | # FURB140
13 14 | {print(x, y) for x, y in zipped()}
FURB140.py:13:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
12 | # FURB140
13 | {print(x, y) for x, y in zipped()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
|
= help: Replace with `itertools.starmap`
Suggested fix
1 |+from itertools import starmap
1 2 | def zipped():
2 3 | return zip([1, 2, 3], "ABC")
3 4 |
--------------------------------------------------------------------------------
10 11 | (print(x, y) for x, y in zipped())
11 12 |
12 13 | # FURB140
13 |-{print(x, y) for x, y in zipped()}
14 |+set(starmap(print, zipped()))
14 15 |
15 16 |
16 17 | from itertools import starmap as sm
FURB140.py:19:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
18 | # FURB140
19 | [print(x, y) for x, y in zipped()]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
20 |
21 | # FURB140
|
= help: Replace with `itertools.starmap`
Suggested fix
16 16 | from itertools import starmap as sm
17 17 |
18 18 | # FURB140
19 |-[print(x, y) for x, y in zipped()]
19 |+list(sm(print, zipped()))
20 20 |
21 21 | # FURB140
22 22 | (print(x, y) for x, y in zipped())
FURB140.py:22:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
21 | # FURB140
22 | (print(x, y) for x, y in zipped())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
23 |
24 | # FURB140
|
= help: Replace with `itertools.starmap`
Suggested fix
19 19 | [print(x, y) for x, y in zipped()]
20 20 |
21 21 | # FURB140
22 |-(print(x, y) for x, y in zipped())
22 |+sm(print, zipped())
23 23 |
24 24 | # FURB140
25 25 | {print(x, y) for x, y in zipped()}
FURB140.py:25:1: FURB140 [*] Use `itertools.starmap` instead of the generator
|
24 | # FURB140
25 | {print(x, y) for x, y in zipped()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB140
26 |
27 | # Non-errors.
|
= help: Replace with `itertools.starmap`
Suggested fix
22 22 | (print(x, y) for x, y in zipped())
23 23 |
24 24 | # FURB140
25 |-{print(x, y) for x, y in zipped()}
25 |+set(sm(print, zipped()))
26 26 |
27 27 | # Non-errors.
28 28 |

View File

@@ -1,4 +1,4 @@
use num_traits::ToPrimitive;
use malachite::Integer;
use ruff_python_ast::{self as ast, Constant, Expr, UnaryOp};
use ruff_diagnostics::{Diagnostic, Violation};
@@ -46,11 +46,11 @@ impl Violation for PairwiseOverZipped {
#[derive(Debug)]
struct SliceInfo {
arg_name: String,
slice_start: Option<i64>,
slice_start: Option<Integer>,
}
impl SliceInfo {
pub(crate) fn new(arg_name: String, slice_start: Option<i64>) -> Self {
pub(crate) fn new(arg_name: String, slice_start: Option<Integer>) -> Self {
Self {
arg_name,
slice_start,
@@ -89,12 +89,12 @@ fn match_slice_info(expr: &Expr) -> Option<SliceInfo> {
))
}
fn to_bound(expr: &Expr) -> Option<i64> {
fn to_bound(expr: &Expr) -> Option<Integer> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) => value.to_i64(),
}) => Some(value.clone()),
Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::USub | UnaryOp::Invert,
operand,
@@ -105,7 +105,7 @@ fn to_bound(expr: &Expr) -> Option<i64> {
..
}) = operand.as_ref()
{
value.to_i64().map(|v| -v)
Some(-value.clone())
} else {
None
}
@@ -155,7 +155,10 @@ pub(crate) fn pairwise_over_zipped(checker: &mut Checker, func: &Expr, args: &[E
}
// Verify that the arguments are successive.
if second_arg_info.slice_start.unwrap_or(0) - first_arg_info.slice_start.unwrap_or(0) != 1 {
if second_arg_info.slice_start.unwrap_or(Integer::from(0))
- first_arg_info.slice_start.unwrap_or(Integer::from(0))
!= 1
{
return;
}

View File

@@ -1,7 +1,5 @@
use std::borrow::Cow;
use num_traits::Zero;
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Comprehension, Constant, Expr};
@@ -115,7 +113,7 @@ fn is_head_slice(expr: &Expr) -> bool {
..
}) = expr
{
value.is_zero()
*value == 0
} else {
false
}

View File

@@ -36,7 +36,7 @@ pub struct UnusedCodes {
/// ```
///
/// ## References
/// - [Automatic `noqa` management](https://beta.ruff.rs/docs/configuration/#automatic-noqa-management)
/// - [Automatic `noqa` management](https://docs.astral.sh/ruff/configuration/#automatic-noqa-management)
#[violation]
pub struct UnusedNOQA {
pub codes: Option<UnusedCodes>,

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