Compare commits

...

55 Commits

Author SHA1 Message Date
Zanie
17e77fe515 Change the Cargo.toml file 2023-12-06 22:13:44 -06:00
Charlie Marsh
946b308197 Ensure that from-style imports are always ordered first in __future__ (#9039)
Closes https://github.com/astral-sh/ruff/issues/8823.
2023-12-06 22:56:23 -05:00
Zanie Blue
d22ce5372d Fix determine changes detection of "code" changes (#9038)
Replaces https://github.com/astral-sh/ruff/pull/9035
Fixes https://github.com/astral-sh/ruff/pull/8225

The issue appears to be that `*/**` was used instead of `**/*` which did
not match _any_ changed file as desired
2023-12-07 03:55:12 +00:00
Charlie Marsh
acab5f3cf2 Enable printf-string-formatting fix with comments on right-hand side (#9037)
## Summary

This was added in https://github.com/astral-sh/ruff/pull/6364 (as a
follow-on to https://github.com/astral-sh/ruff/pull/6342), but I don't
think it applies in the same way, because we don't _remove_ the
right-hand side when converting from `%`-style formatting to `.format`
calls.

Closes https://github.com/astral-sh/ruff/issues/8107.
2023-12-06 22:43:21 -05:00
Zanie Blue
06c9f625b6 Fix detection of changed files in CI (#9035)
These were literals instead of expressions... and were consequently not
evaluated.

Fixes bug from #8225
2023-12-06 21:14:58 -06:00
Charlie Marsh
bbb0a0c360 Ignore underscore references in type annotations (#9036)
## Summary

Occasionally, valid code needs to use `argparse._SubParsersAction` in a
type annotation. This isn't great, but it's indicative of the fact that
public interfaces can return private types. If you accessed that private
type via a private interface, then we should be flagging the call site,
rather than the annotation.

Closes https://github.com/astral-sh/ruff/issues/9013.
2023-12-06 22:05:56 -05:00
Dhruv Manilawala
9361e22fe9 Avoid ANN2xx autofix for abstract methods with empty body (#9034)
## Summary

This PR updates the `ANN201`, `ANN202`, `ANN205`, and `ANN206` rules to
not create a fix for the return type when it's an abstract method and
the function body is empty i.e., it only contains either a pass
statement, docstring or an ellipsis literal.

fixes: #9004

## Test Plan

Add the following test cases:
- Abstract method with pass statement
- Abstract method with docstring
- Abstract method with ellipsis literal
- Abstract method with possible return type
2023-12-06 20:47:36 -06:00
Charlie Marsh
f484df5470 Document use of math.isnan for self-comparisons (#9033)
Closes https://github.com/astral-sh/ruff/issues/8833.
2023-12-07 02:33:38 +00:00
Ondřej Súkup
af88ffc57e Add openSUSE Tumbleweed into install doc (#8996) 2023-12-06 17:07:59 +00:00
Charlie Marsh
b918647927 Avoid removing parentheses on ctypes.WinError (#9027)
Re-resolves https://github.com/astral-sh/ruff/issues/6730.
2023-12-06 17:05:34 +00:00
Dhruv Manilawala
ef7778d794 Fix preorder visitor tests (#9025)
Follow-up PR to #9009 to fix the `PreorderVisitor` test cases as
suggested here: https://github.com/astral-sh/ruff/pull/9009#discussion_r1416459688
2023-12-06 16:58:51 +00:00
Dhruv Manilawala
bd443ebe91 Add visitor tests for strings, bytes, f-strings (#9009)
This PR adds tests for visitor implementation for string literals, bytes
literals and f-strings.
2023-12-06 10:52:19 -06:00
Micha Reiser
ee6548d7dd Enforce valid format options in spec tests (#9021) 2023-12-06 07:15:06 +00:00
Eero Vaher
b4a050c21d Fix formatting of a warning box in docs (#9017)
## Summary

The last few words of a sentence that should be inside a warning box (in
https://docs.astral.sh/ruff/configuration/#default-inclusions) are
currently placed just after it because of a mistake in indentation.
2023-12-06 01:12:10 +00:00
Charlie Marsh
958702ded0 Respect trailing comma in unnecessary-dict-kwargs (#9015)
Closes https://github.com/astral-sh/ruff/issues/9014.
2023-12-05 21:30:29 +00:00
Charlie Marsh
268d95e911 Apply unnecessary index rule prior to enumerate rewrite (#9012)
This PR adds synthetic edits to `PLR1736` to avoid removing the
referenced value as part of `FURB148`.

Closes https://github.com/astral-sh/ruff/issues/9010.
2023-12-05 15:25:28 -05:00
Torbjörn Lönnemark
3def18fc21 Include version number in release archive names (#9002)
## Summary

Add a release's version number to the names of archives containing
binaries that are attached to that GitHub release.

This makes it possible for users to easily tell archives from different
downloaded releases apart.

See also: #8961

## Test Plan

The workflow was tested in my fork. The example release can be found at:
[https://github.com/tobbez/ruff/releases/tag/v0.1.7](https://github.com/tobbez/ruff/releases/tag/v0.1.7).

To allow the workflow run to succeed in the fork while testing, I had to
use a small commit to prevent interaction with external services (ghcr,
PyPI, and the ruff-pre-commit repository):

```diff
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 86eac6ebc..56b9fa908 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -463,10 +463,12 @@ jobs:
       id-token: write
     steps:
       - uses: actions/download-artifact@v3
+        if: false
         with:
           name: wheels
           path: wheels
       - name: Publish to PyPi
+        if: false
         uses: pypa/gh-action-pypi-publish@release/v1
         with:
           skip-existing: true
@@ -517,6 +519,7 @@ jobs:
           tag_name: v${{ inputs.tag }}
 
   docker-publish:
+    if: false
     # This action doesn't need to wait on any other task, it's easy to re-tag if something failed and we're validating
     # the tag here also
     name: Push Docker image ghcr.io/astral-sh/ruff
@@ -575,6 +578,7 @@ jobs:
   # After the release has been published, we update downstream repositories
   # This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
   update-dependents:
+    if: false
     name: Update dependents
     runs-on: ubuntu-latest
     needs: publish-release
```

Those workflow jobs are however not modified by this PR, so they should
not be affected.
2023-12-05 14:42:04 -05:00
Andrew Gallant
c48ba690eb add support for formatting reStructuredText code snippets (#9003)
(This is not possible to actually use until
https://github.com/astral-sh/ruff/pull/8854 is merged.)

ruff_python_formatter: add reStructuredText docstring formatting support

This commit makes use of the refactoring done in prior commits to slot
in reStructuredText support. Essentially, we add a new type of code
example and look for *both* literal blocks and code block directives.
Literal blocks are treated as Python by default because it seems to be a
[common
practice](https://github.com/adamchainz/blacken-docs/issues/195).

That is, literal blocks like this:

```
def example():
    """
    Here's an example::

        foo( 1 )

    All done.
    """
    pass
```

Will get reformatted. And code blocks (via reStructuredText directives)
will also get reformatted:


```
def example():
    """
    Here's an example:

    .. code-block:: python

        foo( 1 )

    All done.
    """
    pass
```

When looking for a code block, it is possible for it to become invalid.
In which case, we back out of looking for a code example and print the
lines out as they are. As with doctest formatting, if reformatting the
code would result in invalid Python or if the code collected from the
block is invalid, then formatting is also skipped.

A number of tests have been added to check both the formatting and
resetting behavior. Mixed indentation is also tested a fair bit, since
one of my initial attempts at dealing with mixed indentation ended up
not working.

I recommend working through this PR commit-by-commit. There is in
particular a somewhat gnarly refactoring before reST support is added.

Closes #8859
2023-12-05 14:14:44 -05:00
Ofek Lev
fd49fb935f Fix example for PLR0203 (#9011) 2023-12-05 13:55:15 -05:00
dependabot[bot]
fe54ef08aa Bump CodSpeedHQ/action from 1 to 2 (#8989)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 00:00:40 +00:00
Charlie Marsh
b7ffd73edd Ignore @overrides and @overloads for too-many-positional (#9000)
Same as `too-many-arguments`.
2023-12-04 23:38:01 +00:00
Charlie Marsh
8d9912a83a Bump version to v0.1.7 (#8999) 2023-12-04 16:28:23 -05:00
Charlie Marsh
93258e8d5b Default max-positional-args to max-args (#8998) 2023-12-04 19:02:10 +00:00
Philipp A
b90027d037 [pylint] Implement too-many-positional (PLR0917) (#8995)
## Summary

Adds a rule that bans too many positional (i.e. not keyword-only)
parameters in function definitions.

Fixes https://github.com/astral-sh/ruff/issues/8946

Rule ID code taken from https://github.com/pylint-dev/pylint/pull/9278

## Test Plan
1. fixtures file checking multiple OKs/fails
2. parametrized test file
2023-12-04 18:03:09 +00:00
Dhruv Manilawala
060a25df09 Rename semantic model flag LITERAL to TYPING_LITERAL (#8997)
This PR renames the semantic model flag `LITERAL` to `TYPING_LITERAL` to
better reflect its purpose. The main motivation behind this change is to
avoid any confusion with the "literal" terminology used in the AST for
literal nodes like string, bytes, numbers, etc.
2023-12-04 11:28:09 -06:00
dependabot[bot]
f5d4676c13 Bump ureq from 2.8.0 to 2.9.1 (#8993) 2023-12-04 09:53:25 -06:00
dependabot[bot]
df69dc9f8d Bump url from 2.4.1 to 2.5.0 (#8994) 2023-12-04 10:16:39 -05:00
dependabot[bot]
cb6c37abd9 Bump js-sys from 0.3.65 to 0.3.66 (#8992) 2023-12-04 19:50:08 +09:00
dependabot[bot]
54de990621 Bump fs-err from 2.10.0 to 2.11.0 (#8991) 2023-12-04 19:49:34 +09:00
dependabot[bot]
b91b09b961 Bump schemars from 0.8.15 to 0.8.16 (#8990)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-04 09:24:53 +00:00
Micha Reiser
0bda1913d1 Create dedicated is_*_enabled functions for each preview style (#8988) 2023-12-04 05:38:54 +00:00
Micha Reiser
7e390d3772 Move ParenthesizedExpr to ruff_python_parser (#8987) 2023-12-04 05:36:28 +00:00
Micha Reiser
0bf0aa28ac Inline trailing comments for type alias similar to assignments (#8941) 2023-12-04 05:27:04 +00:00
Micha Reiser
8088c5367a Refactor the comment handling of a statement's last expression (#8920) 2023-12-04 05:12:12 +00:00
Charlie Marsh
6fe8f8a272 Avoid unstable formatting in ellipsis-only body with trailing comment (#8984)
## Summary

We should avoid inlining the ellipsis in:

```python
def h():
    ...
    # bye
```

Just as we omit the ellipsis in:

```python
def h():
    # bye
    ...
```

Closes https://github.com/astral-sh/ruff/issues/8905.
2023-12-03 19:15:40 -05:00
Charlie Marsh
bfae1f1412 Convert over-indentation rule to use number of characters (#8983)
Closes https://github.com/astral-sh/ruff/issues/8978.
2023-12-03 20:45:30 +00:00
Charlie Marsh
b358cbf398 Fix start >= end error in over-indentation (#8982)
Closes https://github.com/astral-sh/ruff/issues/8977.
2023-12-03 20:19:43 +00:00
Charlie Marsh
17c8817695 Avoid off-by-one error in stripping noqa following multi-byte char (#8979)
Closes https://github.com/astral-sh/ruff/issues/8976.
2023-12-03 11:01:58 -05:00
Charlie Marsh
1dda669f9a Avoid syntax error via invalid ur string prefix (#8971)
## Summary

If a string has a Unicode prefix, we can't add the `r` prefix on top of
that -- we need to remove and replace it. (The Unicode prefix is
redundant anyway in Python 3.)

Closes https://github.com/astral-sh/ruff/issues/8967.
2023-12-02 18:37:49 +00:00
Tom Kuson
3fbabfe126 [flake8-pyi] Check PEP 695 type aliases for snake-case-type-alias and t-suffixed-type-alias (#8966)
## Summary

Check PEP 695 type alias definitions for `snake-case-type-alias`
(`PYI042`) and `t-suffixed-type-alias` (`PYI043`)

Related to #8771.

## Test Plan

`cargo test`
2023-12-02 13:26:43 -05:00
Charlie Marsh
20ab14e354 Avoid unnecessary index diagnostics when value is modified (#8970)
Closes https://github.com/astral-sh/ruff/issues/8969.
2023-12-02 18:17:17 +00:00
Charlie Marsh
22d8a989d4 Avoid underflow in get_model matching (#8965)
Closes https://github.com/astral-sh/ruff/issues/8962.
2023-12-02 13:56:57 +00:00
Tom Kuson
35082b28cd Fix error in t-suffixed-type-alias (PYI043) example (#8963)
## Summary

For `t-suffixed-type-alias` to trigger, the type alias needs to be
marked as such using the `typing.TypeAlias` annotation and the name of
the alias must be marked as private using a leading underscore. The
documentation example was of an unannotated type alias that was not
marked as private, which was misleading.

## Test Plan

The current example doesn't trigger the rule; the example in this merge
request does.
2023-12-02 08:52:50 -05:00
Micha Reiser
5aaf99b856 Implement the fix_power_op_line_length preview style (#8947) 2023-12-02 09:35:34 +09:00
Charlie Marsh
58bf6f5762 Remove todo branches from control-flow graph (#8960) 2023-12-01 23:46:50 +00:00
Charlie Marsh
277cd80175 Add erroneous for-loop test case for CFG (#8957) 2023-12-01 23:11:42 +00:00
Charlie Marsh
20a40771a5 Consider more wildcards in control flow graph matches (#8956) 2023-12-01 17:58:32 -05:00
Michael Essiet
4af3f43e5e Added the command to run ruff using pkgx to the installation.md (#8955)
## Summary

This PR adds the command to run ruff using [pkgx](https://pkgx.sh).

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

It's just showing that ruff is supported in one more package manager.

## Test Plan

You can run `pkgx ruff` if you have pkgx installed or run `sh <(curl
https://pkgx.sh) +github.com/charliermarsh/ruff sh
`
2023-12-01 20:43:01 +00:00
Andrew Gallant
0b1a36f8c8 ruff_python_formatter: light refactoring of code snippet formatting in docstrings (#8950)
In the source of working on #8859, I made a number of smallish refactors
to how code snippet formatting works. Most or all of these were
motivated by writing in support for reStructuredText blocks. They have
some fundamentally different requirements than doctests, and there are a
lot more ways for reStructuredText blocks to become invalid.

(Commit-by-commit review is recommended as the commit messages provide
further context on each change. I split this off from ongoing work to
make review more manageable.)
2023-12-01 14:46:39 -05:00
qdegraaf
64c2535e28 [pylint] Add add_argument utility and autofix for PLW1514 (#8928)
## Summary

- Adds `add_argument` similar to existing `remove_argument` utility to
safely add arguments to functions.
- Adds autofix for `PLW1514` as per specs requested in
https://github.com/astral-sh/ruff/issues/8883 as a test

## Test Plan

Checks on existing fixtures as well as additional test and fixture for
Python 3.9 and lower fix

## Issue Link

Closes: https://github.com/astral-sh/ruff/issues/8883
2023-12-01 18:23:56 +00:00
Charlie Marsh
5510a6131e Ignore @overload and @override methods for too-many-arguments checks (#8954)
Closes https://github.com/astral-sh/ruff/issues/8945.
2023-12-01 18:22:53 +00:00
Charlie Marsh
e5db72459e Detect implicit returns in auto-return-types (#8952)
## Summary

Adds detection for branches without a `return` or `raise`, so that we
can properly `Optional` the return types. I'd like to remove this and
replace it with our code graph analysis from the `unreachable.rs` rule,
but it at least fixes the worst offenders.

Closes #8942.
2023-12-01 12:35:01 -05:00
Tom Kuson
d66063bb33 [flake8-pyi] Check for kwarg and vararg NoReturn type annotations (#8948)
## Summary

Triggers `no-return-argument-annotation-in-stub` (`PYI050`) for vararg
and kwarg `NoReturn` type annotations.

Related to #8771.

## Test Plan

`cargo test`
2023-12-01 12:18:52 -05:00
Micha Reiser
506be68782 Enable Preview mode for formatter benchmarks (#8944) 2023-12-01 10:02:59 +00:00
Steve C
cb1d3df085 [pylint] Implement unnecessary-dict-index-lookup (PLR1733) (#8036)
## Summary

Add
[R1733](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/unnecessary-dict-index-lookup.html)
and autofix!

See #970 

## Test Plan

`cargo test` and manually
2023-12-01 05:09:50 +00:00
161 changed files with 12267 additions and 3541 deletions

View File

@@ -48,8 +48,8 @@ jobs:
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/*
- .github/workflows/ci.yaml
- python/**
- .github/workflows/ci.yaml
formatter:
- Cargo.toml
@@ -68,7 +68,7 @@ jobs:
- .github/workflows/ci.yaml
code:
- "*/**"
- "**/*"
- "!**/*.md"
- "!docs/**"
- "!assets/**"
@@ -86,7 +86,7 @@ jobs:
name: "cargo clippy"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
@@ -102,7 +102,7 @@ jobs:
cargo-test-linux:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
name: "cargo test (linux)"
steps:
- uses: actions/checkout@v4
@@ -128,7 +128,7 @@ jobs:
cargo-test-windows:
runs-on: windows-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
name: "cargo test (windows)"
steps:
- uses: actions/checkout@v4
@@ -147,7 +147,7 @@ jobs:
cargo-test-wasm:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
name: "cargo test (wasm)"
steps:
- uses: actions/checkout@v4
@@ -168,7 +168,7 @@ jobs:
cargo-fuzz:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
name: "cargo fuzz"
steps:
- uses: actions/checkout@v4
@@ -187,7 +187,7 @@ jobs:
name: "test scripts"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v4
- name: "Install Rust toolchain"
@@ -321,7 +321,7 @@ jobs:
name: "cargo udeps"
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v4
- name: "Install nightly Rust toolchain"
@@ -444,7 +444,7 @@ jobs:
needs:
- cargo-test-linux
- determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: extractions/setup-just@v1
env:
@@ -483,7 +483,7 @@ jobs:
benchmarks:
runs-on: ubuntu-latest
needs: determine_changes
if: needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main'
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- name: "Checkout Branch"
uses: actions/checkout@v4
@@ -502,7 +502,7 @@ jobs:
run: cargo codspeed build --features codspeed -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@v1
uses: CodSpeedHQ/action@v2
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -86,7 +86,7 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-x86_64-apple-darwin.tar.gz
ARCHIVE_FILE=ruff-${{ inputs.tag }}-x86_64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
@@ -125,7 +125,7 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-aarch64-apple-darwin.tar.gz
ARCHIVE_FILE=ruff-${{ inputs.tag }}-aarch64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
@@ -177,7 +177,7 @@ jobs:
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.zip
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.zip
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
@@ -224,7 +224,7 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
@@ -291,7 +291,7 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
@@ -343,7 +343,7 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
@@ -399,7 +399,7 @@ jobs:
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
ARCHIVE_FILE=ruff-${{ inputs.tag }}-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"

View File

@@ -1,5 +1,99 @@
# Changelog
## 0.1.7
### Preview features
- Implement multiline dictionary and list hugging for preview style ([#8293](https://github.com/astral-sh/ruff/pull/8293))
- Implement the `fix_power_op_line_length` preview style ([#8947](https://github.com/astral-sh/ruff/pull/8947))
- Use Python version to determine typing rewrite safety ([#8919](https://github.com/astral-sh/ruff/pull/8919))
- \[`flake8-annotations`\] Enable auto-return-type involving `Optional` and `Union` annotations ([#8885](https://github.com/astral-sh/ruff/pull/8885))
- \[`flake8-bandit`\] Implement `django-raw-sql` (`S611`) ([#8651](https://github.com/astral-sh/ruff/pull/8651))
- \[`flake8-bandit`\] Implement `tarfile-unsafe-members` (`S202`) ([#8829](https://github.com/astral-sh/ruff/pull/8829))
- \[`flake8-pyi`\] Implement fix for `unnecessary-literal-union` (`PYI030`) ([#7934](https://github.com/astral-sh/ruff/pull/7934))
- \[`flake8-simplify`\] Extend `dict-get-with-none-default` (`SIM910`) to non-literals ([#8762](https://github.com/astral-sh/ruff/pull/8762))
- \[`pylint`\] - add `unnecessary-list-index-lookup` (`PLR1736`) + autofix ([#7999](https://github.com/astral-sh/ruff/pull/7999))
- \[`pylint`\] - implement R0202 and R0203 with autofixes ([#8335](https://github.com/astral-sh/ruff/pull/8335))
- \[`pylint`\] Implement `repeated-keyword` (`PLe1132`) ([#8706](https://github.com/astral-sh/ruff/pull/8706))
- \[`pylint`\] Implement `too-many-positional` (`PLR0917`) ([#8995](https://github.com/astral-sh/ruff/pull/8995))
- \[`pylint`\] Implement `unnecessary-dict-index-lookup` (`PLR1733`) ([#8036](https://github.com/astral-sh/ruff/pull/8036))
- \[`refurb`\] Implement `redundant-log-base` (`FURB163`) ([#8842](https://github.com/astral-sh/ruff/pull/8842))
### Rule changes
- \[`flake8-boolean-trap`\] Allow booleans in `@override` methods ([#8882](https://github.com/astral-sh/ruff/pull/8882))
- \[`flake8-bugbear`\] Avoid `B015`,`B018` for last expression in a cell ([#8815](https://github.com/astral-sh/ruff/pull/8815))
- \[`flake8-pie`\] Allow ellipses for enum values in stub files ([#8825](https://github.com/astral-sh/ruff/pull/8825))
- \[`flake8-pyi`\] Check PEP 695 type aliases for `snake-case-type-alias` and `t-suffixed-type-alias` ([#8966](https://github.com/astral-sh/ruff/pull/8966))
- \[`flake8-pyi`\] Check for kwarg and vararg `NoReturn` type annotations ([#8948](https://github.com/astral-sh/ruff/pull/8948))
- \[`flake8-simplify`\] Omit select context managers from `SIM117` ([#8801](https://github.com/astral-sh/ruff/pull/8801))
- \[`pep8-naming`\] Allow Django model loads in `non-lowercase-variable-in-function` (`N806`) ([#8917](https://github.com/astral-sh/ruff/pull/8917))
- \[`pycodestyle`\] Avoid `E703` for last expression in a cell ([#8821](https://github.com/astral-sh/ruff/pull/8821))
- \[`pycodestyle`\] Update `E402` to work at cell level for notebooks ([#8872](https://github.com/astral-sh/ruff/pull/8872))
- \[`pydocstyle`\] Avoid `D100` for Jupyter Notebooks ([#8816](https://github.com/astral-sh/ruff/pull/8816))
- \[`pylint`\] Implement fix for `unspecified-encoding` (`PLW1514`) ([#8928](https://github.com/astral-sh/ruff/pull/8928))
### Formatter
- Avoid unstable formatting in ellipsis-only body with trailing comment ([#8984](https://github.com/astral-sh/ruff/pull/8984))
- Inline trailing comments for type alias similar to assignments ([#8941](https://github.com/astral-sh/ruff/pull/8941))
- Insert trailing comma when function breaks with single argument ([#8921](https://github.com/astral-sh/ruff/pull/8921))
### CLI
- Update `ruff check` and `ruff format` to default to the current directory ([#8791](https://github.com/astral-sh/ruff/pull/8791))
- Stop at the first resolved parent configuration ([#8864](https://github.com/astral-sh/ruff/pull/8864))
### Configuration
- \[`pylint`\] Default `max-positional-args` to `max-args` ([#8998](https://github.com/astral-sh/ruff/pull/8998))
- \[`pylint`\] Add `allow-dunder-method-names` setting for `bad-dunder-method-name` (`PLW3201`) ([#8812](https://github.com/astral-sh/ruff/pull/8812))
- \[`isort`\] Add support for `from-first` setting ([#8663](https://github.com/astral-sh/ruff/pull/8663))
- \[`isort`\] Add support for `length-sort` settings ([#8841](https://github.com/astral-sh/ruff/pull/8841))
### Bug fixes
- Add support for `@functools.singledispatch` ([#8934](https://github.com/astral-sh/ruff/pull/8934))
- Avoid off-by-one error in stripping noqa following multi-byte char ([#8979](https://github.com/astral-sh/ruff/pull/8979))
- Avoid off-by-one error in with-item named expressions ([#8915](https://github.com/astral-sh/ruff/pull/8915))
- Avoid syntax error via invalid ur string prefix ([#8971](https://github.com/astral-sh/ruff/pull/8971))
- Avoid underflow in `get_model` matching ([#8965](https://github.com/astral-sh/ruff/pull/8965))
- Avoid unnecessary index diagnostics when value is modified ([#8970](https://github.com/astral-sh/ruff/pull/8970))
- Convert over-indentation rule to use number of characters ([#8983](https://github.com/astral-sh/ruff/pull/8983))
- Detect implicit returns in auto-return-types ([#8952](https://github.com/astral-sh/ruff/pull/8952))
- Fix start >= end error in over-indentation ([#8982](https://github.com/astral-sh/ruff/pull/8982))
- Ignore `@overload` and `@override` methods for too-many-arguments checks ([#8954](https://github.com/astral-sh/ruff/pull/8954))
- Lexer start of line is false only for `Mode::Expression` ([#8880](https://github.com/astral-sh/ruff/pull/8880))
- Mark `pydantic_settings.BaseSettings` as having default copy semantics ([#8793](https://github.com/astral-sh/ruff/pull/8793))
- Respect dictionary unpacking in `NamedTuple` assignments ([#8810](https://github.com/astral-sh/ruff/pull/8810))
- Respect local subclasses in `flake8-type-checking` ([#8768](https://github.com/astral-sh/ruff/pull/8768))
- Support type alias statements in simple statement positions ([#8916](https://github.com/astral-sh/ruff/pull/8916))
- \[`flake8-annotations`\] Avoid filtering out un-representable types in return annotation ([#8881](https://github.com/astral-sh/ruff/pull/8881))
- \[`flake8-pie`\] Retain extra ellipses in protocols and abstract methods ([#8769](https://github.com/astral-sh/ruff/pull/8769))
- \[`flake8-pyi`\] Respect local enum subclasses in `simple-defaults` (`PYI052`) ([#8767](https://github.com/astral-sh/ruff/pull/8767))
- \[`flake8-trio`\] Use correct range for `TRIO115` fix ([#8933](https://github.com/astral-sh/ruff/pull/8933))
- \[`flake8-trio`\] Use full arguments range for zero-sleep-call ([#8936](https://github.com/astral-sh/ruff/pull/8936))
- \[`isort`\] fix: mark `__main__` as first-party import ([#8805](https://github.com/astral-sh/ruff/pull/8805))
- \[`pep8-naming`\] Avoid `N806` errors for type alias statements ([#8785](https://github.com/astral-sh/ruff/pull/8785))
- \[`perflint`\] Avoid `PERF101` if there's an append in loop body ([#8809](https://github.com/astral-sh/ruff/pull/8809))
- \[`pycodestyle`\] Allow space-before-colon after end-of-slice ([#8838](https://github.com/astral-sh/ruff/pull/8838))
- \[`pydocstyle`\] Avoid non-character breaks in `over-indentation` (`D208`) ([#8866](https://github.com/astral-sh/ruff/pull/8866))
- \[`pydocstyle`\] Ignore underlines when determining docstring logical lines ([#8929](https://github.com/astral-sh/ruff/pull/8929))
- \[`pylint`\] Extend `self-assigning-variable` to multi-target assignments ([#8839](https://github.com/astral-sh/ruff/pull/8839))
- \[`tryceratops`\] Avoid repeated triggers in nested `tryceratops` diagnostics ([#8772](https://github.com/astral-sh/ruff/pull/8772))
### Documentation
- Add advice for fixing RUF008 when mutability is not desired ([#8853](https://github.com/astral-sh/ruff/pull/8853))
- Added the command to run ruff using pkgx to the installation.md ([#8955](https://github.com/astral-sh/ruff/pull/8955))
- Document fix safety for flake8-comprehensions and some pyupgrade rules ([#8918](https://github.com/astral-sh/ruff/pull/8918))
- Fix doc formatting for zero-sleep-call ([#8937](https://github.com/astral-sh/ruff/pull/8937))
- Remove duplicate imports from os-stat documentation ([#8930](https://github.com/astral-sh/ruff/pull/8930))
- Replace generated reference to MkDocs ([#8806](https://github.com/astral-sh/ruff/pull/8806))
- Update Arch Linux package URL in installation.md ([#8802](https://github.com/astral-sh/ruff/pull/8802))
- \[`flake8-pyi`\] Fix error in `t-suffixed-type-alias` (`PYI043`) example ([#8963](https://github.com/astral-sh/ruff/pull/8963))
- \[`flake8-pyi`\] Improve motivation for `custom-type-var-return-type` (`PYI019`) ([#8766](https://github.com/astral-sh/ruff/pull/8766))
## 0.1.6
### Preview features

64
Cargo.lock generated
View File

@@ -808,7 +808,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"anyhow",
"clap",
@@ -848,18 +848,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "form_urlencoded"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
dependencies = [
"percent-encoding",
]
[[package]]
name = "fs-err"
version = "2.10.0"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5fd9bcbe8b1087cbd395b51498c01bc997cef73e778a80b77a811af5e2d29f"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
dependencies = [
"autocfg",
]
@@ -987,9 +987,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@@ -1170,9 +1170,9 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "js-sys"
version = "0.3.65"
version = "0.3.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca"
dependencies = [
"wasm-bindgen",
]
@@ -1622,9 +1622,9 @@ dependencies = [
[[package]]
name = "percent-encoding"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "petgraph"
@@ -2062,7 +2062,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"annotate-snippets 0.9.2",
"anyhow",
@@ -2198,7 +2198,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2450,7 +2450,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.6"
version = "0.1.7"
dependencies = [
"anyhow",
"clap",
@@ -2618,9 +2618,9 @@ dependencies = [
[[package]]
name = "schemars"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f7b0ce13155372a76ee2e1c5ffba1fe61ede73fbea5630d61eee6fac4929c0c"
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
dependencies = [
"dyn-clone",
"schemars_derive",
@@ -2630,9 +2630,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "0.8.15"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e85e2a16b12bdb763244c69ab79363d71db2b4b918a2def53f80b02e0574b13c"
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
dependencies = [
"proc-macro2",
"quote",
@@ -3334,9 +3334,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.8.0"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5ccd538d4a604753ebc2f17cd9946e89b77bf87f6a8e2309667c6f2e87855e3"
checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97"
dependencies = [
"base64",
"flate2",
@@ -3350,9 +3350,9 @@ dependencies = [
[[package]]
name = "url"
version = "2.4.1"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
dependencies = [
"form_urlencoded",
"idna",
@@ -3461,9 +3461,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
@@ -3471,9 +3471,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826"
dependencies = [
"bumpalo",
"log",
@@ -3498,9 +3498,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3508,9 +3508,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
@@ -3521,9 +3521,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.88"
version = "0.2.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "wasm-bindgen-test"

View File

@@ -9,7 +9,7 @@ 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"
license = "MIT2"
[workspace.dependencies]
anyhow = { version = "1.0.69" }
@@ -33,7 +33,7 @@ proc-macro2 = { version = "1.0.70" }
quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.15" }
schemars = { version = "0.8.16" }
serde = { version = "1.0.190", features = ["derive"] }
serde_json = { version = "1.0.108" }
shellexpand = { version = "3.0.0" }

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.6
rev: v0.1.7
hooks:
# Run the linter.
- id: ruff

View File

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

View File

@@ -34,8 +34,8 @@ harness = false
once_cell.workspace = true
serde.workspace = true
serde_json.workspace = true
url = "2.3.1"
ureq = "2.8.0"
url = "2.5.0"
ureq = "2.9.1"
criterion = { version = "0.5.1", default-features = false }
codspeed-criterion-compat = { version="2.3.3", default-features = false, optional = true}

View File

@@ -4,7 +4,7 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_python_formatter::{format_module_ast, PyFormatOptions};
use ruff_python_formatter::{format_module_ast, PreviewMode, PyFormatOptions};
use ruff_python_index::CommentRangesBuilder;
use ruff_python_parser::lexer::lex;
use ruff_python_parser::{parse_tokens, Mode};
@@ -69,7 +69,8 @@ fn benchmark_formatter(criterion: &mut Criterion) {
.expect("Input to be a valid python program");
b.iter(|| {
let options = PyFormatOptions::from_extension(Path::new(case.name()));
let options = PyFormatOptions::from_extension(Path::new(case.name()))
.with_preview(PreviewMode::Enabled);
let formatted =
format_module_ast(&module, &comment_ranges, case.code(), options)
.expect("Formatting to succeed");

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.6"
version = "0.1.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -69,7 +69,7 @@ insta = { workspace = true, features = ["filters", "json"] }
insta-cmd = { version = "0.4.0" }
tempfile = "3.8.1"
test-case = { workspace = true }
ureq = { version = "2.8.0", features = [] }
ureq = { version = "2.9.1", features = [] }
[target.'cfg(target_os = "windows")'.dependencies]
mimalloc = "0.1.39"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.6"
version = "0.1.7"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -39,3 +39,18 @@ def func():
for i in range(1110):
if True:
break
# TODO(charlie): The `pass` here does not get properly redirected to the top of the
# loop, unlike below.
def func():
for i in range(5):
pass
else:
return 1
def func():
for i in range(5):
pass
else:
return 1
x = 1

View File

@@ -129,3 +129,11 @@ def func():
print("Grass is green")
case Color.BLUE:
print("I'm feeling the blues :(")
def func(point):
match point:
case (0, 0):
print("Origin")
case foo:
raise ValueError("oops")

View File

@@ -63,3 +63,122 @@ def func(x: int):
return "str"
else:
return None
def func(x: int):
if x:
return 1
def func():
x = 1
def func(x: int):
if x > 0:
return 1
def func(x: int):
match x:
case [1, 2, 3]:
return 1
case 4 as y:
return "foo"
def func(x: int):
for i in range(5):
if i > 0:
return 1
def func(x: int):
for i in range(5):
if i > 0:
return 1
else:
return 4
def func(x: int):
for i in range(5):
if i > 0:
break
else:
return 4
def func(x: int):
try:
pass
except:
return 1
def func(x: int):
try:
pass
except:
return 1
finally:
return 2
def func(x: int):
try:
pass
except:
return 1
else:
return 2
def func(x: int):
try:
return 1
except:
return 2
else:
pass
def func(x: int):
while x > 0:
break
return 1
import abc
from abc import abstractmethod
class Foo(abc.ABC):
@abstractmethod
def method(self):
pass
@abc.abstractmethod
def method(self):
"""Docstring."""
@abc.abstractmethod
def method(self):
...
@staticmethod
@abstractmethod
def method():
pass
@classmethod
@abstractmethod
def method(cls):
pass
@abstractmethod
def method(self):
if self.x > 0:
return 1
else:
return 1.5

View File

@@ -10,7 +10,6 @@ Foo.objects.create(**{**bar}) # PIE804
foo(**{})
foo(**{**data, "foo": "buzz"})
foo(**buzz)
foo(**{"bar-foo": True})
@@ -20,3 +19,5 @@ foo(**{buzz: True})
foo(**{"": True})
foo(**{f"buzz__{bar}": True})
abc(**{"for": 3})
foo(**{},)

View File

@@ -22,3 +22,7 @@ Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type foo_bar = int | str
type FooBar = int | str

View File

@@ -22,3 +22,7 @@ Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type foo_bar = int | str
type FooBar = int | str

View File

@@ -21,3 +21,7 @@ _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type _FooT = str | int
type Foo = str | int

View File

@@ -21,3 +21,7 @@ _PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
# check that this edge case doesn't crash
_: TypeAlias = str | int
# PEP 695
type _FooT = str | int
type Foo = str | int

View File

@@ -10,3 +10,14 @@ def foo_no_return_typing_extensions(
def foo_no_return_kwarg(arg: int, *, arg2: NoReturn): ... # Error: PYI050
def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050
def foo_never(arg: Never): ...
def foo_args(*args: NoReturn): ... # Error: PYI050
def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
def foo_int_args(*args: int): ...
def foo_int_kwargs(**kwargs: int): ...
def foo_int_args_kwargs(*args: int, **kwargs: int): ...
def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050
def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050
def foo_args_never(*args: Never): ...
def foo_kwargs_never(**kwargs: Never): ...
def foo_args_kwargs_never(*args: Never, **kwargs: Never): ...

View File

@@ -82,3 +82,14 @@ raise IndexError();
# RSE102
raise Foo()
# OK
raise ctypes.WinError()
def func():
pass
# OK
raise func()

View File

@@ -0,0 +1,2 @@
import __future__
from __future__ import annotations

View File

@@ -0,0 +1,2 @@
import __future__
from __future__ import annotations

View File

@@ -52,3 +52,6 @@ def model_assign() -> None:
Bad = import_string("django.core.exceptions.ValidationError") # N806
ValidationError = import_string("django.core.exceptions.ValidationError") # OK
Bad = apps.get_model() # N806
Bad = apps.get_model(model_name="Stream") # N806

View File

@@ -43,3 +43,6 @@ regex = '''
''' # noqa
regex = '\\\_'
#: W605:1:7
u'foo\ bar'

View File

@@ -1,40 +1,54 @@
# Same as `W605_0.py` but using f-strings instead.
#: W605:1:10
regex = '\.png$'
regex = f'\.png$'
#: W605:2:1
regex = '''
regex = f'''
\.png$
'''
#: W605:2:6
f(
'\_'
f'\_'
)
#: W605:4:6
"""
f"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: W605:1:38
value = f'new line\nand invalid escape \_ here'
def f():
#: W605:1:11
return'\.png$'
#: Okay
regex = r'\.png$'
regex = '\\.png$'
regex = r'''
regex = fr'\.png$'
regex = f'\\.png$'
regex = fr'''
\.png$
'''
regex = r'''
regex = fr'''
\\.png$
'''
s = '\\'
regex = '\w' # noqa
regex = '''
s = f'\\'
regex = f'\w' # noqa
regex = f'''
\w
''' # noqa
regex = f'\\\_'
value = f'\{{1}}'
value = f'\{1}'
value = f'{1:\}'
value = f"{f"\{1}"}"
value = rf"{f"\{1}"}"
# Okay
value = rf'\{{1}}'
value = rf'\{1}'
value = rf'{1:\}'
value = f"{rf"\{1}"}"

View File

@@ -1,54 +0,0 @@
# Same as `W605_0.py` but using f-strings instead.
#: W605:1:10
regex = f'\.png$'
#: W605:2:1
regex = f'''
\.png$
'''
#: W605:2:6
f(
f'\_'
)
#: W605:4:6
f"""
multi-line
literal
with \_ somewhere
in the middle
"""
#: W605:1:38
value = f'new line\nand invalid escape \_ here'
#: Okay
regex = fr'\.png$'
regex = f'\\.png$'
regex = fr'''
\.png$
'''
regex = fr'''
\\.png$
'''
s = f'\\'
regex = f'\w' # noqa
regex = f'''
\w
''' # noqa
regex = f'\\\_'
value = f'\{{1}}'
value = f'\{1}'
value = f'{1:\}'
value = f"{f"\{1}"}"
value = rf"{f"\{1}"}"
# Okay
value = rf'\{{1}}'
value = rf'\{1}'
value = rf'{1:\}'
value = f"{rf"\{1}"}"

View File

@@ -1,5 +1,16 @@
"""
Author
"""
class Platform:
""" Remove sampler
Args:
    Returns:
"""
def memory_test():
"""
   参数含义precision:精确到小数点后几位
"""

View File

@@ -32,3 +32,16 @@ def f(x, y, z, *, u, v, w): # Too many arguments (6/5)
def f(x, y, z, a, b, c, *, u, v, w): # Too many arguments (9/5)
pass
from typing import override, overload
@override
def f(x, y, z, a, b, c, *, u, v, w): # OK
pass
@overload
def f(x, y, z, a, b, c, *, u, v, w): # OK
pass

View File

@@ -0,0 +1,30 @@
def f(x, y, z, t, u, v, w, r): # Too many positional arguments (8/3)
pass
def f(x): # OK
pass
def f(x, y, z, _t, _u, _v, _w, r): # OK (underscore-prefixed names are ignored
pass
def f(x, y, z, *, u=1, v=1, r=1): # OK
pass
def f(x=1, y=1, z=1): # OK
pass
def f(x, y, z, /, u, v, w): # Too many positional arguments (6/3)
pass
def f(x, y, z, *, u, v, w): # OK
pass
def f(x, y, z, a, b, c, *, u, v, w): # Too many positional arguments (6/3)
pass

View File

@@ -0,0 +1,10 @@
# Too many positional arguments (7/4) for max_positional=4
# OK for dummy_variable_rgx ~ "skip_.*"
def f(w, x, y, z, skip_t, skip_u, skip_v):
pass
# Too many positional arguments (7/4) for max_args=4
# Too many positional arguments (7/3) for dummy_variable_rgx ~ "skip_.*"
def f(w, x, y, z, t, u, v):
pass

View File

@@ -0,0 +1,40 @@
FRUITS = {"apple": 1, "orange": 10, "berry": 22}
def fix_these():
[FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
{FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
{fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
for fruit_name, fruit_count in FRUITS.items():
print(FRUITS[fruit_name]) # PLR1733
blah = FRUITS[fruit_name] # PLR1733
assert FRUITS[fruit_name] == "pear" # PLR1733
def dont_fix_these():
# once there is an assignment to the dict[index], we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
FRUITS[fruit_name] = 0 # OK
assert FRUITS[fruit_name] == 0 # OK
# once there is an assignment to the key, we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
fruit_name = 0 # OK
assert FRUITS[fruit_name] == 0 # OK
# once there is an assignment to the value, we stop emitting diagnostics
for fruit_name, fruit_count in FRUITS.items():
if fruit_count < 5:
fruit_count = -fruit_count
assert FRUITS[fruit_name] == 0 # OK
def value_intentionally_unused():
[FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()] # OK
{FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()} # OK
{fruit_name: FRUITS[fruit_name] for fruit_name, _ in FRUITS.items()} # OK
for fruit_name, _ in FRUITS.items():
print(FRUITS[fruit_name]) # OK
blah = FRUITS[fruit_name] # OK
assert FRUITS[fruit_name] == "pear" # OK

View File

@@ -12,7 +12,7 @@ def fix_these():
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
assert letters[index] == "d" # PLR1736
for index, letter in builtins.enumerate(letters):
print(letters[index]) # PLR1736
blah = letters[index] # PLR1736
@@ -22,38 +22,43 @@ def fix_these():
def dont_fix_these():
# once there is an assignment to the sequence[index], we stop emitting diagnostics
for index, letter in enumerate(letters):
letters[index] = "d" # Ok
letters[index] += "e" # Ok
assert letters[index] == "de" # Ok
letters[index] = "d" # OK
letters[index] += "e" # OK
assert letters[index] == "de" # OK
# once there is an assignment to the index, we stop emitting diagnostics
for index, letter in enumerate(letters):
index += 1 # Ok
print(letters[index]) # Ok
index += 1 # OK
print(letters[index]) # OK
# once there is an assignment to the sequence, we stop emitting diagnostics
for index, letter in enumerate(letters):
letters = ["d", "e", "f"] # Ok
print(letters[index]) # Ok
letters = ["d", "e", "f"] # OK
print(letters[index]) # OK
# once there is an assignment to the value, we stop emitting diagnostics
for index, letter in enumerate(letters):
letter = "d"
print(letters[index]) # OK
# once there is an deletion from or of the sequence or index, we stop emitting diagnostics
for index, letter in enumerate(letters):
del letters[index] # Ok
print(letters[index]) # Ok
del letters[index] # OK
print(letters[index]) # OK
for index, letter in enumerate(letters):
del letters # Ok
print(letters[index]) # Ok
del letters # OK
print(letters[index]) # OK
for index, letter in enumerate(letters):
del index # Ok
print(letters[index]) # Ok
del index # OK
print(letters[index]) # OK
def value_intentionally_unused():
[letters[index] for index, _ in enumerate(letters)] # Ok
{letters[index] for index, _ in enumerate(letters)} # Ok
{index: letters[index] for index, _ in enumerate(letters)} # Ok
[letters[index] for index, _ in enumerate(letters)] # OK
{letters[index] for index, _ in enumerate(letters)} # OK
{index: letters[index] for index, _ in enumerate(letters)} # OK
for index, _ in enumerate(letters):
print(letters[index]) # Ok
blah = letters[index] # Ok
letters[index] = "d" # Ok
print(letters[index]) # OK
blah = letters[index] # OK
letters[index] = "d" # OK

View File

@@ -42,3 +42,30 @@ tempfile.SpooledTemporaryFile(0, "w", encoding="utf-8")
tempfile.SpooledTemporaryFile(0, "w", -1, "utf-8")
tempfile.SpooledTemporaryFile(0, "wb")
tempfile.SpooledTemporaryFile(0, )
open("test.txt",)
open()
open(
"test.txt", # comment
)
open(
"test.txt",
# comment
)
open(("test.txt"),)
open(
("test.txt"), # comment
)
open(
("test.txt"),
# comment
)
open((("test.txt")),)
open(
(("test.txt")), # comment
)
open(
(("test.txt")),
# comment
)

View File

@@ -110,3 +110,10 @@ print('Hello %(arg)s' % bar['bop'])
"%s" % (
x, # comment
)
path = "%s-%s-%s.pem" % (
safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
)

View File

@@ -23,3 +23,6 @@ print(a) # noqa: E501, F821 # comment
print(a) # noqa: E501, F821 # comment
print(a) # noqa: E501, F821 comment
print(a) # noqa: E501, F821 comment
print(a) # comment with unicode µ # noqa: E501
print(a) # comment with unicode µ # noqa: E501, F821

View File

@@ -543,7 +543,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
}
if checker.enabled(Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
flake8_pie::rules::unnecessary_dict_kwargs(checker, call);
}
if checker.enabled(Rule::UnnecessaryRangeStart) {
flake8_pie::rules::unnecessary_range_start(checker, call);
@@ -1333,6 +1333,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1360,6 +1363,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_list_set_comprehension(
checker, expr, elt, generators,
@@ -1386,6 +1392,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_dict_comprehension(
checker, expr, key, value, generators,
@@ -1413,6 +1422,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}

View File

@@ -248,7 +248,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
}
if checker.enabled(Rule::TooManyArguments) {
pylint::rules::too_many_arguments(checker, parameters, stmt);
pylint::rules::too_many_arguments(checker, function_def);
}
if checker.enabled(Rule::TooManyPositional) {
pylint::rules::too_many_positional(checker, function_def);
}
if checker.enabled(Rule::TooManyReturnStatements) {
if let Some(diagnostic) = pylint::rules::too_many_return_statements(
@@ -1280,6 +1283,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {
pylint::rules::unnecessary_list_index_lookup(checker, for_stmt);
}
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup(checker, for_stmt);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
@@ -1531,6 +1537,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::TypeAlias(ast::StmtTypeAlias { name, .. }) => {
if checker.enabled(Rule::SnakeCaseTypeAlias) {
flake8_pyi::rules::snake_case_type_alias(checker, name);
}
if checker.enabled(Rule::TSuffixedTypeAlias) {
flake8_pyi::rules::t_suffixed_type_alias(checker, name);
}
}
Stmt::Delete(delete @ ast::StmtDelete { targets, range: _ }) => {
if checker.enabled(Rule::GlobalStatement) {
for target in targets {

View File

@@ -816,7 +816,7 @@ where
fn visit_expr(&mut self, expr: &'b Expr) {
// Step 0: Pre-processing
if !self.semantic.in_f_string()
&& !self.semantic.in_literal()
&& !self.semantic.in_typing_literal()
&& !self.semantic.in_deferred_type_definition()
&& self.semantic.in_type_definition()
&& self.semantic.future_annotations()
@@ -1198,7 +1198,7 @@ where
) {
// Ex) Literal["Class"]
Some(typing::SubscriptKind::Literal) => {
self.semantic.flags |= SemanticModelFlags::LITERAL;
self.semantic.flags |= SemanticModelFlags::TYPING_LITERAL;
self.visit_expr(slice);
self.visit_expr_context(ctx);
@@ -1239,7 +1239,7 @@ where
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if self.semantic.in_type_definition()
&& !self.semantic.in_literal()
&& !self.semantic.in_typing_literal()
&& !self.semantic.in_f_string()
{
self.deferred.string_type_definitions.push((

View File

@@ -3,10 +3,10 @@
use std::path::Path;
use itertools::Itertools;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_text_size::{Ranged, TextLen, TextRange};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{CommentRanges, PythonWhitespace};
use ruff_source_file::Locator;
use crate::noqa;
@@ -200,17 +200,11 @@ fn delete_noqa(range: TextRange, locator: &Locator) -> Edit {
// Compute the leading space.
let prefix = locator.slice(TextRange::new(line_range.start(), range.start()));
let leading_space = prefix
.rfind(|c: char| !c.is_whitespace())
.map_or(prefix.len(), |i| prefix.len() - i - 1);
let leading_space_len = TextSize::try_from(leading_space).unwrap();
let leading_space_len = prefix.text_len() - prefix.trim_whitespace_end().text_len();
// Compute the trailing space.
let suffix = locator.slice(TextRange::new(range.end(), line_range.end()));
let trailing_space = suffix
.find(|c: char| !c.is_whitespace())
.map_or(suffix.len(), |i| i);
let trailing_space_len = TextSize::try_from(trailing_space).unwrap();
let trailing_space_len = suffix.text_len() - suffix.trim_whitespace_start().text_len();
// Ex) `# noqa`
if line_range

View File

@@ -254,12 +254,14 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments),
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
(Pylint, "R1706") => (RuleGroup::Preview, rules::pylint::rules::AndOrTernary),
(Pylint, "R1722") => (RuleGroup::Stable, rules::pylint::rules::SysExitAlias),
(Pylint, "R1733") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryDictIndexLookup),
(Pylint, "R1736") => (RuleGroup::Preview, rules::pylint::rules::UnnecessaryListIndexLookup),
(Pylint, "R2004") => (RuleGroup::Stable, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Stable, rules::pylint::rules::CollapsibleElseIf),

View File

@@ -3,12 +3,14 @@
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
use ruff_python_ast::{AnyNodeRef, ArgOrKeyword};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
has_leading_content, is_python_whitespace, CommentRanges, PythonWhitespace, SimpleTokenKind,
SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline, UniversalNewlines};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -138,6 +140,32 @@ pub(crate) fn remove_argument<T: Ranged>(
}
}
/// Generic function to add arguments or keyword arguments to function calls.
pub(crate) fn add_argument(
argument: &str,
arguments: &Arguments,
comment_ranges: &CommentRanges,
source: &str,
) -> Edit {
if let Some(last) = arguments.arguments_source_order().last() {
// Case 1: existing arguments, so append after the last argument.
let last = parenthesized_range(
match last {
ArgOrKeyword::Arg(arg) => arg.into(),
ArgOrKeyword::Keyword(keyword) => (&keyword.value).into(),
},
arguments.into(),
comment_ranges,
source,
)
.unwrap_or(last.range());
Edit::insertion(format!(", {argument}"), last.end())
} else {
// Case 2: no arguments. Add argument, without any trailing comma.
Edit::insertion(argument.to_string(), arguments.start() + TextSize::from(1))
}
}
/// Determine if a vector contains only one, specific element.
fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
vec.len() == 1 && vec[0] == *value

View File

@@ -1,10 +1,9 @@
use itertools::Itertools;
use ruff_diagnostics::Edit;
use rustc_hash::FxHashSet;
use crate::importer::{ImportRequest, Importer};
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::{
pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
implicit_return, pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, ExprContext};
@@ -13,6 +12,7 @@ use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_text_size::{TextRange, TextSize};
use crate::importer::{ImportRequest, Importer};
use crate::settings::types::PythonVersion;
/// Return the name of the function, if it's overloaded.
@@ -48,14 +48,19 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
let returns = {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(&function.body);
// Ignore generators.
if visitor.is_generator {
return None;
}
visitor.returns
};
// Determine the return type of the first `return` statement.
let (return_statement, returns) = returns.split_first()?;
let Some((return_statement, returns)) = returns.split_first() else {
return Some(AutoPythonType::Atom(PythonType::None));
};
let mut return_type = return_statement.value.as_deref().map_or(
ResolvedPythonType::Atom(PythonType::None),
ResolvedPythonType::from,
@@ -69,6 +74,16 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
));
}
// If the function has an implicit return, union with `None`, as in:
// ```python
// def func(x: int):
// if x > 0:
// return 1
// ```
if implicit_return(function) {
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
}
match return_type {
ResolvedPythonType::Atom(python_type) => Some(AutoPythonType::Atom(python_type)),
ResolvedPythonType::Union(python_types) => Some(AutoPythonType::Union(python_types)),

View File

@@ -537,6 +537,19 @@ fn check_dynamically_typed<F>(
}
}
fn is_empty_body(body: &[Stmt]) -> bool {
body.iter().all(|stmt| match stmt {
Stmt::Pass(_) => true,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
matches!(
value.as_ref(),
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
)
}
_ => false,
})
}
/// Generate flake8-annotation checks for a given `Definition`.
pub(crate) fn definition(
checker: &Checker,
@@ -725,16 +738,22 @@ pub(crate) fn definition(
) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
@@ -752,16 +771,22 @@ pub(crate) fn definition(
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits));
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| (checker.generator().expr(&return_type), edits))
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
@@ -818,18 +843,25 @@ pub(crate) fn definition(
match visibility {
visibility::Visibility::Public => {
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
});
let return_type =
if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
@@ -853,18 +885,25 @@ pub(crate) fn definition(
}
visibility::Visibility::Private => {
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
let return_type = auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
});
let return_type =
if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),

View File

@@ -200,4 +200,299 @@ auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public f
61 61 | return 1
62 62 | elif x > 5:
auto_return_type.py:68:5: ANN201 [*] Missing return type annotation for public function `func`
|
68 | def func(x: int):
| ^^^^ ANN201
69 | if x:
70 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
65 65 | return None
66 66 |
67 67 |
68 |-def func(x: int):
68 |+def func(x: int) -> int | None:
69 69 | if x:
70 70 | return 1
71 71 |
auto_return_type.py:73:5: ANN201 [*] Missing return type annotation for public function `func`
|
73 | def func():
| ^^^^ ANN201
74 | x = 1
|
= help: Add return type annotation: `None`
Unsafe fix
70 70 | return 1
71 71 |
72 72 |
73 |-def func():
73 |+def func() -> None:
74 74 | x = 1
75 75 |
76 76 |
auto_return_type.py:77:5: ANN201 [*] Missing return type annotation for public function `func`
|
77 | def func(x: int):
| ^^^^ ANN201
78 | if x > 0:
79 | return 1
|
= help: Add return type annotation: `int | None`
Unsafe fix
74 74 | x = 1
75 75 |
76 76 |
77 |-def func(x: int):
77 |+def func(x: int) -> int | None:
78 78 | if x > 0:
79 79 | return 1
80 80 |
auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public function `func`
|
82 | def func(x: int):
| ^^^^ ANN201
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `str | int`
Unsafe fix
79 79 | return 1
80 80 |
81 81 |
82 |-def func(x: int):
82 |+def func(x: int) -> str | int:
83 83 | match x:
84 84 | case [1, 2, 3]:
85 85 | return 1
auto_return_type.py:90:5: ANN201 [*] Missing return type annotation for public function `func`
|
90 | def func(x: int):
| ^^^^ ANN201
91 | for i in range(5):
92 | if i > 0:
|
= help: Add return type annotation: `int | None`
Unsafe fix
87 87 | return "foo"
88 88 |
89 89 |
90 |-def func(x: int):
90 |+def func(x: int) -> int | None:
91 91 | for i in range(5):
92 92 | if i > 0:
93 93 | return 1
auto_return_type.py:96:5: ANN201 [*] Missing return type annotation for public function `func`
|
96 | def func(x: int):
| ^^^^ ANN201
97 | for i in range(5):
98 | if i > 0:
|
= help: Add return type annotation: `int`
Unsafe fix
93 93 | return 1
94 94 |
95 95 |
96 |-def func(x: int):
96 |+def func(x: int) -> int:
97 97 | for i in range(5):
98 98 | if i > 0:
99 99 | return 1
auto_return_type.py:104:5: ANN201 [*] Missing return type annotation for public function `func`
|
104 | def func(x: int):
| ^^^^ ANN201
105 | for i in range(5):
106 | if i > 0:
|
= help: Add return type annotation: `int | None`
Unsafe fix
101 101 | return 4
102 102 |
103 103 |
104 |-def func(x: int):
104 |+def func(x: int) -> int | None:
105 105 | for i in range(5):
106 106 | if i > 0:
107 107 | break
auto_return_type.py:112:5: ANN201 [*] Missing return type annotation for public function `func`
|
112 | def func(x: int):
| ^^^^ ANN201
113 | try:
114 | pass
|
= help: Add return type annotation: `int | None`
Unsafe fix
109 109 | return 4
110 110 |
111 111 |
112 |-def func(x: int):
112 |+def func(x: int) -> int | None:
113 113 | try:
114 114 | pass
115 115 | except:
auto_return_type.py:119:5: ANN201 [*] Missing return type annotation for public function `func`
|
119 | def func(x: int):
| ^^^^ ANN201
120 | try:
121 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
116 116 | return 1
117 117 |
118 118 |
119 |-def func(x: int):
119 |+def func(x: int) -> int:
120 120 | try:
121 121 | pass
122 122 | except:
auto_return_type.py:128:5: ANN201 [*] Missing return type annotation for public function `func`
|
128 | def func(x: int):
| ^^^^ ANN201
129 | try:
130 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
125 125 | return 2
126 126 |
127 127 |
128 |-def func(x: int):
128 |+def func(x: int) -> int:
129 129 | try:
130 130 | pass
131 131 | except:
auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public function `func`
|
137 | def func(x: int):
| ^^^^ ANN201
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
138 138 | try:
139 139 | return 1
140 140 | except:
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
146 | def func(x: int):
| ^^^^ ANN201
147 | while x > 0:
148 | break
|
= help: Add return type annotation: `int | None`
Unsafe fix
143 143 | pass
144 144 |
145 145 |
146 |-def func(x: int):
146 |+def func(x: int) -> int | None:
147 147 | while x > 0:
148 148 | break
149 149 | return 1
auto_return_type.py:158:9: ANN201 Missing return type annotation for public function `method`
|
156 | class Foo(abc.ABC):
157 | @abstractmethod
158 | def method(self):
| ^^^^^^ ANN201
159 | pass
|
= help: Add return type annotation
auto_return_type.py:162:9: ANN201 Missing return type annotation for public function `method`
|
161 | @abc.abstractmethod
162 | def method(self):
| ^^^^^^ ANN201
163 | """Docstring."""
|
= help: Add return type annotation
auto_return_type.py:166:9: ANN201 Missing return type annotation for public function `method`
|
165 | @abc.abstractmethod
166 | def method(self):
| ^^^^^^ ANN201
167 | ...
|
= help: Add return type annotation
auto_return_type.py:171:9: ANN205 Missing return type annotation for staticmethod `method`
|
169 | @staticmethod
170 | @abstractmethod
171 | def method():
| ^^^^^^ ANN205
172 | pass
|
= help: Add return type annotation
auto_return_type.py:176:9: ANN206 Missing return type annotation for classmethod `method`
|
174 | @classmethod
175 | @abstractmethod
176 | def method(cls):
| ^^^^^^ ANN206
177 | pass
|
= help: Add return type annotation
auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public function `method`
|
179 | @abstractmethod
180 | def method(self):
| ^^^^^^ ANN201
181 | if self.x > 0:
182 | return 1
|
= help: Add return type annotation: `float`
Unsafe fix
177 177 | pass
178 178 |
179 179 | @abstractmethod
180 |- def method(self):
180 |+ def method(self) -> float:
181 181 | if self.x > 0:
182 182 | return 1
183 183 | else:

View File

@@ -220,4 +220,334 @@ auto_return_type.py:59:5: ANN201 [*] Missing return type annotation for public f
61 62 | return 1
62 63 | elif x > 5:
auto_return_type.py:68:5: ANN201 [*] Missing return type annotation for public function `func`
|
68 | def func(x: int):
| ^^^^ ANN201
69 | if x:
70 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
65 66 | return None
66 67 |
67 68 |
68 |-def func(x: int):
69 |+def func(x: int) -> Optional[int]:
69 70 | if x:
70 71 | return 1
71 72 |
auto_return_type.py:73:5: ANN201 [*] Missing return type annotation for public function `func`
|
73 | def func():
| ^^^^ ANN201
74 | x = 1
|
= help: Add return type annotation: `None`
Unsafe fix
70 70 | return 1
71 71 |
72 72 |
73 |-def func():
73 |+def func() -> None:
74 74 | x = 1
75 75 |
76 76 |
auto_return_type.py:77:5: ANN201 [*] Missing return type annotation for public function `func`
|
77 | def func(x: int):
| ^^^^ ANN201
78 | if x > 0:
79 | return 1
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
74 75 | x = 1
75 76 |
76 77 |
77 |-def func(x: int):
78 |+def func(x: int) -> Optional[int]:
78 79 | if x > 0:
79 80 | return 1
80 81 |
auto_return_type.py:82:5: ANN201 [*] Missing return type annotation for public function `func`
|
82 | def func(x: int):
| ^^^^ ANN201
83 | match x:
84 | case [1, 2, 3]:
|
= help: Add return type annotation: `Union[str | int]`
Unsafe fix
1 |+from typing import Union
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
79 80 | return 1
80 81 |
81 82 |
82 |-def func(x: int):
83 |+def func(x: int) -> Union[str | int]:
83 84 | match x:
84 85 | case [1, 2, 3]:
85 86 | return 1
auto_return_type.py:90:5: ANN201 [*] Missing return type annotation for public function `func`
|
90 | def func(x: int):
| ^^^^ ANN201
91 | for i in range(5):
92 | if i > 0:
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
87 88 | return "foo"
88 89 |
89 90 |
90 |-def func(x: int):
91 |+def func(x: int) -> Optional[int]:
91 92 | for i in range(5):
92 93 | if i > 0:
93 94 | return 1
auto_return_type.py:96:5: ANN201 [*] Missing return type annotation for public function `func`
|
96 | def func(x: int):
| ^^^^ ANN201
97 | for i in range(5):
98 | if i > 0:
|
= help: Add return type annotation: `int`
Unsafe fix
93 93 | return 1
94 94 |
95 95 |
96 |-def func(x: int):
96 |+def func(x: int) -> int:
97 97 | for i in range(5):
98 98 | if i > 0:
99 99 | return 1
auto_return_type.py:104:5: ANN201 [*] Missing return type annotation for public function `func`
|
104 | def func(x: int):
| ^^^^ ANN201
105 | for i in range(5):
106 | if i > 0:
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
101 102 | return 4
102 103 |
103 104 |
104 |-def func(x: int):
105 |+def func(x: int) -> Optional[int]:
105 106 | for i in range(5):
106 107 | if i > 0:
107 108 | break
auto_return_type.py:112:5: ANN201 [*] Missing return type annotation for public function `func`
|
112 | def func(x: int):
| ^^^^ ANN201
113 | try:
114 | pass
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
109 110 | return 4
110 111 |
111 112 |
112 |-def func(x: int):
113 |+def func(x: int) -> Optional[int]:
113 114 | try:
114 115 | pass
115 116 | except:
auto_return_type.py:119:5: ANN201 [*] Missing return type annotation for public function `func`
|
119 | def func(x: int):
| ^^^^ ANN201
120 | try:
121 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
116 116 | return 1
117 117 |
118 118 |
119 |-def func(x: int):
119 |+def func(x: int) -> int:
120 120 | try:
121 121 | pass
122 122 | except:
auto_return_type.py:128:5: ANN201 [*] Missing return type annotation for public function `func`
|
128 | def func(x: int):
| ^^^^ ANN201
129 | try:
130 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
125 125 | return 2
126 126 |
127 127 |
128 |-def func(x: int):
128 |+def func(x: int) -> int:
129 129 | try:
130 130 | pass
131 131 | except:
auto_return_type.py:137:5: ANN201 [*] Missing return type annotation for public function `func`
|
137 | def func(x: int):
| ^^^^ ANN201
138 | try:
139 | return 1
|
= help: Add return type annotation: `int`
Unsafe fix
134 134 | return 2
135 135 |
136 136 |
137 |-def func(x: int):
137 |+def func(x: int) -> int:
138 138 | try:
139 139 | return 1
140 140 | except:
auto_return_type.py:146:5: ANN201 [*] Missing return type annotation for public function `func`
|
146 | def func(x: int):
| ^^^^ ANN201
147 | while x > 0:
148 | break
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
1 |+from typing import Optional
1 2 | def func():
2 3 | return 1
3 4 |
--------------------------------------------------------------------------------
143 144 | pass
144 145 |
145 146 |
146 |-def func(x: int):
147 |+def func(x: int) -> Optional[int]:
147 148 | while x > 0:
148 149 | break
149 150 | return 1
auto_return_type.py:158:9: ANN201 Missing return type annotation for public function `method`
|
156 | class Foo(abc.ABC):
157 | @abstractmethod
158 | def method(self):
| ^^^^^^ ANN201
159 | pass
|
= help: Add return type annotation
auto_return_type.py:162:9: ANN201 Missing return type annotation for public function `method`
|
161 | @abc.abstractmethod
162 | def method(self):
| ^^^^^^ ANN201
163 | """Docstring."""
|
= help: Add return type annotation
auto_return_type.py:166:9: ANN201 Missing return type annotation for public function `method`
|
165 | @abc.abstractmethod
166 | def method(self):
| ^^^^^^ ANN201
167 | ...
|
= help: Add return type annotation
auto_return_type.py:171:9: ANN205 Missing return type annotation for staticmethod `method`
|
169 | @staticmethod
170 | @abstractmethod
171 | def method():
| ^^^^^^ ANN205
172 | pass
|
= help: Add return type annotation
auto_return_type.py:176:9: ANN206 Missing return type annotation for classmethod `method`
|
174 | @classmethod
175 | @abstractmethod
176 | def method(cls):
| ^^^^^^ ANN206
177 | pass
|
= help: Add return type annotation
auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public function `method`
|
179 | @abstractmethod
180 | def method(self):
| ^^^^^^ ANN201
181 | if self.x > 0:
182 | return 1
|
= help: Add return type annotation: `float`
Unsafe fix
177 177 | pass
178 178 |
179 179 | @abstractmethod
180 |- def method(self):
180 |+ def method(self) -> float:
181 181 | if self.x > 0:
182 182 | return 1
183 183 | else:

View File

@@ -1,14 +1,24 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
annotation_presence.py:5:5: ANN201 Missing return type annotation for public function `foo`
annotation_presence.py:5:5: ANN201 [*] Missing return type annotation for public function `foo`
|
4 | # Error
5 | def foo(a, b):
| ^^^ ANN201
6 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
2 2 | from typing_extensions import override
3 3 |
4 4 | # Error
5 |-def foo(a, b):
5 |+def foo(a, b) -> None:
6 6 | pass
7 7 |
8 8 |
annotation_presence.py:5:9: ANN001 Missing type annotation for function argument `a`
|
@@ -26,14 +36,24 @@ annotation_presence.py:5:12: ANN001 Missing type annotation for function argumen
6 | pass
|
annotation_presence.py:10:5: ANN201 Missing return type annotation for public function `foo`
annotation_presence.py:10:5: ANN201 [*] Missing return type annotation for public function `foo`
|
9 | # Error
10 | def foo(a: int, b):
| ^^^ ANN201
11 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
7 7 |
8 8 |
9 9 | # Error
10 |-def foo(a: int, b):
10 |+def foo(a: int, b) -> None:
11 11 | pass
12 12 |
13 13 |
annotation_presence.py:10:17: ANN001 Missing type annotation for function argument `b`
|
@@ -51,23 +71,43 @@ annotation_presence.py:15:17: ANN001 Missing type annotation for function argume
16 | pass
|
annotation_presence.py:20:5: ANN201 Missing return type annotation for public function `foo`
annotation_presence.py:20:5: ANN201 [*] Missing return type annotation for public function `foo`
|
19 | # Error
20 | def foo(a: int, b: int):
| ^^^ ANN201
21 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
annotation_presence.py:25:5: ANN201 Missing return type annotation for public function `foo`
Unsafe fix
17 17 |
18 18 |
19 19 | # Error
20 |-def foo(a: int, b: int):
20 |+def foo(a: int, b: int) -> None:
21 21 | pass
22 22 |
23 23 |
annotation_presence.py:25:5: ANN201 [*] Missing return type annotation for public function `foo`
|
24 | # Error
25 | def foo():
| ^^^ ANN201
26 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
22 22 |
23 23 |
24 24 | # Error
25 |-def foo():
25 |+def foo() -> None:
26 26 | pass
27 27 |
28 28 |
annotation_presence.py:45:12: ANN401 Dynamically typed expressions (typing.Any) are disallowed in `a`
|

View File

@@ -1,13 +1,23 @@
---
source: crates/ruff_linter/src/rules/flake8_annotations/mod.rs
---
ignore_fully_untyped.py:24:5: ANN201 Missing return type annotation for public function `error_partially_typed_1`
ignore_fully_untyped.py:24:5: ANN201 [*] Missing return type annotation for public function `error_partially_typed_1`
|
24 | def error_partially_typed_1(a: int, b):
| ^^^^^^^^^^^^^^^^^^^^^^^ ANN201
25 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
21 21 | pass
22 22 |
23 23 |
24 |-def error_partially_typed_1(a: int, b):
24 |+def error_partially_typed_1(a: int, b) -> None:
25 25 | pass
26 26 |
27 27 |
ignore_fully_untyped.py:24:37: ANN001 Missing type annotation for function argument `b`
|
@@ -23,15 +33,25 @@ ignore_fully_untyped.py:28:37: ANN001 Missing type annotation for function argum
29 | pass
|
ignore_fully_untyped.py:32:5: ANN201 Missing return type annotation for public function `error_partially_typed_3`
ignore_fully_untyped.py:32:5: ANN201 [*] Missing return type annotation for public function `error_partially_typed_3`
|
32 | def error_partially_typed_3(a: int, b: int):
| ^^^^^^^^^^^^^^^^^^^^^^^ ANN201
33 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
ignore_fully_untyped.py:43:9: ANN201 Missing return type annotation for public function `error_typed_self`
Unsafe fix
29 29 | pass
30 30 |
31 31 |
32 |-def error_partially_typed_3(a: int, b: int):
32 |+def error_partially_typed_3(a: int, b: int) -> None:
33 33 | pass
34 34 |
35 35 |
ignore_fully_untyped.py:43:9: ANN201 [*] Missing return type annotation for public function `error_typed_self`
|
41 | pass
42 |
@@ -39,6 +59,14 @@ ignore_fully_untyped.py:43:9: ANN201 Missing return type annotation for public f
| ^^^^^^^^^^^^^^^^ ANN201
44 | pass
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
40 40 | def ok_untyped_method(self):
41 41 | pass
42 42 |
43 |- def error_typed_self(self: X):
43 |+ def error_typed_self(self: X) -> None:
44 44 | pass

View File

@@ -41,14 +41,24 @@ mypy_init_return.py:11:9: ANN204 [*] Missing return type annotation for special
13 13 |
14 14 |
mypy_init_return.py:40:5: ANN202 Missing return type annotation for private function `__init__`
mypy_init_return.py:40:5: ANN202 [*] Missing return type annotation for private function `__init__`
|
39 | # Error
40 | def __init__(self, foo: int):
| ^^^^^^^^ ANN202
41 | ...
|
= help: Add return type annotation
= help: Add return type annotation: `None`
Unsafe fix
37 37 |
38 38 |
39 39 | # Error
40 |-def __init__(self, foo: int):
40 |+def __init__(self, foo: int) -> None:
41 41 | ...
42 42 |
43 43 |
mypy_init_return.py:47:9: ANN204 [*] Missing return type annotation for special method `__init__`
|

View File

@@ -1,6 +1,6 @@
use itertools::Itertools;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_python_ast::{self as ast, Expr, Keyword};
use ruff_python_ast::{self as ast, Expr};
use ruff_macros::{derive_message_formats, violation};
use ruff_text_size::Ranged;
@@ -8,6 +8,7 @@ use ruff_text_size::Ranged;
use ruff_python_stdlib::identifiers::is_identifier;
use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
/// ## What it does
/// Checks for unnecessary `dict` kwargs.
@@ -52,8 +53,8 @@ impl AlwaysFixableViolation for UnnecessaryDictKwargs {
}
/// PIE804
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
for kw in kwargs {
pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, call: &ast::ExprCall) {
for kw in &call.arguments.keywords {
// keyword is a spread operator (indicated by None)
if kw.arg.is_some() {
continue;
@@ -65,7 +66,7 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
// Ex) `foo(**{**bar})`
if matches!(keys.as_slice(), [None]) {
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, call.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("**{}", checker.locator().slice(values[0].range())),
@@ -86,10 +87,18 @@ pub(crate) fn unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs
continue;
}
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, expr.range());
let mut diagnostic = Diagnostic::new(UnnecessaryDictKwargs, call.range());
if values.is_empty() {
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(kw.start(), kw.end())));
diagnostic.try_set_fix(|| {
remove_argument(
kw,
&call.arguments,
Parentheses::Preserve,
checker.locator().contents(),
)
.map(Fix::safe_edit)
});
} else {
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
kwargs

View File

@@ -106,6 +106,8 @@ PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
10 |
11 | foo(**{})
| ^^^^^^^^^ PIE804
12 |
13 | foo(**{**data, "foo": "buzz"})
|
= help: Remove unnecessary kwargs
@@ -116,7 +118,23 @@ PIE804.py:11:1: PIE804 [*] Unnecessary `dict` kwargs
11 |-foo(**{})
11 |+foo()
12 12 |
13 13 |
14 14 | foo(**{**data, "foo": "buzz"})
13 13 | foo(**{**data, "foo": "buzz"})
14 14 | foo(**buzz)
PIE804.py:23:1: PIE804 [*] Unnecessary `dict` kwargs
|
21 | abc(**{"for": 3})
22 |
23 | foo(**{},)
| ^^^^^^^^^^ PIE804
|
= help: Remove unnecessary kwargs
Safe fix
20 20 | foo(**{f"buzz__{bar}": True})
21 21 | abc(**{"for": 3})
22 22 |
23 |-foo(**{},)
23 |+foo()

View File

@@ -2,7 +2,7 @@ use std::fmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Parameters;
use ruff_python_ast::{Expr, Parameters};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -51,6 +51,9 @@ impl Violation for NoReturnArgumentAnnotationInStub {
/// PYI050
pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &Parameters) {
// Ex) def func(arg: NoReturn): ...
// Ex) def func(arg: NoReturn, /): ...
// Ex) def func(*, arg: NoReturn): ...
for annotation in parameters
.posonlyargs
.iter()
@@ -58,19 +61,37 @@ pub(crate) fn no_return_argument_annotation(checker: &mut Checker, parameters: &
.chain(&parameters.kwonlyargs)
.filter_map(|arg| arg.parameter.annotation.as_ref())
{
if checker.semantic().match_typing_expr(annotation, "NoReturn") {
checker.diagnostics.push(Diagnostic::new(
NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= Py311 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions
},
},
annotation.range(),
));
check_no_return_argument_annotation(checker, annotation);
}
// Ex) def func(*args: NoReturn): ...
if let Some(arg) = &parameters.vararg {
if let Some(annotation) = &arg.annotation {
check_no_return_argument_annotation(checker, annotation);
}
}
// Ex) def func(**kwargs: NoReturn): ...
if let Some(arg) = &parameters.kwarg {
if let Some(annotation) = &arg.annotation {
check_no_return_argument_annotation(checker, annotation);
}
}
}
fn check_no_return_argument_annotation(checker: &mut Checker, annotation: &Expr) {
if checker.semantic().match_typing_expr(annotation, "NoReturn") {
checker.diagnostics.push(Diagnostic::new(
NoReturnArgumentAnnotationInStub {
module: if checker.settings.target_version >= Py311 {
TypingModule::Typing
} else {
TypingModule::TypingExtensions
},
},
annotation.range(),
));
}
}
#[derive(Debug, PartialEq, Eq)]

View File

@@ -46,12 +46,16 @@ impl Violation for SnakeCaseTypeAlias {
///
/// ## Example
/// ```python
/// MyTypeT = int
/// from typing import TypeAlias
///
/// _MyTypeT: TypeAlias = int
/// ```
///
/// Use instead:
/// ```python
/// MyType = int
/// from typing import TypeAlias
///
/// _MyType: TypeAlias = int
/// ```
#[violation]
pub struct TSuffixedTypeAlias {

View File

@@ -29,4 +29,12 @@ PYI042.py:20:1: PYI042 Type alias `_snake_case_alias2` should be CamelCase
21 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
PYI042.py:27:6: PYI042 Type alias `foo_bar` should be CamelCase
|
26 | # PEP 695
27 | type foo_bar = int | str
| ^^^^^^^ PYI042
28 | type FooBar = int | str
|

View File

@@ -29,4 +29,12 @@ PYI042.pyi:20:1: PYI042 Type alias `_snake_case_alias2` should be CamelCase
21 | Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
PYI042.pyi:27:6: PYI042 Type alias `foo_bar` should be CamelCase
|
26 | # PEP 695
27 | type foo_bar = int | str
| ^^^^^^^ PYI042
28 | type FooBar = int | str
|

View File

@@ -30,4 +30,12 @@ PYI043.py:12:1: PYI043 Private type alias `_PrivateAliasT3` should not be suffix
14 | ] # PYI043, since this ends in a T
|
PYI043.py:26:6: PYI043 Private type alias `_FooT` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
25 | # PEP 695
26 | type _FooT = str | int
| ^^^^^ PYI043
27 | type Foo = str | int
|

View File

@@ -30,4 +30,12 @@ PYI043.pyi:12:1: PYI043 Private type alias `_PrivateAliasT3` should not be suffi
14 | ] # PYI043, since this ends in a T
|
PYI043.pyi:26:6: PYI043 Private type alias `_FooT` should not be suffixed with `T` (the `T` suffix implies that an object is a `TypeVar`)
|
25 | # PEP 695
26 | type _FooT = str | int
| ^^^^^ PYI043
27 | type Foo = str | int
|

View File

@@ -28,6 +28,67 @@ PYI050.pyi:11:47: PYI050 Prefer `typing.Never` over `NoReturn` for argument anno
11 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
12 | def foo_never(arg: Never): ...
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
|
PYI050.pyi:13:21: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
11 | def foo_no_return_pos_only(arg: int, /, arg2: NoReturn): ... # Error: PYI050
12 | def foo_never(arg: Never): ...
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
|
PYI050.pyi:14:26: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
12 | def foo_never(arg: Never): ...
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
16 | def foo_int_args(*args: int): ...
|
PYI050.pyi:15:28: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
16 | def foo_int_args(*args: int): ...
17 | def foo_int_kwargs(**kwargs: int): ...
|
PYI050.pyi:15:48: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
13 | def foo_args(*args: NoReturn): ... # Error: PYI050
14 | def foo_kwargs(**kwargs: NoReturn): ... # Error: PYI050
15 | def foo_args_kwargs(*args: NoReturn, **kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
16 | def foo_int_args(*args: int): ...
17 | def foo_int_kwargs(**kwargs: int): ...
|
PYI050.pyi:19:50: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
17 | def foo_int_kwargs(**kwargs: int): ...
18 | def foo_int_args_kwargs(*args: int, **kwargs: int): ...
19 | def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050
| ^^^^^^^^ PYI050
20 | def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050
21 | def foo_args_never(*args: Never): ...
|
PYI050.pyi:20:37: PYI050 Prefer `typing.Never` over `NoReturn` for argument annotations
|
18 | def foo_int_args_kwargs(*args: int, **kwargs: int): ...
19 | def foo_int_args_no_return(*args: int, **kwargs: NoReturn): ... # Error: PYI050
20 | def foo_int_kwargs_no_return(*args: NoReturn, **kwargs: int): ... # Error: PYI050
| ^^^^^^^^ PYI050
21 | def foo_args_never(*args: Never): ...
22 | def foo_kwargs_never(**kwargs: Never): ...
|

View File

@@ -78,9 +78,7 @@ pub(crate) fn unnecessary_paren_on_raise_exception(checker: &mut Checker, expr:
// `ctypes.WinError()` is a function, not a class. It's part of the standard library, so
// we might as well get it right.
if exception_type
.as_ref()
.is_some_and(ExceptionType::is_builtin)
if exception_type.is_none()
&& checker
.semantic()
.resolve_call_path(func)

View File

@@ -266,6 +266,8 @@ RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
83 | # RSE102
84 | raise Foo()
| ^^ RSE102
85 |
86 | # OK
|
= help: Remove unnecessary parentheses
@@ -275,5 +277,8 @@ RSE102.py:84:10: RSE102 [*] Unnecessary parentheses on raised exception
83 83 | # RSE102
84 |-raise Foo()
84 |+raise Foo
85 85 |
86 86 | # OK
87 87 | raise ctypes.WinError()

View File

@@ -66,6 +66,10 @@ pub(crate) fn private_member_access(checker: &mut Checker, expr: &Expr) {
return;
};
if checker.semantic().in_annotation() {
return;
}
if (attr.starts_with("__") && !attr.ends_with("__"))
|| (attr.starts_with('_') && !attr.starts_with("__"))
{

View File

@@ -180,7 +180,7 @@ fn format_import_block(
continue;
};
let imports = order_imports(import_block, settings);
let imports = order_imports(import_block, import_section, settings);
// Add a blank line between every section.
if is_first_block {
@@ -291,6 +291,7 @@ mod tests {
#[test_case(Path::new("force_sort_within_sections.py"))]
#[test_case(Path::new("force_to_top.py"))]
#[test_case(Path::new("force_wrap_aliases.py"))]
#[test_case(Path::new("future_from.py"))]
#[test_case(Path::new("if_elif_else.py"))]
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("inline_comments.py"))]
@@ -701,6 +702,7 @@ mod tests {
#[test_case(Path::new("force_sort_within_sections.py"))]
#[test_case(Path::new("force_sort_within_sections_with_as_names.py"))]
#[test_case(Path::new("force_sort_within_sections_future.py"))]
fn force_sort_within_sections(path: &Path) -> Result<()> {
let snapshot = format!("force_sort_within_sections_{}", path.to_string_lossy());
let mut diagnostics = test_path(

View File

@@ -1,4 +1,5 @@
use crate::rules::isort::sorting::ImportStyle;
use crate::rules::isort::{ImportSection, ImportType};
use itertools::Itertools;
use super::settings::Settings;
@@ -8,6 +9,7 @@ use super::types::{AliasData, CommentSet, ImportBlock, ImportFromStatement};
pub(crate) fn order_imports<'a>(
block: ImportBlock<'a>,
section: &ImportSection,
settings: &Settings,
) -> Vec<EitherImport<'a>> {
let straight_imports = block.import.into_iter();
@@ -52,7 +54,35 @@ pub(crate) fn order_imports<'a>(
},
);
let ordered_imports = if settings.force_sort_within_sections {
let ordered_imports = if matches!(section, ImportSection::Known(ImportType::Future)) {
from_imports
.sorted_by_cached_key(|(import_from, _, _, aliases)| {
ModuleKey::from_module(
import_from.module,
None,
import_from.level,
aliases.first().map(|(alias, _)| (alias.name, alias.asname)),
ImportStyle::From,
settings,
)
})
.map(ImportFrom)
.chain(
straight_imports
.sorted_by_cached_key(|(alias, _)| {
ModuleKey::from_module(
Some(alias.name),
alias.asname,
None,
None,
ImportStyle::Straight,
settings,
)
})
.map(Import),
)
.collect()
} else if settings.force_sort_within_sections {
straight_imports
.map(Import)
.chain(from_imports.map(ImportFrom))

View File

@@ -0,0 +1,16 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
force_sort_within_sections_future.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import __future__
2 | | from __future__ import annotations
|
= help: Organize imports
Safe fix
1 |+from __future__ import annotations
1 2 | import __future__
2 |-from __future__ import annotations

View File

@@ -0,0 +1,16 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
future_from.py:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / import __future__
2 | | from __future__ import annotations
|
= help: Organize imports
Safe fix
1 |+from __future__ import annotations
1 2 | import __future__
2 |-from __future__ import annotations

View File

@@ -101,11 +101,15 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti
return false;
};
if arguments.is_empty() {
return false;
}
// Match against, e.g., `apps.get_model("zerver", "Attachment")`.
if let Some(call_path) = collect_call_path(func.as_ref()) {
if matches!(call_path.as_slice(), [.., "get_model"]) {
if let Some(argument) =
arguments.find_argument("model_name", arguments.args.len() - 1)
arguments.find_argument("model_name", arguments.args.len().saturating_sub(1))
{
if let Some(string_literal) = argument.as_string_literal_expr() {
return string_literal.value.to_str() == name;

View File

@@ -38,4 +38,20 @@ N806.py:53:5: N806 Variable `Bad` in function should be lowercase
54 | ValidationError = import_string("django.core.exceptions.ValidationError") # OK
|
N806.py:56:5: N806 Variable `Bad` in function should be lowercase
|
54 | ValidationError = import_string("django.core.exceptions.ValidationError") # OK
55 |
56 | Bad = apps.get_model() # N806
| ^^^ N806
57 | Bad = apps.get_model(model_name="Stream") # N806
|
N806.py:57:5: N806 Variable `Bad` in function should be lowercase
|
56 | Bad = apps.get_model() # N806
57 | Bad = apps.get_model(model_name="Stream") # N806
| ^^^ N806
|

View File

@@ -31,7 +31,6 @@ mod tests {
#[test_case(Rule::BlankLineWithWhitespace, Path::new("W29.py"))]
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_0.py"))]
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_1.py"))]
#[test_case(Rule::InvalidEscapeSequence, Path::new("W605_2.py"))]
#[test_case(Rule::LineTooLong, Path::new("E501.py"))]
#[test_case(Rule::LineTooLong, Path::new("E501_3.py"))]
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]

View File

@@ -3,9 +3,9 @@ use memchr::memchr_iter;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_python_parser::Tok;
use ruff_python_parser::{StringKind, Tok};
use ruff_source_file::Locator;
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::fix::edits::pad_start;
@@ -58,18 +58,6 @@ impl AlwaysFixableViolation for InvalidEscapeSequence {
}
}
#[derive(Debug, PartialEq, Eq)]
enum FixTitle {
AddBackslash,
UseRawStringLiteral,
}
#[derive(Debug)]
struct InvalidEscapeChar {
ch: char,
range: TextRange,
}
/// W605
pub(crate) fn invalid_escape_sequence(
diagnostics: &mut Vec<Diagnostic>,
@@ -195,41 +183,77 @@ pub(crate) fn invalid_escape_sequence(
if contains_valid_escape_sequence {
// Escape with backslash.
for invalid_escape_char in &invalid_escape_chars {
let diagnostic = Diagnostic::new(
let mut diagnostic = Diagnostic::new(
InvalidEscapeSequence {
ch: invalid_escape_char.ch,
fix_title: FixTitle::AddBackslash,
},
invalid_escape_char.range,
)
.with_fix(Fix::safe_edit(Edit::insertion(
invalid_escape_char.range(),
);
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
r"\".to_string(),
invalid_escape_char.range.start() + TextSize::from(1),
invalid_escape_char.start() + TextSize::from(1),
)));
invalid_escape_sequence.push(diagnostic);
}
} else {
// Turn into raw string.
for invalid_escape_char in &invalid_escape_chars {
let diagnostic = Diagnostic::new(
let mut diagnostic = Diagnostic::new(
InvalidEscapeSequence {
ch: invalid_escape_char.ch,
fix_title: FixTitle::UseRawStringLiteral,
},
invalid_escape_char.range,
)
.with_fix(
// If necessary, add a space between any leading keyword (`return`, `yield`,
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
// `returnr"foo"` is not.
Fix::safe_edit(Edit::insertion(
pad_start("r".to_string(), string_start_location, locator),
string_start_location,
)),
invalid_escape_char.range(),
);
if matches!(
token,
Tok::String {
kind: StringKind::Unicode,
..
}
) {
// Replace the Unicode prefix with `r`.
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
"r".to_string(),
string_start_location,
string_start_location + TextSize::from(1),
)));
} else {
// Insert the `r` prefix.
diagnostic.set_fix(
// If necessary, add a space between any leading keyword (`return`, `yield`,
// `assert`, etc.) and the string. For example, `return"foo"` is valid, but
// `returnr"foo"` is not.
Fix::safe_edit(Edit::insertion(
pad_start("r".to_string(), string_start_location, locator),
string_start_location,
)),
);
}
invalid_escape_sequence.push(diagnostic);
}
}
diagnostics.extend(invalid_escape_sequence);
}
#[derive(Debug, PartialEq, Eq)]
enum FixTitle {
AddBackslash,
UseRawStringLiteral,
}
#[derive(Debug)]
struct InvalidEscapeChar {
ch: char,
range: TextRange,
}
impl Ranged for InvalidEscapeChar {
fn range(&self) -> TextRange {
self.range
}
}

View File

@@ -125,6 +125,8 @@ W605_0.py:45:12: W605 [*] Invalid escape sequence: `\_`
44 |
45 | regex = '\\\_'
| ^^ W605
46 |
47 | #: W605:1:7
|
= help: Add backslash to escape sequence
@@ -134,5 +136,23 @@ W605_0.py:45:12: W605 [*] Invalid escape sequence: `\_`
44 44 |
45 |-regex = '\\\_'
45 |+regex = '\\\\_'
46 46 |
47 47 | #: W605:1:7
48 48 | u'foo\ bar'
W605_0.py:48:6: W605 [*] Invalid escape sequence: `\ `
|
47 | #: W605:1:7
48 | u'foo\ bar'
| ^^ W605
|
= help: Use a raw string literal
Safe fix
45 45 | regex = '\\\_'
46 46 |
47 47 | #: W605:1:7
48 |-u'foo\ bar'
48 |+r'foo\ bar'

View File

@@ -1,104 +1,227 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.`
W605_1.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
1 | #: W605:1:10
2 | regex = '\.png$'
| ^^ W605
3 |
4 | #: W605:2:1
3 | #: W605:1:10
4 | regex = f'\.png$'
| ^^ W605
5 |
6 | #: W605:2:1
|
= help: Use a raw string literal
Safe fix
1 1 | #: W605:1:10
2 |-regex = '\.png$'
2 |+regex = r'\.png$'
3 3 |
4 4 | #: W605:2:1
5 5 | regex = '''
1 1 | # Same as `W605_0.py` but using f-strings instead.
2 2 |
3 3 | #: W605:1:10
4 |-regex = f'\.png$'
4 |+regex = rf'\.png$'
5 5 |
6 6 | #: W605:2:1
7 7 | regex = f'''
W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.`
W605_1.py:8:1: W605 [*] Invalid escape sequence: `\.`
|
4 | #: W605:2:1
5 | regex = '''
6 | \.png$
6 | #: W605:2:1
7 | regex = f'''
8 | \.png$
| ^^ W605
7 | '''
9 | '''
|
= help: Use a raw string literal
Safe fix
2 2 | regex = '\.png$'
3 3 |
4 4 | #: W605:2:1
5 |-regex = '''
5 |+regex = r'''
6 6 | \.png$
7 7 | '''
8 8 |
4 4 | regex = f'\.png$'
5 5 |
6 6 | #: W605:2:1
7 |-regex = f'''
7 |+regex = rf'''
8 8 | \.png$
9 9 | '''
10 10 |
W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_`
W605_1.py:13:7: W605 [*] Invalid escape sequence: `\_`
|
9 | #: W605:2:6
10 | f(
11 | '\_'
11 | #: W605:2:6
12 | f(
13 | f'\_'
| ^^ W605
14 | )
|
= help: Use a raw string literal
Safe fix
10 10 |
11 11 | #: W605:2:6
12 12 | f(
13 |- f'\_'
13 |+ rf'\_'
14 14 | )
15 15 |
16 16 | #: W605:4:6
W605_1.py:20:6: W605 [*] Invalid escape sequence: `\_`
|
18 | multi-line
19 | literal
20 | with \_ somewhere
| ^^ W605
12 | )
21 | in the middle
22 | """
|
= help: Use a raw string literal
Safe fix
8 8 |
9 9 | #: W605:2:6
10 10 | f(
11 |- '\_'
11 |+ r'\_'
12 12 | )
13 13 |
14 14 | #: W605:4:6
14 14 | )
15 15 |
16 16 | #: W605:4:6
17 |-f"""
17 |+rf"""
18 18 | multi-line
19 19 | literal
20 20 | with \_ somewhere
W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_`
W605_1.py:25:40: W605 [*] Invalid escape sequence: `\_`
|
16 | multi-line
17 | literal
18 | with \_ somewhere
| ^^ W605
19 | in the middle
20 | """
24 | #: W605:1:38
25 | value = f'new line\nand invalid escape \_ here'
| ^^ W605
|
= help: Use a raw string literal
= help: Add backslash to escape sequence
Safe fix
12 12 | )
13 13 |
14 14 | #: W605:4:6
15 |-"""
15 |+r"""
16 16 | multi-line
17 17 | literal
18 18 | with \_ somewhere
W605_1.py:25:12: W605 [*] Invalid escape sequence: `\.`
|
23 | def f():
24 | #: W605:1:11
25 | return'\.png$'
| ^^ W605
26 |
27 | #: Okay
|
= help: Use a raw string literal
Safe fix
22 22 |
23 23 | def f():
24 24 | #: W605:1:11
25 |- return'\.png$'
25 |+ return r'\.png$'
22 22 | """
23 23 |
24 24 | #: W605:1:38
25 |-value = f'new line\nand invalid escape \_ here'
25 |+value = f'new line\nand invalid escape \\_ here'
26 26 |
27 27 | #: Okay
28 28 | regex = r'\.png$'
27 27 |
28 28 | #: Okay
W605_1.py:43:13: W605 [*] Invalid escape sequence: `\_`
|
41 | ''' # noqa
42 |
43 | regex = f'\\\_'
| ^^ W605
44 | value = f'\{{1}}'
45 | value = f'\{1}'
|
= help: Add backslash to escape sequence
Safe fix
40 40 | \w
41 41 | ''' # noqa
42 42 |
43 |-regex = f'\\\_'
43 |+regex = f'\\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
W605_1.py:44:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
| ^^ W605
45 | value = f'\{1}'
46 | value = f'{1:\}'
|
= help: Use a raw string literal
Safe fix
41 41 | ''' # noqa
42 42 |
43 43 | regex = f'\\\_'
44 |-value = f'\{{1}}'
44 |+value = rf'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
W605_1.py:45:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
45 | value = f'\{1}'
| ^^ W605
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
42 42 |
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 |-value = f'\{1}'
45 |+value = rf'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
W605_1.py:46:14: W605 [*] Invalid escape sequence: `\}`
|
44 | value = f'\{{1}}'
45 | value = f'\{1}'
46 | value = f'{1:\}'
| ^^ W605
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 |-value = f'{1:\}'
46 |+value = rf'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
W605_1.py:47:14: W605 [*] Invalid escape sequence: `\{`
|
45 | value = f'\{1}'
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
| ^^ W605
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 |-value = f"{f"\{1}"}"
47 |+value = f"{rf"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
50 50 | # Okay
W605_1.py:48:15: W605 [*] Invalid escape sequence: `\{`
|
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
| ^^ W605
49 |
50 | # Okay
|
= help: Use a raw string literal
Safe fix
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 |-value = rf"{f"\{1}"}"
48 |+value = rf"{rf"\{1}"}"
49 49 |
50 50 | # Okay
51 51 | value = rf'\{{1}}'

View File

@@ -1,227 +0,0 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
W605_2.py:4:11: W605 [*] Invalid escape sequence: `\.`
|
3 | #: W605:1:10
4 | regex = f'\.png$'
| ^^ W605
5 |
6 | #: W605:2:1
|
= help: Use a raw string literal
Safe fix
1 1 | # Same as `W605_0.py` but using f-strings instead.
2 2 |
3 3 | #: W605:1:10
4 |-regex = f'\.png$'
4 |+regex = rf'\.png$'
5 5 |
6 6 | #: W605:2:1
7 7 | regex = f'''
W605_2.py:8:1: W605 [*] Invalid escape sequence: `\.`
|
6 | #: W605:2:1
7 | regex = f'''
8 | \.png$
| ^^ W605
9 | '''
|
= help: Use a raw string literal
Safe fix
4 4 | regex = f'\.png$'
5 5 |
6 6 | #: W605:2:1
7 |-regex = f'''
7 |+regex = rf'''
8 8 | \.png$
9 9 | '''
10 10 |
W605_2.py:13:7: W605 [*] Invalid escape sequence: `\_`
|
11 | #: W605:2:6
12 | f(
13 | f'\_'
| ^^ W605
14 | )
|
= help: Use a raw string literal
Safe fix
10 10 |
11 11 | #: W605:2:6
12 12 | f(
13 |- f'\_'
13 |+ rf'\_'
14 14 | )
15 15 |
16 16 | #: W605:4:6
W605_2.py:20:6: W605 [*] Invalid escape sequence: `\_`
|
18 | multi-line
19 | literal
20 | with \_ somewhere
| ^^ W605
21 | in the middle
22 | """
|
= help: Use a raw string literal
Safe fix
14 14 | )
15 15 |
16 16 | #: W605:4:6
17 |-f"""
17 |+rf"""
18 18 | multi-line
19 19 | literal
20 20 | with \_ somewhere
W605_2.py:25:40: W605 [*] Invalid escape sequence: `\_`
|
24 | #: W605:1:38
25 | value = f'new line\nand invalid escape \_ here'
| ^^ W605
|
= help: Add backslash to escape sequence
Safe fix
22 22 | """
23 23 |
24 24 | #: W605:1:38
25 |-value = f'new line\nand invalid escape \_ here'
25 |+value = f'new line\nand invalid escape \\_ here'
26 26 |
27 27 |
28 28 | #: Okay
W605_2.py:43:13: W605 [*] Invalid escape sequence: `\_`
|
41 | ''' # noqa
42 |
43 | regex = f'\\\_'
| ^^ W605
44 | value = f'\{{1}}'
45 | value = f'\{1}'
|
= help: Add backslash to escape sequence
Safe fix
40 40 | \w
41 41 | ''' # noqa
42 42 |
43 |-regex = f'\\\_'
43 |+regex = f'\\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
W605_2.py:44:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
| ^^ W605
45 | value = f'\{1}'
46 | value = f'{1:\}'
|
= help: Use a raw string literal
Safe fix
41 41 | ''' # noqa
42 42 |
43 43 | regex = f'\\\_'
44 |-value = f'\{{1}}'
44 |+value = rf'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
W605_2.py:45:11: W605 [*] Invalid escape sequence: `\{`
|
43 | regex = f'\\\_'
44 | value = f'\{{1}}'
45 | value = f'\{1}'
| ^^ W605
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
42 42 |
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 |-value = f'\{1}'
45 |+value = rf'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
W605_2.py:46:14: W605 [*] Invalid escape sequence: `\}`
|
44 | value = f'\{{1}}'
45 | value = f'\{1}'
46 | value = f'{1:\}'
| ^^ W605
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
43 43 | regex = f'\\\_'
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 |-value = f'{1:\}'
46 |+value = rf'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
W605_2.py:47:14: W605 [*] Invalid escape sequence: `\{`
|
45 | value = f'\{1}'
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
| ^^ W605
48 | value = rf"{f"\{1}"}"
|
= help: Use a raw string literal
Safe fix
44 44 | value = f'\{{1}}'
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 |-value = f"{f"\{1}"}"
47 |+value = f"{rf"\{1}"}"
48 48 | value = rf"{f"\{1}"}"
49 49 |
50 50 | # Okay
W605_2.py:48:15: W605 [*] Invalid escape sequence: `\{`
|
46 | value = f'{1:\}'
47 | value = f"{f"\{1}"}"
48 | value = rf"{f"\{1}"}"
| ^^ W605
49 |
50 | # Okay
|
= help: Use a raw string literal
Safe fix
45 45 | value = f'\{1}'
46 46 | value = f'{1:\}'
47 47 | value = f"{f"\{1}"}"
48 |-value = rf"{f"\{1}"}"
48 |+value = rf"{rf"\{1}"}"
49 49 |
50 50 | # Okay
51 51 | value = rf'\{{1}}'

View File

@@ -172,8 +172,9 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
let mut has_seen_tab = docstring.indentation.contains('\t');
let mut is_over_indented = true;
let mut over_indented_lines = vec![];
let mut over_indented_offset = usize::MAX;
let mut over_indented_size = usize::MAX;
let docstring_indent_size = docstring.indentation.chars().count();
for i in 0..lines.len() {
// First lines and continuations doesn't need any indentation.
if i == 0 || lines[i - 1].ends_with('\\') {
@@ -189,6 +190,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
}
let line_indent = leading_space(line);
let line_indent_size = line_indent.chars().count();
// We only report tab indentation once, so only check if we haven't seen a tab
// yet.
@@ -197,9 +199,7 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
if checker.enabled(Rule::UnderIndentation) {
// We report under-indentation on every line. This isn't great, but enables
// fix.
if (i == lines.len() - 1 || !is_blank)
&& line_indent.len() < docstring.indentation.len()
{
if (i == lines.len() - 1 || !is_blank) && line_indent_size < docstring_indent_size {
let mut diagnostic =
Diagnostic::new(UnderIndentation, TextRange::empty(line.start()));
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
@@ -217,14 +217,12 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
// until we've viewed all the lines, so for now, just track
// the over-indentation status of every line.
if i < lines.len() - 1 {
if line_indent.len() > docstring.indentation.len() {
if line_indent_size > docstring_indent_size {
over_indented_lines.push(line);
// Track the _smallest_ offset we see, in terms of characters.
over_indented_offset = std::cmp::min(
line_indent.chars().count() - docstring.indentation.chars().count(),
over_indented_offset,
);
over_indented_size =
std::cmp::min(line_indent_size - docstring_indent_size, over_indented_size);
} else {
is_over_indented = false;
}
@@ -250,21 +248,21 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
// enables the fix capability.
let mut diagnostic =
Diagnostic::new(OverIndentation, TextRange::empty(line.start()));
let edit = if indent.is_empty() {
Edit::deletion(line.start(), line_indent.text_len())
// Delete the entire indent.
Edit::range_deletion(TextRange::at(line.start(), line_indent.text_len()))
} else {
// Convert the character count to an offset within the source.
let offset = checker
.locator()
.after(line.start() + indent.text_len())
.chars()
.take(over_indented_offset)
.take(over_indented_size)
.map(TextLen::text_len)
.sum::<TextSize>();
Edit::range_replacement(
indent.clone(),
TextRange::at(line.start(), indent.text_len() + offset),
)
let range = TextRange::at(line.start(), indent.text_len() + offset);
Edit::range_replacement(indent, range)
};
diagnostic.set_fix(Fix::safe_edit(edit));
checker.diagnostics.push(diagnostic);
@@ -274,7 +272,8 @@ pub(crate) fn indent(checker: &mut Checker, docstring: &Docstring) {
// If the last line is over-indented...
if let Some(last) = lines.last() {
let line_indent = leading_space(last);
if line_indent.len() > docstring.indentation.len() {
let line_indent_size = line_indent.chars().count();
if line_indent_size > docstring_indent_size {
let mut diagnostic =
Diagnostic::new(OverIndentation, TextRange::empty(last.start()));
let indent = clean_space(docstring.indentation);

View File

@@ -1,57 +1,81 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D208.py:3:1: D208 [*] Docstring is over-indented
D208.py:2:1: D208 [*] Docstring is over-indented
|
1 | class Platform:
2 | """ Remove sampler
3 | Args:
1 | """
2 | Author
| D208
4 |     Returns:
5 | """
3 | """
|
= help: Remove over-indentation
Safe fix
1 1 | class Platform:
2 2 | """ Remove sampler
3 |- Args:
3 |+ Args:
4 4 |     Returns:
5 5 | """
1 1 | """
2 |- Author
2 |+Author
3 3 | """
4 4 |
5 5 |
D208.py:4:1: D208 [*] Docstring is over-indented
|
2 | """ Remove sampler
3 | Args:
4 |     Returns:
| D208
5 | """
|
= help: Remove over-indentation
D208.py:8:1: D208 [*] Docstring is over-indented
|
6 | class Platform:
7 | """ Remove sampler
8 | Args:
| D208
9 |     Returns:
10 | """
|
= help: Remove over-indentation
Safe fix
1 1 | class Platform:
2 2 | """ Remove sampler
3 3 | Args:
4 |-     Returns:
4 |+ Returns:
5 5 | """
5 5 |
6 6 | class Platform:
7 7 | """ Remove sampler
8 |- Args:
8 |+ Args:
9 9 |     Returns:
10 10 | """
11 11 |
D208.py:5:1: D208 [*] Docstring is over-indented
|
3 | Args:
4 |     Returns:
5 | """
| D208
|
= help: Remove over-indentation
D208.py:9:1: D208 [*] Docstring is over-indented
|
7 | """ Remove sampler
8 | Args:
9 |     Returns:
| D208
10 | """
|
= help: Remove over-indentation
Safe fix
2 2 | """ Remove sampler
3 3 | Args:
4 4 |     Returns:
5 |- """
5 |+ """
6 6 | class Platform:
7 7 | """ Remove sampler
8 8 | Args:
9 |-     Returns:
9 |+ Returns:
10 10 | """
11 11 |
12 12 |
D208.py:10:1: D208 [*] Docstring is over-indented
|
8 | Args:
9 |     Returns:
10 | """
| D208
|
= help: Remove over-indentation
Safe fix
7 7 | """ Remove sampler
8 8 | Args:
9 9 |     Returns:
10 |- """
10 |+ """
11 11 |
12 12 |
13 13 | def memory_test():

View File

@@ -1,9 +1,11 @@
use std::fmt;
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, CmpOp, Expr};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{visitor, Arguments, CmpOp, Expr, Stmt};
use ruff_python_semantic::analyze::function_type;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use ruff_text_size::TextRange;
use crate::settings::LinterSettings;
@@ -82,3 +84,116 @@ impl fmt::Display for CmpOpExt {
write!(f, "{representation}")
}
}
/// Visitor to track reads from an iterable in a loop.
#[derive(Debug)]
pub(crate) struct SequenceIndexVisitor<'a> {
/// `letters`, given `for index, letter in enumerate(letters)`.
sequence_name: &'a str,
/// `index`, given `for index, letter in enumerate(letters)`.
index_name: &'a str,
/// `letter`, given `for index, letter in enumerate(letters)`.
value_name: &'a str,
/// The ranges of any `letters[index]` accesses.
accesses: Vec<TextRange>,
/// Whether any of the variables have been modified.
modified: bool,
}
impl<'a> SequenceIndexVisitor<'a> {
pub(crate) fn new(sequence_name: &'a str, index_name: &'a str, value_name: &'a str) -> Self {
Self {
sequence_name,
index_name,
value_name,
accesses: Vec::new(),
modified: false,
}
}
pub(crate) fn into_accesses(self) -> Vec<TextRange> {
self.accesses
}
}
impl SequenceIndexVisitor<'_> {
fn is_assignment(&self, expr: &Expr) -> bool {
// If we see the sequence, a subscript, or the index being modified, we'll stop emitting
// diagnostics.
match expr {
Expr::Name(ast::ExprName { id, .. }) => {
id == self.sequence_name || id == self.index_name || id == self.value_name
}
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return false;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return false;
};
if id == self.index_name {
return true;
}
}
false
}
_ => false,
}
}
}
impl<'a> Visitor<'_> for SequenceIndexVisitor<'a> {
fn visit_stmt(&mut self, stmt: &Stmt) {
if self.modified {
return;
}
match stmt {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
self.visit_expr(value);
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
if let Some(value) = value {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
Stmt::Delete(ast::StmtDelete { targets, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
}
_ => visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
if self.modified {
return;
}
match expr {
Expr::Subscript(ast::ExprSubscript {
value,
slice,
range,
..
}) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return;
};
if id == self.index_name {
self.accesses.push(*range);
}
}
}
_ => visitor::walk_expr(self, expr),
}
}
}

View File

@@ -94,6 +94,7 @@ mod tests {
#[test_case(Rule::RedefinedLoopName, Path::new("redefined_loop_name.py"))]
#[test_case(Rule::ReturnInInit, Path::new("return_in_init.py"))]
#[test_case(Rule::TooManyArguments, Path::new("too_many_arguments.py"))]
#[test_case(Rule::TooManyPositional, Path::new("too_many_positional.py"))]
#[test_case(Rule::TooManyBranches, Path::new("too_many_branches.py"))]
#[test_case(
Rule::TooManyReturnStatements,
@@ -160,6 +161,10 @@ mod tests {
)]
#[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))]
#[test_case(Rule::NoStaticmethodDecorator, Path::new("no_method_decorator.py"))]
#[test_case(
Rule::UnnecessaryDictIndexLookup,
Path::new("unnecessary_dict_index_lookup.py")
)]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -245,6 +250,22 @@ mod tests {
Ok(())
}
#[test]
fn max_positional_args() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/too_many_positional_params.py"),
&LinterSettings {
pylint: pylint::settings::Settings {
max_positional_args: 4,
..pylint::settings::Settings::default()
},
..LinterSettings::for_rule(Rule::TooManyPositional)
},
)?;
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn max_branches() -> Result<()> {
let diagnostics = test_path(
@@ -324,4 +345,15 @@ mod tests {
assert_messages!(diagnostics);
Ok(())
}
#[test]
fn unspecified_encoding_python39_or_lower() -> Result<()> {
let diagnostics = test_path(
Path::new("pylint/unspecified_encoding.py"),
&LinterSettings::for_rule(Rule::UnspecifiedEncoding)
.with_target_version(PythonVersion::Py39),
)?;
assert_messages!(diagnostics);
Ok(())
}
}

View File

@@ -21,6 +21,14 @@ use crate::rules::pylint::helpers::CmpOpExt;
/// foo == foo
/// ```
///
/// In some cases, self-comparisons are used to determine whether a float is
/// NaN. Instead, prefer `math.isnan`:
/// ```python
/// import math
///
/// math.isnan(foo)
/// ```
///
/// ## References
/// - [Python documentation: Comparisons](https://docs.python.org/3/reference/expressions.html#comparisons)
#[violation]

View File

@@ -55,6 +55,7 @@ pub(crate) use sys_exit_alias::*;
pub(crate) use too_many_arguments::*;
pub(crate) use too_many_boolean_expressions::*;
pub(crate) use too_many_branches::*;
pub(crate) use too_many_positional::*;
pub(crate) use too_many_public_methods::*;
pub(crate) use too_many_return_statements::*;
pub(crate) use too_many_statements::*;
@@ -62,6 +63,7 @@ pub(crate) use type_bivariance::*;
pub(crate) use type_name_incorrect_variance::*;
pub(crate) use type_param_name_mismatch::*;
pub(crate) use unexpected_special_method_signature::*;
pub(crate) use unnecessary_dict_index_lookup::*;
pub(crate) use unnecessary_direct_lambda_call::*;
pub(crate) use unnecessary_lambda::*;
pub(crate) use unnecessary_list_index_lookup::*;
@@ -130,6 +132,7 @@ mod sys_exit_alias;
mod too_many_arguments;
mod too_many_boolean_expressions;
mod too_many_branches;
mod too_many_positional;
mod too_many_public_methods;
mod too_many_return_statements;
mod too_many_statements;
@@ -137,6 +140,7 @@ mod type_bivariance;
mod type_name_incorrect_variance;
mod type_param_name_mismatch;
mod unexpected_special_method_signature;
mod unnecessary_dict_index_lookup;
mod unnecessary_direct_lambda_call;
mod unnecessary_lambda;
mod unnecessary_list_index_lookup;

View File

@@ -53,7 +53,7 @@ impl AlwaysFixableViolation for NoClassmethodDecorator {
/// ## Example
/// ```python
/// class Foo:
/// def bar(cls):
/// def bar(arg1, arg2):
/// ...
///
/// bar = staticmethod(bar)
@@ -63,7 +63,7 @@ impl AlwaysFixableViolation for NoClassmethodDecorator {
/// ```python
/// class Foo:
/// @staticmethod
/// def bar(cls):
/// def bar(arg1, arg2):
/// ...
/// ```
#[violation]

View File

@@ -1,8 +1,8 @@
use ruff_python_ast::{Parameters, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::identifier::Identifier;
use ruff_python_semantic::analyze::visibility;
use crate::checkers::ast::Checker;
@@ -58,12 +58,13 @@ impl Violation for TooManyArguments {
}
/// PLR0913
pub(crate) fn too_many_arguments(checker: &mut Checker, parameters: &Parameters, stmt: &Stmt) {
let num_arguments = parameters
pub(crate) fn too_many_arguments(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
let num_arguments = function_def
.parameters
.args
.iter()
.chain(&parameters.kwonlyargs)
.chain(&parameters.posonlyargs)
.chain(&function_def.parameters.kwonlyargs)
.chain(&function_def.parameters.posonlyargs)
.filter(|arg| {
!checker
.settings
@@ -71,13 +72,22 @@ pub(crate) fn too_many_arguments(checker: &mut Checker, parameters: &Parameters,
.is_match(&arg.parameter.name)
})
.count();
if num_arguments > checker.settings.pylint.max_args {
// Allow excessive arguments in `@override` or `@overload` methods, since they're required
// to adhere to the parent signature.
if visibility::is_override(&function_def.decorator_list, checker.semantic())
|| visibility::is_overload(&function_def.decorator_list, checker.semantic())
{
return;
}
checker.diagnostics.push(Diagnostic::new(
TooManyArguments {
c_args: num_arguments,
max_args: checker.settings.pylint.max_args,
},
stmt.identifier(),
function_def.identifier(),
));
}
}

View File

@@ -0,0 +1,90 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, identifier::Identifier};
use ruff_python_semantic::analyze::visibility;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for function definitions that include too many positional arguments.
///
/// By default, this rule allows up to five arguments, as configured by the
/// [`pylint.max-positional-args`] option.
///
/// ## Why is this bad?
/// Functions with many arguments are harder to understand, maintain, and call.
/// This is especially true for functions with many positional arguments, as
/// providing arguments positionally is more error-prone and less clear to
/// readers than providing arguments by name.
///
/// Consider refactoring functions with many arguments into smaller functions
/// with fewer arguments, using objects to group related arguments, or
/// migrating to keyword-only arguments.
///
/// ## Example
/// ```python
/// def plot(x, y, z, color, mark, add_trendline):
/// ...
///
///
/// plot(1, 2, 3, "r", "*", True)
/// ```
///
/// Use instead:
/// ```python
/// def plot(x, y, z, *, color, mark, add_trendline):
/// ...
///
///
/// plot(1, 2, 3, color="r", mark="*", add_trendline=True)
/// ```
///
/// ## Options
/// - `pylint.max-positional-args`
#[violation]
pub struct TooManyPositional {
c_pos: usize,
max_pos: usize,
}
impl Violation for TooManyPositional {
#[derive_message_formats]
fn message(&self) -> String {
let TooManyPositional { c_pos, max_pos } = self;
format!("Too many positional arguments: ({c_pos}/{max_pos})")
}
}
/// PLR0917
pub(crate) fn too_many_positional(checker: &mut Checker, function_def: &ast::StmtFunctionDef) {
let num_positional_args = function_def
.parameters
.args
.iter()
.chain(&function_def.parameters.posonlyargs)
.filter(|arg| {
!checker
.settings
.dummy_variable_rgx
.is_match(&arg.parameter.name)
})
.count();
if num_positional_args > checker.settings.pylint.max_positional_args {
// Allow excessive arguments in `@override` or `@overload` methods, since they're required
// to adhere to the parent signature.
if visibility::is_override(&function_def.decorator_list, checker.semantic())
|| visibility::is_overload(&function_def.decorator_list, checker.semantic())
{
return;
}
checker.diagnostics.push(Diagnostic::new(
TooManyPositional {
c_pos: num_positional_args,
max_pos: checker.settings.pylint.max_positional_args,
},
function_def.identifier(),
));
}
}

View File

@@ -0,0 +1,167 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, StmtFor};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::SequenceIndexVisitor;
/// ## What it does
/// Checks for key-based dict accesses during `.items()` iterations.
///
/// ## Why is this bad?
/// When iterating over a dict via `.items()`, the current value is already
/// available alongside its key. Using the key to look up the value is
/// unnecessary.
///
/// ## Example
/// ```python
/// FRUITS = {"apple": 1, "orange": 10, "berry": 22}
///
/// for fruit_name, fruit_count in FRUITS.items():
/// print(FRUITS[fruit_name])
/// ```
///
/// Use instead:
/// ```python
/// FRUITS = {"apple": 1, "orange": 10, "berry": 22}
///
/// for fruit_name, fruit_count in FRUITS.items():
/// print(fruit_count)
/// ```
#[violation]
pub struct UnnecessaryDictIndexLookup;
impl AlwaysFixableViolation for UnnecessaryDictIndexLookup {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary lookup of dictionary value by key")
}
fn fix_title(&self) -> String {
format!("Use existing variable")
}
}
/// PLR1733
pub(crate) fn unnecessary_dict_index_lookup(checker: &mut Checker, stmt_for: &StmtFor) {
let Some((dict_name, index_name, value_name)) = dict_items(&stmt_for.iter, &stmt_for.target)
else {
return;
};
let ranges = {
let mut visitor = SequenceIndexVisitor::new(&dict_name.id, &index_name.id, &value_name.id);
visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);
visitor.into_accesses()
};
for range in ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range);
diagnostic.set_fix(Fix::safe_edits(
Edit::range_replacement(value_name.id.to_string(), range),
[noop(index_name), noop(value_name)],
));
checker.diagnostics.push(diagnostic);
}
}
/// PLR1733
pub(crate) fn unnecessary_dict_index_lookup_comprehension(checker: &mut Checker, expr: &Expr) {
let (Expr::GeneratorExp(ast::ExprGeneratorExp {
elt, generators, ..
})
| Expr::DictComp(ast::ExprDictComp {
value: elt,
generators,
..
})
| Expr::SetComp(ast::ExprSetComp {
elt, generators, ..
})
| Expr::ListComp(ast::ExprListComp {
elt, generators, ..
})) = expr
else {
return;
};
for comp in generators {
let Some((dict_name, index_name, value_name)) = dict_items(&comp.iter, &comp.target) else {
continue;
};
let ranges = {
let mut visitor =
SequenceIndexVisitor::new(&dict_name.id, &index_name.id, &value_name.id);
visitor.visit_expr(elt.as_ref());
for expr in &comp.ifs {
visitor.visit_expr(expr);
}
visitor.into_accesses()
};
for range in ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryDictIndexLookup, range);
diagnostic.set_fix(Fix::safe_edits(
Edit::range_replacement(value_name.id.to_string(), range),
[noop(index_name), noop(value_name)],
));
checker.diagnostics.push(diagnostic);
}
}
}
fn dict_items<'a>(
call_expr: &'a Expr,
tuple_expr: &'a Expr,
) -> Option<(&'a ast::ExprName, &'a ast::ExprName, &'a ast::ExprName)> {
let ast::ExprCall {
func, arguments, ..
} = call_expr.as_call_expr()?;
if !arguments.is_empty() {
return None;
}
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
return None;
};
if attr != "items" {
return None;
}
let Expr::Name(dict_name) = value.as_ref() else {
return None;
};
let Expr::Tuple(ast::ExprTuple { elts, .. }) = tuple_expr else {
return None;
};
let [index, value] = elts.as_slice() else {
return None;
};
// Grab the variable names.
let Expr::Name(index_name) = index else {
return None;
};
let Expr::Name(value_name) = value else {
return None;
};
// If either of the variable names are intentionally ignored by naming them `_`, then don't
// emit.
if index_name.id == "_" || value_name.id == "_" {
return None;
}
Some((dict_name, index_name, value_name))
}
/// Return a no-op edit for the given name.
fn noop(name: &ast::ExprName) -> Edit {
Edit::range_replacement(name.id.to_string(), name.range())
}

View File

@@ -1,13 +1,12 @@
use ruff_python_ast::{self as ast, Expr, Stmt, StmtFor};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, StmtFor};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::TextRange;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::pylint::helpers::SequenceIndexVisitor;
/// ## What it does
/// Checks for index-based list accesses during `enumerate` iterations.
@@ -55,18 +54,18 @@ pub(crate) fn unnecessary_list_index_lookup(checker: &mut Checker, stmt_for: &St
};
let ranges = {
let mut visitor = SubscriptVisitor::new(sequence, index_name);
let mut visitor = SequenceIndexVisitor::new(&sequence.id, &index_name.id, &value_name.id);
visitor.visit_body(&stmt_for.body);
visitor.visit_body(&stmt_for.orelse);
visitor.diagnostic_ranges
visitor.into_accesses()
};
for range in ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
value_name.to_string(),
range,
)));
diagnostic.set_fix(Fix::safe_edits(
Edit::range_replacement(value_name.id.to_string(), range),
[noop(index_name), noop(value_name)],
));
checker.diagnostics.push(diagnostic);
}
}
@@ -99,17 +98,18 @@ pub(crate) fn unnecessary_list_index_lookup_comprehension(checker: &mut Checker,
};
let ranges = {
let mut visitor = SubscriptVisitor::new(sequence, index_name);
let mut visitor =
SequenceIndexVisitor::new(&sequence.id, &index_name.id, &value_name.id);
visitor.visit_expr(elt.as_ref());
visitor.diagnostic_ranges
visitor.into_accesses()
};
for range in ranges {
let mut diagnostic = Diagnostic::new(UnnecessaryListIndexLookup, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
value_name.to_string(),
range,
)));
diagnostic.set_fix(Fix::safe_edits(
Edit::range_replacement(value_name.id.to_string(), range),
[noop(index_name), noop(value_name)],
));
checker.diagnostics.push(diagnostic);
}
}
@@ -119,7 +119,7 @@ fn enumerate_items<'a>(
call_expr: &'a Expr,
tuple_expr: &'a Expr,
semantic: &SemanticModel,
) -> Option<(&'a str, &'a str, &'a str)> {
) -> Option<(&'a ast::ExprName, &'a ast::ExprName, &'a ast::ExprName)> {
let ast::ExprCall {
func, arguments, ..
} = call_expr.as_call_expr()?;
@@ -140,125 +140,29 @@ fn enumerate_items<'a>(
};
// Grab the variable names.
let Expr::Name(ast::ExprName { id: index_name, .. }) = index else {
let Expr::Name(index_name) = index else {
return None;
};
let Expr::Name(ast::ExprName { id: value_name, .. }) = value else {
let Expr::Name(value_name) = value else {
return None;
};
// If either of the variable names are intentionally ignored by naming them `_`, then don't
// emit.
if index_name == "_" || value_name == "_" {
if index_name.id == "_" || value_name.id == "_" {
return None;
}
// Get the first argument of the enumerate call.
let Some(Expr::Name(ast::ExprName { id: sequence, .. })) = arguments.args.first() else {
let Some(Expr::Name(sequence)) = arguments.args.first() else {
return None;
};
Some((sequence, index_name, value_name))
}
#[derive(Debug)]
struct SubscriptVisitor<'a> {
sequence_name: &'a str,
index_name: &'a str,
diagnostic_ranges: Vec<TextRange>,
modified: bool,
}
impl<'a> SubscriptVisitor<'a> {
fn new(sequence_name: &'a str, index_name: &'a str) -> Self {
Self {
sequence_name,
index_name,
diagnostic_ranges: Vec::new(),
modified: false,
}
}
}
impl SubscriptVisitor<'_> {
fn is_assignment(&self, expr: &Expr) -> bool {
// If we see the sequence, a subscript, or the index being modified, we'll stop emitting
// diagnostics.
match expr {
Expr::Name(ast::ExprName { id, .. }) => {
id == self.sequence_name || id == self.index_name
}
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return false;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return false;
};
if id == self.index_name {
return true;
}
}
false
}
_ => false,
}
}
}
impl<'a> Visitor<'_> for SubscriptVisitor<'a> {
fn visit_stmt(&mut self, stmt: &Stmt) {
if self.modified {
return;
}
match stmt {
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
self.visit_expr(value);
}
Stmt::AnnAssign(ast::StmtAnnAssign { target, value, .. }) => {
if let Some(value) = value {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
}
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
self.modified = self.is_assignment(target);
self.visit_expr(value);
}
Stmt::Delete(ast::StmtDelete { targets, .. }) => {
self.modified = targets.iter().any(|target| self.is_assignment(target));
}
_ => visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &Expr) {
if self.modified {
return;
}
match expr {
Expr::Subscript(ast::ExprSubscript {
value,
slice,
range,
..
}) => {
let Expr::Name(ast::ExprName { id, .. }) = value.as_ref() else {
return;
};
if id == self.sequence_name {
let Expr::Name(ast::ExprName { id, .. }) = slice.as_ref() else {
return;
};
if id == self.index_name {
self.diagnostic_ranges.push(*range);
}
}
}
_ => visitor::walk_expr(self, expr),
}
}
/// Return a no-op edit for the given name.
fn noop(name: &ast::ExprName) -> Edit {
Edit::range_replacement(name.id.to_string(), name.range())
}

View File

@@ -1,10 +1,16 @@
use ruff_diagnostics::{Diagnostic, Violation};
use anyhow::Result;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::call_path::{format_call_path, CallPath};
use ruff_text_size::Ranged;
use ruff_python_ast::Expr;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
use crate::importer::ImportRequest;
use crate::settings::types::PythonVersion;
/// ## What it does
/// Checks for uses of `open` and related calls without an explicit `encoding`
@@ -15,7 +21,9 @@ use crate::checkers::ast::Checker;
/// non-portable code, with differing behavior across platforms.
///
/// Instead, consider using the `encoding` parameter to enforce a specific
/// encoding.
/// encoding. [PEP 597] recommends using `locale.getpreferredencoding(False)`
/// as the default encoding on versions earlier than Python 3.10, and
/// `encoding="locale"` on Python 3.10 and later.
///
/// ## Example
/// ```python
@@ -29,13 +37,15 @@ use crate::checkers::ast::Checker;
///
/// ## References
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
///
/// [PEP 597]: https://peps.python.org/pep-0597/
#[violation]
pub struct UnspecifiedEncoding {
function_name: String,
mode: Mode,
}
impl Violation for UnspecifiedEncoding {
impl AlwaysFixableViolation for UnspecifiedEncoding {
#[derive_message_formats]
fn message(&self) -> String {
let UnspecifiedEncoding {
@@ -52,6 +62,10 @@ impl Violation for UnspecifiedEncoding {
}
}
}
fn fix_title(&self) -> String {
format!("Add explicit `encoding` argument")
}
}
/// PLW1514
@@ -70,17 +84,63 @@ pub(crate) fn unspecified_encoding(checker: &mut Checker, call: &ast::ExprCall)
return;
};
checker.diagnostics.push(Diagnostic::new(
let mut diagnostic = Diagnostic::new(
UnspecifiedEncoding {
function_name,
mode,
},
call.func.range(),
));
);
if checker.settings.target_version >= PythonVersion::Py310 {
diagnostic.set_fix(generate_keyword_fix(checker, call));
} else {
diagnostic.try_set_fix(|| generate_import_fix(checker, call));
}
checker.diagnostics.push(diagnostic);
}
/// Generate an [`Edit`] for Python 3.10 and later.
fn generate_keyword_fix(checker: &Checker, call: &ast::ExprCall) -> Fix {
Fix::unsafe_edit(add_argument(
&format!(
"encoding={}",
checker
.generator()
.expr(&Expr::StringLiteral(ast::ExprStringLiteral {
value: ast::StringLiteralValue::single(ast::StringLiteral {
value: "locale".to_string(),
unicode: false,
range: TextRange::default(),
}),
range: TextRange::default(),
}))
),
&call.arguments,
checker.indexer().comment_ranges(),
checker.locator().contents(),
))
}
/// Generate an [`Edit`] for Python 3.9 and earlier.
fn generate_import_fix(checker: &Checker, call: &ast::ExprCall) -> Result<Fix> {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("locale", "getpreferredencoding"),
call.start(),
checker.semantic(),
)?;
let argument_edit = add_argument(
&format!("encoding={binding}(False)"),
&call.arguments,
checker.indexer().comment_ranges(),
checker.locator().contents(),
);
Ok(Fix::unsafe_edits(import_edit, [argument_edit]))
}
/// Returns `true` if the given expression is a string literal containing a `b` character.
fn is_binary_mode(expr: &ast::Expr) -> Option<bool> {
fn is_binary_mode(expr: &Expr) -> Option<bool> {
Some(
expr.as_string_literal_expr()?
.value
@@ -92,12 +152,7 @@ fn is_binary_mode(expr: &ast::Expr) -> Option<bool> {
/// Returns `true` if the given call lacks an explicit `encoding`.
fn is_violation(call: &ast::ExprCall, call_path: &CallPath) -> bool {
// If we have something like `*args`, which might contain the encoding argument, abort.
if call
.arguments
.args
.iter()
.any(ruff_python_ast::Expr::is_starred_expr)
{
if call.arguments.args.iter().any(Expr::is_starred_expr) {
return false;
}
// If we have something like `**kwargs`, which might contain the encoding argument, abort.

View File

@@ -39,6 +39,7 @@ pub struct Settings {
pub allow_magic_value_types: Vec<ConstantType>,
pub allow_dunder_method_names: FxHashSet<String>,
pub max_args: usize,
pub max_positional_args: usize,
pub max_returns: usize,
pub max_bool_expr: usize,
pub max_branches: usize,
@@ -52,6 +53,7 @@ impl Default for Settings {
allow_magic_value_types: vec![ConstantType::Str, ConstantType::Bytes],
allow_dunder_method_names: FxHashSet::default(),
max_args: 5,
max_positional_args: 5,
max_returns: 6,
max_bool_expr: 5,
max_branches: 12,

View File

@@ -0,0 +1,25 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
too_many_positional.py:1:5: PLR0917 Too many positional arguments: (8/5)
|
1 | def f(x, y, z, t, u, v, w, r): # Too many positional arguments (8/3)
| ^ PLR0917
2 | pass
|
too_many_positional.py:21:5: PLR0917 Too many positional arguments: (6/5)
|
21 | def f(x, y, z, /, u, v, w): # Too many positional arguments (6/3)
| ^ PLR0917
22 | pass
|
too_many_positional.py:29:5: PLR0917 Too many positional arguments: (6/5)
|
29 | def f(x, y, z, a, b, c, *, u, v, w): # Too many positional arguments (6/3)
| ^ PLR0917
30 | pass
|

View File

@@ -0,0 +1,124 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unnecessary_dict_index_lookup.py:4:6: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
3 | def fix_these():
4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
|
= help: Use existing variable
Safe fix
1 1 | FRUITS = {"apple": 1, "orange": 10, "berry": 22}
2 2 |
3 3 | def fix_these():
4 |- [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
4 |+ [fruit_count for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
unnecessary_dict_index_lookup.py:5:6: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
3 | def fix_these():
4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
|
= help: Use existing variable
Safe fix
2 2 |
3 3 | def fix_these():
4 4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 |- {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
5 |+ {fruit_count for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
unnecessary_dict_index_lookup.py:6:18: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
7 |
8 | for fruit_name, fruit_count in FRUITS.items():
|
= help: Use existing variable
Safe fix
3 3 | def fix_these():
4 4 | [FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()] # PLR1733
5 5 | {FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 |- {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
6 |+ {fruit_name: fruit_count for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 9 | print(FRUITS[fruit_name]) # PLR1733
unnecessary_dict_index_lookup.py:9:15: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
8 | for fruit_name, fruit_count in FRUITS.items():
9 | print(FRUITS[fruit_name]) # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
10 | blah = FRUITS[fruit_name] # PLR1733
11 | assert FRUITS[fruit_name] == "pear" # PLR1733
|
= help: Use existing variable
Safe fix
6 6 | {fruit_name: FRUITS[fruit_name] for fruit_name, fruit_count in FRUITS.items()} # PLR1733
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 |- print(FRUITS[fruit_name]) # PLR1733
9 |+ print(fruit_count) # PLR1733
10 10 | blah = FRUITS[fruit_name] # PLR1733
11 11 | assert FRUITS[fruit_name] == "pear" # PLR1733
12 12 |
unnecessary_dict_index_lookup.py:10:16: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
8 | for fruit_name, fruit_count in FRUITS.items():
9 | print(FRUITS[fruit_name]) # PLR1733
10 | blah = FRUITS[fruit_name] # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
11 | assert FRUITS[fruit_name] == "pear" # PLR1733
|
= help: Use existing variable
Safe fix
7 7 |
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 9 | print(FRUITS[fruit_name]) # PLR1733
10 |- blah = FRUITS[fruit_name] # PLR1733
10 |+ blah = fruit_count # PLR1733
11 11 | assert FRUITS[fruit_name] == "pear" # PLR1733
12 12 |
13 13 |
unnecessary_dict_index_lookup.py:11:16: PLR1733 [*] Unnecessary lookup of dictionary value by key
|
9 | print(FRUITS[fruit_name]) # PLR1733
10 | blah = FRUITS[fruit_name] # PLR1733
11 | assert FRUITS[fruit_name] == "pear" # PLR1733
| ^^^^^^^^^^^^^^^^^^ PLR1733
|
= help: Use existing variable
Safe fix
8 8 | for fruit_name, fruit_count in FRUITS.items():
9 9 | print(FRUITS[fruit_name]) # PLR1733
10 10 | blah = FRUITS[fruit_name] # PLR1733
11 |- assert FRUITS[fruit_name] == "pear" # PLR1733
11 |+ assert fruit_count == "pear" # PLR1733
12 12 |
13 13 |
14 14 | def dont_fix_these():

View File

@@ -80,7 +80,7 @@ unnecessary_list_index_lookup.py:12:15: PLR1736 [*] Unnecessary lookup of list i
12 |+ print(letter) # PLR1736
13 13 | blah = letters[index] # PLR1736
14 14 | assert letters[index] == "d" # PLR1736
15 15 |
15 15 |
unnecessary_list_index_lookup.py:13:16: PLR1736 [*] Unnecessary lookup of list item by index
|
@@ -99,7 +99,7 @@ unnecessary_list_index_lookup.py:13:16: PLR1736 [*] Unnecessary lookup of list i
13 |- blah = letters[index] # PLR1736
13 |+ blah = letter # PLR1736
14 14 | assert letters[index] == "d" # PLR1736
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
unnecessary_list_index_lookup.py:14:16: PLR1736 [*] Unnecessary lookup of list item by index
@@ -108,7 +108,7 @@ unnecessary_list_index_lookup.py:14:16: PLR1736 [*] Unnecessary lookup of list i
13 | blah = letters[index] # PLR1736
14 | assert letters[index] == "d" # PLR1736
| ^^^^^^^^^^^^^^ PLR1736
15 |
15 |
16 | for index, letter in builtins.enumerate(letters):
|
= help: Use existing variable
@@ -119,7 +119,7 @@ unnecessary_list_index_lookup.py:14:16: PLR1736 [*] Unnecessary lookup of list i
13 13 | blah = letters[index] # PLR1736
14 |- assert letters[index] == "d" # PLR1736
14 |+ assert letter == "d" # PLR1736
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
17 17 | print(letters[index]) # PLR1736
@@ -135,7 +135,7 @@ unnecessary_list_index_lookup.py:17:15: PLR1736 [*] Unnecessary lookup of list i
Safe fix
14 14 | assert letters[index] == "d" # PLR1736
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
17 |- print(letters[index]) # PLR1736
17 |+ print(letter) # PLR1736
@@ -154,7 +154,7 @@ unnecessary_list_index_lookup.py:18:16: PLR1736 [*] Unnecessary lookup of list i
= help: Use existing variable
Safe fix
15 15 |
15 15 |
16 16 | for index, letter in builtins.enumerate(letters):
17 17 | print(letters[index]) # PLR1736
18 |- blah = letters[index] # PLR1736

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unspecified_encoding.py:8:1: PLW1514 `open` in text mode without explicit `encoding` argument
unspecified_encoding.py:8:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
@@ -9,8 +9,19 @@ unspecified_encoding.py:8:1: PLW1514 `open` in text mode without explicit `encod
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
|
= help: Add explicit `encoding` argument
unspecified_encoding.py:9:1: PLW1514 `io.TextIOWrapper` without explicit `encoding` argument
Unsafe fix
5 5 | import codecs
6 6 |
7 7 | # Errors.
8 |-open("test.txt")
8 |+open("test.txt", encoding="locale")
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
unspecified_encoding.py:9:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
@@ -19,8 +30,19 @@ unspecified_encoding.py:9:1: PLW1514 `io.TextIOWrapper` without explicit `encodi
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
|
= help: Add explicit `encoding` argument
unspecified_encoding.py:10:1: PLW1514 `io.TextIOWrapper` without explicit `encoding` argument
Unsafe fix
6 6 |
7 7 | # Errors.
8 8 | open("test.txt")
9 |-io.TextIOWrapper(io.FileIO("test.txt"))
9 |+io.TextIOWrapper(io.FileIO("test.txt"), encoding="locale")
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
unspecified_encoding.py:10:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
|
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
@@ -29,8 +51,19 @@ unspecified_encoding.py:10:1: PLW1514 `io.TextIOWrapper` without explicit `encod
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
|
= help: Add explicit `encoding` argument
unspecified_encoding.py:11:1: PLW1514 `tempfile.NamedTemporaryFile` in text mode without explicit `encoding` argument
Unsafe fix
7 7 | # Errors.
8 8 | open("test.txt")
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 |-hugo.TextIOWrapper(hugo.FileIO("test.txt"))
10 |+hugo.TextIOWrapper(hugo.FileIO("test.txt"), encoding="locale")
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
13 13 | codecs.open("test.txt")
unspecified_encoding.py:11:1: PLW1514 [*] `tempfile.NamedTemporaryFile` in text mode without explicit `encoding` argument
|
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
@@ -39,8 +72,19 @@ unspecified_encoding.py:11:1: PLW1514 `tempfile.NamedTemporaryFile` in text mode
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
|
= help: Add explicit `encoding` argument
unspecified_encoding.py:12:1: PLW1514 `tempfile.TemporaryFile` in text mode without explicit `encoding` argument
Unsafe fix
8 8 | open("test.txt")
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 |-tempfile.NamedTemporaryFile("w")
11 |+tempfile.NamedTemporaryFile("w", encoding="locale")
12 12 | tempfile.TemporaryFile("w")
13 13 | codecs.open("test.txt")
14 14 | tempfile.SpooledTemporaryFile(0, "w")
unspecified_encoding.py:12:1: PLW1514 [*] `tempfile.TemporaryFile` in text mode without explicit `encoding` argument
|
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
@@ -49,8 +93,19 @@ unspecified_encoding.py:12:1: PLW1514 `tempfile.TemporaryFile` in text mode with
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
unspecified_encoding.py:13:1: PLW1514 `codecs.open` in text mode without explicit `encoding` argument
Unsafe fix
9 9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
12 |-tempfile.TemporaryFile("w")
12 |+tempfile.TemporaryFile("w", encoding="locale")
13 13 | codecs.open("test.txt")
14 14 | tempfile.SpooledTemporaryFile(0, "w")
15 15 |
unspecified_encoding.py:13:1: PLW1514 [*] `codecs.open` in text mode without explicit `encoding` argument
|
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
@@ -58,8 +113,19 @@ unspecified_encoding.py:13:1: PLW1514 `codecs.open` in text mode without explici
| ^^^^^^^^^^^ PLW1514
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
unspecified_encoding.py:14:1: PLW1514 `tempfile.SpooledTemporaryFile` in text mode without explicit `encoding` argument
Unsafe fix
10 10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
13 |-codecs.open("test.txt")
13 |+codecs.open("test.txt", encoding="locale")
14 14 | tempfile.SpooledTemporaryFile(0, "w")
15 15 |
16 16 | # Non-errors.
unspecified_encoding.py:14:1: PLW1514 [*] `tempfile.SpooledTemporaryFile` in text mode without explicit `encoding` argument
|
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
@@ -68,5 +134,223 @@ unspecified_encoding.py:14:1: PLW1514 `tempfile.SpooledTemporaryFile` in text mo
15 |
16 | # Non-errors.
|
= help: Add explicit `encoding` argument
Unsafe fix
11 11 | tempfile.NamedTemporaryFile("w")
12 12 | tempfile.TemporaryFile("w")
13 13 | codecs.open("test.txt")
14 |-tempfile.SpooledTemporaryFile(0, "w")
14 |+tempfile.SpooledTemporaryFile(0, "w", encoding="locale")
15 15 |
16 16 | # Non-errors.
17 17 | open("test.txt", encoding="utf-8")
unspecified_encoding.py:46:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
44 | tempfile.SpooledTemporaryFile(0, )
45 |
46 | open("test.txt",)
| ^^^^ PLW1514
47 | open()
48 | open(
|
= help: Add explicit `encoding` argument
Unsafe fix
43 43 | tempfile.SpooledTemporaryFile(0, "wb")
44 44 | tempfile.SpooledTemporaryFile(0, )
45 45 |
46 |-open("test.txt",)
46 |+open("test.txt", encoding="locale",)
47 47 | open()
48 48 | open(
49 49 | "test.txt", # comment
unspecified_encoding.py:47:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
| ^^^^ PLW1514
48 | open(
49 | "test.txt", # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
44 44 | tempfile.SpooledTemporaryFile(0, )
45 45 |
46 46 | open("test.txt",)
47 |-open()
47 |+open(encoding="locale")
48 48 | open(
49 49 | "test.txt", # comment
50 50 | )
unspecified_encoding.py:48:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
48 | open(
| ^^^^ PLW1514
49 | "test.txt", # comment
50 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
46 46 | open("test.txt",)
47 47 | open()
48 48 | open(
49 |- "test.txt", # comment
49 |+ "test.txt", encoding="locale", # comment
50 50 | )
51 51 | open(
52 52 | "test.txt",
unspecified_encoding.py:51:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
49 | "test.txt", # comment
50 | )
51 | open(
| ^^^^ PLW1514
52 | "test.txt",
53 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
49 49 | "test.txt", # comment
50 50 | )
51 51 | open(
52 |- "test.txt",
52 |+ "test.txt", encoding="locale",
53 53 | # comment
54 54 | )
55 55 | open(("test.txt"),)
unspecified_encoding.py:55:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
53 | # comment
54 | )
55 | open(("test.txt"),)
| ^^^^ PLW1514
56 | open(
57 | ("test.txt"), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
52 52 | "test.txt",
53 53 | # comment
54 54 | )
55 |-open(("test.txt"),)
55 |+open(("test.txt"), encoding="locale",)
56 56 | open(
57 57 | ("test.txt"), # comment
58 58 | )
unspecified_encoding.py:56:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
54 | )
55 | open(("test.txt"),)
56 | open(
| ^^^^ PLW1514
57 | ("test.txt"), # comment
58 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
54 54 | )
55 55 | open(("test.txt"),)
56 56 | open(
57 |- ("test.txt"), # comment
57 |+ ("test.txt"), encoding="locale", # comment
58 58 | )
59 59 | open(
60 60 | ("test.txt"),
unspecified_encoding.py:59:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
57 | ("test.txt"), # comment
58 | )
59 | open(
| ^^^^ PLW1514
60 | ("test.txt"),
61 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
57 57 | ("test.txt"), # comment
58 58 | )
59 59 | open(
60 |- ("test.txt"),
60 |+ ("test.txt"), encoding="locale",
61 61 | # comment
62 62 | )
63 63 |
unspecified_encoding.py:64:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
62 | )
63 |
64 | open((("test.txt")),)
| ^^^^ PLW1514
65 | open(
66 | (("test.txt")), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
61 61 | # comment
62 62 | )
63 63 |
64 |-open((("test.txt")),)
64 |+open((("test.txt")), encoding="locale",)
65 65 | open(
66 66 | (("test.txt")), # comment
67 67 | )
unspecified_encoding.py:65:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
64 | open((("test.txt")),)
65 | open(
| ^^^^ PLW1514
66 | (("test.txt")), # comment
67 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
63 63 |
64 64 | open((("test.txt")),)
65 65 | open(
66 |- (("test.txt")), # comment
66 |+ (("test.txt")), encoding="locale", # comment
67 67 | )
68 68 | open(
69 69 | (("test.txt")),
unspecified_encoding.py:68:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
66 | (("test.txt")), # comment
67 | )
68 | open(
| ^^^^ PLW1514
69 | (("test.txt")),
70 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
66 66 | (("test.txt")), # comment
67 67 | )
68 68 | open(
69 |- (("test.txt")),
69 |+ (("test.txt")), encoding="locale",
70 70 | # comment
71 71 | )

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
too_many_positional_params.py:3:5: PLR0917 Too many positional arguments: (7/4)
|
1 | # Too many positional arguments (7/4) for max_positional=4
2 | # OK for dummy_variable_rgx ~ "skip_.*"
3 | def f(w, x, y, z, skip_t, skip_u, skip_v):
| ^ PLR0917
4 | pass
|
too_many_positional_params.py:9:5: PLR0917 Too many positional arguments: (7/4)
|
7 | # Too many positional arguments (7/4) for max_args=4
8 | # Too many positional arguments (7/3) for dummy_variable_rgx ~ "skip_.*"
9 | def f(w, x, y, z, t, u, v):
| ^ PLR0917
10 | pass
|

View File

@@ -0,0 +1,477 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
unspecified_encoding.py:8:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
| ^^^^ PLW1514
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 |-open("test.txt")
9 |+open("test.txt", encoding=locale.getpreferredencoding(False))
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
unspecified_encoding.py:9:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
|
7 | # Errors.
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
| ^^^^^^^^^^^^^^^^ PLW1514
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 |-io.TextIOWrapper(io.FileIO("test.txt"))
10 |+io.TextIOWrapper(io.FileIO("test.txt"), encoding=locale.getpreferredencoding(False))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
unspecified_encoding.py:10:1: PLW1514 [*] `io.TextIOWrapper` without explicit `encoding` argument
|
8 | open("test.txt")
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
| ^^^^^^^^^^^^^^^^^^ PLW1514
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 |-hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 |+hugo.TextIOWrapper(hugo.FileIO("test.txt"), encoding=locale.getpreferredencoding(False))
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
13 14 | codecs.open("test.txt")
unspecified_encoding.py:11:1: PLW1514 [*] `tempfile.NamedTemporaryFile` in text mode without explicit `encoding` argument
|
9 | io.TextIOWrapper(io.FileIO("test.txt"))
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW1514
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 |-tempfile.NamedTemporaryFile("w")
12 |+tempfile.NamedTemporaryFile("w", encoding=locale.getpreferredencoding(False))
12 13 | tempfile.TemporaryFile("w")
13 14 | codecs.open("test.txt")
14 15 | tempfile.SpooledTemporaryFile(0, "w")
unspecified_encoding.py:12:1: PLW1514 [*] `tempfile.TemporaryFile` in text mode without explicit `encoding` argument
|
10 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
| ^^^^^^^^^^^^^^^^^^^^^^ PLW1514
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
9 10 | io.TextIOWrapper(io.FileIO("test.txt"))
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
12 |-tempfile.TemporaryFile("w")
13 |+tempfile.TemporaryFile("w", encoding=locale.getpreferredencoding(False))
13 14 | codecs.open("test.txt")
14 15 | tempfile.SpooledTemporaryFile(0, "w")
15 16 |
unspecified_encoding.py:13:1: PLW1514 [*] `codecs.open` in text mode without explicit `encoding` argument
|
11 | tempfile.NamedTemporaryFile("w")
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
| ^^^^^^^^^^^ PLW1514
14 | tempfile.SpooledTemporaryFile(0, "w")
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
10 11 | hugo.TextIOWrapper(hugo.FileIO("test.txt"))
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
13 |-codecs.open("test.txt")
14 |+codecs.open("test.txt", encoding=locale.getpreferredencoding(False))
14 15 | tempfile.SpooledTemporaryFile(0, "w")
15 16 |
16 17 | # Non-errors.
unspecified_encoding.py:14:1: PLW1514 [*] `tempfile.SpooledTemporaryFile` in text mode without explicit `encoding` argument
|
12 | tempfile.TemporaryFile("w")
13 | codecs.open("test.txt")
14 | tempfile.SpooledTemporaryFile(0, "w")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLW1514
15 |
16 | # Non-errors.
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
11 12 | tempfile.NamedTemporaryFile("w")
12 13 | tempfile.TemporaryFile("w")
13 14 | codecs.open("test.txt")
14 |-tempfile.SpooledTemporaryFile(0, "w")
15 |+tempfile.SpooledTemporaryFile(0, "w", encoding=locale.getpreferredencoding(False))
15 16 |
16 17 | # Non-errors.
17 18 | open("test.txt", encoding="utf-8")
unspecified_encoding.py:46:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
44 | tempfile.SpooledTemporaryFile(0, )
45 |
46 | open("test.txt",)
| ^^^^ PLW1514
47 | open()
48 | open(
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
43 44 | tempfile.SpooledTemporaryFile(0, "wb")
44 45 | tempfile.SpooledTemporaryFile(0, )
45 46 |
46 |-open("test.txt",)
47 |+open("test.txt", encoding=locale.getpreferredencoding(False),)
47 48 | open()
48 49 | open(
49 50 | "test.txt", # comment
unspecified_encoding.py:47:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
| ^^^^ PLW1514
48 | open(
49 | "test.txt", # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
44 45 | tempfile.SpooledTemporaryFile(0, )
45 46 |
46 47 | open("test.txt",)
47 |-open()
48 |+open(encoding=locale.getpreferredencoding(False))
48 49 | open(
49 50 | "test.txt", # comment
50 51 | )
unspecified_encoding.py:48:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
46 | open("test.txt",)
47 | open()
48 | open(
| ^^^^ PLW1514
49 | "test.txt", # comment
50 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
46 47 | open("test.txt",)
47 48 | open()
48 49 | open(
49 |- "test.txt", # comment
50 |+ "test.txt", encoding=locale.getpreferredencoding(False), # comment
50 51 | )
51 52 | open(
52 53 | "test.txt",
unspecified_encoding.py:51:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
49 | "test.txt", # comment
50 | )
51 | open(
| ^^^^ PLW1514
52 | "test.txt",
53 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
49 50 | "test.txt", # comment
50 51 | )
51 52 | open(
52 |- "test.txt",
53 |+ "test.txt", encoding=locale.getpreferredencoding(False),
53 54 | # comment
54 55 | )
55 56 | open(("test.txt"),)
unspecified_encoding.py:55:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
53 | # comment
54 | )
55 | open(("test.txt"),)
| ^^^^ PLW1514
56 | open(
57 | ("test.txt"), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
52 53 | "test.txt",
53 54 | # comment
54 55 | )
55 |-open(("test.txt"),)
56 |+open(("test.txt"), encoding=locale.getpreferredencoding(False),)
56 57 | open(
57 58 | ("test.txt"), # comment
58 59 | )
unspecified_encoding.py:56:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
54 | )
55 | open(("test.txt"),)
56 | open(
| ^^^^ PLW1514
57 | ("test.txt"), # comment
58 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
54 55 | )
55 56 | open(("test.txt"),)
56 57 | open(
57 |- ("test.txt"), # comment
58 |+ ("test.txt"), encoding=locale.getpreferredencoding(False), # comment
58 59 | )
59 60 | open(
60 61 | ("test.txt"),
unspecified_encoding.py:59:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
57 | ("test.txt"), # comment
58 | )
59 | open(
| ^^^^ PLW1514
60 | ("test.txt"),
61 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
57 58 | ("test.txt"), # comment
58 59 | )
59 60 | open(
60 |- ("test.txt"),
61 |+ ("test.txt"), encoding=locale.getpreferredencoding(False),
61 62 | # comment
62 63 | )
63 64 |
unspecified_encoding.py:64:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
62 | )
63 |
64 | open((("test.txt")),)
| ^^^^ PLW1514
65 | open(
66 | (("test.txt")), # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
61 62 | # comment
62 63 | )
63 64 |
64 |-open((("test.txt")),)
65 |+open((("test.txt")), encoding=locale.getpreferredencoding(False),)
65 66 | open(
66 67 | (("test.txt")), # comment
67 68 | )
unspecified_encoding.py:65:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
64 | open((("test.txt")),)
65 | open(
| ^^^^ PLW1514
66 | (("test.txt")), # comment
67 | )
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
63 64 |
64 65 | open((("test.txt")),)
65 66 | open(
66 |- (("test.txt")), # comment
67 |+ (("test.txt")), encoding=locale.getpreferredencoding(False), # comment
67 68 | )
68 69 | open(
69 70 | (("test.txt")),
unspecified_encoding.py:68:1: PLW1514 [*] `open` in text mode without explicit `encoding` argument
|
66 | (("test.txt")), # comment
67 | )
68 | open(
| ^^^^ PLW1514
69 | (("test.txt")),
70 | # comment
|
= help: Add explicit `encoding` argument
Unsafe fix
3 3 | import tempfile
4 4 | import io as hugo
5 5 | import codecs
6 |+import locale
6 7 |
7 8 | # Errors.
8 9 | open("test.txt")
--------------------------------------------------------------------------------
66 67 | (("test.txt")), # comment
67 68 | )
68 69 | open(
69 |- (("test.txt")),
70 |+ (("test.txt")), encoding=locale.getpreferredencoding(False),
70 71 | # comment
71 72 | )

View File

@@ -490,18 +490,10 @@ pub(crate) fn printf_string_formatting(checker: &mut Checker, expr: &Expr, right
contents.push_str(&format!(".format{params_string}"));
let mut diagnostic = Diagnostic::new(PrintfStringFormatting, expr.range());
// Avoid fix if there are comments within the right-hand side:
// ```
// "%s" % (
// 0, # 0
// )
// ```
if !checker.indexer().comment_ranges().intersects(right.range()) {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents,
expr.range(),
)));
}
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
contents,
expr.range(),
)));
checker.diagnostics.push(diagnostic);
}

View File

@@ -896,7 +896,7 @@ UP031_0.py:104:5: UP031 [*] Use format specifiers instead of percent format
109 108 |
110 109 | "%s" % (
UP031_0.py:110:1: UP031 Use format specifiers instead of percent format
UP031_0.py:110:1: UP031 [*] Use format specifiers instead of percent format
|
108 | )
109 |
@@ -907,4 +907,36 @@ UP031_0.py:110:1: UP031 Use format specifiers instead of percent format
|
= help: Replace with format specifiers
Unsafe fix
107 107 | % (x,)
108 108 | )
109 109 |
110 |-"%s" % (
110 |+"{}".format(
111 111 | x, # comment
112 112 | )
113 113 |
UP031_0.py:115:8: UP031 [*] Use format specifiers instead of percent format
|
115 | path = "%s-%s-%s.pem" % (
| ________^
116 | | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
117 | | cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
118 | | hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix
119 | | )
| |_^ UP031
|
= help: Replace with format specifiers
Unsafe fix
112 112 | )
113 113 |
114 114 |
115 |-path = "%s-%s-%s.pem" % (
115 |+path = "{}-{}-{}.pem".format(
116 116 | safe_domain_name(cn), # common name, which should be filename safe because it is IDNA-encoded, but in case of a malformed cert make sure it's ok to use as a filename
117 117 | cert.not_valid_after.date().isoformat().replace("-", ""), # expiration date
118 118 | hexlify(cert.fingerprint(hashes.SHA256())).decode("ascii")[0:8], # fingerprint prefix

View File

@@ -238,4 +238,65 @@ flowchart TD
block0 --> return
```
## Function 8
### Source
```python
def func():
for i in range(5):
pass
else:
return 1
```
### Control Flow Graph
```mermaid
flowchart TD
start(("Start"))
return(("End"))
block0["pass\n"]
block1["return 1\n"]
block2["for i in range(5):
pass
else:
return 1\n"]
start --> block2
block2 -- "range(5)" --> block0
block2 -- "else" --> block1
block1 --> return
block0 --> return
```
## Function 9
### Source
```python
def func():
for i in range(5):
pass
else:
return 1
x = 1
```
### Control Flow Graph
```mermaid
flowchart TD
start(("Start"))
return(("End"))
block0["x = 1\n"]
block1["pass\n"]
block2["return 1\n"]
block3["for i in range(5):
pass
else:
return 1\n"]
start --> block3
block3 -- "range(5)" --> block1
block3 -- "else" --> block2
block2 --> return
block1 --> block3
block0 --> return
```

View File

@@ -773,4 +773,43 @@ flowchart TD
block0 --> return
```
## Function 14
### Source
```python
def func(point):
match point:
case (0, 0):
print("Origin")
case foo:
raise ValueError("oops")
```
### Control Flow Graph
```mermaid
flowchart TD
start(("Start"))
return(("End"))
block0[["`*(empty)*`"]]
block1["raise ValueError(#quot;oops#quot;)\n"]
block2["match point:
case (0, 0):
print(#quot;Origin#quot;)
case foo:
raise ValueError(#quot;oops#quot;)\n"]
block3["print(#quot;Origin#quot;)\n"]
block4["match point:
case (0, 0):
print(#quot;Origin#quot;)
case foo:
raise ValueError(#quot;oops#quot;)\n"]
start --> block4
block4 -- "case (0, 0)" --> block3
block4 -- "else" --> block2
block3 --> block0
block2 --> block1
block1 --> return
block0 --> return
```

View File

@@ -2,8 +2,8 @@ use std::{fmt, iter, usize};
use log::error;
use ruff_python_ast::{
Expr, ExprBooleanLiteral, Identifier, MatchCase, Pattern, PatternMatchAs, Stmt, StmtFor,
StmtMatch, StmtReturn, StmtTry, StmtWhile, StmtWith,
Expr, ExprBooleanLiteral, Identifier, MatchCase, Pattern, PatternMatchAs, PatternMatchOr, Stmt,
StmtFor, StmtMatch, StmtReturn, StmtTry, StmtWhile, StmtWith,
};
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -416,13 +416,6 @@ fn match_case<'stmt>(
}
last_statement_index
};
// TODO: handle named arguments, e.g.
// ```python
// match $subject:
// case $binding:
// print($binding)
// ```
// These should also return `NextBlock::Always`.
let next = if is_wildcard(case) {
// Wildcard case is always taken.
NextBlock::Always(next_block_index)
@@ -436,10 +429,25 @@ fn match_case<'stmt>(
BasicBlock { stmts, next }
}
/// Returns true if `pattern` is a wildcard (`_`) pattern.
/// Returns true if the [`MatchCase`] is a wildcard pattern.
fn is_wildcard(pattern: &MatchCase) -> bool {
pattern.guard.is_none()
&& matches!(&pattern.pattern, Pattern::MatchAs(PatternMatchAs { pattern, name, .. }) if pattern.is_none() && name.is_none())
/// Returns true if the [`Pattern`] is a wildcard pattern.
fn is_wildcard_pattern(pattern: &Pattern) -> bool {
match pattern {
Pattern::MatchValue(_)
| Pattern::MatchSingleton(_)
| Pattern::MatchSequence(_)
| Pattern::MatchMapping(_)
| Pattern::MatchClass(_)
| Pattern::MatchStar(_) => false,
Pattern::MatchAs(PatternMatchAs { pattern, .. }) => pattern.is_none(),
Pattern::MatchOr(PatternMatchOr { patterns, .. }) => {
patterns.iter().all(is_wildcard_pattern)
}
}
}
pattern.guard.is_none() && is_wildcard_pattern(&pattern.pattern)
}
#[derive(Debug, Default)]
@@ -477,6 +485,8 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Stmt::AugAssign(_)
| Stmt::AnnAssign(_)
| Stmt::Break(_)
| Stmt::TypeAlias(_)
| Stmt::IpyEscapeCommand(_)
| Stmt::Pass(_) => self.unconditional_next_block(after),
Stmt::Continue(_) => {
// NOTE: the next branch gets fixed up in `change_next_block`.
@@ -638,6 +648,7 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Expr::Starred(_)
| Expr::Name(_)
| Expr::List(_)
| Expr::IpyEscapeCommand(_)
| Expr::Tuple(_)
| Expr::Slice(_) => self.unconditional_next_block(after),
// TODO: handle these expressions.
@@ -651,13 +662,10 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Expr::Await(_)
| Expr::Yield(_)
| Expr::YieldFrom(_) => self.unconditional_next_block(after),
Expr::IpyEscapeCommand(_) => todo!(),
}
}
// The tough branches are done, here is an easy one.
Stmt::Return(_) => NextBlock::Terminate,
Stmt::TypeAlias(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
};
// Include any statements in the block that don't divert the control flow.
@@ -890,6 +898,8 @@ fn needs_next_block(stmts: &[Stmt]) -> bool {
| Stmt::AnnAssign(_)
| Stmt::Expr(_)
| Stmt::Pass(_)
| Stmt::TypeAlias(_)
| Stmt::IpyEscapeCommand(_)
// TODO: check below.
| Stmt::Break(_)
| Stmt::Continue(_)
@@ -899,8 +909,6 @@ fn needs_next_block(stmts: &[Stmt]) -> bool {
| Stmt::Match(_)
| Stmt::Try(_)
| Stmt::Assert(_) => true,
Stmt::TypeAlias(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
}
}
@@ -919,6 +927,8 @@ fn is_control_flow_stmt(stmt: &Stmt) -> bool {
| Stmt::AugAssign(_)
| Stmt::AnnAssign(_)
| Stmt::Expr(_)
| Stmt::TypeAlias(_)
| Stmt::IpyEscapeCommand(_)
| Stmt::Pass(_) => false,
Stmt::Return(_)
| Stmt::For(_)
@@ -931,8 +941,6 @@ fn is_control_flow_stmt(stmt: &Stmt) -> bool {
| Stmt::Assert(_)
| Stmt::Break(_)
| Stmt::Continue(_) => true,
Stmt::TypeAlias(_) => todo!(),
Stmt::IpyEscapeCommand(_) => todo!(),
}
}

View File

@@ -344,6 +344,7 @@ RUF100_3.py:23:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
23 |+print(a) # noqa: F821 # comment
24 24 | print(a) # noqa: E501, F821 comment
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
RUF100_3.py:24:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
@@ -362,6 +363,8 @@ RUF100_3.py:24:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
24 |-print(a) # noqa: E501, F821 comment
24 |+print(a) # noqa: F821 comment
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
27 27 | print(a) # comment with unicode µ # noqa: E501
RUF100_3.py:25:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
@@ -369,6 +372,8 @@ RUF100_3.py:25:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
24 | print(a) # noqa: E501, F821 comment
25 | print(a) # noqa: E501, F821 comment
| ^^^^^^^^^^^^^^^^^^ RUF100
26 |
27 | print(a) # comment with unicode µ # noqa: E501
|
= help: Remove unused `noqa` directive
@@ -378,5 +383,50 @@ RUF100_3.py:25:11: RUF100 [*] Unused `noqa` directive (unused: `E501`)
24 24 | print(a) # noqa: E501, F821 comment
25 |-print(a) # noqa: E501, F821 comment
25 |+print(a) # noqa: F821 comment
26 26 |
27 27 | print(a) # comment with unicode µ # noqa: E501
28 28 | print(a) # comment with unicode µ # noqa: E501, F821
RUF100_3.py:27:7: F821 Undefined name `a`
|
25 | print(a) # noqa: E501, F821 comment
26 |
27 | print(a) # comment with unicode µ # noqa: E501
| ^ F821
28 | print(a) # comment with unicode µ # noqa: E501, F821
|
RUF100_3.py:27:39: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
25 | print(a) # noqa: E501, F821 comment
26 |
27 | print(a) # comment with unicode µ # noqa: E501
| ^^^^^^^^^^^^ RUF100
28 | print(a) # comment with unicode µ # noqa: E501, F821
|
= help: Remove unused `noqa` directive
Safe fix
24 24 | print(a) # noqa: E501, F821 comment
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
27 |-print(a) # comment with unicode µ # noqa: E501
27 |+print(a) # comment with unicode µ
28 28 | print(a) # comment with unicode µ # noqa: E501, F821
RUF100_3.py:28:39: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
27 | print(a) # comment with unicode µ # noqa: E501
28 | print(a) # comment with unicode µ # noqa: E501, F821
| ^^^^^^^^^^^^^^^^^^ RUF100
|
= help: Remove unused `noqa` directive
Safe fix
25 25 | print(a) # noqa: E501, F821 comment
26 26 |
27 27 | print(a) # comment with unicode µ # noqa: E501
28 |-print(a) # comment with unicode µ # noqa: E501, F821
28 |+print(a) # comment with unicode µ # noqa: F821

View File

@@ -911,6 +911,180 @@ where
}
}
/// Returns `true` if the function has an implicit return.
pub fn implicit_return(function: &ast::StmtFunctionDef) -> bool {
/// Returns `true` if the body may break via a `break` statement.
fn sometimes_breaks(stmts: &[Stmt]) -> bool {
for stmt in stmts {
match stmt {
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
if returns(body) {
return false;
}
if sometimes_breaks(orelse) {
return true;
}
}
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if returns(body) {
return false;
}
if sometimes_breaks(orelse) {
return true;
}
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if std::iter::once(body)
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.any(|body| sometimes_breaks(body))
{
return true;
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
if cases.iter().any(|case| sometimes_breaks(&case.body)) {
return true;
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
if sometimes_breaks(body)
|| handlers.iter().any(|handler| {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
body,
..
}) = handler;
sometimes_breaks(body)
})
|| sometimes_breaks(orelse)
|| sometimes_breaks(finalbody)
{
return true;
}
}
Stmt::With(ast::StmtWith { body, .. }) => {
if sometimes_breaks(body) {
return true;
}
}
Stmt::Break(_) => return true,
Stmt::Return(_) => return false,
Stmt::Raise(_) => return false,
_ => {}
}
}
false
}
/// Returns `true` if the body may break via a `break` statement.
fn always_breaks(stmts: &[Stmt]) -> bool {
for stmt in stmts {
match stmt {
Stmt::Break(_) => return true,
Stmt::Return(_) => return false,
Stmt::Raise(_) => return false,
_ => {}
}
}
false
}
/// Returns `true` if the body contains a branch that ends without an explicit `return` or
/// `raise` statement.
fn returns(stmts: &[Stmt]) -> bool {
for stmt in stmts.iter().rev() {
match stmt {
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
if always_breaks(body) {
return false;
}
if returns(body) {
return true;
}
if returns(orelse) && !sometimes_breaks(body) {
return true;
}
}
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if always_breaks(body) {
return false;
}
if returns(body) {
return true;
}
if returns(orelse) && !sometimes_breaks(body) {
return true;
}
}
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
if elif_else_clauses.iter().any(|clause| clause.test.is_none())
&& std::iter::once(body)
.chain(elif_else_clauses.iter().map(|clause| &clause.body))
.all(|body| returns(body))
{
return true;
}
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {
// Note: we assume the `match` is exhaustive.
if cases.iter().all(|case| returns(&case.body)) {
return true;
}
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
// If the `finally` block returns, the `try` block must also return.
if returns(finalbody) {
return true;
}
// If the `body` or the `else` block returns, the `try` block must also return.
if (returns(body) || returns(orelse))
&& handlers.iter().all(|handler| {
let ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
body,
..
}) = handler;
returns(body)
})
{
return true;
}
}
Stmt::With(ast::StmtWith { body, .. }) => {
if returns(body) {
return true;
}
}
Stmt::Return(_) => return true,
Stmt::Raise(_) => return true,
_ => {}
}
}
false
}
!returns(&function.body)
}
/// A [`StatementVisitor`] that collects all `raise` statements in a function or method.
#[derive(Default)]
pub struct RaiseStatementVisitor<'a> {

View File

@@ -3729,204 +3729,6 @@ impl Ranged for crate::nodes::ParameterWithDefault {
}
}
/// An expression that may be parenthesized.
#[derive(Clone, Debug)]
pub struct ParenthesizedExpr {
/// The range of the expression, including any parentheses.
pub range: TextRange,
/// The underlying expression.
pub expr: Expr,
}
impl ParenthesizedExpr {
/// Returns `true` if the expression is may be parenthesized.
pub fn is_parenthesized(&self) -> bool {
self.range != self.expr.range()
}
}
impl Ranged for ParenthesizedExpr {
fn range(&self) -> TextRange {
self.range
}
}
impl From<Expr> for ParenthesizedExpr {
fn from(expr: Expr) -> Self {
ParenthesizedExpr {
range: expr.range(),
expr,
}
}
}
impl From<ParenthesizedExpr> for Expr {
fn from(parenthesized_expr: ParenthesizedExpr) -> Self {
parenthesized_expr.expr
}
}
impl From<ExprIpyEscapeCommand> for ParenthesizedExpr {
fn from(payload: ExprIpyEscapeCommand) -> Self {
Expr::IpyEscapeCommand(payload).into()
}
}
impl From<ExprBoolOp> for ParenthesizedExpr {
fn from(payload: ExprBoolOp) -> Self {
Expr::BoolOp(payload).into()
}
}
impl From<ExprNamedExpr> for ParenthesizedExpr {
fn from(payload: ExprNamedExpr) -> Self {
Expr::NamedExpr(payload).into()
}
}
impl From<ExprBinOp> for ParenthesizedExpr {
fn from(payload: ExprBinOp) -> Self {
Expr::BinOp(payload).into()
}
}
impl From<ExprUnaryOp> for ParenthesizedExpr {
fn from(payload: ExprUnaryOp) -> Self {
Expr::UnaryOp(payload).into()
}
}
impl From<ExprLambda> for ParenthesizedExpr {
fn from(payload: ExprLambda) -> Self {
Expr::Lambda(payload).into()
}
}
impl From<ExprIfExp> for ParenthesizedExpr {
fn from(payload: ExprIfExp) -> Self {
Expr::IfExp(payload).into()
}
}
impl From<ExprDict> for ParenthesizedExpr {
fn from(payload: ExprDict) -> Self {
Expr::Dict(payload).into()
}
}
impl From<ExprSet> for ParenthesizedExpr {
fn from(payload: ExprSet) -> Self {
Expr::Set(payload).into()
}
}
impl From<ExprListComp> for ParenthesizedExpr {
fn from(payload: ExprListComp) -> Self {
Expr::ListComp(payload).into()
}
}
impl From<ExprSetComp> for ParenthesizedExpr {
fn from(payload: ExprSetComp) -> Self {
Expr::SetComp(payload).into()
}
}
impl From<ExprDictComp> for ParenthesizedExpr {
fn from(payload: ExprDictComp) -> Self {
Expr::DictComp(payload).into()
}
}
impl From<ExprGeneratorExp> for ParenthesizedExpr {
fn from(payload: ExprGeneratorExp) -> Self {
Expr::GeneratorExp(payload).into()
}
}
impl From<ExprAwait> for ParenthesizedExpr {
fn from(payload: ExprAwait) -> Self {
Expr::Await(payload).into()
}
}
impl From<ExprYield> for ParenthesizedExpr {
fn from(payload: ExprYield) -> Self {
Expr::Yield(payload).into()
}
}
impl From<ExprYieldFrom> for ParenthesizedExpr {
fn from(payload: ExprYieldFrom) -> Self {
Expr::YieldFrom(payload).into()
}
}
impl From<ExprCompare> for ParenthesizedExpr {
fn from(payload: ExprCompare) -> Self {
Expr::Compare(payload).into()
}
}
impl From<ExprCall> for ParenthesizedExpr {
fn from(payload: ExprCall) -> Self {
Expr::Call(payload).into()
}
}
impl From<ExprFormattedValue> for ParenthesizedExpr {
fn from(payload: ExprFormattedValue) -> Self {
Expr::FormattedValue(payload).into()
}
}
impl From<ExprFString> for ParenthesizedExpr {
fn from(payload: ExprFString) -> Self {
Expr::FString(payload).into()
}
}
impl From<ExprStringLiteral> for ParenthesizedExpr {
fn from(payload: ExprStringLiteral) -> Self {
Expr::StringLiteral(payload).into()
}
}
impl From<ExprBytesLiteral> for ParenthesizedExpr {
fn from(payload: ExprBytesLiteral) -> Self {
Expr::BytesLiteral(payload).into()
}
}
impl From<ExprNumberLiteral> for ParenthesizedExpr {
fn from(payload: ExprNumberLiteral) -> Self {
Expr::NumberLiteral(payload).into()
}
}
impl From<ExprBooleanLiteral> for ParenthesizedExpr {
fn from(payload: ExprBooleanLiteral) -> Self {
Expr::BooleanLiteral(payload).into()
}
}
impl From<ExprNoneLiteral> for ParenthesizedExpr {
fn from(payload: ExprNoneLiteral) -> Self {
Expr::NoneLiteral(payload).into()
}
}
impl From<ExprEllipsisLiteral> for ParenthesizedExpr {
fn from(payload: ExprEllipsisLiteral) -> Self {
Expr::EllipsisLiteral(payload).into()
}
}
impl From<ExprAttribute> for ParenthesizedExpr {
fn from(payload: ExprAttribute) -> Self {
Expr::Attribute(payload).into()
}
}
impl From<ExprSubscript> for ParenthesizedExpr {
fn from(payload: ExprSubscript) -> Self {
Expr::Subscript(payload).into()
}
}
impl From<ExprStarred> for ParenthesizedExpr {
fn from(payload: ExprStarred) -> Self {
Expr::Starred(payload).into()
}
}
impl From<ExprName> for ParenthesizedExpr {
fn from(payload: ExprName) -> Self {
Expr::Name(payload).into()
}
}
impl From<ExprList> for ParenthesizedExpr {
fn from(payload: ExprList) -> Self {
Expr::List(payload).into()
}
}
impl From<ExprTuple> for ParenthesizedExpr {
fn from(payload: ExprTuple) -> Self {
Expr::Tuple(payload).into()
}
}
impl From<ExprSlice> for ParenthesizedExpr {
fn from(payload: ExprSlice) -> Self {
Expr::Slice(payload).into()
}
}
#[cfg(target_pointer_width = "64")]
mod size_assertions {
use static_assertions::assert_eq_size;

View File

@@ -139,7 +139,6 @@ pub trait PreorderVisitor<'a> {
}
#[inline]
fn visit_pattern_keyword(&mut self, pattern_keyword: &'a PatternKeyword) {
walk_pattern_keyword(self, pattern_keyword);
}

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