Compare commits
52 Commits
implicit-s
...
rc-extensi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f53feee00 | ||
|
|
b793611fa6 | ||
|
|
be61cb3a68 | ||
|
|
8dc22d5793 | ||
|
|
15b87ea8be | ||
|
|
c25f1cd12a | ||
|
|
f5904a20d5 | ||
|
|
77c5561646 | ||
|
|
ab4bd71755 | ||
|
|
5abf662365 | ||
|
|
14fa1c5b52 | ||
|
|
0421c41ff7 | ||
|
|
ad4695d3eb | ||
|
|
8c58ebee37 | ||
|
|
5023874355 | ||
|
|
5554510597 | ||
|
|
1341e064a7 | ||
|
|
bd98d6884b | ||
|
|
761d4d42f1 | ||
|
|
fc8738f52a | ||
|
|
1711bca4a0 | ||
|
|
51ce88bb23 | ||
|
|
36d8b03b5f | ||
|
|
a284c711bf | ||
|
|
8c20f14e62 | ||
|
|
946028e358 | ||
|
|
6fe15e7289 | ||
|
|
7d9ce5049a | ||
|
|
ecd5a7035d | ||
|
|
175c266de3 | ||
|
|
4997c681f1 | ||
|
|
7eafba2a4d | ||
|
|
0f70c99c42 | ||
|
|
ee4efdba96 | ||
|
|
0d363ab239 | ||
|
|
68b8abf9c6 | ||
|
|
1c8851e5fb | ||
|
|
4ac19993cf | ||
|
|
5cd3c6ef07 | ||
|
|
e94a2615a8 | ||
|
|
77f577cba7 | ||
|
|
49a46c2880 | ||
|
|
67e17e2750 | ||
|
|
e1928be36e | ||
|
|
235cfb7976 | ||
|
|
91ae81b565 | ||
|
|
d46c5d8ac8 | ||
|
|
20217e9bbd | ||
|
|
72bf1c2880 | ||
|
|
c47ff658e4 | ||
|
|
c3bba54b6b | ||
|
|
fe79798c12 |
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/ci.yaml
vendored
11
.github/workflows/ci.yaml
vendored
@@ -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:
|
||||
|
||||
74
.github/workflows/release.yaml
vendored
74
.github/workflows/release.yaml
vendored
@@ -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:
|
||||
|
||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -1,5 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.2
|
||||
|
||||
Highlights include:
|
||||
|
||||
- Initial support formatting f-strings (in `--preview`).
|
||||
- Support for overriding arbitrary configuration options via the CLI through an expanded `--config`
|
||||
argument (e.g., `--config "lint.isort.combine-as-imports=false"`).
|
||||
- Significant performance improvements in Ruff's lexer, parser, and lint rules.
|
||||
|
||||
### Preview features
|
||||
|
||||
- Implement minimal f-string formatting ([#9642](https://github.com/astral-sh/ruff/pull/9642))
|
||||
- \[`pycodestyle`\] Add blank line(s) rules (`E301`, `E302`, `E303`, `E304`, `E305`, `E306`) ([#9266](https://github.com/astral-sh/ruff/pull/9266))
|
||||
- \[`refurb`\] Implement `readlines_in_for` (`FURB129`) ([#9880](https://github.com/astral-sh/ruff/pull/9880))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`ruff`\] Ensure closing parentheses for multiline sequences are always on their own line (`RUF022`, `RUF023`) ([#9793](https://github.com/astral-sh/ruff/pull/9793))
|
||||
- \[`numpy`\] Add missing deprecation violations (`NPY002`) ([#9862](https://github.com/astral-sh/ruff/pull/9862))
|
||||
- \[`flake8-bandit`\] Detect `mark_safe` usages in decorators ([#9887](https://github.com/astral-sh/ruff/pull/9887))
|
||||
- \[`ruff`\] Expand `asyncio-dangling-task` (`RUF006`) to include `new_event_loop` ([#9976](https://github.com/astral-sh/ruff/pull/9976))
|
||||
- \[`flake8-pyi`\] Ignore 'unused' private type dicts in class scopes ([#9952](https://github.com/astral-sh/ruff/pull/9952))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Docstring formatting: Preserve tab indentation when using `indent-style=tabs` ([#9915](https://github.com/astral-sh/ruff/pull/9915))
|
||||
- Disable top-level docstring formatting for notebooks ([#9957](https://github.com/astral-sh/ruff/pull/9957))
|
||||
- Stabilize quote-style's `preserve` mode ([#9922](https://github.com/astral-sh/ruff/pull/9922))
|
||||
|
||||
### CLI
|
||||
|
||||
- Allow arbitrary configuration options to be overridden via the CLI ([#9599](https://github.com/astral-sh/ruff/pull/9599))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Make `show-settings` filters directory-agnostic ([#9866](https://github.com/astral-sh/ruff/pull/9866))
|
||||
- Respect duplicates when rewriting type aliases ([#9905](https://github.com/astral-sh/ruff/pull/9905))
|
||||
- Respect tuple assignments in typing analyzer ([#9969](https://github.com/astral-sh/ruff/pull/9969))
|
||||
- Use atomic write when persisting cache ([#9981](https://github.com/astral-sh/ruff/pull/9981))
|
||||
- Use non-parenthesized range for `DebugText` ([#9953](https://github.com/astral-sh/ruff/pull/9953))
|
||||
- \[`flake8-simplify`\] Avoid false positive with `async` for loops (`SIM113`) ([#9996](https://github.com/astral-sh/ruff/pull/9996))
|
||||
- \[`flake8-trio`\] Respect `async with` in `timeout-without-await` ([#9859](https://github.com/astral-sh/ruff/pull/9859))
|
||||
- \[`perflint`\] Catch a wider range of mutations in `PERF101` ([#9955](https://github.com/astral-sh/ruff/pull/9955))
|
||||
- \[`pycodestyle`\] Fix `E30X` panics on blank lines with trailing white spaces ([#9907](https://github.com/astral-sh/ruff/pull/9907))
|
||||
- \[`pydocstyle`\] Allow using `parameters` as a subsection header (`D405`) ([#9894](https://github.com/astral-sh/ruff/pull/9894))
|
||||
- \[`pydocstyle`\] Fix blank-line docstring rules for module-level docstrings ([#9878](https://github.com/astral-sh/ruff/pull/9878))
|
||||
- \[`pylint`\] Accept 0.0 and 1.0 as common magic values (`PLR2004`) ([#9964](https://github.com/astral-sh/ruff/pull/9964))
|
||||
- \[`pylint`\] Avoid suggesting set rewrites for non-hashable types ([#9956](https://github.com/astral-sh/ruff/pull/9956))
|
||||
- \[`ruff`\] Avoid false negatives with string literals inside of method calls (`RUF027`) ([#9865](https://github.com/astral-sh/ruff/pull/9865))
|
||||
- \[`ruff`\] Fix panic on with f-string detection (`RUF027`) ([#9990](https://github.com/astral-sh/ruff/pull/9990))
|
||||
- \[`ruff`\] Ignore builtins when detecting missing f-strings ([#9849](https://github.com/astral-sh/ruff/pull/9849))
|
||||
|
||||
### Performance
|
||||
|
||||
- Use `memchr` for string lexing ([#9888](https://github.com/astral-sh/ruff/pull/9888))
|
||||
- Use `memchr` for tab-indentation detection ([#9853](https://github.com/astral-sh/ruff/pull/9853))
|
||||
- Reduce `Result<Tok, LexicalError>` size by using `Box<str>` instead of `String` ([#9885](https://github.com/astral-sh/ruff/pull/9885))
|
||||
- Reduce size of `Expr` from 80 to 64 bytes ([#9900](https://github.com/astral-sh/ruff/pull/9900))
|
||||
- Improve trailing comma rule performance ([#9867](https://github.com/astral-sh/ruff/pull/9867))
|
||||
- Remove unnecessary string cloning from the parser ([#9884](https://github.com/astral-sh/ruff/pull/9884))
|
||||
|
||||
## 0.2.1
|
||||
|
||||
This release includes support for range formatting (i.e., the ability to format specific lines
|
||||
|
||||
@@ -39,7 +39,7 @@ For small changes (e.g., bug fixes), feel free to submit a PR.
|
||||
|
||||
For larger changes (e.g., new lint rules, new functionality, new configuration options), consider
|
||||
creating an [**issue**](https://github.com/astral-sh/ruff/issues) outlining your proposed change.
|
||||
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5) to discuss your idea with the
|
||||
You can also join us on [**Discord**](https://discord.com/invite/astral-sh) to discuss your idea with the
|
||||
community. We've labeled [beginner-friendly tasks](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
|
||||
in the issue tracker, along with [bugs](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Abug)
|
||||
and [improvements](https://github.com/astral-sh/ruff/issues?q=is%3Aissue+is%3Aopen+label%3Aaccepted)
|
||||
|
||||
126
Cargo.lock
generated
126
Cargo.lock
generated
@@ -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]]
|
||||
@@ -1979,7 +1950,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2140,7 +2111,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2212,7 +2183,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.48",
|
||||
"syn 2.0.51",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2394,7 +2365,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_shrinking"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -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]]
|
||||
|
||||
46
Cargo.toml
46
Cargo.toml
@@ -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" }
|
||||
|
||||
11
README.md
11
README.md
@@ -8,7 +8,7 @@
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](https://github.com/astral-sh/ruff/actions)
|
||||
|
||||
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
|
||||
[**Discord**](https://discord.com/invite/astral-sh) | [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/)
|
||||
|
||||
An extremely fast Python linter and code formatter, written in Rust.
|
||||
|
||||
@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.2.1
|
||||
rev: v0.2.2
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -172,7 +172,7 @@ jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: chartboost/ruff-action@v1
|
||||
```
|
||||
|
||||
@@ -341,14 +341,14 @@ For a complete enumeration of the supported rules, see [_Rules_](https://docs.as
|
||||
Contributions are welcome and highly appreciated. To get started, check out the
|
||||
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
|
||||
|
||||
You can also join us on [**Discord**](https://discord.gg/c9MhzV8aU5).
|
||||
You can also join us on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Support
|
||||
|
||||
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
|
||||
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
|
||||
|
||||
You can also ask for help on [**Discord**](https://discord.gg/c9MhzV8aU5).
|
||||
You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -193,3 +193,11 @@ def func():
|
||||
for y in range(5):
|
||||
g(x, idx)
|
||||
idx += 1
|
||||
|
||||
async def func():
|
||||
# OK (for loop is async)
|
||||
idx = 0
|
||||
|
||||
async for x in async_gen():
|
||||
g(x, idx)
|
||||
idx += 1
|
||||
|
||||
@@ -147,3 +147,9 @@ ham[upper : ]
|
||||
|
||||
#: E203:1:10
|
||||
ham[upper :]
|
||||
|
||||
#: Okay
|
||||
ham[lower +1 :, "columnname"]
|
||||
|
||||
#: E203:1:13
|
||||
ham[lower + 1 :, "columnname"]
|
||||
|
||||
7
crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_2.py
vendored
Normal file
7
crates/ruff_linter/resources/test/fixtures/pycodestyle/E402_2.py
vendored
Normal 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
|
||||
@@ -14,3 +14,6 @@ class Chassis(RobotModuleTemplate):
|
||||
" \
|
||||
\
|
||||
|
||||
'''blank line with whitespace
|
||||
|
||||
inside a multiline string'''
|
||||
|
||||
32
crates/ruff_linter/resources/test/fixtures/pylint/dict_iter_missing_items.py
vendored
Normal file
32
crates/ruff_linter/resources/test/fixtures/pylint/dict_iter_missing_items.py
vendored
Normal 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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -68,3 +68,7 @@ def method_calls():
|
||||
first = "Wendy"
|
||||
last = "Appleseed"
|
||||
value.method("{first} {last}") # RUF027
|
||||
|
||||
def format_specifiers():
|
||||
a = 4
|
||||
b = "{a:b} {a:^5}"
|
||||
|
||||
@@ -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))
|
||||
|
||||
2
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_2.py
vendored
Normal file
2
crates/ruff_linter/resources/test/fixtures/ruff/RUF027_2.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# 测试eval函数,eval()函数用来执行一个字符串表达式,并返t表达式的值。另外,可以讲字符串转换成列表或元组或字典
|
||||
a = "{1: 'a', 2: 'b'}"
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,10 +2,16 @@ use ruff_python_ast::StringLike;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_bandit, flake8_pyi};
|
||||
use crate::rules::{flake8_bandit, flake8_pyi, ruff};
|
||||
|
||||
/// Run lint rules over a [`StringLike`] syntax nodes.
|
||||
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
|
||||
if checker.any_enabled(&[
|
||||
Rule::AmbiguousUnicodeCharacterString,
|
||||
Rule::AmbiguousUnicodeCharacterDocstring,
|
||||
]) {
|
||||
ruff::rules::ambiguous_unicode_character_string(checker, string_like);
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
|
||||
flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) = ¶meter_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) = ¶meter_with_default.default {
|
||||
@@ -619,28 +630,46 @@ where
|
||||
}
|
||||
if let Some(arg) = ¶meters.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) = ¶meters.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();
|
||||
|
||||
@@ -6,17 +6,14 @@ use ruff_notebook::CellOffsets;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::lexer::LexResult;
|
||||
use ruff_python_parser::Tok;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::directives::TodoComment;
|
||||
use crate::lex::docstring_detection::StateMachine;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pycodestyle::rules::BlankLinesChecker;
|
||||
use crate::rules::ruff::rules::Context;
|
||||
use crate::rules::{
|
||||
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
|
||||
flake8_pyi, flake8_quotes, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
|
||||
@@ -66,31 +63,15 @@ pub(crate) fn check_tokens(
|
||||
pylint::rules::empty_comments(&mut diagnostics, indexer, locator);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
Rule::AmbiguousUnicodeCharacterString,
|
||||
Rule::AmbiguousUnicodeCharacterDocstring,
|
||||
Rule::AmbiguousUnicodeCharacterComment,
|
||||
]) {
|
||||
let mut state_machine = StateMachine::default();
|
||||
for &(ref tok, range) in tokens.iter().flatten() {
|
||||
let is_docstring = state_machine.consume(tok);
|
||||
let context = match tok {
|
||||
Tok::String { .. } => {
|
||||
if is_docstring {
|
||||
Context::Docstring
|
||||
} else {
|
||||
Context::String
|
||||
}
|
||||
}
|
||||
Tok::FStringMiddle { .. } => Context::String,
|
||||
Tok::Comment(_) => Context::Comment,
|
||||
_ => continue,
|
||||
};
|
||||
ruff::rules::ambiguous_unicode_character(
|
||||
if settings
|
||||
.rules
|
||||
.enabled(Rule::AmbiguousUnicodeCharacterComment)
|
||||
{
|
||||
for range in indexer.comment_ranges() {
|
||||
ruff::rules::ambiguous_unicode_character_comment(
|
||||
&mut diagnostics,
|
||||
locator,
|
||||
range,
|
||||
context,
|
||||
*range,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -256,8 +256,6 @@ impl Rule {
|
||||
| Rule::MixedSpacesAndTabs
|
||||
| Rule::TrailingWhitespace => LintSource::PhysicalLines,
|
||||
Rule::AmbiguousUnicodeCharacterComment
|
||||
| Rule::AmbiguousUnicodeCharacterDocstring
|
||||
| Rule::AmbiguousUnicodeCharacterString
|
||||
| Rule::AvoidableEscapedQuote
|
||||
| Rule::BadQuotesDocstring
|
||||
| Rule::BadQuotesInlineString
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_copyright/mod.rs
|
||||
---
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -49,6 +49,11 @@ impl Violation for EnumerateForLoop {
|
||||
|
||||
/// SIM113
|
||||
pub(crate) fn enumerate_for_loop(checker: &mut Checker, for_stmt: &ast::StmtFor) {
|
||||
// If the loop is async, abort.
|
||||
if for_stmt.is_async {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the loop contains a `continue`, abort.
|
||||
let mut visitor = LoopControlFlowVisitor::default();
|
||||
visitor.visit_body(&for_stmt.body);
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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__{}_{}",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
|
||||
@@ -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'''
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
|
||||
---
|
||||
|
||||
@@ -87,12 +87,17 @@ impl Violation for IndentWithSpaces {
|
||||
/// """
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent indentation, making the rule redundant.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
|
||||
///
|
||||
/// [PEP 257]: https://peps.python.org/pep-0257/
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct UnderIndentation;
|
||||
|
||||
|
||||
@@ -27,10 +27,16 @@ use crate::docstrings::Docstring;
|
||||
/// """Return the pathname of the KOS root directory."""
|
||||
/// ```
|
||||
///
|
||||
/// ## Formatter compatibility
|
||||
/// We recommend against using this rule alongside the [formatter]. The
|
||||
/// formatter enforces consistent quotes, making the rule redundant.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [NumPy Style Guide](https://numpydoc.readthedocs.io/en/latest/format.html)
|
||||
/// - [Google Python Style Guide - Docstrings](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
|
||||
///
|
||||
/// [formatter]: https://docs.astral.sh/ruff/formatter/
|
||||
#[violation]
|
||||
pub struct TripleSingleQuotes {
|
||||
expected_quote: Quote,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |+
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
|
||||
|
||||
@@ -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()))
|
||||
};
|
||||
|
||||
@@ -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()))
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -48,6 +48,7 @@ mod tests {
|
||||
#[test_case(Rule::DefaultFactoryKwarg, Path::new("RUF026.py"))]
|
||||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_0.py"))]
|
||||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_1.py"))]
|
||||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_2.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -4,9 +4,11 @@ use bitflags::bitflags;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::StringLike;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::ruff::rules::confusables::confusable;
|
||||
use crate::rules::ruff::rules::Context;
|
||||
@@ -171,16 +173,59 @@ impl Violation for AmbiguousUnicodeCharacterComment {
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF001, RUF002, RUF003
|
||||
pub(crate) fn ambiguous_unicode_character(
|
||||
/// RUF003
|
||||
pub(crate) fn ambiguous_unicode_character_comment(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
locator: &Locator,
|
||||
range: TextRange,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let text = locator.slice(range);
|
||||
ambiguous_unicode_character(diagnostics, text, range, Context::Comment, settings);
|
||||
}
|
||||
|
||||
/// RUF001, RUF002
|
||||
pub(crate) fn ambiguous_unicode_character_string(checker: &mut Checker, string_like: StringLike) {
|
||||
let context = if checker.semantic().in_docstring() {
|
||||
Context::Docstring
|
||||
} else {
|
||||
Context::String
|
||||
};
|
||||
|
||||
match string_like {
|
||||
StringLike::StringLiteral(string_literal) => {
|
||||
for string in &string_literal.value {
|
||||
let text = checker.locator().slice(string);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
string.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
}
|
||||
StringLike::FStringLiteral(f_string_literal) => {
|
||||
let text = checker.locator().slice(f_string_literal);
|
||||
ambiguous_unicode_character(
|
||||
&mut checker.diagnostics,
|
||||
text,
|
||||
f_string_literal.range(),
|
||||
context,
|
||||
checker.settings,
|
||||
);
|
||||
}
|
||||
StringLike::BytesLiteral(_) => (),
|
||||
}
|
||||
}
|
||||
|
||||
fn ambiguous_unicode_character(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
text: &str,
|
||||
range: TextRange,
|
||||
context: Context,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let text = locator.slice(range);
|
||||
|
||||
// Most of the time, we don't need to check for ambiguous unicode characters at all.
|
||||
if text.is_ascii() {
|
||||
return;
|
||||
|
||||
@@ -162,6 +162,7 @@ fn concatenate_expressions(expr: &Expr) -> Option<(Expr, Type)> {
|
||||
elts: new_elts,
|
||||
ctx: ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
parenthesized: true,
|
||||
}
|
||||
.into(),
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
@@ -88,8 +113,10 @@ fn should_be_fstring(
|
||||
return false;
|
||||
}
|
||||
|
||||
let Ok(ast::Expr::FString(ast::ExprFString { value, .. })) =
|
||||
parse_expression(&format!("f{}", locator.slice(literal.range())))
|
||||
let fstring_expr = format!("f{}", locator.slice(literal));
|
||||
|
||||
// Note: Range offsets for `value` are based on `fstring_expr`
|
||||
let Ok(ast::Expr::FString(ast::ExprFString { value, .. })) = parse_expression(&fstring_expr)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
@@ -159,7 +186,7 @@ fn should_be_fstring(
|
||||
has_name = true;
|
||||
}
|
||||
if let Some(spec) = &element.format_spec {
|
||||
let spec = locator.slice(spec.range());
|
||||
let spec = &fstring_expr[spec.range()];
|
||||
if FormatSpec::parse(spec).is_err() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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] },
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -285,6 +285,8 @@ RUF027_0.py:70:18: RUF027 [*] Possible f-string without an `f` prefix
|
||||
69 | last = "Appleseed"
|
||||
70 | value.method("{first} {last}") # RUF027
|
||||
| ^^^^^^^^^^^^^^^^ RUF027
|
||||
71 |
|
||||
72 | def format_specifiers():
|
||||
|
|
||||
= help: Add `f` prefix
|
||||
|
||||
@@ -294,5 +296,24 @@ RUF027_0.py:70:18: RUF027 [*] Possible f-string without an `f` prefix
|
||||
69 69 | last = "Appleseed"
|
||||
70 |- value.method("{first} {last}") # RUF027
|
||||
70 |+ value.method(f"{first} {last}") # RUF027
|
||||
71 71 |
|
||||
72 72 | def format_specifiers():
|
||||
73 73 | a = 4
|
||||
|
||||
RUF027_0.py:74:9: RUF027 [*] Possible f-string without an `f` prefix
|
||||
|
|
||||
72 | def format_specifiers():
|
||||
73 | a = 4
|
||||
74 | b = "{a:b} {a:^5}"
|
||||
| ^^^^^^^^^^^^^^ RUF027
|
||||
|
|
||||
= help: Add `f` prefix
|
||||
|
||||
ℹ Unsafe fix
|
||||
71 71 |
|
||||
72 72 | def format_specifiers():
|
||||
73 73 | a = 4
|
||||
74 |- b = "{a:b} {a:^5}"
|
||||
74 |+ b = f"{a:b} {a:^5}"
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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())
|
||||
|
||||
8
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.options.json
vendored
Normal file
8
crates/ruff_python_formatter/resources/test/fixtures/ruff/expression/fstring.options.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
[
|
||||
{
|
||||
"preview": "enabled"
|
||||
},
|
||||
{
|
||||
"preview": "disabled"
|
||||
}
|
||||
]
|
||||
@@ -30,22 +30,22 @@ result_f = (
|
||||
# an expression inside a formatted value
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
# comment 1
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}' # comment
|
||||
f'{1}' # comment 2
|
||||
f'{2}'
|
||||
)
|
||||
|
||||
(
|
||||
f'{1}'
|
||||
f'{2}' # comment
|
||||
f'{2}' # comment 3
|
||||
)
|
||||
|
||||
(
|
||||
1, ( # comment
|
||||
1, ( # comment 4
|
||||
f'{2}'
|
||||
)
|
||||
)
|
||||
@@ -53,7 +53,7 @@ result_f = (
|
||||
(
|
||||
(
|
||||
f'{1}'
|
||||
# comment
|
||||
# comment 5
|
||||
),
|
||||
2
|
||||
)
|
||||
@@ -62,3 +62,221 @@ result_f = (
|
||||
x = f'''a{""}b'''
|
||||
y = f'''c{1}d"""e'''
|
||||
z = f'''a{""}b''' f'''c{1}d"""e'''
|
||||
|
||||
# F-String formatting test cases (Preview)
|
||||
|
||||
# Simple expression with a mix of debug expression and comments.
|
||||
x = f"{a}"
|
||||
x = f"{
|
||||
a = }"
|
||||
x = f"{ # comment 6
|
||||
a }"
|
||||
x = f"{ # comment 7
|
||||
a = }"
|
||||
|
||||
# Remove the parentheses as adding them doesn't make then fit within the line length limit.
|
||||
# This is similar to how we format it before f-string formatting.
|
||||
aaaaaaaaaaa = (
|
||||
f"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd } cccccccccc"
|
||||
)
|
||||
# Here, we would use the best fit layout to put the f-string indented on the next line
|
||||
# similar to the next example.
|
||||
aaaaaaaaaaa = f"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
|
||||
aaaaaaaaaaa = (
|
||||
f"asaaaaaaaaaaaaaaaa { aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc } cccccccccc"
|
||||
)
|
||||
|
||||
# This should never add the optional parentheses because even after adding them, the
|
||||
# f-string exceeds the line length limit.
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { "bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 8
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" } ccccccccccccccc"
|
||||
x = f"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa { # comment 9
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" = } ccccccccccccccc"
|
||||
|
||||
# Multiple larger expressions which exceeds the line length limit. Here, we need to decide
|
||||
# whether to split at the first or second expression. This should work similarly to the
|
||||
# assignment statement formatting where we split from right to left in preview mode.
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
|
||||
# The above example won't split but when we start introducing line breaks:
|
||||
x = f"aaaaaaaaaaaa {
|
||||
bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb
|
||||
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc {
|
||||
ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb } cccccccccccccccccccc { ddddddddddddddd
|
||||
} eeeeeeeeeeeeee"
|
||||
|
||||
# But, in case comments are present, we would split at the expression containing the
|
||||
# comments:
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb # comment 10
|
||||
} cccccccccccccccccccc { ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
x = f"aaaaaaaaaaaa { bbbbbbbbbbbbbb
|
||||
} cccccccccccccccccccc { # comment 11
|
||||
ddddddddddddddd } eeeeeeeeeeeeee"
|
||||
|
||||
# Here, the expression part itself starts with a curly brace so we need to add an extra
|
||||
# space between the opening curly brace and the expression.
|
||||
x = f"{ {'x': 1, 'y': 2} }"
|
||||
# Although the extra space isn't required before the ending curly brace, we add it for
|
||||
# consistency.
|
||||
x = f"{ {'x': 1, 'y': 2}}"
|
||||
x = f"{ {'x': 1, 'y': 2} = }"
|
||||
x = f"{ # comment 12
|
||||
{'x': 1, 'y': 2} }"
|
||||
x = f"{ # comment 13
|
||||
{'x': 1, 'y': 2} = }"
|
||||
|
||||
# But, in this case, we would split the expression itself because it exceeds the line
|
||||
# length limit so we need not add the extra space.
|
||||
xxxxxxx = f"{
|
||||
{'aaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbb', 'ccccccccccccccccccccc'}
|
||||
}"
|
||||
# And, split the expression itself because it exceeds the line length.
|
||||
xxxxxxx = f"{
|
||||
{'aaaaaaaaaaaaaaaaaaaaaaaaa', 'bbbbbbbbbbbbbbbbbbbbbbbbbbb', 'cccccccccccccccccccccccccc'}
|
||||
}"
|
||||
|
||||
# Quotes
|
||||
f"foo 'bar' {x}"
|
||||
f"foo \"bar\" {x}"
|
||||
f'foo "bar" {x}'
|
||||
f'foo \'bar\' {x}'
|
||||
f"foo {"bar"}"
|
||||
f"foo {'\'bar\''}"
|
||||
|
||||
# Here, the formatter will remove the escapes which is correct because they aren't allowed
|
||||
# pre 3.12. This means we can assume that the f-string is used in the context of 3.12.
|
||||
f"foo {'\"bar\"'}"
|
||||
|
||||
|
||||
# Triple-quoted strings
|
||||
# It's ok to use the same quote char for the inner string if it's single-quoted.
|
||||
f"""test {'inner'}"""
|
||||
f"""test {"inner"}"""
|
||||
# But if the inner string is also triple-quoted then we should preserve the existing quotes.
|
||||
f"""test {'''inner'''}"""
|
||||
|
||||
# Magic trailing comma
|
||||
#
|
||||
# The expression formatting will result in breaking it across multiple lines with a
|
||||
# trailing comma but as the expression isn't already broken, we will remove all the line
|
||||
# breaks which results in the trailing comma being present. This test case makes sure
|
||||
# that the trailing comma is removed as well.
|
||||
f"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee']} aaaaaaa"
|
||||
|
||||
# And, if the trailing comma is already present, we still need to remove it.
|
||||
f"aaaaaaa {['aaaaaaaaaaaaaaa', 'bbbbbbbbbbbbb', 'ccccccccccccccccc', 'ddddddddddddddd', 'eeeeeeeeeeeeee',]} aaaaaaa"
|
||||
|
||||
# Keep this Multiline by breaking it at the square brackets.
|
||||
f"""aaaaaa {[
|
||||
xxxxxxxx,
|
||||
yyyyyyyy,
|
||||
]} ccc"""
|
||||
|
||||
# Add the magic trailing comma because the elements don't fit within the line length limit
|
||||
# when collapsed.
|
||||
f"aaaaaa {[
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
xxxxxxxxxxxx,
|
||||
yyyyyyyyyyyy
|
||||
]} ccccccc"
|
||||
|
||||
# Remove the parenthese because they aren't required
|
||||
xxxxxxxxxxxxxxx = (
|
||||
f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb {
|
||||
xxxxxxxxxxx # comment 14
|
||||
+ yyyyyyyyyy
|
||||
} dddddddddd"
|
||||
)
|
||||
|
||||
# Comments
|
||||
|
||||
# No comments should be dropped!
|
||||
f"{ # comment 15
|
||||
# comment 16
|
||||
foo # comment 17
|
||||
# comment 18
|
||||
}" # comment 19
|
||||
# comment 20
|
||||
|
||||
# Conversion flags
|
||||
#
|
||||
# This is not a valid Python code because of the additional whitespace between the `!`
|
||||
# and conversion type. But, our parser isn't strict about this. This should probably be
|
||||
# removed once we have a strict parser.
|
||||
x = f"aaaaaaaaa { x ! r }"
|
||||
|
||||
# Even in the case of debug expresions, we only need to preserve the whitespace within
|
||||
# the expression part of the replacement field.
|
||||
x = f"aaaaaaaaa { x = ! r }"
|
||||
|
||||
# Combine conversion flags with format specifiers
|
||||
x = f"{x = ! s
|
||||
:>0
|
||||
|
||||
}"
|
||||
# This is interesting. There can be a comment after the format specifier but only if it's
|
||||
# on it's own line. Refer to https://github.com/astral-sh/ruff/pull/7787 for more details.
|
||||
# We'll format is as trailing comments.
|
||||
x = f"{x !s
|
||||
:>0
|
||||
# comment 21
|
||||
}"
|
||||
|
||||
x = f"""
|
||||
{ # comment 22
|
||||
x = :.0{y # comment 23
|
||||
}f}"""
|
||||
|
||||
# Here, the debug expression is in a nested f-string so we should start preserving
|
||||
# whitespaces from that point onwards. This means we should format the outer f-string.
|
||||
x = f"""{"foo " + # comment 24
|
||||
f"{ x =
|
||||
|
||||
}" # comment 25
|
||||
}
|
||||
"""
|
||||
|
||||
# Mix of various features.
|
||||
f"{ # comment 26
|
||||
foo # after foo
|
||||
:>{
|
||||
x # after x
|
||||
}
|
||||
# comment 27
|
||||
# comment 28
|
||||
} woah {x}"
|
||||
|
||||
# Indentation
|
||||
|
||||
# What should be the indentation?
|
||||
# https://github.com/astral-sh/ruff/discussions/9785#discussioncomment-8470590
|
||||
if indent0:
|
||||
if indent1:
|
||||
if indent2:
|
||||
foo = f"""hello world
|
||||
hello {
|
||||
f"aaaaaaa {
|
||||
[
|
||||
'aaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbb',
|
||||
'ccccccccccccccccccccc',
|
||||
'ddddddddddddddddddddd'
|
||||
]
|
||||
} bbbbbbbb" +
|
||||
[
|
||||
'aaaaaaaaaaaaaaaaaaaaa',
|
||||
'bbbbbbbbbbbbbbbbbbbbb',
|
||||
'ccccccccccccccccccccc',
|
||||
'ddddddddddddddddddddd'
|
||||
]
|
||||
} --------
|
||||
"""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user