Compare commits

...

44 Commits

Author SHA1 Message Date
Robin Caloudis
0f53feee00 Extend doc 2024-02-27 23:08:21 +01:00
Robin Caloudis
b793611fa6 Extend rule
This implementation makes the test
pass again.
2024-02-27 23:08:02 +01:00
Robin Caloudis
be61cb3a68 Reproduce lint rule limitation by a test
This little test makes sure that we have
something to test against when extending
the rule.
2024-02-27 22:49:09 +01:00
Micha Reiser
8dc22d5793 Perf: Skip string normalization when possible (#10116) 2024-02-26 17:35:29 +00:00
Micha Reiser
15b87ea8be E203: Don't warn about single whitespace before tuple , (#10094) 2024-02-26 18:22:35 +01:00
Alex Waygood
c25f1cd12a Explicitly ban overriding extend as part of a --config flag (#10135) 2024-02-26 16:07:28 +00:00
Charlie Marsh
f5904a20d5 Add package name to requirements-insiders.txt (#10090) 2024-02-26 10:52:58 -05:00
Micha Reiser
77c5561646 Add parenthesized flag to ExprTuple and ExprGenerator (#9614) 2024-02-26 15:35:20 +00:00
Arkin Modi
ab4bd71755 docs: fix pycodestyle.max-line-length link (#10136) 2024-02-26 14:58:13 +01:00
Micha Reiser
5abf662365 Upgrade codspeed-criterion for perf boost (#10134) 2024-02-26 11:37:23 +00:00
Alex Waygood
14fa1c5b52 [Minor] Improve the style of some tests in crates/ruff/tests/format.rs (#10132) 2024-02-26 11:02:09 +00:00
Alex Waygood
0421c41ff7 Add Alex Waygood as a CODEOWNER for flake8-pyi (#10129) 2024-02-26 10:25:46 +00:00
dependabot[bot]
ad4695d3eb Bump serde from 1.0.196 to 1.0.197 (#10128)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:26:00 +00:00
dependabot[bot]
8c58ebee37 Bump insta from 1.34.0 to 1.35.1 (#10127)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:15:13 +00:00
dependabot[bot]
5023874355 Bump bstr from 1.9.0 to 1.9.1 (#10124)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 10:14:22 +01:00
dependabot[bot]
5554510597 Bump syn from 2.0.49 to 2.0.51 (#10126)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:14:00 +00:00
dependabot[bot]
1341e064a7 Bump serde-wasm-bindgen from 0.6.3 to 0.6.4 (#10125)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-26 09:13:15 +00:00
Micha Reiser
bd98d6884b Upgrade upload/download artifact actions in release workflow (#10105) 2024-02-26 08:23:10 +01:00
Micha Reiser
761d4d42f1 Add cold attribute to less likely printer queue branches (#10121) 2024-02-26 08:19:40 +01:00
Robin Caloudis
fc8738f52a [ruff] Avoid f-string false positives in gettext calls (RUF027) (#10118)
## Summary

It is a convention to use the `_()` alias for `gettext()`. We want to
avoid
statement expressions and assignments related to aliases of the gettext
API.
See https://docs.python.org/3/library/gettext.html for details. When one
uses `_() to mark a string for translation, the tools look for these
markers
and replace the original string with its translated counterpart. If the
string contains variable placeholders or formatting, it can complicate
the
translation process, lead to errors or incorrect translations.

## Test Plan

* Test file `RUF027_1.py` was extended such that the test reproduces the
false-positive

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

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-25 18:17:56 -05:00
Micha Reiser
1711bca4a0 FString formatting: remove fstring handling in normalize_string (#10119) 2024-02-25 18:28:46 +01:00
Raphael Boidol
51ce88bb23 docs: update actions in documentation to recent versions without deprecation warnings (#10109) 2024-02-24 13:16:10 +00:00
Micha Reiser
36d8b03b5f Upgrade upload/download artifact actions in CI.yaml workflow (#10104) 2024-02-23 21:36:28 +01:00
Micha Reiser
a284c711bf Refactor trailing comma rule into explicit check and state update code (#10100) 2024-02-23 17:56:05 +01:00
Micha Reiser
8c20f14e62 Set PowerPC Page Size to 64KB (#10080) 2024-02-23 08:32:21 +01:00
Charlie Marsh
946028e358 Respect runtime-required decorators for function signatures (#10091)
## Summary

The original implementation of this applied the runtime-required context
to definitions _within_ the function, but not the signature itself. (We
had test coverage; the snapshot was just correctly showing the wrong
outcome.)

Closes https://github.com/astral-sh/ruff/issues/10089.
2024-02-23 03:33:08 +00:00
Charlie Marsh
6fe15e7289 Allow © in copyright notices (#10065)
Closes https://github.com/astral-sh/ruff/issues/10061.
2024-02-22 12:44:22 -05:00
Seo Sanghyeon
7d9ce5049a PLR0203: Delete entire statement, including semicolons (#10074)
Co-authored-by: Micha Reiser <micha@reiser.io>
2024-02-22 16:03:00 +00:00
Vladimir Iglovikov
ecd5a7035d Added Image Augmentation library Albumentations (13k stars) to Readme to "Who is using Ruff" section (#10075) 2024-02-22 16:59:13 +01:00
Arjun Munji
175c266de3 Omit repeated equality comparison for sys (#10054)
## Summary
Update PLR1714 to ignore `sys.platform` and `sys.version` checks. 
I'm not sure if these checks or if we need to add more. Please advise.

Fixes #10017

## Test Plan
Added a new test case and ran `cargo nextest run`
2024-02-20 19:03:32 +00:00
Charlie Marsh
4997c681f1 [pycodestyle] Allow os.environ modifications between imports (E402) (#10066)
## Summary

Allows, e.g.:

```python
import os

os.environ["WORLD_SIZE"] = "1"
os.putenv("CUDA_VISIBLE_DEVICES", "4")

import torch
```

For now, this is only allowed in preview.

Closes https://github.com/astral-sh/ruff/issues/10059
2024-02-20 13:24:27 -05:00
Seo Sanghyeon
7eafba2a4d [pyupgrade] Detect literals with unary operators (UP018) (#10060)
Fix #10029.
2024-02-20 18:21:06 +00:00
Ottavio Hartman
0f70c99c42 feat(ERA001): detect single-line code for try:, except:, etc. (#10057)
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-20 18:40:18 +01:00
Micha Reiser
ee4efdba96 Fix ecosystem (#10064) 2024-02-20 16:54:50 +01:00
Ottavio Hartman
0d363ab239 fix(ERA001): detect commented out case statements, add more one-line support (#10055)
## Summary

Closes #10031 

- Detect commented out `case` statements. Playground repro:
https://play.ruff.rs/5a305aa9-6e5c-4fa4-999a-8fc427ab9a23
- Add more support for one-line commented out code.

## Test Plan

Unit tested and tested with
```sh
cargo run -p ruff -- check crates/ruff_linter/resources/test/fixtures/eradicate/ERA001.py --no-cache --preview --select ERA001
```

TODO:
- [x] `cargo insta test`
2024-02-19 22:56:42 -05:00
Daniël van Noord
68b8abf9c6 [pylint] Add PLE1141 DictIterMissingItems (#9845)
## Summary

References https://github.com/astral-sh/ruff/issues/970.

Implements
[`dict-iter-missing-items`](https://pylint.readthedocs.io/en/latest/user_guide/messages/error/dict-iter-missing-items.html).

Took the tests from "upstream"
[here](https://github.com/DanielNoord/pylint/blob/main/tests/functional/d/dict_iter_missing_items.py).

~I wasn't able to implement code for one false positive, but it is
pretty estoric: https://github.com/pylint-dev/pylint/issues/3283. I
would personally argue that adding this check as preview rule without
supporting this specific use case is fine. I did add a "test" for it.~
This was implemented.

## Test Plan

Followed the Contributing guide to create tests, hopefully I didn't miss
any.
Also ran CI on my own fork and seemed to be all okay 😄 

~Edit: the ecosystem check seems a bit all over the place? 😅~ All good.

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2024-02-19 19:56:55 +05:30
Seo Sanghyeon
1c8851e5fb Do multiline string test for W293 too (#10049) 2024-02-19 11:58:56 +00:00
Micha Reiser
4ac19993cf Add Micha as owner to formatter and parser (#10048) 2024-02-19 10:26:12 +00:00
dependabot[bot]
5cd3c6ef07 Bump anyhow from 1.0.79 to 1.0.80 (#10043)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:42 +01:00
dependabot[bot]
e94a2615a8 Bump semver from 1.0.21 to 1.0.22 (#10044)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:31 +01:00
dependabot[bot]
77f577cba7 Bump syn from 2.0.48 to 2.0.49 (#10045)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:21 +01:00
dependabot[bot]
49a46c2880 Bump clap from 4.5.0 to 4.5.1 (#10046)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:38:11 +01:00
dependabot[bot]
67e17e2750 Bump ureq from 2.9.5 to 2.9.6 (#10047)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-19 10:37:50 +01:00
Charlie Marsh
e1928be36e Allow boolean positionals in __post_init__ (#10027)
Closes https://github.com/astral-sh/ruff/issues/10011.
2024-02-18 15:03:17 +00:00
127 changed files with 1896 additions and 724 deletions

6
.github/CODEOWNERS vendored
View File

@@ -7,3 +7,9 @@
# Jupyter
/crates/ruff_linter/src/jupyter/ @dhruvmanila
/crates/ruff_formatter/ @MichaReiser
/crates/ruff_python_formatter/ @MichaReiser
/crates/ruff_python_parser/ @MichaReiser
# flake8-pyi
/crates/ruff_linter/src/rules/flake8_pyi/ @AlexWaygood

View File

@@ -133,7 +133,7 @@ jobs:
env:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
RUSTDOCFLAGS: "-D warnings"
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: ruff
path: target/debug/ruff
@@ -238,7 +238,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -250,6 +250,7 @@ jobs:
with:
name: ruff
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- name: Install ruff-ecosystem
@@ -324,13 +325,13 @@ jobs:
run: |
echo ${{ github.event.number }} > pr-number
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload PR Number
with:
name: pr-number
path: pr-number
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
name: Upload Results
with:
name: ecosystem-result
@@ -484,7 +485,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
name: Download development ruff binary
id: ruff-target
with:

View File

@@ -52,9 +52,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload sdist"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-sdist
path: dist
macos-x86_64:
@@ -80,9 +80,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-macos-x86_64
path: dist
- name: "Archive binary"
run: |
@@ -90,9 +90,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-macos-x86_64
path: |
*.tar.gz
*.sha256
@@ -119,9 +119,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-aarch64-apple-darwin
path: dist
- name: "Archive binary"
run: |
@@ -129,9 +129,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-aarch64-apple-darwin
path: |
*.tar.gz
*.sha256
@@ -170,9 +170,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
shell: bash
@@ -181,9 +181,9 @@ jobs:
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.zip
*.sha256
@@ -218,9 +218,9 @@ jobs:
ruff --help
python -m ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
run: |
@@ -228,9 +228,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
@@ -251,8 +251,12 @@ jobs:
arch: s390x
- target: powerpc64le-unknown-linux-gnu
arch: ppc64le
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
- target: powerpc64-unknown-linux-gnu
arch: ppc64
# see https://github.com/astral-sh/ruff/issues/10073
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
steps:
- uses: actions/checkout@v4
@@ -285,9 +289,9 @@ jobs:
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
run: |
@@ -295,9 +299,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
@@ -337,9 +341,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.target }}
path: dist
- name: "Archive binary"
run: |
@@ -347,9 +351,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.target }}
path: |
*.tar.gz
*.sha256
@@ -394,9 +398,9 @@ jobs:
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: wheels
name: wheels-${{ matrix.platform.target }}
path: dist
- name: "Archive binary"
run: |
@@ -404,9 +408,9 @@ jobs:
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: binaries
name: binaries-${{ matrix.platform.target }}
path: |
*.tar.gz
*.sha256
@@ -463,10 +467,11 @@ jobs:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: wheels
pattern: wheels-*
path: wheels
merge-multiple: true
- name: Publish to PyPi
uses: pypa/gh-action-pypi-publish@release/v1
with:
@@ -506,10 +511,11 @@ jobs:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: binaries
pattern: binaries-*
path: binaries
merge-multiple: true
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v1
with:

120
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.79"
version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
[[package]]
name = "argfile"
@@ -217,9 +217,9 @@ checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
[[package]]
name = "bstr"
version = "1.9.0"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c48f0051a4b4c5e0b6d365cd04af53aeaa209e3cc15ec2cdb69e73cc87fbd0dc"
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
dependencies = [
"memchr",
"regex-automata 0.4.5",
@@ -312,9 +312,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
dependencies = [
"clap_builder",
"clap_derive",
@@ -322,9 +322,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.0"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
dependencies = [
"anstream",
"anstyle",
@@ -383,7 +383,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -407,9 +407,9 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.3.3"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb4ab4dcb6554eb4f590fb16f99d3b102ab76f5f56554c9a5340518b32c499b"
checksum = "4b85b056aa0541d1975ebc524149dde72803a5d7352b6aebf9eabc44f9017246"
dependencies = [
"colored",
"libc",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "2.3.3"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc07a3d3f7e0c8961d0ffdee149d39b231bafdcdc3d978dc5ad790c615f55f3f"
checksum = "02ae9de916d6315a5129bca2fc7957285f0b9f77a2f6a8734a0a146caee2b0b6"
dependencies = [
"codspeed",
"colored",
@@ -592,7 +592,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -603,7 +603,7 @@ checksum = "1d1545d67a2149e1d93b7e5c7752dce5a7426eb5d1357ddcfd89336b94444f77"
dependencies = [
"darling_core",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -914,35 +914,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "hoot"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df22a4d90f1b0e65fe3e0d6ee6a4608cc4d81f4b2eb3e670f44bb6bde711e452"
dependencies = [
"httparse",
"log",
]
[[package]]
name = "hootbin"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "354e60868e49ea1a39c44b9562ad207c4259dc6eabf9863bf3b0f058c55cfdb2"
dependencies = [
"fastrand",
"hoot",
"serde",
"serde_json",
"thiserror",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "humantime"
version = "2.1.0"
@@ -1077,9 +1048,9 @@ dependencies = [
[[package]]
name = "insta"
version = "1.34.0"
version = "1.35.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d64600be34b2fcfc267740a243fa7744441bb4947a619ac4e5bb6507f35fbfc"
checksum = "7c985c1bef99cf13c58fade470483d81a2bfe846ebde60ed28cc2dddec2df9e2"
dependencies = [
"console",
"globset",
@@ -1130,7 +1101,7 @@ dependencies = [
"Inflector",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -1709,7 +1680,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -1960,7 +1931,7 @@ dependencies = [
"pmutil",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2212,7 +2183,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2612,24 +2583,24 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "semver"
version = "1.0.21"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
[[package]]
name = "serde"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132"
checksum = "4c1432112bce8b966497ac46519535189a3250a3812cd27a999678a69756f79f"
dependencies = [
"js-sys",
"serde",
@@ -2638,13 +2609,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.196"
version = "1.0.197"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2707,7 +2678,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2817,7 +2788,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2839,9 +2810,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.48"
version = "2.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
checksum = "6ab617d94515e94ae53b8406c628598680aa0c9587474ecbe58188f7b345d66c"
dependencies = [
"proc-macro2",
"quote",
@@ -2927,7 +2898,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -2938,7 +2909,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
"test-case-core",
]
@@ -2959,7 +2930,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3096,7 +3067,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3262,13 +3233,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "2.9.5"
version = "2.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b52731d03d6bb2fd18289d4028aee361d6c28d44977846793b994b13cdcc64d"
checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35"
dependencies = [
"base64",
"flate2",
"hootbin",
"log",
"once_cell",
"rustls",
@@ -3316,7 +3286,7 @@ checksum = "7abb14ae1a50dad63eaa768a458ef43d298cd1bd44951677bd10b732a9ba2a2d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3410,7 +3380,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
"wasm-bindgen-shared",
]
@@ -3444,7 +3414,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3477,7 +3447,7 @@ checksum = "a5211b7550606857312bba1d978a8ec75692eae187becc5e680444fffc5e6f89"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]
@@ -3742,7 +3712,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
"syn 2.0.51",
]
[[package]]

View File

@@ -14,39 +14,39 @@ license = "MIT"
[workspace.dependencies]
aho-corasick = { version = "1.1.2" }
annotate-snippets = { version = "0.9.2", features = ["color"] }
anyhow = { version = "1.0.79" }
anyhow = { version = "1.0.80" }
argfile = { version = "0.1.6" }
assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
bstr = { version = "1.9.0" }
bstr = { version = "1.9.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.34", default-features = false, features = ["clock"] }
clap = { version = "4.4.18", features = ["derive"] }
clap = { version = "4.5.1", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
codspeed-criterion-compat = { version = "2.3.3", default-features = false }
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
colored = { version = "2.1.0" }
configparser = { version = "3.0.3" }
console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version ="3.0.1"}
countme = { version = "3.0.1" }
criterion = { version = "0.5.1", default-features = false }
dirs = { version = "5.0.0" }
drop_bomb = { version = "0.1.5" }
env_logger = { version ="0.10.1"}
env_logger = { version = "0.10.1" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.23" }
fs-err = { version ="2.11.0"}
fs-err = { version = "2.11.0" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
hexf-parse = { version ="0.2.1"}
hexf-parse = { version = "0.2.1" }
ignore = { version = "0.4.22" }
imara-diff ={ version = "0.1.5"}
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indicatif ={ version = "0.17.8"}
indoc ={ version = "2.0.4"}
insta = { version = "1.34.0", feature = ["filters", "glob"] }
indicatif = { version = "0.17.8" }
indoc = { version = "2.0.4" }
insta = { version = "1.35.1", feature = ["filters", "glob"] }
insta-cmd = { version = "0.4.0" }
is-macro = { version = "0.3.5" }
is-wsl = { version = "0.4.0" }
@@ -57,7 +57,7 @@ lexical-parse-float = { version = "0.8.0", features = ["format"] }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
memchr = { version = "2.7.1" }
mimalloc = { version ="0.1.39"}
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
notify = { version = "6.1.1" }
once_cell = { version = "1.19.0" }
@@ -75,35 +75,35 @@ regex = { version = "1.10.2" }
result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
seahash = { version ="4.1.0"}
semver = { version = "1.0.21" }
serde = { version = "1.0.196", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
seahash = { version = "4.1.0" }
semver = { version = "1.0.22" }
serde = { version = "1.0.197", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.4" }
serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.6.0", default-features = false, features = ["macros"] }
shellexpand = { version = "3.0.0" }
shlex = { version ="1.3.0"}
shlex = { version = "1.3.0" }
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.13.1" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
tempfile = { version ="3.9.0"}
syn = { version = "2.0.51" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.57" }
tikv-jemallocator = { version ="0.5.0"}
tikv-jemallocator = { version = "0.5.0" }
toml = { version = "0.8.9" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
typed-arena = { version = "2.0.2" }
unic-ucd-category = { version ="0.9"}
unic-ucd-category = { version = "0.9" }
unicode-ident = { version = "1.0.12" }
unicode-width = { version = "0.1.11" }
unicode_names2 = { version = "1.2.1" }
ureq = { version = "2.9.1" }
ureq = { version = "2.9.6" }
url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
walkdir = { version = "2.3.2" }

View File

@@ -172,7 +172,7 @@ jobs:
ruff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: chartboost/ruff-action@v1
```
@@ -378,6 +378,7 @@ Ruff is released under the MIT license.
Ruff is used by a number of major open-source projects and companies, including:
- [Albumentations](https://github.com/albumentations-team/albumentations)
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))
- [Apache Airflow](https://github.com/apache/airflow)

View File

@@ -745,38 +745,34 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
}
}
/// Enumeration of various ways in which a --config CLI flag
/// could be invalid
#[derive(Debug)]
enum TomlParseFailureKind {
SyntaxError,
UnknownOption,
enum InvalidConfigFlagReason {
InvalidToml(toml::de::Error),
/// It was valid TOML, but not a valid ruff config file.
/// E.g. the user tried to select a rule that doesn't exist,
/// or tried to enable a setting that doesn't exist
ValidTomlButInvalidRuffSchema(toml::de::Error),
/// It was a valid ruff config file, but the user tried to pass a
/// value for `extend` as part of the config override.
// `extend` is special, because it affects which config files we look at
/// in the first place. We currently only parse --config overrides *after*
/// we've combined them with all the arguments from the various config files
/// that we found, so trying to override `extend` as part of a --config
/// override is forbidden.
ExtendPassedViaConfigFlag,
}
impl std::fmt::Display for TomlParseFailureKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let display = match self {
Self::SyntaxError => "The supplied argument is not valid TOML",
Self::UnknownOption => {
impl InvalidConfigFlagReason {
const fn description(&self) -> &'static str {
match self {
Self::InvalidToml(_) => "The supplied argument is not valid TOML",
Self::ValidTomlButInvalidRuffSchema(_) => {
"Could not parse the supplied argument as a `ruff.toml` configuration option"
}
};
write!(f, "{display}")
}
}
#[derive(Debug)]
struct TomlParseFailure {
kind: TomlParseFailureKind,
underlying_error: toml::de::Error,
}
impl std::fmt::Display for TomlParseFailure {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let TomlParseFailure {
kind,
underlying_error,
} = self;
let display = format!("{kind}:\n\n{underlying_error}");
write!(f, "{}", display.trim_end())
Self::ExtendPassedViaConfigFlag => "Cannot include `extend` in a --config flag value",
}
}
}
@@ -827,18 +823,19 @@ impl TypedValueParser for ConfigArgumentParser {
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
let toml_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into() {
Ok(option) => return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option))),
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::UnknownOption,
underlying_error,
},
},
Err(underlying_error) => TomlParseFailure {
kind: TomlParseFailureKind::SyntaxError,
underlying_error,
let config_parse_error = match toml::Table::from_str(value) {
Ok(table) => match table.try_into::<Options>() {
Ok(option) => {
if option.extend.is_none() {
return Ok(SingleConfigArgument::SettingsOverride(Arc::new(option)));
}
InvalidConfigFlagReason::ExtendPassedViaConfigFlag
}
Err(underlying_error) => {
InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error)
}
},
Err(underlying_error) => InvalidConfigFlagReason::InvalidToml(underlying_error),
};
let mut new_error = clap::Error::new(clap::error::ErrorKind::ValueValidation).with_cmd(cmd);
@@ -853,6 +850,21 @@ impl TypedValueParser for ConfigArgumentParser {
clap::error::ContextValue::String(value.to_string()),
);
let underlying_error = match &config_parse_error {
InvalidConfigFlagReason::ExtendPassedViaConfigFlag => {
let tip = config_parse_error.description().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip]),
);
return Err(new_error);
}
InvalidConfigFlagReason::InvalidToml(underlying_error)
| InvalidConfigFlagReason::ValidTomlButInvalidRuffSchema(underlying_error) => {
underlying_error
}
};
// small hack so that multiline tips
// have the same indent on the left-hand side:
let tip_indent = " ".repeat(" tip: ".len());
@@ -881,12 +893,16 @@ The path `{value}` does not exist"
));
}
} else if value.contains('=') {
tip.push_str(&format!("\n\n{toml_parse_error}"));
tip.push_str(&format!(
"\n\n{}:\n\n{underlying_error}",
config_parse_error.description()
));
}
let tip = tip.trim_end().to_owned().into();
new_error.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![tip.into()]),
clap::error::ContextValue::StyledStrs(vec![tip]),
);
Err(new_error)

View File

@@ -7,10 +7,15 @@ use std::str;
use anyhow::Result;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use regex::escape;
use tempfile::TempDir;
const BIN_NAME: &str = "ruff";
fn tempdir_filter(tempdir: &TempDir) -> String {
format!(r"{}\\?/?", escape(tempdir.path().to_str().unwrap()))
}
#[test]
fn default_options() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
@@ -147,28 +152,29 @@ fn too_many_config_files() -> Result<()> {
let ruff2_dot_toml = tempdir.path().join("ruff2.toml");
fs::File::create(&ruff_dot_toml)?;
fs::File::create(&ruff2_dot_toml)?;
let expected_stderr = format!(
"\
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config={}` or `--config={}`.
For more information, try `--help`.
",
ruff_dot_toml.display(),
ruff2_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--config")
.arg(&ruff2_dot_toml)
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: You cannot specify more than one configuration file on the command line.
tip: remove either `--config=[TMP]/ruff.toml` or `--config=[TMP]/ruff2.toml`.
For more information, try `--help`.
"###);
});
Ok(())
}
@@ -177,27 +183,29 @@ fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_dot_toml = tempdir.path().join("ruff.toml");
fs::File::create(&ruff_dot_toml)?;
let expected_stderr = format!(
"\
ruff failed
Cause: The argument `--config={}` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
",
ruff_dot_toml.display(),
);
let cmd = Command::new(get_cargo_bin(BIN_NAME))
insta::with_settings!({
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
}, {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.arg("format")
.arg("--config")
.arg(&ruff_dot_toml)
.arg("--isolated")
.arg(".")
.output()?;
let stderr = std::str::from_utf8(&cmd.stderr)?;
assert_eq!(stderr, expected_stderr);
.arg("."), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
ruff failed
Cause: The argument `--config=[TMP]/ruff.toml` cannot be used with `--isolated`
tip: You cannot specify a configuration file and also specify `--isolated`,
as `--isolated` causes ruff to ignore all configuration files.
For more information, try `--help`.
"###);
});
Ok(())
}

View File

@@ -595,6 +595,24 @@ fn too_many_config_files() -> Result<()> {
Ok(())
}
#[test]
fn extend_passed_via_config_argument() {
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(STDIN_BASE_OPTIONS)
.args(["--config", "extend = 'foo.toml'", "."]), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'extend = 'foo.toml'' for '--config <CONFIG_OPTION>'
tip: Cannot include `extend` in a --config flag value
For more information, try '--help'.
"###);
}
#[test]
fn config_file_and_isolated() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -232,7 +232,7 @@ linter.flake8_bandit.check_typed_exception = false
linter.flake8_bugbear.extend_immutable_calls = []
linter.flake8_builtins.builtins_ignorelist = []
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}(-\d{4})*
linter.flake8_copyright.author = none
linter.flake8_copyright.min_file_size = 0
linter.flake8_errmsg.max_string_length = 0

View File

@@ -78,27 +78,28 @@ impl<'a> PrintQueue<'a> {
impl<'a> Queue<'a> for PrintQueue<'a> {
fn pop(&mut self) -> Option<&'a FormatElement> {
let elements = self.element_slices.last_mut()?;
elements.next().or_else(|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
})
elements.next().or_else(
#[cold]
|| {
self.element_slices.pop();
let elements = self.element_slices.last_mut()?;
elements.next()
},
)
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
let mut slices = self.element_slices.iter().rev();
let slice = slices.next()?;
match slice.as_slice().first() {
Some(element) => Some(element),
None => {
if let Some(next_elements) = slices.next() {
next_elements.as_slice().first()
} else {
None
}
}
}
slice.as_slice().first().or_else(
#[cold]
|| {
slices
.next()
.and_then(|next_elements| next_elements.as_slice().first())
},
)
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {
@@ -146,24 +147,30 @@ impl<'a, 'print> FitsQueue<'a, 'print> {
impl<'a, 'print> Queue<'a> for FitsQueue<'a, 'print> {
fn pop(&mut self) -> Option<&'a FormatElement> {
self.queue.pop().or_else(|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
})
self.queue.pop().or_else(
#[cold]
|| {
if let Some(next_slice) = self.rest_elements.next_back() {
self.queue.extend_back(next_slice.as_slice());
self.queue.pop()
} else {
None
}
},
)
}
fn top_with_interned(&self) -> Option<&'a FormatElement> {
self.queue.top_with_interned().or_else(|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
})
self.queue.top_with_interned().or_else(
#[cold]
|| {
if let Some(next_elements) = self.rest_elements.as_slice().last() {
next_elements.as_slice().first()
} else {
None
}
},
)
}
fn extend_back(&mut self, elements: &'a [FormatElement]) {

View File

@@ -28,3 +28,11 @@ dictionary = {
}
#import os # noqa
# case 1:
# try:
# try: # with comment
# try: print()
# except:
# except Foo:
# except Exception as e: print(e)

View File

@@ -119,3 +119,16 @@ def func(x: bool):
settings(True)
from dataclasses import dataclass, InitVar
@dataclass
class Fit:
force: InitVar[bool] = False
def __post_init__(self, force: bool) -> None:
print(force)
Fit(force=True)

View File

@@ -147,3 +147,9 @@ ham[upper : ]
#: E203:1:10
ham[upper :]
#: Okay
ham[lower +1 :, "columnname"]
#: E203:1:13
ham[lower + 1 :, "columnname"]

View File

@@ -0,0 +1,7 @@
import os
os.environ["WORLD_SIZE"] = "1"
os.putenv("CUDA_VISIBLE_DEVICES", "4")
del os.environ["WORLD_SIZE"]
import torch

View File

@@ -14,3 +14,6 @@ class Chassis(RobotModuleTemplate):
" \
\
'''blank line with whitespace
inside a multiline string'''

View File

@@ -0,0 +1,32 @@
from typing import Any
d = {1: 1, 2: 2}
d_tuple = {(1, 2): 3, (4, 5): 6}
d_tuple_annotated: Any = {(1, 2): 3, (4, 5): 6}
d_tuple_incorrect_tuple = {(1,): 3, (4, 5): 6}
l = [1, 2]
s1 = {1, 2}
s2 = {1, 2, 3}
# Errors
for k, v in d:
pass
for k, v in d_tuple_incorrect_tuple:
pass
# Non errors
for k, v in d.items():
pass
for k in d.keys():
pass
for i, v in enumerate(l):
pass
for i, v in s1.intersection(s2):
pass
for a, b in d_tuple:
pass
for a, b in d_tuple_annotated:
pass

View File

@@ -17,3 +17,14 @@ class Fruit:
return choice(Fruit.COLORS)
pick_one_color = staticmethod(pick_one_color)
class Class:
def class_method(cls):
pass
class_method = classmethod(class_method);another_statement
def static_method():
pass
static_method = staticmethod(static_method);

View File

@@ -51,3 +51,7 @@ foo == foo or foo == bar # Self-comparison.
foo[0] == "a" or foo[0] == "b" # Subscripts.
foo() == "a" or foo() == "b" # Calls.
import sys
sys.platform == "win32" or sys.platform == "emscripten" # sys attributes

View File

@@ -33,7 +33,7 @@ bool(b"")
bool(1.0)
int().denominator
# These become string or byte literals
# These become literals
str()
str("foo")
str("""
@@ -53,3 +53,9 @@ bool(False)
# These become a literal but retain parentheses
int(1).denominator
# These too are literals in spirit
int(+1)
int(-1)
float(+1.0)
float(-1.0)

View File

@@ -57,6 +57,11 @@ revision_heads_map_ast = [
list(zip(x, y))[0]
[*zip(x, y)][0]
# RUF015 (pop)
list(x).pop(0)
# OK
list(x).pop(1)
def test():
zip = list # Overwrite the builtin zip

View File

@@ -25,6 +25,10 @@ def negative_cases():
json3 = "{ 'positive': 'false' }"
alternative_formatter("{a}", a=5)
formatted = "{a}".fmt(a=7)
partial = "partial sentence"
a = _("formatting of {partial} in a translation string is bad practice")
_("formatting of {partial} in a translation string is bad practice")
print(_("formatting of {partial} in a translation string is bad practice"))
print(do_nothing("{a}".format(a=3)))
print(do_nothing(alternative_formatter("{a}", a=5)))
print(format(do_nothing("{a}"), a=5))

View File

@@ -116,7 +116,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_simplify::rules::use_capital_environment_variables(checker, expr);
}
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, subscript);
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr);
}
if checker.enabled(Rule::InvalidIndexType) {
ruff::rules::invalid_index_type(checker, subscript);
@@ -134,6 +134,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
elts,
ctx,
range: _,
parenthesized: _,
})
| Expr::List(ast::ExprList {
elts,
@@ -964,6 +965,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::DefaultFactoryKwarg) {
ruff::rules::default_factory_kwarg(checker, call);
}
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, expr);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[
@@ -1451,6 +1455,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
generators,
elt: _,
range: _,
parenthesized: _,
},
) => {
if checker.enabled(Rule::UnnecessaryListIndexLookup) {

View File

@@ -386,10 +386,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
},
) => {
if checker.enabled(Rule::NoClassmethodDecorator) {
pylint::rules::no_classmethod_decorator(checker, class_def);
pylint::rules::no_classmethod_decorator(checker, stmt);
}
if checker.enabled(Rule::NoStaticmethodDecorator) {
pylint::rules::no_staticmethod_decorator(checker, class_def);
pylint::rules::no_staticmethod_decorator(checker, stmt);
}
if checker.enabled(Rule::DjangoNullableModelStringField) {
flake8_django::rules::nullable_model_string_field(checker, body);
@@ -1299,6 +1299,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::IterationOverSet) {
pylint::rules::iteration_over_set(checker, iter);
}
if checker.enabled(Rule::DictIterMissingItems) {
pylint::rules::dict_iter_missing_items(checker, target, iter);
}
if checker.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, target, body);
}

View File

@@ -1,3 +1,4 @@
use ruff_python_ast::StmtFunctionDef;
use ruff_python_semantic::{ScopeKind, SemanticModel};
use crate::rules::flake8_type_checking;
@@ -26,6 +27,8 @@ pub(super) enum AnnotationContext {
}
impl AnnotationContext {
/// Determine the [`AnnotationContext`] for an annotation based on the current scope of the
/// semantic model.
pub(super) fn from_model(semantic: &SemanticModel, settings: &LinterSettings) -> Self {
// If the annotation is in a class scope (e.g., an annotated assignment for a
// class field) or a function scope, and that class or function is marked as
@@ -71,4 +74,23 @@ impl AnnotationContext {
Self::TypingOnly
}
/// Determine the [`AnnotationContext`] to use for annotations in a function signature.
pub(super) fn from_function(
function_def: &StmtFunctionDef,
semantic: &SemanticModel,
settings: &LinterSettings,
) -> Self {
if flake8_type_checking::helpers::runtime_required_function(
function_def,
&settings.flake8_type_checking.runtime_required_decorators,
semantic,
) {
Self::RuntimeRequired
} else if semantic.future_annotations() {
Self::TypingOnly
} else {
Self::RuntimeEvaluated
}
}
}

View File

@@ -374,7 +374,9 @@ where
|| helpers::is_assignment_to_a_dunder(stmt)
|| helpers::in_nested_block(self.semantic.current_statements())
|| imports::is_matplotlib_activation(stmt, self.semantic())
|| imports::is_sys_path_modification(stmt, self.semantic()))
|| imports::is_sys_path_modification(stmt, self.semantic())
|| (self.settings.preview.is_enabled()
&& imports::is_os_environ_modification(stmt, self.semantic())))
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
@@ -582,7 +584,8 @@ where
// Function annotations are always evaluated at runtime, unless future annotations
// are enabled.
let runtime_annotation = !self.semantic.future_annotations();
let annotation =
AnnotationContext::from_function(function_def, &self.semantic, self.settings);
// The first parameter may be a single dispatch.
let mut singledispatch =
@@ -606,10 +609,18 @@ where
if let Some(expr) = &parameter_with_default.parameter.annotation {
if singledispatch {
self.visit_runtime_required_annotation(expr);
} else if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
};
}
if let Some(expr) = &parameter_with_default.default {
@@ -619,28 +630,46 @@ where
}
if let Some(arg) = &parameters.vararg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
};
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
}
}
if let Some(arg) = &parameters.kwarg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
};
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
}
}
for expr in returns {
if runtime_annotation {
self.visit_runtime_evaluated_annotation(expr);
} else {
self.visit_annotation(expr);
};
match annotation {
AnnotationContext::RuntimeRequired => {
self.visit_runtime_required_annotation(expr);
}
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(expr);
}
AnnotationContext::TypingOnly => {
self.visit_annotation(expr);
}
}
}
let definition = docstrings::extraction::extract_definition(
@@ -978,6 +1007,7 @@ where
elt,
generators,
range: _,
parenthesized: _,
}) => {
self.visit_generators(generators);
self.visit_expr(elt);
@@ -1298,6 +1328,7 @@ where
elts,
ctx,
range: _,
parenthesized: _,
}) = slice.as_ref()
{
let mut iter = elts.iter();

View File

@@ -244,6 +244,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError),
(Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E1141") => (RuleGroup::Preview, rules::pylint::rules::DictIterMissingItems),
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),
(Pylint, "E1205") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooManyArgs),
(Pylint, "E1206") => (RuleGroup::Stable, rules::pylint::rules::LoggingTooFewArgs),

View File

@@ -26,7 +26,7 @@ static HASH_NUMBER: Lazy<Regex> = Lazy::new(|| Regex::new(r"#\d").unwrap());
static POSITIVE_CASES: Lazy<RegexSet> = Lazy::new(|| {
RegexSet::new([
// Keywords
r"^(?:elif\s+.*\s*:|else\s*:|try\s*:|finally\s*:|except\s+.*\s*:)$",
r"^(?:elif\s+.*\s*:.*|else\s*:.*|try\s*:.*|finally\s*:.*|except.*:.*|case\s+.*\s*:.*)$",
// Partial dictionary
r#"^['"]\w+['"]\s*:.+[,{]\s*(#.*)?$"#,
// Multiline assignment
@@ -147,6 +147,27 @@ mod tests {
assert!(!comment_contains_code("#to print", &[]));
}
#[test]
fn comment_contains_code_single_line() {
assert!(comment_contains_code("# case 1: print()", &[]));
assert!(comment_contains_code("# try: get(1, 2, 3)", &[]));
assert!(comment_contains_code("# else: print()", &[]));
assert!(comment_contains_code("# elif x == 10: print()", &[]));
assert!(comment_contains_code(
"# except Exception as e: print(e)",
&[]
));
assert!(comment_contains_code("# except: print()", &[]));
assert!(comment_contains_code("# finally: close_handle()", &[]));
assert!(!comment_contains_code("# try: use cache", &[]));
assert!(!comment_contains_code("# else: we should return", &[]));
assert!(!comment_contains_code(
"# call function except: without cache",
&[]
));
}
#[test]
fn comment_contains_code_with_multiline() {
assert!(comment_contains_code("#else:", &[]));
@@ -155,11 +176,15 @@ mod tests {
assert!(comment_contains_code("#elif True:", &[]));
assert!(comment_contains_code("#x = foo(", &[]));
assert!(comment_contains_code("#except Exception:", &[]));
assert!(comment_contains_code("# case 1:", &[]));
assert!(comment_contains_code("#case 1:", &[]));
assert!(comment_contains_code("# try:", &[]));
assert!(!comment_contains_code("# this is = to that :(", &[]));
assert!(!comment_contains_code("#else", &[]));
assert!(!comment_contains_code("#or else:", &[]));
assert!(!comment_contains_code("#else True:", &[]));
assert!(!comment_contains_code("# in that case:", &[]));
// Unpacking assignments
assert!(comment_contains_code(

View File

@@ -47,7 +47,8 @@ fn is_standalone_comment(line: &str) -> bool {
for char in line.chars() {
if char == '#' {
return true;
} else if !char.is_whitespace() {
}
if !char.is_whitespace() {
return false;
}
}

View File

@@ -148,4 +148,132 @@ ERA001.py:27:5: ERA001 Found commented-out code
29 28 |
30 29 | #import os # noqa
ERA001.py:32:1: ERA001 Found commented-out code
|
30 | #import os # noqa
31 |
32 | # case 1:
| ^^^^^^^^^ ERA001
33 | # try:
34 | # try: # with comment
|
= help: Remove commented-out code
Display-only fix
29 29 |
30 30 | #import os # noqa
31 31 |
32 |-# case 1:
33 32 | # try:
34 33 | # try: # with comment
35 34 | # try: print()
ERA001.py:33:1: ERA001 Found commented-out code
|
32 | # case 1:
33 | # try:
| ^^^^^^ ERA001
34 | # try: # with comment
35 | # try: print()
|
= help: Remove commented-out code
Display-only fix
30 30 | #import os # noqa
31 31 |
32 32 | # case 1:
33 |-# try:
34 33 | # try: # with comment
35 34 | # try: print()
36 35 | # except:
ERA001.py:34:1: ERA001 Found commented-out code
|
32 | # case 1:
33 | # try:
34 | # try: # with comment
| ^^^^^^^^^^^^^^^^^^^^^^ ERA001
35 | # try: print()
36 | # except:
|
= help: Remove commented-out code
Display-only fix
31 31 |
32 32 | # case 1:
33 33 | # try:
34 |-# try: # with comment
35 34 | # try: print()
36 35 | # except:
37 36 | # except Foo:
ERA001.py:35:1: ERA001 Found commented-out code
|
33 | # try:
34 | # try: # with comment
35 | # try: print()
| ^^^^^^^^^^^^^^ ERA001
36 | # except:
37 | # except Foo:
|
= help: Remove commented-out code
Display-only fix
32 32 | # case 1:
33 33 | # try:
34 34 | # try: # with comment
35 |-# try: print()
36 35 | # except:
37 36 | # except Foo:
38 37 | # except Exception as e: print(e)
ERA001.py:36:1: ERA001 Found commented-out code
|
34 | # try: # with comment
35 | # try: print()
36 | # except:
| ^^^^^^^^^ ERA001
37 | # except Foo:
38 | # except Exception as e: print(e)
|
= help: Remove commented-out code
Display-only fix
33 33 | # try:
34 34 | # try: # with comment
35 35 | # try: print()
36 |-# except:
37 36 | # except Foo:
38 37 | # except Exception as e: print(e)
ERA001.py:37:1: ERA001 Found commented-out code
|
35 | # try: print()
36 | # except:
37 | # except Foo:
| ^^^^^^^^^^^^^ ERA001
38 | # except Exception as e: print(e)
|
= help: Remove commented-out code
Display-only fix
34 34 | # try: # with comment
35 35 | # try: print()
36 36 | # except:
37 |-# except Foo:
38 37 | # except Exception as e: print(e)
ERA001.py:38:1: ERA001 Found commented-out code
|
36 | # except:
37 | # except Foo:
38 | # except Exception as e: print(e)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ERA001
|
= help: Remove commented-out code
Display-only fix
35 35 | # try: print()
36 36 | # except:
37 37 | # except Foo:
38 |-# except Exception as e: print(e)

View File

@@ -45,7 +45,7 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
/// Returns `true` if a function definition is allowed to use a boolean trap.
pub(super) fn is_allowed_func_def(name: &str) -> bool {
matches!(name, "__setitem__")
matches!(name, "__setitem__" | "__post_init__")
}
/// Returns `true` if an argument is allowed to use a boolean trap. To return

View File

@@ -109,6 +109,7 @@ fn type_pattern(elts: Vec<&Expr>) -> Expr {
elts: elts.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
}
.into()
}

View File

@@ -29,7 +29,7 @@ enum TokenType {
/// Simplified token specialized for the task.
#[derive(Copy, Clone)]
struct Token {
r#type: TokenType,
ty: TokenType,
range: TextRange,
}
@@ -40,13 +40,13 @@ impl Ranged for Token {
}
impl Token {
fn new(r#type: TokenType, range: TextRange) -> Self {
Self { r#type, range }
fn new(ty: TokenType, range: TextRange) -> Self {
Self { ty, range }
}
fn irrelevant() -> Token {
Token {
r#type: TokenType::Irrelevant,
ty: TokenType::Irrelevant,
range: TextRange::default(),
}
}
@@ -54,7 +54,7 @@ impl Token {
impl From<(&Tok, TextRange)> for Token {
fn from((tok, range): (&Tok, TextRange)) -> Self {
let r#type = match tok {
let ty = match tok {
Tok::Name { .. } => TokenType::Named,
Tok::String { .. } => TokenType::String,
Tok::Newline => TokenType::Newline,
@@ -75,7 +75,7 @@ impl From<(&Tok, TextRange)> for Token {
_ => TokenType::Irrelevant,
};
#[allow(clippy::inconsistent_struct_constructor)]
Self { range, r#type }
Self { range, ty }
}
}
@@ -102,16 +102,13 @@ enum ContextType {
/// Comma context - described a comma-delimited "situation".
#[derive(Copy, Clone)]
struct Context {
r#type: ContextType,
ty: ContextType,
num_commas: u32,
}
impl Context {
const fn new(r#type: ContextType) -> Self {
Self {
r#type,
num_commas: 0,
}
const fn new(ty: ContextType) -> Self {
Self { ty, num_commas: 0 }
}
fn inc(&mut self) {
@@ -277,9 +274,7 @@ pub(crate) fn trailing_commas(
let mut stack = vec![Context::new(ContextType::No)];
for token in tokens {
if prev.r#type == TokenType::NonLogicalNewline
&& token.r#type == TokenType::NonLogicalNewline
{
if prev.ty == TokenType::NonLogicalNewline && token.ty == TokenType::NonLogicalNewline {
// Collapse consecutive newlines to the first one -- trailing commas are
// added before the first newline.
continue;
@@ -288,87 +283,18 @@ pub(crate) fn trailing_commas(
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack);
// Is it allowed to have a trailing comma before this token?
let comma_allowed = token.r#type == TokenType::ClosingBracket
&& match context.r#type {
ContextType::No => false,
ContextType::FunctionParameters => true,
ContextType::CallArguments => true,
// `(1)` is not equivalent to `(1,)`.
ContextType::Tuple => context.num_commas != 0,
// `x[1]` is not equivalent to `x[1,]`.
ContextType::Subscript => context.num_commas != 0,
ContextType::List => true,
ContextType::Dict => true,
// Lambdas are required to be a single line, trailing comma never makes sense.
ContextType::LambdaParameters => false,
};
// Is prev a prohibited trailing comma?
let comma_prohibited = prev.r#type == TokenType::Comma && {
// Is `(1,)` or `x[1,]`?
let is_singleton_tuplish =
matches!(context.r#type, ContextType::Subscript | ContextType::Tuple)
&& context.num_commas <= 1;
// There was no non-logical newline, so prohibit (except in `(1,)` or `x[1,]`).
if comma_allowed && !is_singleton_tuplish {
true
// Lambdas not handled by comma_allowed so handle it specially.
} else {
context.r#type == ContextType::LambdaParameters && token.r#type == TokenType::Colon
}
};
if comma_prohibited {
let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic.range())));
diagnostics.push(diagnostic);
}
// Is prev a prohibited trailing comma on a bare tuple?
// Approximation: any comma followed by a statement-ending newline.
let bare_comma_prohibited =
prev.r#type == TokenType::Comma && token.r#type == TokenType::Newline;
if bare_comma_prohibited {
diagnostics.push(Diagnostic::new(TrailingCommaOnBareTuple, prev.range()));
}
// Comma is required if:
// - It is allowed,
// - Followed by a newline,
// - Not already present,
// - Not on an empty (), {}, [].
let comma_required = comma_allowed
&& prev.r#type == TokenType::NonLogicalNewline
&& !matches!(
prev_prev.r#type,
TokenType::Comma
| TokenType::OpeningBracket
| TokenType::OpeningSquareBracket
| TokenType::OpeningCurlyBracket
);
if comma_required {
let mut diagnostic =
Diagnostic::new(MissingTrailingComma, TextRange::empty(prev_prev.end()));
// Create a replacement that includes the final bracket (or other token),
// rather than just inserting a comma at the end. This prevents the UP034 fix
// removing any brackets in the same linter pass - doing both at the same time could
// lead to a syntax error.
let contents = locator.slice(prev_prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{contents},"),
prev_prev.range(),
)));
if let Some(diagnostic) = check_token(token, prev, prev_prev, context, locator) {
diagnostics.push(diagnostic);
}
// Pop the current context if the current token ended it.
// The top context is never popped (if unbalanced closing brackets).
let pop_context = match context.r#type {
let pop_context = match context.ty {
// Lambda terminated by `:`.
ContextType::LambdaParameters => token.r#type == TokenType::Colon,
ContextType::LambdaParameters => token.ty == TokenType::Colon,
// All others terminated by a closing bracket.
// flake8-commas doesn't verify that it matches the opening...
_ => token.r#type == TokenType::ClosingBracket,
_ => token.ty == TokenType::ClosingBracket,
};
if pop_context && stack.len() > 1 {
stack.pop();
@@ -379,21 +305,107 @@ pub(crate) fn trailing_commas(
}
}
fn check_token(
token: Token,
prev: Token,
prev_prev: Token,
context: Context,
locator: &Locator,
) -> Option<Diagnostic> {
// Is it allowed to have a trailing comma before this token?
let comma_allowed = token.ty == TokenType::ClosingBracket
&& match context.ty {
ContextType::No => false,
ContextType::FunctionParameters => true,
ContextType::CallArguments => true,
// `(1)` is not equivalent to `(1,)`.
ContextType::Tuple => context.num_commas != 0,
// `x[1]` is not equivalent to `x[1,]`.
ContextType::Subscript => context.num_commas != 0,
ContextType::List => true,
ContextType::Dict => true,
// Lambdas are required to be a single line, trailing comma never makes sense.
ContextType::LambdaParameters => false,
};
// Is prev a prohibited trailing comma?
let comma_prohibited = prev.ty == TokenType::Comma && {
// Is `(1,)` or `x[1,]`?
let is_singleton_tuplish =
matches!(context.ty, ContextType::Subscript | ContextType::Tuple)
&& context.num_commas <= 1;
// There was no non-logical newline, so prohibit (except in `(1,)` or `x[1,]`).
if comma_allowed && !is_singleton_tuplish {
true
// Lambdas not handled by comma_allowed so handle it specially.
} else {
context.ty == ContextType::LambdaParameters && token.ty == TokenType::Colon
}
};
if comma_prohibited {
let mut diagnostic = Diagnostic::new(ProhibitedTrailingComma, prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(diagnostic.range())));
return Some(diagnostic);
}
// Is prev a prohibited trailing comma on a bare tuple?
// Approximation: any comma followed by a statement-ending newline.
let bare_comma_prohibited = prev.ty == TokenType::Comma && token.ty == TokenType::Newline;
if bare_comma_prohibited {
return Some(Diagnostic::new(TrailingCommaOnBareTuple, prev.range()));
}
if !comma_allowed {
return None;
}
// Comma is required if:
// - It is allowed,
// - Followed by a newline,
// - Not already present,
// - Not on an empty (), {}, [].
let comma_required = prev.ty == TokenType::NonLogicalNewline
&& !matches!(
prev_prev.ty,
TokenType::Comma
| TokenType::OpeningBracket
| TokenType::OpeningSquareBracket
| TokenType::OpeningCurlyBracket
);
if comma_required {
let mut diagnostic =
Diagnostic::new(MissingTrailingComma, TextRange::empty(prev_prev.end()));
// Create a replacement that includes the final bracket (or other token),
// rather than just inserting a comma at the end. This prevents the UP034 fix
// removing any brackets in the same linter pass - doing both at the same time could
// lead to a syntax error.
let contents = locator.slice(prev_prev.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{contents},"),
prev_prev.range(),
)));
Some(diagnostic)
} else {
None
}
}
fn update_context(
token: Token,
prev: Token,
prev_prev: Token,
stack: &mut Vec<Context>,
) -> Context {
let new_context = match token.r#type {
TokenType::OpeningBracket => match (prev.r#type, prev_prev.r#type) {
let new_context = match token.ty {
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
(TokenType::Named, TokenType::Def) => Context::new(ContextType::FunctionParameters),
(TokenType::Named | TokenType::ClosingBracket, _) => {
Context::new(ContextType::CallArguments)
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket => match prev.r#type {
TokenType::OpeningSquareBracket => match prev.ty {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)
}

View File

@@ -29,6 +29,20 @@ import os
r"
# Copyright (C) 2023
import os
"
.trim(),
&settings::LinterSettings::for_rules(vec![Rule::MissingCopyrightNotice]),
);
assert_messages!(diagnostics);
}
#[test]
fn notice_with_unicode_c() {
let diagnostics = test_snippet(
r"
# Copyright © 2023
import os
"
.trim(),

View File

@@ -15,7 +15,7 @@ pub struct Settings {
}
pub static COPYRIGHT: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)Copyright\s+(\(C\)\s+)?\d{4}(-\d{4})*").unwrap());
Lazy::new(|| Regex::new(r"(?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}(-\d{4})*").unwrap());
impl Default for Settings {
fn default() -> Self {

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/flake8_copyright/mod.rs
---

View File

@@ -173,6 +173,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
});
let node1 = Expr::Name(ast::ExprName {
id: arg_name.into(),

View File

@@ -72,6 +72,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
elts,
range: _,
ctx: _,
parenthesized: _,
}) = slice.as_ref()
{
for expr in elts {
@@ -123,6 +124,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
elts: literal_exprs.into_iter().cloned().collect(),
range: TextRange::default(),
ctx: ExprContext::Load,
parenthesized: true,
})),
range: TextRange::default(),
ctx: ExprContext::Load,
@@ -148,6 +150,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
elts,
range: TextRange::default(),
ctx: ExprContext::Load,
parenthesized: true,
})),
range: TextRange::default(),
ctx: ExprContext::Load,

View File

@@ -130,6 +130,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
})),
ctx: ExprContext::Load,
range: TextRange::default(),
@@ -151,6 +152,7 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
elts: exprs.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
})),
ctx: ExprContext::Load,
range: TextRange::default(),

View File

@@ -337,6 +337,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
format!("({})", checker.generator().expr(&node)),
@@ -444,6 +445,7 @@ fn check_names(checker: &mut Checker, decorator: &Decorator, expr: &Expr) {
elts: elts.clone(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
});
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
format!("({})", checker.generator().expr(&node)),

View File

@@ -428,6 +428,7 @@ pub(crate) fn duplicate_isinstance_call(checker: &mut Checker, expr: &Expr) {
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
};
let node1 = ast::ExprName {
id: "isinstance".into(),
@@ -543,6 +544,7 @@ pub(crate) fn compare_with_tuple(checker: &mut Checker, expr: &Expr) {
elts: comparators.into_iter().map(Clone::clone).collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
};
let node1 = ast::ExprName {
id: id.into(),
@@ -718,7 +720,7 @@ fn get_short_circuit_edit(
generator.expr(expr)
};
Edit::range_replacement(
if matches!(expr, Expr::Tuple(ast::ExprTuple { elts, ctx: _, range: _}) if !elts.is_empty())
if matches!(expr, Expr::Tuple(ast::ExprTuple { elts, ctx: _, range: _, parenthesized: _}) if !elts.is_empty())
{
format!("({content})")
} else {

View File

@@ -382,6 +382,7 @@ fn return_stmt(id: &str, test: &Expr, target: &Expr, iter: &Expr, generator: Gen
range: TextRange::default(),
}],
range: TextRange::default(),
parenthesized: false,
};
let node1 = ast::ExprName {
id: id.into(),

View File

@@ -30,31 +30,4 @@ runtime_evaluated_decorators_3.py:6:18: TCH003 [*] Move standard library import
13 16 |
14 17 | @attrs.define(auto_attribs=True)
runtime_evaluated_decorators_3.py:7:29: TCH003 [*] Move standard library import `collections.abc.Sequence` into a type-checking block
|
5 | from dataclasses import dataclass
6 | from uuid import UUID # TCH003
7 | from collections.abc import Sequence
| ^^^^^^^^ TCH003
8 | from pydantic import validate_call
|
= help: Move into type-checking block
Unsafe fix
4 4 | from array import array
5 5 | from dataclasses import dataclass
6 6 | from uuid import UUID # TCH003
7 |-from collections.abc import Sequence
8 7 | from pydantic import validate_call
9 8 |
10 9 | import attrs
11 10 | from attrs import frozen
11 |+from typing import TYPE_CHECKING
12 |+
13 |+if TYPE_CHECKING:
14 |+ from collections.abc import Sequence
12 15 |
13 16 |
14 17 | @attrs.define(auto_attribs=True)

View File

@@ -38,6 +38,7 @@ mod tests {
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))]
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
#[test_case(Rule::MultipleStatementsOnOneLineColon, Path::new("E70.py"))]
@@ -69,6 +70,7 @@ mod tests {
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_2.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -213,11 +213,11 @@ pub(crate) fn extraneous_whitespace(line: &LogicalLine, context: &mut LogicalLin
diagnostic.range(),
)));
context.push_diagnostic(diagnostic);
} else if iter
.peek()
.is_some_and(|token| token.kind() == TokenKind::Rsqb)
{
} else if iter.peek().is_some_and(|token| {
matches!(token.kind(), TokenKind::Rsqb | TokenKind::Comma)
}) {
// Allow `foo[1 :]`, but not `foo[1 :]`.
// Or `foo[index :, 2]`, but not `foo[index :, 2]`.
if let (Whitespace::Many | Whitespace::Tab, offset) = whitespace
{
let mut diagnostic = Diagnostic::new(

View File

@@ -17,6 +17,9 @@ use crate::checkers::ast::Checker;
/// `sys.path.insert`, `sys.path.append`, and similar modifications between import
/// statements.
///
/// In [preview], this rule also allows `os.environ` modifications between import
/// statements.
///
/// ## Example
/// ```python
/// "One string"
@@ -37,6 +40,7 @@ use crate::checkers::ast::Checker;
/// ```
///
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
/// [preview]: https://docs.astral.sh/ruff/preview/
#[violation]
pub struct ModuleImportNotAtTopOfFile {
source_type: PySourceType,

View File

@@ -86,30 +86,33 @@ pub(crate) fn trailing_whitespace(
.sum();
if whitespace_len > TextSize::from(0) {
let range = TextRange::new(line.end() - whitespace_len, line.end());
// Removing trailing whitespace is not safe inside multiline strings.
let applicability = if indexer.multiline_ranges().contains_range(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
if range == line.range() {
if settings.rules.enabled(Rule::BlankLineWithWhitespace) {
let mut diagnostic = Diagnostic::new(BlankLineWithWhitespace, range);
// Remove any preceding continuations, to avoid introducing a potential
// syntax error.
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
indexer
.preceded_by_continuations(line.start(), locator)
.unwrap_or(range.start()),
range.end(),
))));
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_deletion(TextRange::new(
indexer
.preceded_by_continuations(line.start(), locator)
.unwrap_or(range.start()),
range.end(),
)),
applicability,
));
return Some(diagnostic);
}
} else if settings.rules.enabled(Rule::TrailingWhitespace) {
let mut diagnostic = Diagnostic::new(TrailingWhitespace, range);
diagnostic.set_fix(Fix::applicable_edit(
Edit::range_deletion(range),
// Removing trailing whitespace is not safe inside multiline strings.
if indexer.multiline_ranges().contains_range(range) {
Applicability::Unsafe
} else {
Applicability::Safe
},
applicability,
));
return Some(diagnostic);
}

View File

@@ -231,6 +231,8 @@ E20.py:149:10: E203 [*] Whitespace before ':'
148 | #: E203:1:10
149 | ham[upper :]
| ^^ E203
150 |
151 | #: Okay
|
= help: Remove whitespace before ':'
@@ -240,5 +242,21 @@ E20.py:149:10: E203 [*] Whitespace before ':'
148 148 | #: E203:1:10
149 |-ham[upper :]
149 |+ham[upper:]
150 150 |
151 151 | #: Okay
152 152 | ham[lower +1 :, "columnname"]
E20.py:155:14: E203 [*] Whitespace before ':'
|
154 | #: E203:1:13
155 | ham[lower + 1 :, "columnname"]
| ^^ E203
|
= help: Remove whitespace before ':'
Safe fix
152 152 | ham[lower +1 :, "columnname"]
153 153 |
154 154 | #: E203:1:13
155 |-ham[lower + 1 :, "columnname"]
155 |+ham[lower + 1:, "columnname"]

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402_2.py:7:1: E402 Module level import not at top of file
|
5 | del os.environ["WORLD_SIZE"]
6 |
7 | import torch
| ^^^^^^^^^^^^ E402
|

View File

@@ -11,7 +11,7 @@ W293.py:4:1: W293 [*] Blank line contains whitespace
|
= help: Remove whitespace from blank line
Safe fix
Unsafe fix
1 1 | # See: https://github.com/astral-sh/ruff/issues/9323
2 2 | class Chassis(RobotModuleTemplate):
3 3 | """底盘信息推送控制
@@ -48,6 +48,7 @@ W293.py:16:1: W293 [*] Blank line contains whitespace
15 | \
16 |
| ^^^^ W293
17 | '''blank line with whitespace
|
= help: Remove whitespace from blank line
@@ -59,5 +60,25 @@ W293.py:16:1: W293 [*] Blank line contains whitespace
15 |- \
16 |-
14 |+ "
17 15 | '''blank line with whitespace
18 16 |
19 17 | inside a multiline string'''
W293.py:18:1: W293 [*] Blank line contains whitespace
|
17 | '''blank line with whitespace
18 |
| ^ W293
19 | inside a multiline string'''
|
= help: Remove whitespace from blank line
Unsafe fix
15 15 | \
16 16 |
17 17 | '''blank line with whitespace
18 |-
18 |+
19 19 | inside a multiline string'''

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---

View File

@@ -171,6 +171,7 @@ mod tests {
#[test_case(Rule::PotentialIndexError, Path::new("potential_index_error.py"))]
#[test_case(Rule::SuperWithoutBrackets, Path::new("super_without_brackets.py"))]
#[test_case(Rule::TooManyNestedBlocks, Path::new("too_many_nested_blocks.py"))]
#[test_case(Rule::DictIterMissingItems, Path::new("dict_iter_missing_items.py"))]
#[test_case(
Rule::UnnecessaryDictIndexLookup,
Path::new("unnecessary_dict_index_lookup.py")

View File

@@ -0,0 +1,110 @@
use ruff_python_ast::{Expr, ExprTuple};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing::is_dict;
use ruff_python_semantic::{Binding, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for dictionary unpacking in a for loop without calling `.items()`.
///
/// ## Why is this bad?
/// When iterating over a dictionary in a for loop, if a dictionary is unpacked
/// without calling `.items()`, it could lead to a runtime error if the keys are not
/// a tuple of two elements.
///
/// It is likely that you're looking for an iteration over (key, value) pairs which
/// can only be achieved when calling `.items()`.
///
/// ## Example
/// ```python
/// data = {"Paris": 2_165_423, "New York City": 8_804_190, "Tokyo": 13_988_129}
///
/// for city, population in data:
/// print(f"{city} has population {population}.")
/// ```
///
/// Use instead:
/// ```python
/// data = {"Paris": 2_165_423, "New York City": 8_804_190, "Tokyo": 13_988_129}
///
/// for city, population in data.items():
/// print(f"{city} has population {population}.")
/// ```
#[violation]
pub struct DictIterMissingItems;
impl AlwaysFixableViolation for DictIterMissingItems {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unpacking a dictionary in iteration without calling `.items()`")
}
fn fix_title(&self) -> String {
format!("Add a call to `.items()`")
}
}
pub(crate) fn dict_iter_missing_items(checker: &mut Checker, target: &Expr, iter: &Expr) {
let Expr::Tuple(ExprTuple { elts, .. }) = target else {
return;
};
if elts.len() != 2 {
return;
};
let Some(name) = iter.as_name_expr() else {
return;
};
let Some(binding) = checker
.semantic()
.only_binding(name)
.map(|id| checker.semantic().binding(id))
else {
return;
};
if !is_dict(binding, checker.semantic()) {
return;
}
// If we can reliably determine that a dictionary has keys that are tuples of two we don't warn
if is_dict_key_tuple_with_two_elements(checker.semantic(), binding) {
return;
}
let mut diagnostic = Diagnostic::new(DictIterMissingItems, iter.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format!("{}.items()", name.id),
iter.range(),
)));
checker.diagnostics.push(diagnostic);
}
/// Returns true if the binding is a dictionary where each key is a tuple with two elements.
fn is_dict_key_tuple_with_two_elements(semantic: &SemanticModel, binding: &Binding) -> bool {
let Some(statement) = binding.statement(semantic) else {
return false;
};
let Some(assign_stmt) = statement.as_assign_stmt() else {
return false;
};
let Some(dict_expr) = assign_stmt.value.as_dict_expr() else {
return false;
};
dict_expr.keys.iter().all(|elt| {
elt.as_ref().is_some_and(|x| {
if let Some(tuple) = x.as_tuple_expr() {
return tuple.elts.len() == 2;
}
false
})
})
}

View File

@@ -13,6 +13,7 @@ pub(crate) use compare_to_empty_string::*;
pub(crate) use comparison_of_constant::*;
pub(crate) use comparison_with_itself::*;
pub(crate) use continue_in_finally::*;
pub(crate) use dict_iter_missing_items::*;
pub(crate) use duplicate_bases::*;
pub(crate) use empty_comment::*;
pub(crate) use eq_without_hash::*;
@@ -98,6 +99,7 @@ mod compare_to_empty_string;
mod comparison_of_constant;
mod comparison_with_itself;
mod continue_in_finally;
mod dict_iter_missing_items;
mod duplicate_bases;
mod empty_comment;
mod eq_without_hash;

View File

@@ -4,9 +4,10 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, DiagnosticKind, Edit,
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_trivia::indentation_at_offset;
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix;
/// ## What it does
/// Checks for the use of a classmethod being made without the decorator.
@@ -86,21 +87,21 @@ enum MethodType {
}
/// PLR0202
pub(crate) fn no_classmethod_decorator(checker: &mut Checker, class_def: &ast::StmtClassDef) {
get_undecorated_methods(checker, class_def, &MethodType::Classmethod);
pub(crate) fn no_classmethod_decorator(checker: &mut Checker, stmt: &Stmt) {
get_undecorated_methods(checker, stmt, &MethodType::Classmethod);
}
/// PLR0203
pub(crate) fn no_staticmethod_decorator(checker: &mut Checker, class_def: &ast::StmtClassDef) {
get_undecorated_methods(checker, class_def, &MethodType::Staticmethod);
pub(crate) fn no_staticmethod_decorator(checker: &mut Checker, stmt: &Stmt) {
get_undecorated_methods(checker, stmt, &MethodType::Staticmethod);
}
fn get_undecorated_methods(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
method_type: &MethodType,
) {
let mut explicit_decorator_calls: HashMap<String, TextRange> = HashMap::default();
fn get_undecorated_methods(checker: &mut Checker, class_stmt: &Stmt, method_type: &MethodType) {
let Stmt::ClassDef(class_def) = class_stmt else {
return;
};
let mut explicit_decorator_calls: HashMap<String, &Stmt> = HashMap::default();
let (method_name, diagnostic_type): (&str, DiagnosticKind) = match method_type {
MethodType::Classmethod => ("classmethod", NoClassmethodDecorator.into()),
@@ -131,7 +132,7 @@ fn get_undecorated_methods(
if let Expr::Name(ast::ExprName { id, .. }) = &arguments.args[0] {
if target_name == *id {
explicit_decorator_calls.insert(id.clone(), stmt.range());
explicit_decorator_calls.insert(id.clone(), stmt);
}
};
}
@@ -151,7 +152,7 @@ fn get_undecorated_methods(
..
}) = stmt
{
if !explicit_decorator_calls.contains_key(name.as_str()) {
let Some(decorator_call_statement) = explicit_decorator_calls.get(name.as_str()) else {
continue;
};
@@ -177,18 +178,16 @@ fn get_undecorated_methods(
match indentation {
Some(indentation) => {
let range = &explicit_decorator_calls[name.as_str()];
// SAFETY: Ruff only supports formatting files <= 4GB
#[allow(clippy::cast_possible_truncation)]
diagnostic.set_fix(Fix::safe_edits(
Edit::insertion(
format!("@{method_name}\n{indentation}"),
stmt.range().start(),
),
[Edit::deletion(
range.start() - TextSize::from(indentation.len() as u32),
range.end(),
[fix::edits::delete_stmt(
decorator_call_statement,
Some(class_stmt),
checker.locator(),
checker.indexer(),
)],
));
checker.diagnostics.push(diagnostic);

View File

@@ -305,6 +305,7 @@ fn assignment_targets_from_expr<'a>(
ctx: ExprContext::Store,
elts,
range: _,
parenthesized: _,
}) => Box::new(
elts.iter()
.flat_map(|elt| assignment_targets_from_expr(elt, dummy_variable_rgx)),

View File

@@ -9,7 +9,9 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::hashable::HashableExpr;
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::{self as ast, BoolOp, CmpOp, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -74,7 +76,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
if bool_op
.values
.iter()
.any(|value| !is_allowed_value(bool_op.op, value))
.any(|value| !is_allowed_value(bool_op.op, value, checker.semantic()))
{
return;
}
@@ -143,6 +145,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
elts: comparators.iter().copied().cloned().collect(),
range: TextRange::default(),
ctx: ExprContext::Load,
parenthesized: true,
})]),
range: bool_op.range(),
})),
@@ -157,7 +160,7 @@ pub(crate) fn repeated_equality_comparison(checker: &mut Checker, bool_op: &ast:
/// Return `true` if the given expression is compatible with a membership test.
/// E.g., `==` operators can be joined with `or` and `!=` operators can be
/// joined with `and`.
fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
fn is_allowed_value(bool_op: BoolOp, value: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Compare(ast::ExprCompare {
left,
ops,
@@ -196,6 +199,16 @@ fn is_allowed_value(bool_op: BoolOp, value: &Expr) -> bool {
return false;
}
// Ignore `sys.version_info` and `sys.platform` comparisons, which are only
// respected by type checkers when enforced via equality.
if any_over_expr(value, &|expr| {
semantic.resolve_call_path(expr).is_some_and(|call_path| {
matches!(call_path.as_slice(), ["sys", "version_info" | "platform"])
})
}) {
return false;
}
true
}

View File

@@ -0,0 +1,43 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
dict_iter_missing_items.py:13:13: PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
|
12 | # Errors
13 | for k, v in d:
| ^ PLE1141
14 | pass
|
= help: Add a call to `.items()`
Safe fix
10 10 | s2 = {1, 2, 3}
11 11 |
12 12 | # Errors
13 |-for k, v in d:
13 |+for k, v in d.items():
14 14 | pass
15 15 |
16 16 | for k, v in d_tuple_incorrect_tuple:
dict_iter_missing_items.py:16:13: PLE1141 [*] Unpacking a dictionary in iteration without calling `.items()`
|
14 | pass
15 |
16 | for k, v in d_tuple_incorrect_tuple:
| ^^^^^^^^^^^^^^^^^^^^^^^ PLE1141
17 | pass
|
= help: Add a call to `.items()`
Safe fix
13 13 | for k, v in d:
14 14 | pass
15 15 |
16 |-for k, v in d_tuple_incorrect_tuple:
16 |+for k, v in d_tuple_incorrect_tuple.items():
17 17 | pass
18 18 |
19 19 |

View File

@@ -23,9 +23,28 @@ no_method_decorator.py:9:5: PLR0202 [*] Class method defined without decorator
12 13 |
13 |- pick_colors = classmethod(pick_colors)
14 14 |
15 |+
15 16 | def pick_one_color(): # [no-staticmethod-decorator]
16 17 | """staticmethod to pick one fruit color"""
17 18 | return choice(Fruit.COLORS)
15 15 | def pick_one_color(): # [no-staticmethod-decorator]
16 16 | """staticmethod to pick one fruit color"""
no_method_decorator.py:22:5: PLR0202 [*] Class method defined without decorator
|
21 | class Class:
22 | def class_method(cls):
| PLR0202
23 | pass
|
= help: Add @classmethod decorator
Safe fix
19 19 | pick_one_color = staticmethod(pick_one_color)
20 20 |
21 21 | class Class:
22 |+ @classmethod
22 23 | def class_method(cls):
23 24 | pass
24 25 |
25 |- class_method = classmethod(class_method);another_statement
26 |+ another_statement
26 27 |
27 28 | def static_method():
28 29 | pass

View File

@@ -22,6 +22,27 @@ no_method_decorator.py:15:5: PLR0203 [*] Static method defined without decorator
17 18 | return choice(Fruit.COLORS)
18 19 |
19 |- pick_one_color = staticmethod(pick_one_color)
20 |+
20 20 |
21 21 | class Class:
22 22 | def class_method(cls):
no_method_decorator.py:27:5: PLR0203 [*] Static method defined without decorator
|
25 | class_method = classmethod(class_method);another_statement
26 |
27 | def static_method():
| PLR0203
28 | pass
|
= help: Add @staticmethod decorator
Safe fix
24 24 |
25 25 | class_method = classmethod(class_method);another_statement
26 26 |
27 |+ @staticmethod
27 28 | def static_method():
28 29 | pass
29 30 |
30 |- static_method = staticmethod(static_method);
31 |+

View File

@@ -3,7 +3,7 @@ use std::str::FromStr;
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef};
use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef, UnaryOp};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
@@ -198,15 +198,31 @@ pub(crate) fn native_literals(
checker.diagnostics.push(diagnostic);
}
Some(arg) => {
let Some(literal_expr) = arg.as_literal_expr() else {
let literal_expr = if let Some(literal_expr) = arg.as_literal_expr() {
// Skip implicit concatenated strings.
if literal_expr.is_implicit_concatenated() {
return;
}
literal_expr
} else if let Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::UAdd | UnaryOp::USub,
operand,
..
}) = arg
{
if let Some(literal_expr) = operand
.as_literal_expr()
.filter(|expr| matches!(expr, LiteralExpressionRef::NumberLiteral(_)))
{
literal_expr
} else {
// Only allow unary operators for numbers.
return;
}
} else {
return;
};
// Skip implicit string concatenations.
if literal_expr.is_implicit_concatenated() {
return;
}
let Ok(arg_literal_type) = LiteralType::try_from(literal_expr) else {
return;
};
@@ -221,14 +237,8 @@ pub(crate) fn native_literals(
// Ex) `(7).denominator` is valid but `7.denominator` is not
// Note that floats do not have this problem
// Ex) `(1.0).real` is valid and `1.0.real` is too
let content = match (parent_expr, arg) {
(
Some(Expr::Attribute(_)),
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(_),
..
}),
) => format!("({arg_code})"),
let content = match (parent_expr, literal_type) {
(Some(Expr::Attribute(_)), LiteralType::Int) => format!("({arg_code})"),
_ => arg_code.to_string(),
};

View File

@@ -127,6 +127,7 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
elts: remaining,
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
};
format!("({})", checker.generator().expr(&node.into()))
};

View File

@@ -141,6 +141,7 @@ fn tuple_diagnostic(checker: &mut Checker, tuple: &ast::ExprTuple, aliases: &[&E
elts: remaining,
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
};
format!("({})", checker.generator().expr(&node.into()))
};

View File

@@ -127,6 +127,7 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
range: TextRange::default(),
elts: constraints.into_iter().cloned().collect(),
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
None => None,

View File

@@ -3,7 +3,7 @@ source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP018.py:37:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
|
36 | # These become string or byte literals
36 | # These become literals
37 | str()
| ^^^^^ UP018
38 | str("foo")
@@ -14,7 +14,7 @@ UP018.py:37:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
Safe fix
34 34 | int().denominator
35 35 |
36 36 | # These become string or byte literals
36 36 | # These become literals
37 |-str()
37 |+""
38 38 | str("foo")
@@ -23,7 +23,7 @@ UP018.py:37:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
UP018.py:38:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
|
36 | # These become string or byte literals
36 | # These become literals
37 | str()
38 | str("foo")
| ^^^^^^^^^^ UP018
@@ -34,7 +34,7 @@ UP018.py:38:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
Safe fix
35 35 |
36 36 | # These become string or byte literals
36 36 | # These become literals
37 37 | str()
38 |-str("foo")
38 |+"foo"
@@ -55,7 +55,7 @@ UP018.py:39:1: UP018 [*] Unnecessary `str` call (rewrite as a literal)
= help: Replace with string literal
Safe fix
36 36 | # These become string or byte literals
36 36 | # These become literals
37 37 | str()
38 38 | str("foo")
39 |-str("""
@@ -304,6 +304,8 @@ UP018.py:55:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
54 | # These become a literal but retain parentheses
55 | int(1).denominator
| ^^^^^^ UP018
56 |
57 | # These too are literals in spirit
|
= help: Replace with integer literal
@@ -313,5 +315,82 @@ UP018.py:55:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
54 54 | # These become a literal but retain parentheses
55 |-int(1).denominator
55 |+(1).denominator
56 56 |
57 57 | # These too are literals in spirit
58 58 | int(+1)
UP018.py:58:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
57 | # These too are literals in spirit
58 | int(+1)
| ^^^^^^^ UP018
59 | int(-1)
60 | float(+1.0)
|
= help: Replace with integer literal
Safe fix
55 55 | int(1).denominator
56 56 |
57 57 | # These too are literals in spirit
58 |-int(+1)
58 |++1
59 59 | int(-1)
60 60 | float(+1.0)
61 61 | float(-1.0)
UP018.py:59:1: UP018 [*] Unnecessary `int` call (rewrite as a literal)
|
57 | # These too are literals in spirit
58 | int(+1)
59 | int(-1)
| ^^^^^^^ UP018
60 | float(+1.0)
61 | float(-1.0)
|
= help: Replace with integer literal
Safe fix
56 56 |
57 57 | # These too are literals in spirit
58 58 | int(+1)
59 |-int(-1)
59 |+-1
60 60 | float(+1.0)
61 61 | float(-1.0)
UP018.py:60:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
58 | int(+1)
59 | int(-1)
60 | float(+1.0)
| ^^^^^^^^^^^ UP018
61 | float(-1.0)
|
= help: Replace with float literal
Safe fix
57 57 | # These too are literals in spirit
58 58 | int(+1)
59 59 | int(-1)
60 |-float(+1.0)
60 |++1.0
61 61 | float(-1.0)
UP018.py:61:1: UP018 [*] Unnecessary `float` call (rewrite as a literal)
|
59 | int(-1)
60 | float(+1.0)
61 | float(-1.0)
| ^^^^^^^^^^^ UP018
|
= help: Replace with float literal
Safe fix
58 58 | int(+1)
59 59 | int(-1)
60 60 | float(+1.0)
61 |-float(-1.0)
61 |+-1.0

View File

@@ -347,6 +347,7 @@ fn make_suggestion(group: &AppendGroup, generator: Generator) -> String {
elts,
ctx: ast::ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
};
// Make `var.extend`.
// NOTE: receiver is the same for all appends and that's why we can take the first.

View File

@@ -162,6 +162,7 @@ fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> {
elts: new_elts,
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
}
.into(),
};

View File

@@ -70,6 +70,11 @@ pub(crate) fn missing_fstring_syntax(
}
}
// We also want to avoid expressions that are intended to be translated.
if semantic.current_expressions().any(is_gettext) {
return;
}
if should_be_fstring(literal, locator, semantic) {
let diagnostic = Diagnostic::new(MissingFStringSyntax, literal.range())
.with_fix(fix_fstring_syntax(literal.range()));
@@ -77,6 +82,26 @@ pub(crate) fn missing_fstring_syntax(
}
}
/// Returns `true` if an expression appears to be a `gettext` call.
///
/// We want to avoid statement expressions and assignments related to aliases
/// of the gettext API.
///
/// See <https://docs.python.org/3/library/gettext.html> for details. When one
/// uses `_` to mark a string for translation, the tools look for these markers
/// and replace the original string with its translated counterpart. If the
/// string contains variable placeholders or formatting, it can complicate the
/// translation process, lead to errors or incorrect translations.
fn is_gettext(expr: &ast::Expr) -> bool {
let ast::Expr::Call(ast::ExprCall { func, .. }) = expr else {
return false;
};
let ast::Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
return false;
};
matches!(id.as_str(), "_" | "gettext" | "ngettext")
}
/// Returns `true` if `literal` is likely an f-string with a missing `f` prefix.
/// See [`MissingFStringSyntax`] for the validation criteria.
fn should_be_fstring(

View File

@@ -116,6 +116,7 @@ pub(crate) fn never_union(checker: &mut Checker, expr: &Expr) {
elts,
ctx: _,
range: _,
parenthesized: _,
}) = slice.as_ref()
else {
return;
@@ -157,6 +158,7 @@ pub(crate) fn never_union(checker: &mut Checker, expr: &Expr) {
elts: rest,
ctx: ast::ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
})),
ctx: ast::ExprContext::Load,
range: TextRange::default(),

View File

@@ -134,23 +134,23 @@ impl InferredMemberType {
/// single-line tuple literals *can* be unparenthesized.
/// We keep the original AST node around for the
/// Tuple variant so that this can be queried later.
#[derive(Debug)]
pub(super) enum SequenceKind<'a> {
#[derive(Copy, Clone, Debug)]
pub(super) enum SequenceKind {
List,
Set,
Tuple(&'a ast::ExprTuple),
Tuple { parenthesized: bool },
}
impl SequenceKind<'_> {
impl SequenceKind {
// N.B. We only need the source code for the Tuple variant here,
// but if you already have a `Locator` instance handy,
// getting the source code is very cheap.
fn surrounding_brackets(&self, source: &str) -> (&'static str, &'static str) {
fn surrounding_brackets(self) -> (&'static str, &'static str) {
match self {
Self::List => ("[", "]"),
Self::Set => ("{", "}"),
Self::Tuple(ast_node) => {
if ast_node.is_parenthesized(source) {
Self::Tuple { parenthesized } => {
if parenthesized {
("(", ")")
} else {
("", "")
@@ -159,19 +159,19 @@ impl SequenceKind<'_> {
}
}
const fn opening_token_for_multiline_definition(&self) -> TokenKind {
const fn opening_token_for_multiline_definition(self) -> TokenKind {
match self {
Self::List => TokenKind::Lsqb,
Self::Set => TokenKind::Lbrace,
Self::Tuple(_) => TokenKind::Lpar,
Self::Tuple { .. } => TokenKind::Lpar,
}
}
const fn closing_token_for_multiline_definition(&self) -> TokenKind {
const fn closing_token_for_multiline_definition(self) -> TokenKind {
match self {
Self::List => TokenKind::Rsqb,
Self::Set => TokenKind::Rbrace,
Self::Tuple(_) => TokenKind::Rpar,
Self::Tuple { .. } => TokenKind::Rpar,
}
}
}
@@ -217,7 +217,7 @@ impl<'a> SequenceElements<'a> {
/// that can be inserted into the
/// source code as a `range_replacement` autofix.
pub(super) fn sort_single_line_elements_sequence(
kind: &SequenceKind,
kind: SequenceKind,
elts: &[ast::Expr],
elements: &[&str],
locator: &Locator,
@@ -225,7 +225,7 @@ pub(super) fn sort_single_line_elements_sequence(
) -> String {
let element_pairs = SequenceElements::new(elements, elts);
let last_item_index = element_pairs.last_item_index();
let (opening_paren, closing_paren) = kind.surrounding_brackets(locator.contents());
let (opening_paren, closing_paren) = kind.surrounding_brackets();
let mut result = String::from(opening_paren);
// We grab the original source-code ranges using `locator.slice()`
// rather than using the expression generator, as this approach allows
@@ -334,7 +334,7 @@ impl MultilineStringSequenceValue {
/// Return `None` if the analysis fails for whatever reason.
pub(super) fn from_source_range(
range: TextRange,
kind: &SequenceKind,
kind: SequenceKind,
locator: &Locator,
) -> Option<MultilineStringSequenceValue> {
// Parse the multiline string sequence using the raw tokens.
@@ -486,7 +486,7 @@ impl Ranged for MultilineStringSequenceValue {
/// in the original source code.
fn collect_string_sequence_lines(
range: TextRange,
kind: &SequenceKind,
kind: SequenceKind,
locator: &Locator,
) -> Option<(Vec<StringSequenceLine>, bool)> {
// These first two variables are used for keeping track of state

View File

@@ -152,9 +152,13 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr)
let (elts, range, kind) = match node {
ast::Expr::List(ast::ExprList { elts, range, .. }) => (elts, *range, SequenceKind::List),
ast::Expr::Tuple(tuple_node @ ast::ExprTuple { elts, range, .. }) => {
(elts, *range, SequenceKind::Tuple(tuple_node))
}
ast::Expr::Tuple(tuple_node @ ast::ExprTuple { elts, range, .. }) => (
elts,
*range,
SequenceKind::Tuple {
parenthesized: tuple_node.parenthesized,
},
),
_ => return,
};
@@ -166,7 +170,7 @@ fn sort_dunder_all(checker: &mut Checker, target: &ast::Expr, node: &ast::Expr)
let mut diagnostic = Diagnostic::new(UnsortedDunderAll, range);
if let SortClassification::UnsortedAndMaybeFixable { items } = elts_analysis {
if let Some(fix) = create_fix(range, elts, &items, &kind, checker) {
if let Some(fix) = create_fix(range, elts, &items, kind, checker) {
diagnostic.set_fix(fix);
}
}
@@ -187,7 +191,7 @@ fn create_fix(
range: TextRange,
elts: &[ast::Expr],
string_items: &[&str],
kind: &SequenceKind,
kind: SequenceKind,
checker: &Checker,
) -> Option<Fix> {
let locator = checker.locator();

View File

@@ -157,7 +157,9 @@ impl<'a> StringLiteralDisplay<'a> {
}
}
ast::Expr::Tuple(tuple_node @ ast::ExprTuple { elts, range, .. }) => {
let display_kind = DisplayKind::Sequence(SequenceKind::Tuple(tuple_node));
let display_kind = DisplayKind::Sequence(SequenceKind::Tuple {
parenthesized: tuple_node.parenthesized,
});
Self {
elts: Cow::Borrowed(elts),
range: *range,
@@ -211,7 +213,7 @@ impl<'a> StringLiteralDisplay<'a> {
(DisplayKind::Sequence(sequence_kind), true) => {
let analyzed_sequence = MultilineStringSequenceValue::from_source_range(
self.range(),
sequence_kind,
*sequence_kind,
locator,
)?;
assert_eq!(analyzed_sequence.len(), self.elts.len());
@@ -220,7 +222,7 @@ impl<'a> StringLiteralDisplay<'a> {
// Sorting multiline dicts is unsupported
(DisplayKind::Dict { .. }, true) => return None,
(DisplayKind::Sequence(sequence_kind), false) => sort_single_line_elements_sequence(
sequence_kind,
*sequence_kind,
&self.elts,
items,
locator,
@@ -242,7 +244,7 @@ impl<'a> StringLiteralDisplay<'a> {
/// Python provides for builtin containers.
#[derive(Debug)]
enum DisplayKind<'a> {
Sequence(SequenceKind<'a>),
Sequence(SequenceKind),
Dict { values: &'a [ast::Expr] },
}

View File

@@ -11,14 +11,21 @@ use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
/// ## What it does
/// Checks for uses of `list(...)[0]` that can be replaced with
/// `next(iter(...))`.
/// Checks the following constructs, all of which can be replaced by
/// `next(iter(...))`:
///
/// - `list(...)[0]`
/// - `tuple(...)[0]`
/// - `list(i for i in ...)[0]`
/// - `[i for i in ...][0]`
/// - `list(...).pop(0)`
///
/// ## Why is this bad?
/// Calling `list(...)` will create a new list of the entire collection, which
/// can be very expensive for large collections. If you only need the first
/// element of the collection, you can use `next(...)` or `next(iter(...)` to
/// lazily fetch the first element.
/// Calling e.g. `list(...)` will create a new list of the entire collection,
/// which can be very expensive for large collections. If you only need the
/// first element of the collection, you can use `next(...)` or
/// `next(iter(...)` to lazily fetch the first element. The same is true for
/// the other constructs.
///
/// ## Example
/// ```python
@@ -33,14 +40,16 @@ use crate::fix::snippet::SourceCodeSnippet;
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as migrating from `list(...)[0]` to
/// `next(iter(...))` can change the behavior of your program in two ways:
/// This rule's fix is marked as unsafe, as migrating from e.g. `list(...)[0]`
/// to `next(iter(...))` can change the behavior of your program in two ways:
///
/// 1. First, `list(...)` will eagerly evaluate the entire collection, while
/// `next(iter(...))` will only evaluate the first element. As such, any
/// side effects that occur during iteration will be delayed.
/// 2. Second, `list(...)[0]` will raise `IndexError` if the collection is
/// empty, while `next(iter(...))` will raise `StopIteration`.
/// 1. First, all above mentioned constructs will eagerly evaluate the entire
/// collection, while `next(iter(...))` will only evaluate the first
/// element. As such, any side effects that occur during iteration will be
/// delayed.
/// 2. Second, accessing members of a collection via square bracket notation
/// `[0]` of the `pop()` function will raise `IndexError` if the collection
/// is empty, while `next(iter(...))` will raise `StopIteration`.
///
/// ## References
/// - [Iterators and Iterables in Python: Run Efficient Iterations](https://realpython.com/python-iterators-iterables/#when-to-use-an-iterator-in-python)
@@ -67,18 +76,37 @@ impl AlwaysFixableViolation for UnnecessaryIterableAllocationForFirstElement {
/// RUF015
pub(crate) fn unnecessary_iterable_allocation_for_first_element(
checker: &mut Checker,
subscript: &ast::ExprSubscript,
expr: &ast::Expr,
) {
let ast::ExprSubscript {
value,
slice,
range,
..
} = subscript;
if !is_head_slice(slice) {
return;
}
let (value, range) = match expr {
ast::Expr::Subscript(ast::ExprSubscript {
value,
slice,
range,
..
}) => {
if !is_head_slice(slice) {
return;
}
(value, range)
}
ast::Expr::Call(ast::ExprCall {
func, arguments, ..
}) => {
let Some(arg) = arguments.args.first() else {
return;
};
if !is_head_slice(arg) {
return;
}
let ast::Expr::Attribute(ast::ExprAttribute { range, value, .. }) = func.as_ref()
else {
return;
};
(value, range)
}
_ => return,
};
let Some(target) = match_iteration_target(value, checker.semantic()) else {
return;

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/ruff/mod.rs
assertion_line: 58
---
RUF015.py:4:1: RUF015 [*] Prefer `next(iter(x))` over single element slice
|
@@ -383,7 +384,7 @@ RUF015.py:57:1: RUF015 [*] Prefer `next(zip(x, y))` over single element slice
57 |+next(zip(x, y))
58 58 | [*zip(x, y)][0]
59 59 |
60 60 |
60 60 | # RUF015 (pop)
RUF015.py:58:1: RUF015 [*] Prefer `next(zip(x, y))` over single element slice
|
@@ -391,6 +392,8 @@ RUF015.py:58:1: RUF015 [*] Prefer `next(zip(x, y))` over single element slice
57 | list(zip(x, y))[0]
58 | [*zip(x, y)][0]
| ^^^^^^^^^^^^^^^ RUF015
59 |
60 | # RUF015 (pop)
|
= help: Replace with `next(zip(x, y))`
@@ -401,23 +404,41 @@ RUF015.py:58:1: RUF015 [*] Prefer `next(zip(x, y))` over single element slice
58 |-[*zip(x, y)][0]
58 |+next(zip(x, y))
59 59 |
60 60 |
61 61 | def test():
60 60 | # RUF015 (pop)
61 61 | list(x).pop(0)
RUF015.py:63:5: RUF015 [*] Prefer `next(iter(zip(x, y)))` over single element slice
RUF015.py:61:1: RUF015 [*] Prefer `next(iter(x))` over single element slice
|
61 | def test():
62 | zip = list # Overwrite the builtin zip
63 | list(zip(x, y))[0]
60 | # RUF015 (pop)
61 | list(x).pop(0)
| ^^^^^^^^^^^ RUF015
62 |
63 | # OK
|
= help: Replace with `next(iter(x))`
Unsafe fix
58 58 | [*zip(x, y)][0]
59 59 |
60 60 | # RUF015 (pop)
61 |-list(x).pop(0)
61 |+next(iter(x))(0)
62 62 |
63 63 | # OK
64 64 | list(x).pop(1)
RUF015.py:68:5: RUF015 [*] Prefer `next(iter(zip(x, y)))` over single element slice
|
66 | def test():
67 | zip = list # Overwrite the builtin zip
68 | list(zip(x, y))[0]
| ^^^^^^^^^^^^^^^^^^ RUF015
|
= help: Replace with `next(iter(zip(x, y)))`
Unsafe fix
60 60 |
61 61 | def test():
62 62 | zip = list # Overwrite the builtin zip
63 |- list(zip(x, y))[0]
63 |+ next(iter(zip(x, y)))
65 65 |
66 66 | def test():
67 67 | zip = list # Overwrite the builtin zip
68 |- list(zip(x, y))[0]
68 |+ next(iter(zip(x, y)))

View File

@@ -977,6 +977,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
elt,
generators,
range: _,
parenthesized: _,
}) => Self::GeneratorExp(ExprGeneratorExp {
elt: elt.into(),
generators: generators.iter().map(Into::into).collect(),
@@ -1072,6 +1073,7 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
elts,
ctx: _,
range: _,
parenthesized: _,
}) => Self::Tuple(ExprTuple {
elts: elts.iter().map(Into::into).collect(),
}),

View File

@@ -183,6 +183,7 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
elt,
generators,
range: _,
parenthesized: _,
}) => {
any_over_expr(elt, func)
|| generators.iter().any(|generator| {
@@ -1423,6 +1424,7 @@ pub fn pep_604_union(elts: &[Expr]) -> Expr {
elts: vec![],
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
}),
[Expr::Tuple(ast::ExprTuple { elts, .. })] => pep_604_union(elts),
[elt] => elt.clone(),
@@ -1457,6 +1459,7 @@ pub fn typing_union(elts: &[Expr], binding: String) -> Expr {
elts: vec![],
ctx: ExprContext::Load,
range: TextRange::default(),
parenthesized: true,
}),
[Expr::Tuple(ast::ExprTuple { elts, .. })] => typing_union(elts, binding),
[elt] => elt.clone(),

View File

@@ -2430,6 +2430,7 @@ impl AstNode for ast::ExprGeneratorExp {
elt,
generators,
range: _,
parenthesized: _,
} = self;
visitor.visit_expr(elt);
for comprehension in generators {
@@ -3256,6 +3257,7 @@ impl AstNode for ast::ExprTuple {
elts,
ctx: _,
range: _,
parenthesized: _,
} = self;
for expr in elts {

View File

@@ -8,7 +8,6 @@ use std::slice::{Iter, IterMut};
use itertools::Itertools;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::{int, LiteralExpressionRef};
@@ -842,6 +841,7 @@ pub struct ExprGeneratorExp {
pub range: TextRange,
pub elt: Box<Expr>,
pub generators: Vec<Comprehension>,
pub parenthesized: bool,
}
impl From<ExprGeneratorExp> for Expr {
@@ -1796,6 +1796,9 @@ pub struct ExprTuple {
pub range: TextRange,
pub elts: Vec<Expr>,
pub ctx: ExprContext,
/// Whether the tuple is parenthesized in the source code.
pub parenthesized: bool,
}
impl From<ExprTuple> for Expr {
@@ -1804,37 +1807,6 @@ impl From<ExprTuple> for Expr {
}
}
impl ExprTuple {
/// Return `true` if a tuple is parenthesized in the source code.
pub fn is_parenthesized(&self, source: &str) -> bool {
let Some(elt) = self.elts.first() else {
return true;
};
// Count the number of open parentheses between the start of the tuple and the first element.
let open_parentheses_count =
SimpleTokenizer::new(source, TextRange::new(self.start(), elt.start()))
.skip_trivia()
.filter(|token| token.kind() == SimpleTokenKind::LParen)
.count();
if open_parentheses_count == 0 {
return false;
}
// Count the number of parentheses between the end of the first element and its trailing comma.
let close_parentheses_count =
SimpleTokenizer::new(source, TextRange::new(elt.end(), self.end()))
.skip_trivia()
.take_while(|token| token.kind() != SimpleTokenKind::Comma)
.filter(|token| token.kind() == SimpleTokenKind::RParen)
.count();
// If the number of open parentheses is greater than the number of close parentheses, the tuple
// is parenthesized.
open_parentheses_count > close_parentheses_count
}
}
/// See also [Slice](https://docs.python.org/3/library/ast.html#ast.Slice)
#[derive(Clone, Debug, PartialEq)]
pub struct ExprSlice {
@@ -3911,7 +3883,7 @@ mod tests {
assert_eq!(std::mem::size_of::<ExprDictComp>(), 48);
assert_eq!(std::mem::size_of::<ExprEllipsisLiteral>(), 8);
assert_eq!(std::mem::size_of::<ExprFString>(), 48);
assert_eq!(std::mem::size_of::<ExprGeneratorExp>(), 40);
assert_eq!(std::mem::size_of::<ExprGeneratorExp>(), 48);
assert_eq!(std::mem::size_of::<ExprIfExp>(), 32);
assert_eq!(std::mem::size_of::<ExprIpyEscapeCommand>(), 32);
assert_eq!(std::mem::size_of::<ExprLambda>(), 24);

View File

@@ -441,6 +441,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
elt,
generators,
range: _,
parenthesized: _,
}) => {
for comprehension in generators {
visitor.visit_comprehension(comprehension);
@@ -539,6 +540,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
elts,
ctx,
range: _,
parenthesized: _,
}) => {
for expr in elts {
visitor.visit_expr(expr);

View File

@@ -428,6 +428,7 @@ pub fn walk_expr<V: Transformer + ?Sized>(visitor: &V, expr: &mut Expr) {
elt,
generators,
range: _,
parenthesized: _,
}) => {
for comprehension in generators {
visitor.visit_comprehension(comprehension);
@@ -528,6 +529,7 @@ pub fn walk_expr<V: Transformer + ?Sized>(visitor: &V, expr: &mut Expr) {
elts,
ctx,
range: _,
parenthesized: _,
}) => {
for expr in elts {
visitor.visit_expr(expr);

View File

@@ -970,6 +970,7 @@ impl<'a> Generator<'a> {
Expr::GeneratorExp(ast::ExprGeneratorExp {
elt,
generators,
parenthesized: _,
range: _,
}) => {
self.p("(");
@@ -1037,6 +1038,7 @@ impl<'a> Generator<'a> {
elt,
generators,
range: _,
parenthesized: _,
})],
[],
) = (arguments.args.as_ref(), arguments.keywords.as_ref())

View File

@@ -12,7 +12,6 @@ use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use crate::comments::visitor::{CommentPlacement, DecoratedComment};
use crate::expression::expr_generator_exp::is_generator_parenthesized;
use crate::expression::expr_slice::{assign_comment_in_slice, ExprSliceCommentSection};
use crate::other::parameters::{
assign_argument_separator_comment_placement, find_parameter_separators,
@@ -315,12 +314,11 @@ fn handle_enclosed_comment<'a>(
| AnyNodeRef::ExprSet(_)
| AnyNodeRef::ExprListComp(_)
| AnyNodeRef::ExprSetComp(_) => handle_bracketed_end_of_line_comment(comment, locator),
AnyNodeRef::ExprTuple(tuple) if tuple.is_parenthesized(locator.contents()) => {
handle_bracketed_end_of_line_comment(comment, locator)
}
AnyNodeRef::ExprGeneratorExp(generator)
if is_generator_parenthesized(generator, locator.contents()) =>
{
AnyNodeRef::ExprTuple(ast::ExprTuple {
parenthesized: true,
..
}) => handle_bracketed_end_of_line_comment(comment, locator),
AnyNodeRef::ExprGeneratorExp(generator) if generator.parenthesized => {
handle_bracketed_end_of_line_comment(comment, locator)
}
_ => CommentPlacement::Default(comment),

View File

@@ -1,8 +1,6 @@
use ruff_formatter::{format_args, write, FormatRuleWithOptions};
use ruff_python_ast::AnyNodeRef;
use ruff_python_ast::ExprGeneratorExp;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange};
use crate::comments::SourceComment;
use crate::expression::parentheses::{parenthesized, NeedsParentheses, OptionalParentheses};
@@ -42,6 +40,7 @@ impl FormatNodeRule<ExprGeneratorExp> for FormatExprGeneratorExp {
range: _,
elt,
generators,
parenthesized: is_parenthesized,
} = item;
let joined = format_with(|f| {
@@ -55,7 +54,7 @@ impl FormatNodeRule<ExprGeneratorExp> for FormatExprGeneratorExp {
if self.parentheses == GeneratorExpParentheses::Preserve
&& dangling.is_empty()
&& !is_generator_parenthesized(item, f.context().source())
&& !is_parenthesized
{
write!(
f,
@@ -101,37 +100,3 @@ impl NeedsParentheses for ExprGeneratorExp {
}
}
}
/// Return `true` if a generator is parenthesized in the source code.
pub(crate) fn is_generator_parenthesized(generator: &ExprGeneratorExp, source: &str) -> bool {
// Count the number of open parentheses between the start of the generator and the first element.
let open_parentheses_count = SimpleTokenizer::new(
source,
TextRange::new(generator.start(), generator.elt.start()),
)
.skip_trivia()
.filter(|token| token.kind() == SimpleTokenKind::LParen)
.count();
if open_parentheses_count == 0 {
return false;
}
// Count the number of parentheses between the end of the generator and its trailing comma.
let close_parentheses_count = SimpleTokenizer::new(
source,
TextRange::new(
generator.elt.end(),
generator
.generators
.first()
.map_or(generator.end(), Ranged::start),
),
)
.skip_trivia()
.filter(|token| token.kind() == SimpleTokenKind::RParen)
.count();
// If the number of open parentheses is greater than the number of close parentheses, the
// generator is parenthesized.
open_parentheses_count > close_parentheses_count
}

View File

@@ -116,6 +116,7 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
elts,
ctx: _,
range: _,
parenthesized: is_parenthesized,
} = item;
let comments = f.context().comments().clone();
@@ -136,7 +137,7 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
return empty_parenthesized("(", dangling, ")").fmt(f);
}
[single] => match self.parentheses {
TupleParentheses::Preserve if !item.is_parenthesized(f.context().source()) => {
TupleParentheses::Preserve if !is_parenthesized => {
write!(f, [single.format(), token(",")])
}
_ =>
@@ -152,7 +153,7 @@ impl FormatNodeRule<ExprTuple> for FormatExprTuple {
//
// Unlike other expression parentheses, tuple parentheses are part of the range of the
// tuple itself.
_ if item.is_parenthesized(f.context().source())
_ if *is_parenthesized
&& !(self.parentheses == TupleParentheses::NeverPreserve
&& dangling.is_empty()) =>
{

View File

@@ -14,7 +14,6 @@ use ruff_text_size::Ranged;
use crate::builders::parenthesize_if_expands;
use crate::comments::{leading_comments, trailing_comments, LeadingDanglingTrailingComments};
use crate::context::{NodeLevel, WithNodeLevel};
use crate::expression::expr_generator_exp::is_generator_parenthesized;
use crate::expression::parentheses::{
is_expression_parenthesized, optional_parentheses, parenthesized, NeedsParentheses,
OptionalParentheses, Parentheses, Parenthesize,
@@ -661,15 +660,16 @@ impl<'input> CanOmitOptionalParenthesesVisitor<'input> {
return;
}
Expr::Tuple(tuple) if tuple.is_parenthesized(self.context.source()) => {
Expr::Tuple(ast::ExprTuple {
parenthesized: true,
..
}) => {
self.any_parenthesized_expressions = true;
// The values are always parenthesized, don't visit.
return;
}
Expr::GeneratorExp(generator)
if is_generator_parenthesized(generator, self.context.source()) =>
{
Expr::GeneratorExp(generator) if generator.parenthesized => {
self.any_parenthesized_expressions = true;
// The values are always parenthesized, don't visit.
return;
@@ -1035,11 +1035,7 @@ pub(crate) fn has_own_parentheses(
Some(OwnParentheses::NonEmpty)
}
Expr::GeneratorExp(generator)
if is_generator_parenthesized(generator, context.source()) =>
{
Some(OwnParentheses::NonEmpty)
}
Expr::GeneratorExp(generator) if generator.parenthesized => Some(OwnParentheses::NonEmpty),
// These expressions must contain _some_ child or trivia token in order to be non-empty.
Expr::List(ast::ExprList { elts, .. }) | Expr::Set(ast::ExprSet { elts, .. }) => {
@@ -1050,7 +1046,12 @@ pub(crate) fn has_own_parentheses(
}
}
Expr::Tuple(tuple) if tuple.is_parenthesized(context.source()) => {
Expr::Tuple(
tuple @ ast::ExprTuple {
parenthesized: true,
..
},
) => {
if !tuple.elts.is_empty() || context.comments().has_dangling(AnyNodeRef::from(expr)) {
Some(OwnParentheses::NonEmpty)
} else {

View File

@@ -59,16 +59,16 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
return result;
}
let quotes = normalizer.choose_quotes(&string, &locator);
let quote_selection = normalizer.choose_quotes(&string, &locator);
let context = FStringContext::new(
string.prefix(),
quotes,
quote_selection.quotes(),
FStringLayout::from_f_string(self.value, &locator),
);
// Starting prefix and quote
write!(f, [string.prefix(), quotes])?;
write!(f, [string.prefix(), quote_selection.quotes()])?;
f.join()
.entries(
@@ -80,7 +80,7 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
.finish()?;
// Ending quote
quotes.fmt(f)
quote_selection.quotes().fmt(f)
}
}

View File

@@ -59,9 +59,11 @@ impl Format<PyFormatContext<'_>> for FormatFStringLiteralElement<'_> {
let literal_content = f.context().locator().slice(self.element.range());
let normalized = normalize_string(
literal_content,
0,
self.context.quotes(),
self.context.prefix(),
is_hex_codes_in_unicode_sequences_enabled(f.context()),
true,
);
match &normalized {
Cow::Borrowed(_) => source_text_slice(self.element.range()).fmt(f),

View File

@@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::iter::FusedIterator;
use ruff_formatter::FormatContext;
use ruff_source_file::Locator;
@@ -7,7 +8,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::context::FStringState;
use crate::options::PythonVersion;
use crate::prelude::*;
use crate::preview::is_hex_codes_in_unicode_sequences_enabled;
use crate::preview::{is_f_string_formatting_enabled, is_hex_codes_in_unicode_sequences_enabled};
use crate::string::{QuoteChar, Quoting, StringPart, StringPrefix, StringQuotes};
use crate::QuoteStyle;
@@ -18,6 +19,7 @@ pub(crate) struct StringNormalizer {
f_string_state: FStringState,
target_version: PythonVersion,
normalize_hex: bool,
format_fstring: bool,
}
impl StringNormalizer {
@@ -29,6 +31,7 @@ impl StringNormalizer {
f_string_state: context.f_string_state(),
target_version: context.options().target_version(),
normalize_hex: is_hex_codes_in_unicode_sequences_enabled(context),
format_fstring: is_f_string_formatting_enabled(context),
}
}
@@ -42,68 +45,8 @@ impl StringNormalizer {
self
}
/// Computes the strings preferred quotes.
pub(crate) fn choose_quotes(&self, string: &StringPart, locator: &Locator) -> StringQuotes {
// Per PEP 8, always prefer double quotes for triple-quoted strings.
// Except when using quote-style-preserve.
let preferred_style = if string.quotes().triple {
// ... unless we're formatting a code snippet inside a docstring,
// then we specifically want to invert our quote style to avoid
// writing out invalid Python.
//
// It's worth pointing out that we can actually wind up being
// somewhat out of sync with PEP8 in this case. Consider this
// example:
//
// def foo():
// '''
// Something.
//
// >>> """tricksy"""
// '''
// pass
//
// Ideally, this would be reformatted as:
//
// def foo():
// """
// Something.
//
// >>> '''tricksy'''
// """
// pass
//
// But the logic here results in the original quoting being
// preserved. This is because the quoting style of the outer
// docstring is determined, in part, by looking at its contents. In
// this case, it notices that it contains a `"""` and thus infers
// that using `'''` would overall read better because it avoids
// the need to escape the interior `"""`. Except... in this case,
// the `"""` is actually part of a code snippet that could get
// reformatted to using a different quoting style itself.
//
// Fixing this would, I believe, require some fairly seismic
// changes to how formatting strings works. Namely, we would need
// to look for code snippets before normalizing the docstring, and
// then figure out the quoting style more holistically by looking
// at the various kinds of quotes used in the code snippets and
// what reformatting them might look like.
//
// Overall this is a bit of a corner case and just inverting the
// style from what the parent ultimately decided upon works, even
// if it doesn't have perfect alignment with PEP8.
if let Some(quote) = self.parent_docstring_quote_char {
QuoteStyle::from(quote.invert())
} else if self.preferred_quote_style.is_preserve() {
QuoteStyle::Preserve
} else {
QuoteStyle::Double
}
} else {
self.preferred_quote_style
};
let quoting = if let FStringState::InsideExpressionElement(context) = self.f_string_state {
fn quoting(&self, string: &StringPart) -> Quoting {
if let FStringState::InsideExpressionElement(context) = self.f_string_state {
// If we're inside an f-string, we need to make sure to preserve the
// existing quotes unless we're inside a triple-quoted f-string and
// the inner string itself isn't triple-quoted. For example:
@@ -127,22 +70,110 @@ impl StringNormalizer {
}
} else {
self.quoting
};
}
}
match quoting {
/// Computes the strings preferred quotes.
pub(crate) fn choose_quotes(&self, string: &StringPart, locator: &Locator) -> QuoteSelection {
let raw_content = locator.slice(string.content_range());
let first_quote_or_normalized_char_offset = raw_content
.bytes()
.position(|b| matches!(b, b'\\' | b'"' | b'\'' | b'\r' | b'{'));
let quotes = match self.quoting(string) {
Quoting::Preserve => string.quotes(),
Quoting::CanChange => {
if let Some(preferred_quote) = QuoteChar::from_style(preferred_style) {
let raw_content = locator.slice(string.content_range());
if string.prefix().is_raw_string() {
choose_quotes_for_raw_string(raw_content, string.quotes(), preferred_quote)
// Per PEP 8, always prefer double quotes for triple-quoted strings.
// Except when using quote-style-preserve.
let preferred_style = if string.quotes().triple {
// ... unless we're formatting a code snippet inside a docstring,
// then we specifically want to invert our quote style to avoid
// writing out invalid Python.
//
// It's worth pointing out that we can actually wind up being
// somewhat out of sync with PEP8 in this case. Consider this
// example:
//
// def foo():
// '''
// Something.
//
// >>> """tricksy"""
// '''
// pass
//
// Ideally, this would be reformatted as:
//
// def foo():
// """
// Something.
//
// >>> '''tricksy'''
// """
// pass
//
// But the logic here results in the original quoting being
// preserved. This is because the quoting style of the outer
// docstring is determined, in part, by looking at its contents. In
// this case, it notices that it contains a `"""` and thus infers
// that using `'''` would overall read better because it avoids
// the need to escape the interior `"""`. Except... in this case,
// the `"""` is actually part of a code snippet that could get
// reformatted to using a different quoting style itself.
//
// Fixing this would, I believe, require some fairly seismic
// changes to how formatting strings works. Namely, we would need
// to look for code snippets before normalizing the docstring, and
// then figure out the quoting style more holistically by looking
// at the various kinds of quotes used in the code snippets and
// what reformatting them might look like.
//
// Overall this is a bit of a corner case and just inverting the
// style from what the parent ultimately decided upon works, even
// if it doesn't have perfect alignment with PEP8.
if let Some(quote) = self.parent_docstring_quote_char {
QuoteStyle::from(quote.invert())
} else if self.preferred_quote_style.is_preserve() {
QuoteStyle::Preserve
} else {
choose_quotes_impl(raw_content, string.quotes(), preferred_quote)
QuoteStyle::Double
}
} else {
self.preferred_quote_style
};
if let Some(preferred_quote) = QuoteChar::from_style(preferred_style) {
if let Some(first_quote_or_normalized_char_offset) =
first_quote_or_normalized_char_offset
{
if string.prefix().is_raw_string() {
choose_quotes_for_raw_string(
&raw_content[first_quote_or_normalized_char_offset..],
string.quotes(),
preferred_quote,
)
} else {
choose_quotes_impl(
&raw_content[first_quote_or_normalized_char_offset..],
string.quotes(),
preferred_quote,
)
}
} else {
StringQuotes {
quote_char: preferred_quote,
triple: string.quotes().is_triple(),
}
}
} else {
string.quotes()
}
}
};
QuoteSelection {
quotes,
first_quote_or_normalized_char_offset,
}
}
@@ -154,19 +185,48 @@ impl StringNormalizer {
) -> NormalizedString<'a> {
let raw_content = locator.slice(string.content_range());
let quotes = self.choose_quotes(string, locator);
let quote_selection = self.choose_quotes(string, locator);
let normalized = normalize_string(raw_content, quotes, string.prefix(), self.normalize_hex);
let normalized = if let Some(first_quote_or_escape_offset) =
quote_selection.first_quote_or_normalized_char_offset
{
normalize_string(
raw_content,
first_quote_or_escape_offset,
quote_selection.quotes,
string.prefix(),
self.normalize_hex,
// TODO: Remove the `b'{'` in `choose_quotes` when promoting the
// `format_fstring` preview style
self.format_fstring,
)
} else {
Cow::Borrowed(raw_content)
};
NormalizedString {
prefix: string.prefix(),
content_range: string.content_range(),
text: normalized,
quotes,
quotes: quote_selection.quotes,
}
}
}
#[derive(Debug)]
pub(crate) struct QuoteSelection {
quotes: StringQuotes,
/// Offset to the first quote character or character that needs special handling in [`normalize_string`].
first_quote_or_normalized_char_offset: Option<usize>,
}
impl QuoteSelection {
pub(crate) fn quotes(&self) -> StringQuotes {
self.quotes
}
}
#[derive(Debug)]
pub(crate) struct NormalizedString<'a> {
prefix: crate::string::StringPrefix,
@@ -391,9 +451,11 @@ fn choose_quotes_impl(
/// Returns the normalized string and whether it contains new lines.
pub(crate) fn normalize_string(
input: &str,
start_offset: usize,
quotes: StringQuotes,
prefix: StringPrefix,
normalize_hex: bool,
format_fstring: bool,
) -> Cow<str> {
// The normalized string if `input` is not yet normalized.
// `output` must remain empty if `input` is already normalized.
@@ -406,10 +468,10 @@ pub(crate) fn normalize_string(
let preferred_quote = quote.as_char();
let opposite_quote = quote.invert().as_char();
let mut chars = input.char_indices().peekable();
let mut chars = CharIndicesWithOffset::new(input, start_offset).peekable();
let is_raw = prefix.is_raw_string();
let is_fstring = prefix.is_fstring();
let is_fstring = !format_fstring && prefix.is_fstring();
let mut formatted_value_nesting = 0u32;
while let Some((index, c)) = chars.next() {
@@ -445,13 +507,11 @@ pub(crate) fn normalize_string(
// Skip over escaped backslashes
chars.next();
} else if normalize_hex {
// Length of the `\` plus the length of the escape sequence character (`u` | `U` | `x`)
let escape_start_len = '\\'.len_utf8() + next.len_utf8();
if let Some(normalised) = UnicodeEscape::new(next, !prefix.is_byte())
.and_then(|escape| {
escape.normalize(&input[index + c.len_utf8() + next.len_utf8()..])
})
.and_then(|escape| escape.normalize(&input[index + escape_start_len..]))
{
// Length of the `\` plus the length of the escape sequence character (`u` | `U` | `x`)
let escape_start_len = '\\'.len_utf8() + next.len_utf8();
let escape_start_offset = index + escape_start_len;
if let Cow::Owned(normalised) = &normalised {
output.push_str(&input[last_index..escape_start_offset]);
@@ -501,6 +561,35 @@ pub(crate) fn normalize_string(
normalized
}
#[derive(Clone, Debug)]
struct CharIndicesWithOffset<'str> {
chars: std::str::Chars<'str>,
next_offset: usize,
}
impl<'str> CharIndicesWithOffset<'str> {
fn new(input: &'str str, start_offset: usize) -> Self {
Self {
chars: input[start_offset..].chars(),
next_offset: start_offset,
}
}
}
impl<'str> Iterator for CharIndicesWithOffset<'str> {
type Item = (usize, char);
fn next(&mut self) -> Option<Self::Item> {
self.chars.next().map(|c| {
let index = self.next_offset;
self.next_offset += c.len_utf8();
(index, c)
})
}
}
impl FusedIterator for CharIndicesWithOffset<'_> {}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum UnicodeEscape {
/// A hex escape sequence of either 2 (`\x`), 4 (`\u`) or 8 (`\U`) hex characters.
@@ -642,12 +731,14 @@ mod tests {
let normalized = normalize_string(
input,
0,
StringQuotes {
triple: false,
quote_char: QuoteChar::Double,
},
StringPrefix::BYTE,
true,
true,
);
assert_eq!(r"\x89\x50\x4e\x47\x0d\x0a\x1a\x0a", &normalized);

View File

@@ -3,10 +3,16 @@ use ruff_python_ast::{self as ast, Expr, ExprContext};
pub(crate) fn set_context(expr: Expr, ctx: ExprContext) -> Expr {
match expr {
Expr::Name(ast::ExprName { id, range, .. }) => ast::ExprName { range, id, ctx }.into(),
Expr::Tuple(ast::ExprTuple { elts, range, .. }) => ast::ExprTuple {
Expr::Tuple(ast::ExprTuple {
elts,
range,
parenthesized: is_parenthesized,
ctx: _,
}) => ast::ExprTuple {
elts: elts.into_iter().map(|elt| set_context(elt, ctx)).collect(),
range,
ctx,
parenthesized: is_parenthesized,
}
.into(),

View File

@@ -1505,4 +1505,20 @@ u"foo" f"bar {baz} really" u"bar" "no"
let parse_ast = parse_suite(r#"x = "\N{BACKSPACE}another cool trick""#).unwrap();
insta::assert_debug_snapshot!(parse_ast);
}
#[test]
fn test_tuple() {
let parse_ast = parse_suite(
r#"
a,b
(a,b)
()
(a,)
((a,b))
"#
.trim(),
)
.unwrap();
insta::assert_debug_snapshot!(parse_ast);
}
}

View File

@@ -483,7 +483,8 @@ MatchStatement: ast::Stmt = {
ast::ExprTuple {
elts: vec![subject.into()],
ctx: ast::ExprContext::Load,
range: (tuple_location..tuple_end_location).into()
range: (tuple_location..tuple_end_location).into(),
parenthesized: false
},
)),
cases,
@@ -506,7 +507,8 @@ MatchStatement: ast::Stmt = {
ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (tuple_location..tuple_end_location).into()
range: (tuple_location..tuple_end_location).into(),
parenthesized: false
},
)),
cases,
@@ -1573,6 +1575,7 @@ SubscriptList: crate::parser::ParenthesizedExpr = {
elts: vec![s1.into()],
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: false
}.into()
},
<location:@L> <elts:TwoOrMoreSep<Subscript, ",">> ","? <end_location:@R> => {
@@ -1581,6 +1584,7 @@ SubscriptList: crate::parser::ParenthesizedExpr = {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: false
}.into()
}
};
@@ -1726,7 +1730,12 @@ Atom<Goal>: crate::parser::ParenthesizedExpr = {
}
} else {
let elts = elts.into_iter().map(ast::Expr::from).collect();
ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into()
ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into()
}
},
<location:@L> "(" <left:(<OneOrMore<Test<"all">>> ",")?> <mid:NamedOrStarExpr> <right:("," <TestOrStarNamedExpr>)*> <trailing_comma:","?> ")" <end_location:@R> =>? {
@@ -1743,13 +1752,19 @@ Atom<Goal>: crate::parser::ParenthesizedExpr = {
})
} else {
let elts = left.into_iter().flatten().chain([mid]).chain(right).map(ast::Expr::from).collect();
Ok(ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into())
Ok(ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into())
}
},
<location:@L> "(" ")" <end_location:@R> => ast::ExprTuple {
elts: Vec::new(),
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into(),
<location:@L> "(" <e:YieldExpr> ")" <end_location:@R> => crate::parser::ParenthesizedExpr {
expr: e.into(),
@@ -1759,6 +1774,7 @@ Atom<Goal>: crate::parser::ParenthesizedExpr = {
elt: Box::new(elt.into()),
generators,
range: (location..end_location).into(),
parenthesized: true
}.into(),
"(" <location:@L> "**" <e:Expression<"all">> ")" <end_location:@R> =>? {
Err(LexicalError::new(
@@ -1852,7 +1868,12 @@ GenericList<Element>: crate::parser::ParenthesizedExpr = {
}
} else {
let elts = elts.into_iter().map(ast::Expr::from).collect();
ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into()
ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: false
}.into()
}
}
}
@@ -1904,7 +1925,8 @@ FunctionArgument: (Option<(TextSize, TextSize, Option<ast::Identifier>)>, ast::E
ast::ExprGeneratorExp {
elt: Box::new(elt.into()),
generators,
range: (location..end_location).into()
range: (location..end_location).into(),
parenthesized: false
}
),
None => elt.into(),

View File

@@ -1,5 +1,5 @@
// auto-generated: "lalrpop 0.20.0"
// sha3: 8c85e4bbac54760ed8be03b56a428d76e14d18e6dbde62b424d0b2b5e8e65dbe
// sha3: d64ca7ff27121baee9d7a1b4d0f341932391a365fe75f115987b05bf2aaf538e
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_python_ast::{self as ast, Int, IpyEscapeKind};
use crate::{
@@ -33938,7 +33938,8 @@ fn __action86<
ast::ExprTuple {
elts: vec![subject.into()],
ctx: ast::ExprContext::Load,
range: (tuple_location..tuple_end_location).into()
range: (tuple_location..tuple_end_location).into(),
parenthesized: false
},
)),
cases,
@@ -33982,7 +33983,8 @@ fn __action87<
ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (tuple_location..tuple_end_location).into()
range: (tuple_location..tuple_end_location).into(),
parenthesized: false
},
)),
cases,
@@ -36227,6 +36229,7 @@ fn __action208<
elts: vec![s1.into()],
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: false
}.into()
}
}
@@ -36249,6 +36252,7 @@ fn __action209<
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: false
}.into()
}
}
@@ -36796,7 +36800,8 @@ fn __action242<
ast::ExprGeneratorExp {
elt: Box::new(elt.into()),
generators,
range: (location..end_location).into()
range: (location..end_location).into(),
parenthesized: false
}
),
None => elt.into(),
@@ -37049,7 +37054,12 @@ fn __action259<
}
} else {
let elts = elts.into_iter().map(ast::Expr::from).collect();
ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into()
ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: false
}.into()
}
}
}
@@ -37103,7 +37113,12 @@ fn __action262<
}
} else {
let elts = elts.into_iter().map(ast::Expr::from).collect();
ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into()
ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: false
}.into()
}
}
}
@@ -41292,7 +41307,12 @@ fn __action553<
}
} else {
let elts = elts.into_iter().map(ast::Expr::from).collect();
ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into()
ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into()
}
}
}
@@ -41327,7 +41347,12 @@ fn __action554<
})
} else {
let elts = left.into_iter().flatten().chain([mid]).chain(right).map(ast::Expr::from).collect();
Ok(ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into())
Ok(ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into())
}
}
}
@@ -41348,6 +41373,7 @@ fn __action555<
elts: Vec::new(),
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into()
}
@@ -41388,6 +41414,7 @@ fn __action557<
elt: Box::new(elt.into()),
generators,
range: (location..end_location).into(),
parenthesized: true
}.into()
}
@@ -42025,7 +42052,12 @@ fn __action596<
})
} else {
let elts = left.into_iter().flatten().chain([mid]).chain(right).map(ast::Expr::from).collect();
Ok(ast::ExprTuple { elts, ctx: ast::ExprContext::Load, range: (location..end_location).into() }.into())
Ok(ast::ExprTuple {
elts,
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into())
}
}
}
@@ -42046,6 +42078,7 @@ fn __action597<
elts: Vec::new(),
ctx: ast::ExprContext::Load,
range: (location..end_location).into(),
parenthesized: true
}.into()
}
@@ -42086,6 +42119,7 @@ fn __action599<
elt: Box::new(elt.into()),
generators,
range: (location..end_location).into(),
parenthesized: true
}.into()
}

View File

@@ -55,6 +55,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
},

View File

@@ -44,6 +44,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
body: [

View File

@@ -60,6 +60,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
},

View File

@@ -65,6 +65,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
ifs: [],

View File

@@ -45,6 +45,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
},

View File

@@ -65,6 +65,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
ifs: [],

View File

@@ -33,6 +33,7 @@ expression: parse_ast
),
],
ctx: Store,
parenthesized: true,
},
),
],
@@ -66,6 +67,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
},

View File

@@ -58,6 +58,7 @@ expression: parse_ast
),
],
ctx: Load,
parenthesized: true,
},
),
},

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