Compare commits
168 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67076b2dcb | ||
|
|
7e3ba7f32a | ||
|
|
09dbd2029c | ||
|
|
1380bd94da | ||
|
|
c10a4535b9 | ||
|
|
97802e7466 | ||
|
|
4fd4a65718 | ||
|
|
d78c614764 | ||
|
|
3f3dd7af99 | ||
|
|
871b92a385 | ||
|
|
9158f13ee6 | ||
|
|
72e0ffc1ac | ||
|
|
ffcf0618c7 | ||
|
|
1ccef5150d | ||
|
|
6a52577630 | ||
|
|
3c2f41b615 | ||
|
|
b76b4b6016 | ||
|
|
bbadbb5de5 | ||
|
|
ba6370e5d0 | ||
|
|
be6e00ef6e | ||
|
|
865205d992 | ||
|
|
572adf7994 | ||
|
|
3b26bf84f5 | ||
|
|
f4f88308ae | ||
|
|
ea3d3a655d | ||
|
|
fd34797d0f | ||
|
|
6532455672 | ||
|
|
257c571c43 | ||
|
|
ccdee55e6e | ||
|
|
6d6d7abf70 | ||
|
|
0096938789 | ||
|
|
853d8354cb | ||
|
|
5f64d2346f | ||
|
|
ddbe5a1243 | ||
|
|
04097d194c | ||
|
|
a2b8487ae3 | ||
|
|
8969ad5879 | ||
|
|
bfa1c28c00 | ||
|
|
cf7aa26aa4 | ||
|
|
d66ce76691 | ||
|
|
b8bb9e8b92 | ||
|
|
5e46dcbf21 | ||
|
|
045449ab12 | ||
|
|
d5ff8d7c43 | ||
|
|
d92fb11e80 | ||
|
|
3d947196f8 | ||
|
|
e846f2688b | ||
|
|
7b91a162c6 | ||
|
|
8c2cfade90 | ||
|
|
a435c0df4b | ||
|
|
48e1852893 | ||
|
|
03f141f53d | ||
|
|
8dea47afc1 | ||
|
|
d3b71f1e04 | ||
|
|
04e8e74499 | ||
|
|
318653c427 | ||
|
|
f08fd5cbf0 | ||
|
|
99a755f936 | ||
|
|
e7dfb35778 | ||
|
|
085fd37209 | ||
|
|
83536cf87b | ||
|
|
9366eb919d | ||
|
|
8be51942dd | ||
|
|
d365dab904 | ||
|
|
f23851130a | ||
|
|
efdf383f5e | ||
|
|
61f21a6513 | ||
|
|
43d6aa9173 | ||
|
|
c54e48dce5 | ||
|
|
b913e99bde | ||
|
|
4ac506526b | ||
|
|
cd41de2588 | ||
|
|
3344d367f5 | ||
|
|
d7a369e7dc | ||
|
|
1b1788c8ad | ||
|
|
4d5a339d9e | ||
|
|
0801f14046 | ||
|
|
edaf891042 | ||
|
|
3beff29026 | ||
|
|
5ac2c7d293 | ||
|
|
e66fdb83d0 | ||
|
|
a95bafefb0 | ||
|
|
539af34f58 | ||
|
|
983bb31577 | ||
|
|
b98b604071 | ||
|
|
cd27b39aff | ||
|
|
a9fc648faf | ||
|
|
c1f0661225 | ||
|
|
2c91412321 | ||
|
|
11e1380df4 | ||
|
|
e93f378635 | ||
|
|
2124feb0e7 | ||
|
|
c0e7269b07 | ||
|
|
c2921e957b | ||
|
|
93cfce674a | ||
|
|
b71cc3789f | ||
|
|
717128112d | ||
|
|
e9e194ab32 | ||
|
|
890e630c41 | ||
|
|
d78287540d | ||
|
|
494e807315 | ||
|
|
6db1a32eb9 | ||
|
|
bb2cbf1f25 | ||
|
|
badfdab61a | ||
|
|
59d40f9f81 | ||
|
|
37aae666c7 | ||
|
|
460023a959 | ||
|
|
d0e3ca29d9 | ||
|
|
ccfc78e2d5 | ||
|
|
b14358fbfe | ||
|
|
ac600bb3da | ||
|
|
8cb76f85eb | ||
|
|
56c45013c2 | ||
|
|
a4ce746892 | ||
|
|
2d6d51f3a1 | ||
|
|
814731364a | ||
|
|
8c97e7922b | ||
|
|
a32617911a | ||
|
|
64b7280eb8 | ||
|
|
8d64747d34 | ||
|
|
2115d99c43 | ||
|
|
39ed75f643 | ||
|
|
8f61eae1e7 | ||
|
|
f0f4bf2929 | ||
|
|
03144b2fad | ||
|
|
0172cc51a7 | ||
|
|
12d64a223b | ||
|
|
432ea6f2e2 | ||
|
|
b34804ceb5 | ||
|
|
ee6d8f7467 | ||
|
|
089b64e9c1 | ||
|
|
3e81403fbe | ||
|
|
3c9f5e2fdc | ||
|
|
17db2e2a62 | ||
|
|
e04ef42334 | ||
|
|
f3e6ddda62 | ||
|
|
cab65b25da | ||
|
|
ee91598835 | ||
|
|
ab65eaea7f | ||
|
|
19d8913e32 | ||
|
|
b9c06b48e1 | ||
|
|
7266eb0d69 | ||
|
|
4df7bc0bcd | ||
|
|
464a0ff483 | ||
|
|
fd7ccb4c9e | ||
|
|
ae6f38344a | ||
|
|
bbf658d4c5 | ||
|
|
1f3b0fd602 | ||
|
|
37483f3ac9 | ||
|
|
4d3a1e0581 | ||
|
|
9e5f348a17 | ||
|
|
5e91211e6d | ||
|
|
df77595426 | ||
|
|
407af6e0ae | ||
|
|
d64146683e | ||
|
|
0e7914010f | ||
|
|
cfc7d8a2b5 | ||
|
|
f5cd659292 | ||
|
|
260138b427 | ||
|
|
2da149fd7e | ||
|
|
e33887718d | ||
|
|
ba4f4f4672 | ||
|
|
b7a57ce120 | ||
|
|
82abbc7234 | ||
|
|
ba98149022 | ||
|
|
7fd44a3e12 | ||
|
|
6e8d561090 | ||
|
|
cb762f4cad |
@@ -33,4 +33,5 @@ rustflags = [
|
||||
"-Wclippy::rc_buffer",
|
||||
"-Wclippy::rc_mutex",
|
||||
"-Wclippy::rest_pat_in_fully_bound_structs",
|
||||
"-Wunreachable_pub"
|
||||
]
|
||||
|
||||
@@ -12,3 +12,6 @@ indent_size = 2
|
||||
|
||||
[*.{rs,py}]
|
||||
indent_size = 4
|
||||
|
||||
[*.snap]
|
||||
trim_trailing_whitespace = false
|
||||
29
.github/workflows/ci.yaml
vendored
29
.github/workflows/ci.yaml
vendored
@@ -121,15 +121,6 @@ jobs:
|
||||
- run: cargo check
|
||||
- run: cargo fmt --all --check
|
||||
|
||||
typos:
|
||||
name: "spell check"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: crate-ci/typos@master
|
||||
with:
|
||||
files: .
|
||||
|
||||
ecosystem:
|
||||
name: "ecosystem"
|
||||
runs-on: ubuntu-latest
|
||||
@@ -230,3 +221,23 @@ jobs:
|
||||
exit_code=${PIPESTATUS[0]}
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
exit $exit_code
|
||||
|
||||
docs:
|
||||
name: "mkdocs"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install dependencies"
|
||||
run: pip install -r docs/requirements.txt
|
||||
- name: "Update README File"
|
||||
run: python scripts/transform_readme.py --target mkdocs
|
||||
- name: "Generate docs"
|
||||
run: python scripts/generate_mkdocs.py
|
||||
- name: "Check docs formatting"
|
||||
run: python scripts/check_docs_formatted.py
|
||||
- name: "Build docs"
|
||||
run: mkdocs build --strict
|
||||
|
||||
@@ -33,9 +33,10 @@ jobs:
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --out dist --sdist
|
||||
- name: "Install built wheel - x86_64"
|
||||
- name: "Test wheel - x86_64"
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -68,9 +69,10 @@ jobs:
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
args: --release --universal2 --out dist
|
||||
- name: "Install built wheel - universal2"
|
||||
- name: "Test wheel - universal2"
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -113,11 +115,12 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --out dist
|
||||
- name: "Install built wheel"
|
||||
- name: "Test wheel"
|
||||
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
|
||||
shell: bash
|
||||
run: |
|
||||
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -158,10 +161,11 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
args: --release --out dist
|
||||
- name: "Install built wheel"
|
||||
- name: "Test wheel"
|
||||
if: ${{ startsWith(matrix.target, 'x86_64') }}
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -187,6 +191,9 @@ jobs:
|
||||
platform:
|
||||
- target: aarch64-unknown-linux-gnu
|
||||
arch: aarch64
|
||||
# see https://github.com/charliermarsh/ruff/issues/3791
|
||||
# and https://github.com/gnzlbg/jemallocator/issues/170#issuecomment-1503228963
|
||||
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
- target: armv7-unknown-linux-gnueabihf
|
||||
arch: armv7
|
||||
- target: s390x-unknown-linux-gnu
|
||||
@@ -195,6 +202,7 @@ jobs:
|
||||
arch: ppc64le
|
||||
- target: powerpc64-unknown-linux-gnu
|
||||
arch: ppc64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
@@ -207,10 +215,11 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
docker-options: ${{ matrix.platform.maturin_docker_options }}
|
||||
args: --release --out dist
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
if: matrix.platform.arch != 'ppc64'
|
||||
name: Install built wheel
|
||||
name: Test wheel
|
||||
with:
|
||||
arch: ${{ matrix.platform.arch }}
|
||||
distro: ubuntu20.04
|
||||
@@ -221,6 +230,7 @@ jobs:
|
||||
pip3 install -U pip
|
||||
run: |
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -260,7 +270,7 @@ jobs:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
args: --release --out dist
|
||||
- name: "Install built wheel"
|
||||
- name: "Test wheel"
|
||||
if: matrix.target == 'x86_64-unknown-linux-musl'
|
||||
uses: addnab/docker-run-action@v3
|
||||
with:
|
||||
@@ -269,6 +279,7 @@ jobs:
|
||||
run: |
|
||||
apk add py3-pip
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
|
||||
ruff --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
@@ -294,8 +305,10 @@ jobs:
|
||||
platform:
|
||||
- target: aarch64-unknown-linux-musl
|
||||
arch: aarch64
|
||||
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
- target: armv7-unknown-linux-musleabihf
|
||||
arch: armv7
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
@@ -309,8 +322,9 @@ jobs:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
args: --release --out dist
|
||||
docker-options: ${{ matrix.platform.maturin_docker_options }}
|
||||
- uses: uraimo/run-on-arch-action@v2
|
||||
name: Install built wheel
|
||||
name: Test wheel
|
||||
with:
|
||||
arch: ${{ matrix.platform.arch }}
|
||||
distro: alpine_latest
|
||||
@@ -319,6 +333,7 @@ jobs:
|
||||
apk add py3-pip
|
||||
run: |
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
ruff check --help
|
||||
- name: "Upload wheels"
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,7 +3,8 @@
|
||||
crates/ruff/resources/test/cpython
|
||||
mkdocs.yml
|
||||
.overrides
|
||||
github_search.jsonl
|
||||
ruff-old
|
||||
github_search*.jsonl
|
||||
|
||||
###
|
||||
# Rust.gitignore
|
||||
|
||||
@@ -23,6 +23,11 @@ repos:
|
||||
- MD033 # no-inline-html
|
||||
- --
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.14.8
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: cargo-fmt
|
||||
@@ -58,11 +63,6 @@ repos:
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
exclude: |
|
||||
(?x)^(
|
||||
crates/ruff/resources/.*|
|
||||
crates/ruff_python_formatter/resources/.*
|
||||
)$
|
||||
|
||||
ci:
|
||||
skip: [cargo-fmt, clippy, dev-generate-all]
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.0.266
|
||||
|
||||
### `update-check` is no longer a valid configuration option ([#4313](https://github.com/charliermarsh/ruff/pull/4313))
|
||||
|
||||
The `update-check` functionality was deprecated in [#2530](https://github.com/charliermarsh/ruff/pull/2530),
|
||||
in that the behavior itself was removed, and Ruff was changed to warn when that option was enabled.
|
||||
|
||||
Now, Ruff will throw an error when `update-check` is provided via a configuration file (e.g.,
|
||||
`update-check = false`) or through the command-line, since it has no effect. Users should remove
|
||||
this option from their configuration.
|
||||
|
||||
## 0.0.265
|
||||
|
||||
### `--fix-only` now exits with a zero exit code, unless `--exit-non-zero-on-fix` is specified ([#4146](https://github.com/charliermarsh/ruff/pull/4146))
|
||||
|
||||
Previously, `--fix-only` would exit with a non-zero exit code if any fixes were applied. This
|
||||
behavior was inconsistent with `--fix`, and further, meant that `--exit-non-zero-on-fix` was
|
||||
effectively ignored when `--fix-only` was specified.
|
||||
|
||||
Now, `--fix-only` will exit with a zero exit code, unless `--exit-non-zero-on-fix` is specified,
|
||||
in which case it will exit with a non-zero exit code if any fixes were applied.
|
||||
|
||||
## 0.0.260
|
||||
|
||||
### Fixes are now represented as a list of edits ([#3709](https://github.com/charliermarsh/ruff/pull/3709))
|
||||
|
||||
@@ -225,7 +225,7 @@ python scripts/check_ecosystem.py path/to/your/ruff path/to/older/ruff
|
||||
|
||||
You can also run the Ecosystem CI check in a Docker container across a larger set of projects by
|
||||
downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-aggregate/blob/master/data/known-github-tomls.jsonl)
|
||||
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](scripts/Dockerfile.ecosystem).
|
||||
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/charliermarsh/ruff/blob/main/scripts/Dockerfile.ecosystem).
|
||||
Note that this check will take a while to run.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
974
Cargo.lock
generated
974
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -3,7 +3,7 @@ members = ["crates/*"]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.67"
|
||||
rust-version = "1.69"
|
||||
homepage = "https://beta.ruff.rs/docs/"
|
||||
documentation = "https://beta.ruff.rs/docs/"
|
||||
repository = "https://github.com/charliermarsh/ruff"
|
||||
@@ -11,7 +11,7 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.69" }
|
||||
bitflags = { version = "1.3.2" }
|
||||
bitflags = { version = "2.2.1" }
|
||||
chrono = { version = "0.4.23", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.1.8", features = ["derive"] }
|
||||
colored = { version = "2.0.0" }
|
||||
@@ -30,12 +30,11 @@ path-absolutize = { version = "3.0.14" }
|
||||
proc-macro2 = { version = "1.0.51" }
|
||||
quote = { version = "1.0.23" }
|
||||
regex = { version = "1.7.1" }
|
||||
ruff_text_size = { git = "https://github.com/RustPython/Parser.git", rev = "947fb53d0b41fec465db3d8e725bdb2eec1299ec" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "c15f670f2c30cfae6b41a1874893590148c74bc4" }
|
||||
rustpython-parser = { features = [
|
||||
"lalrpop",
|
||||
"serde",
|
||||
], git = "https://github.com/RustPython/RustPython.git", rev = "c15f670f2c30cfae6b41a1874893590148c74bc4" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "f3e4d3409253660bd4fa7f3d24d3db747e7dca61" }
|
||||
rustpython-literal = { git = "https://github.com/RustPython/Parser.git", rev = "947fb53d0b41fec465db3d8e725bdb2eec1299ec" }
|
||||
rustpython-parser = { git = "https://github.com/RustPython/Parser.git", rev = "947fb53d0b41fec465db3d8e725bdb2eec1299ec" , default-features = false}
|
||||
schemars = { version = "0.8.12" }
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = { version = "1.0.93", features = ["preserve_order"] }
|
||||
@@ -44,7 +43,7 @@ similar = { version = "2.2.1" }
|
||||
smallvec = { version = "1.10.0" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.24.3" }
|
||||
syn = { version = "1.0.109" }
|
||||
syn = { version = "2.0.15" }
|
||||
test-case = { version = "3.0.0" }
|
||||
textwrap = { version = "0.16.0" }
|
||||
toml = { version = "0.7.2" }
|
||||
|
||||
24
LICENSE
24
LICENSE
@@ -550,6 +550,30 @@ are:
|
||||
THE SOFTWARE.
|
||||
"""
|
||||
|
||||
- flynt, licensed as follows:
|
||||
"""
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2022 Ilya Kamenshchikov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"""
|
||||
|
||||
- isort, licensed as follows:
|
||||
"""
|
||||
|
||||
13
README.md
13
README.md
@@ -55,8 +55,8 @@ Ruff is extremely actively developed and used in major open-source projects like
|
||||
|
||||
...and many more.
|
||||
|
||||
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster) or
|
||||
the most recent [project update](https://notes.crmarsh.com/ruff-the-first-200-releases).
|
||||
Ruff is backed by [Astral](https://astral.sh). Read the [launch post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff),
|
||||
or the original [project announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
|
||||
|
||||
## Testimonials
|
||||
|
||||
@@ -137,7 +137,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
```yaml
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.262'
|
||||
rev: 'v0.0.266'
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
@@ -183,6 +183,7 @@ exclude = [
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
@@ -280,12 +281,13 @@ quality tools, including:
|
||||
- [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
|
||||
- [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
|
||||
- [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
|
||||
- [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/charliermarsh/ruff/issues/2102))
|
||||
- [isort](https://pypi.org/project/isort/)
|
||||
- [mccabe](https://pypi.org/project/mccabe/)
|
||||
- [pandas-vet](https://pypi.org/project/pandas-vet/)
|
||||
- [pep8-naming](https://pypi.org/project/pep8-naming/)
|
||||
- [pydocstyle](https://pypi.org/project/pydocstyle/)
|
||||
- [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) ([#980](https://github.com/charliermarsh/ruff/issues/980))
|
||||
- [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks)
|
||||
- [pyupgrade](https://pypi.org/project/pyupgrade/)
|
||||
- [tryceratops](https://pypi.org/project/tryceratops/)
|
||||
- [yesqa](https://pypi.org/project/yesqa/)
|
||||
@@ -367,9 +369,10 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PDM](https://github.com/pdm-project/pdm)
|
||||
- [PaddlePaddle](https://github.com/PaddlePaddle/Paddle)
|
||||
- [Pandas](https://github.com/pandas-dev/pandas)
|
||||
- [Poetry](https://github.com/python-poetry/poetry)
|
||||
- [Polars](https://github.com/pola-rs/polars)
|
||||
- [PostHog](https://github.com/PostHog/posthog)
|
||||
- Prefect ([Marvin](https://github.com/PrefectHQ/marvin))
|
||||
- Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin))
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [PyInstaller](https://github.com/pyinstaller/pyinstaller)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
|
||||
@@ -6,3 +6,5 @@ trivias = "trivias"
|
||||
hel = "hel"
|
||||
whos = "whos"
|
||||
spawnve = "spawnve"
|
||||
ned = "ned"
|
||||
poit = "poit"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.262"
|
||||
version = "0.0.266"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ requires-python = ">=3.7"
|
||||
repository = "https://github.com/charliermarsh/ruff#subdirectory=crates/flake8_to_ruff"
|
||||
|
||||
[build-system]
|
||||
requires = ["maturin>=0.14,<0.15"]
|
||||
requires = ["maturin>=0.15.1,<0.16"]
|
||||
build-backend = "maturin"
|
||||
|
||||
[tool.maturin]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.262"
|
||||
version = "0.0.266"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
@@ -17,11 +17,11 @@ name = "ruff"
|
||||
ruff_cache = { path = "../ruff_cache" }
|
||||
ruff_diagnostics = { path = "../ruff_diagnostics", features = ["serde"] }
|
||||
ruff_macros = { path = "../ruff_macros" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast", features = ["serde"] }
|
||||
ruff_python_semantic = { path = "../ruff_python_semantic" }
|
||||
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
|
||||
ruff_rustpython = { path = "../ruff_rustpython" }
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
||||
anyhow = { workspace = true }
|
||||
@@ -29,7 +29,7 @@ bitflags = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "string"], optional = true }
|
||||
colored = { workspace = true }
|
||||
dirs = { version = "4.0.0" }
|
||||
dirs = { version = "5.0.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
glob = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
@@ -56,7 +56,7 @@ result-like = { version = "0.4.6" }
|
||||
rustc-hash = { workspace = true }
|
||||
rustpython-common = { workspace = true }
|
||||
rustpython-parser = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
semver = { version = "1.0.16" }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
@@ -80,5 +80,7 @@ colored = { workspace = true, features = ["no-color"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
schemars = ["dep:schemars"]
|
||||
logical_lines = []
|
||||
jupyter_notebook = []
|
||||
ecosystem_ci = []
|
||||
|
||||
@@ -14,3 +14,8 @@ def foo(x, y, z):
|
||||
return False
|
||||
|
||||
#import os # noqa: ERA001
|
||||
|
||||
|
||||
class A():
|
||||
pass
|
||||
# b = c
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""
|
||||
Should emit:
|
||||
B017 - on lines 20
|
||||
B017 - on lines 23 and 41
|
||||
"""
|
||||
import asyncio
|
||||
import unittest
|
||||
import pytest
|
||||
|
||||
CONSTANT = True
|
||||
|
||||
@@ -34,3 +35,14 @@ class Foobar(unittest.TestCase):
|
||||
def raises_with_absolute_reference(self):
|
||||
with self.assertRaises(asyncio.CancelledError):
|
||||
Foo()
|
||||
|
||||
|
||||
def test_pytest_raises():
|
||||
with pytest.raises(Exception):
|
||||
raise ValueError("Hello")
|
||||
|
||||
with pytest.raises(Exception, "hello"):
|
||||
raise ValueError("This is fine")
|
||||
|
||||
with pytest.raises(Exception, match="hello"):
|
||||
raise ValueError("This is also fine")
|
||||
|
||||
@@ -172,3 +172,14 @@ def iter_f(names):
|
||||
|
||||
if False:
|
||||
return [lambda: i for i in range(3)] # error
|
||||
|
||||
|
||||
for val in range(3):
|
||||
def make_func(val=val):
|
||||
def tmp():
|
||||
return print(val)
|
||||
|
||||
return tmp
|
||||
|
||||
|
||||
funcs.append(make_func())
|
||||
|
||||
@@ -4,7 +4,12 @@ B027 - on lines 13, 16, 19, 23
|
||||
"""
|
||||
import abc
|
||||
from abc import ABC
|
||||
from abc import abstractmethod, abstractproperty
|
||||
from abc import (
|
||||
abstractmethod,
|
||||
abstractproperty,
|
||||
abstractclassmethod,
|
||||
abstractstaticmethod,
|
||||
)
|
||||
from abc import abstractmethod as notabstract
|
||||
from abc import abstractproperty as notabstract_property
|
||||
|
||||
@@ -55,6 +60,22 @@ class AbstractClass(ABC):
|
||||
def abstract_6(self):
|
||||
...
|
||||
|
||||
@abstractclassmethod
|
||||
def abstract_7(self):
|
||||
pass
|
||||
|
||||
@abc.abstractclassmethod
|
||||
def abstract_8(self):
|
||||
...
|
||||
|
||||
@abstractstaticmethod
|
||||
def abstract_9(self):
|
||||
pass
|
||||
|
||||
@abc.abstractstaticmethod
|
||||
def abstract_10(self):
|
||||
...
|
||||
|
||||
def body_1(self):
|
||||
print("foo")
|
||||
...
|
||||
|
||||
@@ -149,6 +149,16 @@ for group in groupby(items, key=lambda p: p[1]):
|
||||
collect_shop_items("Joe", group[1])
|
||||
|
||||
|
||||
# https://github.com/charliermarsh/ruff/issues/4050
|
||||
for _section, section_items in itertools.groupby(items, key=lambda p: p[1]):
|
||||
if _section == "greens":
|
||||
for item in section_items:
|
||||
collect_shop_items(shopper, item)
|
||||
elif _section == "frozen items":
|
||||
_ = [item for item in section_items]
|
||||
else:
|
||||
collect_shop_items(shopper, section_items)
|
||||
|
||||
# Make sure we ignore - but don't fail on more complicated invocations
|
||||
for _key, (_value1, _value2) in groupby(
|
||||
[("a", (1, 2)), ("b", (3, 4)), ("a", (5, 6))], key=lambda p: p[1]
|
||||
|
||||
@@ -17,3 +17,23 @@ all((x.id for x in bar))
|
||||
|
||||
async def f() -> bool:
|
||||
return all([await use_greeting(greeting) for greeting in await greetings()])
|
||||
|
||||
|
||||
# Special comment handling
|
||||
any(
|
||||
[ # lbracket comment
|
||||
# second line comment
|
||||
i.bit_count()
|
||||
# random middle comment
|
||||
for i in range(5) # rbracket comment
|
||||
] # rpar comment
|
||||
# trailing comment
|
||||
)
|
||||
|
||||
# Weird case where the function call, opening bracket, and comment are all
|
||||
# on the same line.
|
||||
any([ # lbracket comment
|
||||
# second line comment
|
||||
i.bit_count() for i in range(5) # rbracket comment
|
||||
] # rpar comment
|
||||
)
|
||||
|
||||
@@ -21,3 +21,36 @@ def f_c():
|
||||
def f_ok():
|
||||
msg = "hello"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
def f_unfixable():
|
||||
msg = "hello"
|
||||
raise RuntimeError("This is an example exception")
|
||||
|
||||
|
||||
def f_msg_in_nested_scope():
|
||||
def nested():
|
||||
msg = "hello"
|
||||
|
||||
raise RuntimeError("This is an example exception")
|
||||
|
||||
|
||||
def f_msg_in_parent_scope():
|
||||
msg = "hello"
|
||||
|
||||
def nested():
|
||||
raise RuntimeError("This is an example exception")
|
||||
|
||||
|
||||
def f_fix_indentation_check(foo):
|
||||
if foo:
|
||||
raise RuntimeError("This is an example exception")
|
||||
else:
|
||||
if foo == "foo":
|
||||
raise RuntimeError(f"This is an exception: {foo}")
|
||||
raise RuntimeError("This is an exception: {}".format(foo))
|
||||
|
||||
|
||||
# Report these, but don't fix them
|
||||
if foo: raise RuntimeError("This is an example exception")
|
||||
if foo: x = 1; raise RuntimeError("This is an example exception")
|
||||
|
||||
10
crates/ruff/resources/test/fixtures/flake8_import_conventions/custom_banned_from.py
vendored
Normal file
10
crates/ruff/resources/test/fixtures/flake8_import_conventions/custom_banned_from.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
from logging.config import BaseConfigurator # banned
|
||||
from typing import Any, Dict # banned
|
||||
from typing import * # banned
|
||||
|
||||
from pandas import DataFrame # banned
|
||||
from pandas import * # banned
|
||||
|
||||
import logging.config # ok
|
||||
import typing # ok
|
||||
import pandas # ok
|
||||
@@ -7,3 +7,12 @@ foo.info("Hello {}".format("World!"))
|
||||
logging.log(logging.INFO, msg="Hello {}".format("World!"))
|
||||
logging.log(level=logging.INFO, msg="Hello {}".format("World!"))
|
||||
logging.log(msg="Hello {}".format("World!"), level=logging.INFO)
|
||||
|
||||
# Flask support
|
||||
import flask
|
||||
from flask import current_app
|
||||
from flask import current_app as app
|
||||
|
||||
flask.current_app.logger.info("Hello {}".format("World!"))
|
||||
current_app.logger.info("Hello {}".format("World!"))
|
||||
app.logger.log(logging.INFO, "Hello {}".format("World!"))
|
||||
|
||||
@@ -84,3 +84,10 @@ class Class1:
|
||||
|
||||
# We shouldn't emit Y015 for __all__
|
||||
__all__ = ["Class1"]
|
||||
|
||||
# Ignore the following for PYI015
|
||||
field26 = typing.Sequence[int]
|
||||
field27 = list[str]
|
||||
field28 = builtins.str
|
||||
field29 = str
|
||||
field30 = str | bytes | None
|
||||
|
||||
@@ -91,3 +91,10 @@ class Class1:
|
||||
|
||||
# We shouldn't emit Y015 for __all__
|
||||
__all__ = ["Class1"]
|
||||
|
||||
# Ignore the following for PYI015
|
||||
field26 = typing.Sequence[int]
|
||||
field27 = list[str]
|
||||
field28 = builtins.str
|
||||
field29 = str
|
||||
field30 = str | bytes | None
|
||||
|
||||
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py
vendored
Normal file
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.py
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
import typing
|
||||
from typing import Annotated, Literal, TypeAlias, TypeVar
|
||||
|
||||
import typing_extensions
|
||||
|
||||
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
|
||||
def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
"""Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
|
||||
if sys.platform == "linux":
|
||||
f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
elif sys.platform == "win32":
|
||||
f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
else:
|
||||
f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
|
||||
k = "" # Y052 Need type annotation for "k"
|
||||
el = r"" # Y052 Need type annotation for "el"
|
||||
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi
vendored
Normal file
28
crates/ruff/resources/test/fixtures/flake8_pyi/PYI020.pyi
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
import sys
|
||||
import typing
|
||||
from typing import Annotated, Literal, TypeAlias, TypeVar
|
||||
|
||||
import typing_extensions
|
||||
|
||||
def f(x: "int"): ... # Y020 Quoted annotations should never be used in stubs
|
||||
def g(x: list["int"]): ... # Y020 Quoted annotations should never be used in stubs
|
||||
_T = TypeVar("_T", bound="int") # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
def h(w: Literal["a", "b"], x: typing.Literal["c"], y: typing_extensions.Literal["d"], z: _T) -> _T: ...
|
||||
|
||||
def j() -> "int": ... # Y020 Quoted annotations should never be used in stubs
|
||||
Alias: TypeAlias = list["int"] # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
class Child(list["int"]): # Y020 Quoted annotations should never be used in stubs
|
||||
"""Documented and guaranteed useful.""" # Y021 Docstrings should not be included in stubs
|
||||
|
||||
if sys.platform == "linux":
|
||||
f: "int" # Y020 Quoted annotations should never be used in stubs
|
||||
elif sys.platform == "win32":
|
||||
f: "str" # Y020 Quoted annotations should never be used in stubs
|
||||
else:
|
||||
f: "bytes" # Y020 Quoted annotations should never be used in stubs
|
||||
|
||||
# These two shouldn't trigger Y020 -- empty strings can't be "quoted annotations"
|
||||
k = "" # Y052 Need type annotation for "k"
|
||||
el = r"" # Y052 Need type annotation for "el"
|
||||
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.py
vendored
Normal file
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.py
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
just_literals_pipe_union: TypeAlias = (
|
||||
Literal[True] | Literal["idk"]
|
||||
) # not PYI042 (not a stubfile)
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
snake_case_alias1: TypeAlias = str | int # not PYI042 (not a stubfile)
|
||||
_snake_case_alias2: TypeAlias = Literal["whatever"] # not PYI042 (not a stubfile)
|
||||
Snake_case_alias: TypeAlias = int | float # not PYI042 (not a stubfile)
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.pyi
vendored
Normal file
24
crates/ruff/resources/test/fixtures/flake8_pyi/PYI042.pyi
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
just_literals_pipe_union: TypeAlias = (
|
||||
Literal[True] | Literal["idk"]
|
||||
) # PYI042, since not camel case
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
|
||||
_snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
|
||||
Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.py
vendored
Normal file
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
_PrivateAliasT: TypeAlias = str | int # not PYI043 (not a stubfile)
|
||||
_PrivateAliasT2: TypeAlias = typing.Any # not PYI043 (not a stubfile)
|
||||
_PrivateAliasT3: TypeAlias = Literal[
|
||||
"not", "a", "chance"
|
||||
] # not PYI043 (not a stubfile)
|
||||
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.pyi
vendored
Normal file
23
crates/ruff/resources/test/fixtures/flake8_pyi/PYI043.pyi
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import typing
|
||||
from collections.abc import Mapping
|
||||
from typing import (
|
||||
Annotated,
|
||||
TypeAlias,
|
||||
Union,
|
||||
Literal,
|
||||
)
|
||||
|
||||
_PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
|
||||
_PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
|
||||
_PrivateAliasT3: TypeAlias = Literal[
|
||||
"not", "a", "chance"
|
||||
] # PYI043, since this ends in a T
|
||||
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
|
||||
PublicAliasT: TypeAlias = str | int
|
||||
PublicAliasT2: TypeAlias = Union[str, bytes]
|
||||
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
|
||||
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
|
||||
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
|
||||
|
||||
# check that this edge case doesn't crash
|
||||
_: TypeAlias = str | int
|
||||
@@ -1,73 +1,96 @@
|
||||
def foo():
|
||||
pass
|
||||
|
||||
try:
|
||||
foo()
|
||||
except ValueError: # SIM105
|
||||
pass
|
||||
|
||||
try:
|
||||
foo()
|
||||
except (ValueError, OSError): # SIM105
|
||||
pass
|
||||
|
||||
try:
|
||||
foo()
|
||||
except: # SIM105
|
||||
pass
|
||||
|
||||
try:
|
||||
foo()
|
||||
except (a.Error, b.Error): # SIM105
|
||||
pass
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
print('foo')
|
||||
pass
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
except:
|
||||
pass
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
except (a.Error, b.Error):
|
||||
pass
|
||||
|
||||
# OK
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
print("foo")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# OK
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
print('bar')
|
||||
print("bar")
|
||||
|
||||
# OK
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
pass
|
||||
finally:
|
||||
print('bar')
|
||||
print("bar")
|
||||
|
||||
# OK
|
||||
try:
|
||||
foo()
|
||||
foo()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# OK
|
||||
try:
|
||||
for i in range(3):
|
||||
foo()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def bar():
|
||||
# OK
|
||||
try:
|
||||
return foo()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
def with_ellipsis():
|
||||
# OK
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
...
|
||||
|
||||
|
||||
def with_ellipsis_and_return():
|
||||
# OK
|
||||
try:
|
||||
return foo()
|
||||
except ValueError:
|
||||
...
|
||||
|
||||
|
||||
def with_comment():
|
||||
try:
|
||||
foo()
|
||||
except (ValueError, OSError):
|
||||
pass # Trailing comment.
|
||||
8
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_1.py
vendored
Normal file
8
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_1.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Case: There's a random import, so it should add `contextlib` after it."""
|
||||
import math
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
math.sqrt(-1)
|
||||
except ValueError:
|
||||
pass
|
||||
13
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_2.py
vendored
Normal file
13
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_2.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Case: `contextlib` already imported."""
|
||||
import contextlib
|
||||
|
||||
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
pass
|
||||
16
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_3.py
vendored
Normal file
16
crates/ruff/resources/test/fixtures/flake8_simplify/SIM105_3.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Case: `contextlib` is imported after the call site."""
|
||||
|
||||
|
||||
def foo():
|
||||
pass
|
||||
|
||||
|
||||
def bar():
|
||||
# SIM105
|
||||
try:
|
||||
foo()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
import contextlib
|
||||
@@ -12,3 +12,10 @@ if not a == b: # OK
|
||||
|
||||
if not a != b: # OK
|
||||
pass
|
||||
|
||||
a = not not b # SIM208
|
||||
|
||||
f(not not a) # SIM208
|
||||
|
||||
if 1 + (not (not a)): # SIM208
|
||||
pass
|
||||
|
||||
@@ -6,6 +6,7 @@ a = True if b + c else False # SIM210
|
||||
|
||||
a = False if b else True # OK
|
||||
|
||||
|
||||
def f():
|
||||
# OK
|
||||
def bool():
|
||||
|
||||
@@ -42,3 +42,113 @@ if False and f() and a and g() and b: # OK
|
||||
|
||||
if a and False and f() and b and g(): # OK
|
||||
pass
|
||||
|
||||
|
||||
a or "" or True # SIM222
|
||||
|
||||
a or "foo" or True or "bar" # SIM222
|
||||
|
||||
a or 0 or True # SIM222
|
||||
|
||||
a or 1 or True or 2 # SIM222
|
||||
|
||||
a or 0.0 or True # SIM222
|
||||
|
||||
a or 0.1 or True or 0.2 # SIM222
|
||||
|
||||
a or [] or True # SIM222
|
||||
|
||||
a or list([]) or True # SIM222
|
||||
|
||||
a or [1] or True or [2] # SIM222
|
||||
|
||||
a or list([1]) or True or list([2]) # SIM222
|
||||
|
||||
a or {} or True # SIM222
|
||||
|
||||
a or dict() or True # SIM222
|
||||
|
||||
a or {1: 1} or True or {2: 2} # SIM222
|
||||
|
||||
a or dict({1: 1}) or True or dict({2: 2}) # SIM222
|
||||
|
||||
a or set() or True # SIM222
|
||||
|
||||
a or set(set()) or True # SIM222
|
||||
|
||||
a or {1} or True or {2} # SIM222
|
||||
|
||||
a or set({1}) or True or set({2}) # SIM222
|
||||
|
||||
a or () or True # SIM222
|
||||
|
||||
a or tuple(()) or True # SIM222
|
||||
|
||||
a or (1,) or True or (2,) # SIM222
|
||||
|
||||
a or tuple((1,)) or True or tuple((2,)) # SIM222
|
||||
|
||||
a or frozenset() or True # SIM222
|
||||
|
||||
a or frozenset(frozenset()) or True # SIM222
|
||||
|
||||
a or frozenset({1}) or True or frozenset({2}) # SIM222
|
||||
|
||||
a or frozenset(frozenset({1})) or True or frozenset(frozenset({2})) # SIM222
|
||||
|
||||
|
||||
# Inside test `a` is simplified.
|
||||
|
||||
bool(a or [1] or True or [2]) # SIM222
|
||||
|
||||
assert a or [1] or True or [2] # SIM222
|
||||
|
||||
if (a or [1] or True or [2]) and (a or [1] or True or [2]): # SIM222
|
||||
pass
|
||||
|
||||
0 if a or [1] or True or [2] else 1 # SIM222
|
||||
|
||||
while a or [1] or True or [2]: # SIM222
|
||||
pass
|
||||
|
||||
[
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
]
|
||||
|
||||
{
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
}
|
||||
|
||||
{
|
||||
0: 0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
}
|
||||
|
||||
(
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a or [1] or True or [2] # SIM222
|
||||
if b or [1] or True or [2] # SIM222
|
||||
)
|
||||
|
||||
# Outside test `a` is not simplified.
|
||||
|
||||
a or [1] or True or [2] # SIM222
|
||||
|
||||
if (a or [1] or True or [2]) == (a or [1]): # SIM222
|
||||
pass
|
||||
|
||||
if f(a or [1] or True or [2]): # SIM222
|
||||
pass
|
||||
|
||||
@@ -37,3 +37,113 @@ if True or f() or a or g() or b: # OK
|
||||
|
||||
if a or True or f() or b or g(): # OK
|
||||
pass
|
||||
|
||||
|
||||
a and "" and False # SIM223
|
||||
|
||||
a and "foo" and False and "bar" # SIM223
|
||||
|
||||
a and 0 and False # SIM223
|
||||
|
||||
a and 1 and False and 2 # SIM223
|
||||
|
||||
a and 0.0 and False # SIM223
|
||||
|
||||
a and 0.1 and False and 0.2 # SIM223
|
||||
|
||||
a and [] and False # SIM223
|
||||
|
||||
a and list([]) and False # SIM223
|
||||
|
||||
a and [1] and False and [2] # SIM223
|
||||
|
||||
a and list([1]) and False and list([2]) # SIM223
|
||||
|
||||
a and {} and False # SIM223
|
||||
|
||||
a and dict() and False # SIM223
|
||||
|
||||
a and {1: 1} and False and {2: 2} # SIM223
|
||||
|
||||
a and dict({1: 1}) and False and dict({2: 2}) # SIM223
|
||||
|
||||
a and set() and False # SIM223
|
||||
|
||||
a and set(set()) and False # SIM223
|
||||
|
||||
a and {1} and False and {2} # SIM223
|
||||
|
||||
a and set({1}) and False and set({2}) # SIM223
|
||||
|
||||
a and () and False # SIM222
|
||||
|
||||
a and tuple(()) and False # SIM222
|
||||
|
||||
a and (1,) and False and (2,) # SIM222
|
||||
|
||||
a and tuple((1,)) and False and tuple((2,)) # SIM222
|
||||
|
||||
a and frozenset() and False # SIM222
|
||||
|
||||
a and frozenset(frozenset()) and False # SIM222
|
||||
|
||||
a and frozenset({1}) and False and frozenset({2}) # SIM222
|
||||
|
||||
a and frozenset(frozenset({1})) and False and frozenset(frozenset({2})) # SIM222
|
||||
|
||||
|
||||
# Inside test `a` is simplified.
|
||||
|
||||
bool(a and [] and False and []) # SIM223
|
||||
|
||||
assert a and [] and False and [] # SIM223
|
||||
|
||||
if (a and [] and False and []) or (a and [] and False and []): # SIM223
|
||||
pass
|
||||
|
||||
0 if a and [] and False and [] else 1 # SIM222
|
||||
|
||||
while a and [] and False and []: # SIM223
|
||||
pass
|
||||
|
||||
[
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
]
|
||||
|
||||
{
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
}
|
||||
|
||||
{
|
||||
0: 0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
}
|
||||
|
||||
(
|
||||
0
|
||||
for a in range(10)
|
||||
for b in range(10)
|
||||
if a and [] and False and [] # SIM223
|
||||
if b and [] and False and [] # SIM223
|
||||
)
|
||||
|
||||
# Outside test `a` is not simplified.
|
||||
|
||||
a and [] and False and [] # SIM223
|
||||
|
||||
if (a and [] and False and []) == (a and []): # SIM223
|
||||
pass
|
||||
|
||||
if f(a and [] and False and []): # SIM223
|
||||
pass
|
||||
|
||||
18
crates/ruff/resources/test/fixtures/flynt/FLY002.py
vendored
Normal file
18
crates/ruff/resources/test/fixtures/flynt/FLY002.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
import secrets
|
||||
from random import random, choice
|
||||
|
||||
a = "Hello"
|
||||
ok1 = " ".join([a, " World"]) # OK
|
||||
ok2 = "".join(["Finally, ", a, " World"]) # OK
|
||||
ok3 = "x".join(("1", "2", "3")) # OK
|
||||
ok4 = "y".join([1, 2, 3]) # Technically OK, though would've been an error originally
|
||||
ok5 = "a".join([random(), random()]) # OK (simple calls)
|
||||
ok6 = "a".join([secrets.token_urlsafe(), secrets.token_hex()]) # OK (attr calls)
|
||||
|
||||
nok1 = "x".join({"4", "5", "yee"}) # Not OK (set)
|
||||
nok2 = a.join(["1", "2", "3"]) # Not OK (not a static joiner)
|
||||
nok3 = "a".join(a) # Not OK (not a static joinee)
|
||||
nok4 = "a".join([a, a, *a]) # Not OK (not a static length)
|
||||
nok5 = "a".join([choice("flarp")]) # Not OK (not a simple call)
|
||||
nok6 = "a".join(x for x in "feefoofum") # Not OK (generator)
|
||||
nok7 = "a".join([f"foo{8}", "bar"]) # Not OK (contains an f-string)
|
||||
@@ -22,3 +22,6 @@ from bar import (
|
||||
a, # comment 7
|
||||
b, # comment 8
|
||||
)
|
||||
|
||||
# comment 9
|
||||
from baz import * # comment 10
|
||||
|
||||
4
crates/ruff/resources/test/fixtures/isort/propagate_inline_comments.py
vendored
Normal file
4
crates/ruff/resources/test/fixtures/isort/propagate_inline_comments.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
from mypackage.subpackage import ( # long comment that seems to be a problem
|
||||
a_long_variable_name_that_causes_problems,
|
||||
items,
|
||||
)
|
||||
@@ -22,3 +22,7 @@ if True:
|
||||
x.drop(["a"], axis=1, **kwargs, inplace=True)
|
||||
x.drop(["a"], axis=1, inplace=True, **kwargs)
|
||||
f(x.drop(["a"], axis=1, inplace=True))
|
||||
|
||||
x.apply(lambda x: x.sort_values('a', inplace=True))
|
||||
import torch
|
||||
torch.m.ReLU(inplace=True) # safe because this isn't a pandas call
|
||||
|
||||
@@ -13,3 +13,11 @@ class C:
|
||||
myObj2 = namedtuple("MyObj2", ["a", "b"])
|
||||
Employee = NamedTuple('Employee', [('name', str), ('id', int)])
|
||||
Point2D = TypedDict('Point2D', {'in': int, 'x-y': int})
|
||||
|
||||
|
||||
class D(TypedDict):
|
||||
lower: int
|
||||
CONSTANT: str
|
||||
mixedCase: bool
|
||||
_mixedCase: list
|
||||
mixed_Case: set
|
||||
|
||||
@@ -4,11 +4,11 @@ if not X is Y:
|
||||
#: E714
|
||||
if not X.B is Y:
|
||||
pass
|
||||
#: E714
|
||||
|
||||
#: Okay
|
||||
if not X is Y is not Z:
|
||||
pass
|
||||
|
||||
#: Okay
|
||||
if not X is not Y:
|
||||
pass
|
||||
|
||||
|
||||
@@ -2,14 +2,30 @@
|
||||
"""Here's a top-level docstring that's over the limit."""
|
||||
|
||||
|
||||
def f():
|
||||
def f1():
|
||||
"""Here's a docstring that's also over the limit."""
|
||||
|
||||
x = 1 # Here's a comment that's over the limit, but it's not standalone.
|
||||
|
||||
# Here's a standalone comment that's over the limit.
|
||||
|
||||
x = 2
|
||||
# Another standalone that is preceded by a newline and indent toke and is over the limit.
|
||||
|
||||
print("Here's a string that's over the limit, but it's not a docstring.")
|
||||
|
||||
|
||||
"This is also considered a docstring, and is over the limit."
|
||||
|
||||
|
||||
def f2():
|
||||
"""Here's a multi-line docstring.
|
||||
|
||||
It's over the limit on this line, which isn't the first line in the docstring.
|
||||
"""
|
||||
|
||||
|
||||
def f3():
|
||||
"""Here's a multi-line docstring.
|
||||
|
||||
It's over the limit on this line, which isn't the first line in the docstring."""
|
||||
|
||||
21
crates/ruff/resources/test/fixtures/pydocstyle/D214_module.py
vendored
Normal file
21
crates/ruff/resources/test/fixtures/pydocstyle/D214_module.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"""A module docstring with D214 violations
|
||||
|
||||
Returns
|
||||
-----
|
||||
valid returns
|
||||
|
||||
Args
|
||||
-----
|
||||
valid args
|
||||
"""
|
||||
|
||||
import os
|
||||
from .expected import Expectation
|
||||
|
||||
expectation = Expectation()
|
||||
expect = expectation.expect
|
||||
|
||||
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
|
||||
"D214: Section is over-indented ('Returns')")
|
||||
expect(os.path.normcase(__file__ if __file__[-1] != 'c' else __file__[:-1]),
|
||||
"D214: Section is over-indented ('Args')")
|
||||
@@ -13,3 +13,15 @@ def another_function():
|
||||
|
||||
def utf8_function():
|
||||
"""éste docstring is capitalized."""
|
||||
|
||||
def uppercase_char_not_possible():
|
||||
"""'args' is not capitalized."""
|
||||
|
||||
def non_alphabetic():
|
||||
"""th!is is not capitalized."""
|
||||
|
||||
def non_ascii():
|
||||
"""th•s is not capitalized."""
|
||||
|
||||
def all_caps():
|
||||
"""th•s is not capitalized."""
|
||||
|
||||
@@ -115,6 +115,20 @@ def f(x, *args, **kwargs):
|
||||
return x
|
||||
|
||||
|
||||
def f(x, *, y, z):
|
||||
"""Do something.
|
||||
|
||||
Args:
|
||||
x: some first value
|
||||
|
||||
Keyword Args:
|
||||
y (int): the other value
|
||||
z (int): the last value
|
||||
|
||||
"""
|
||||
return x, y, z
|
||||
|
||||
|
||||
class Test:
|
||||
def f(self, /, arg1: int) -> None:
|
||||
"""
|
||||
|
||||
18
crates/ruff/resources/test/fixtures/pydocstyle/all.py
vendored
Normal file
18
crates/ruff/resources/test/fixtures/pydocstyle/all.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
def public_func():
|
||||
pass
|
||||
|
||||
|
||||
def private_func():
|
||||
pass
|
||||
|
||||
|
||||
class PublicClass:
|
||||
class PublicNestedClass:
|
||||
pass
|
||||
|
||||
|
||||
class PrivateClass:
|
||||
pass
|
||||
|
||||
|
||||
__all__ = ("public_func", "PublicClass")
|
||||
13
crates/ruff/resources/test/fixtures/pyflakes/F811_22.py
vendored
Normal file
13
crates/ruff/resources/test/fixtures/pyflakes/F811_22.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
def redef(value):
|
||||
match value:
|
||||
case True:
|
||||
|
||||
def fun(x, y):
|
||||
return x
|
||||
|
||||
case False:
|
||||
|
||||
def fun(x, y):
|
||||
return y
|
||||
|
||||
return fun
|
||||
@@ -132,3 +132,8 @@ def in_ipython_notebook() -> bool:
|
||||
except NameError:
|
||||
return False # not in notebook
|
||||
return True
|
||||
|
||||
|
||||
def named_expr():
|
||||
if any((key := (value := x)) for x in ["ok"]):
|
||||
print(key)
|
||||
|
||||
@@ -121,3 +121,8 @@ def f(x: int):
|
||||
print("A")
|
||||
case y:
|
||||
pass
|
||||
|
||||
|
||||
def f():
|
||||
if any((key := (value := x)) for x in ["ok"]):
|
||||
print(key)
|
||||
|
||||
19
crates/ruff/resources/test/fixtures/pygrep-hooks/PGH005_0.py
vendored
Normal file
19
crates/ruff/resources/test/fixtures/pygrep-hooks/PGH005_0.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Errors
|
||||
assert my_mock.not_called()
|
||||
assert my_mock.called_once_with()
|
||||
assert my_mock.not_called
|
||||
assert my_mock.called_once_with
|
||||
my_mock.assert_not_called
|
||||
my_mock.assert_called
|
||||
my_mock.assert_called_once_with
|
||||
my_mock.assert_called_once_with
|
||||
MyMock.assert_called_once_with
|
||||
|
||||
# OK
|
||||
assert my_mock.call_count == 1
|
||||
assert my_mock.called
|
||||
my_mock.assert_not_called()
|
||||
my_mock.assert_called()
|
||||
my_mock.assert_called_once_with()
|
||||
"""like :meth:`Mock.assert_called_once_with`"""
|
||||
"""like :meth:`MagicMock.assert_called_once_with`"""
|
||||
0
crates/ruff/resources/test/fixtures/pylint/import_self/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pylint/import_self/__init__.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/pylint/import_self/module.py
vendored
Normal file
3
crates/ruff/resources/test/fixtures/pylint/import_self/module.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import import_self.module
|
||||
from import_self import module
|
||||
from . import module
|
||||
21
crates/ruff/resources/test/fixtures/pylint/nested_min_max.py
vendored
Normal file
21
crates/ruff/resources/test/fixtures/pylint/nested_min_max.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
min(1, 2, 3)
|
||||
min(1, min(2, 3))
|
||||
min(1, min(2, min(3, 4)))
|
||||
min(1, foo("a", "b"), min(3, 4))
|
||||
min(1, max(2, 3))
|
||||
max(1, 2, 3)
|
||||
max(1, max(2, 3))
|
||||
max(1, max(2, max(3, 4)))
|
||||
max(1, foo("a", "b"), max(3, 4))
|
||||
|
||||
# These should not trigger; we do not flag cases with keyword args.
|
||||
min(1, min(2, 3), key=test)
|
||||
min(1, min(2, 3, key=test))
|
||||
# This will still trigger, to merge the calls without keyword args.
|
||||
min(1, min(2, 3, key=test), min(4, 5))
|
||||
|
||||
# Don't provide a fix if there are comments within the call.
|
||||
min(
|
||||
1, # This is a comment.
|
||||
min(2, 3),
|
||||
)
|
||||
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_7.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_7.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def main():
|
||||
exit(0)
|
||||
|
||||
|
||||
import functools
|
||||
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_8.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_8.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from sys import argv
|
||||
|
||||
|
||||
def main():
|
||||
exit(0)
|
||||
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_9.py
vendored
Normal file
5
crates/ruff/resources/test/fixtures/pylint/sys_exit_alias_9.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
def main():
|
||||
exit(0)
|
||||
|
||||
|
||||
from sys import argv
|
||||
@@ -22,13 +22,13 @@ def f(x=1, y=1, z=1): # OK
|
||||
pass
|
||||
|
||||
|
||||
def f(x, y, z, /, u, v, w): # OK
|
||||
def f(x, y, z, /, u, v, w): # Too many arguments (6/5)
|
||||
pass
|
||||
|
||||
|
||||
def f(x, y, z, *, u, v, w): # OK
|
||||
def f(x, y, z, *, u, v, w): # Too many arguments (6/5)
|
||||
pass
|
||||
|
||||
|
||||
def f(x, y, z, a, b, c, *, u, v, w): # Too many arguments (6/5)
|
||||
def f(x, y, z, a, b, c, *, u, v, w): # Too many arguments (9/5)
|
||||
pass
|
||||
|
||||
75
crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py
vendored
Normal file
75
crates/ruff/resources/test/fixtures/pylint/unexpected_special_method_signature.py
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
class TestClass:
|
||||
def __bool__(self):
|
||||
...
|
||||
|
||||
def __bool__(self, x): # too many mandatory args
|
||||
...
|
||||
|
||||
def __bool__(self, x=1): # additional optional args OK
|
||||
...
|
||||
|
||||
def __bool__(): # ignored; should be caughty by E0211/N805
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def __bool__():
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def __bool__(x): # too many mandatory args
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def __bool__(x=1): # additional optional args OK
|
||||
...
|
||||
|
||||
def __eq__(self, other): # multiple args
|
||||
...
|
||||
|
||||
def __eq__(self, other=1): # expected arg is optional
|
||||
...
|
||||
|
||||
def __eq__(self): # too few mandatory args
|
||||
...
|
||||
|
||||
def __eq__(self, other, other_other): # too many mandatory args
|
||||
...
|
||||
|
||||
def __round__(self): # allow zero additional args
|
||||
...
|
||||
|
||||
def __round__(self, x): # allow one additional args
|
||||
...
|
||||
|
||||
def __round__(self, x, y): # disallow 2 args
|
||||
...
|
||||
|
||||
def __round__(self, x, y, z=2): # disallow 3 args even when one is optional
|
||||
...
|
||||
|
||||
def __eq__(self, *args): # ignore *args
|
||||
...
|
||||
|
||||
def __eq__(self, x, *args): # extra *args is ok
|
||||
...
|
||||
|
||||
def __eq__(self, x, y, *args): # too many args with *args
|
||||
...
|
||||
|
||||
def __round__(self, *args): # allow zero additional args
|
||||
...
|
||||
|
||||
def __round__(self, x, *args): # allow one additional args
|
||||
...
|
||||
|
||||
def __round__(self, x, y, *args): # disallow 2 args
|
||||
...
|
||||
|
||||
def __eq__(self, **kwargs): # ignore **kwargs
|
||||
...
|
||||
|
||||
def __eq__(self, /, other=42): # ignore positional-only args
|
||||
...
|
||||
|
||||
def __eq__(self, *, other=42): # ignore positional-only args
|
||||
...
|
||||
@@ -59,3 +59,14 @@ u"foo".encode("utf-8") # b"foo"
|
||||
R"foo\o".encode("utf-8") # br"foo\o"
|
||||
U"foo".encode("utf-8") # b"foo"
|
||||
print("foo".encode()) # print(b"foo")
|
||||
|
||||
# `encode` on parenthesized strings.
|
||||
(
|
||||
"abc"
|
||||
"def"
|
||||
).encode()
|
||||
|
||||
((
|
||||
"abc"
|
||||
"def"
|
||||
)).encode()
|
||||
|
||||
@@ -46,6 +46,14 @@ print("foo {} ".format(x))
|
||||
|
||||
'({}={{0!e}})'.format(a)
|
||||
|
||||
"{[b]}".format(a)
|
||||
|
||||
'{[b]}'.format(a)
|
||||
|
||||
"""{[b]}""".format(a)
|
||||
|
||||
'''{[b]}'''.format(a)
|
||||
|
||||
###
|
||||
# Non-errors
|
||||
###
|
||||
1
crates/ruff/resources/test/fixtures/pyupgrade/UP032_1.py
vendored
Normal file
1
crates/ruff/resources/test/fixtures/pyupgrade/UP032_1.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"{} {}".format(a, b) # Intentionally at start-of-file, to ensure graceful handling.
|
||||
@@ -1,4 +1,6 @@
|
||||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
from typing import ClassVar, Sequence
|
||||
|
||||
KNOWINGLY_MUTABLE_DEFAULT = []
|
||||
|
||||
@@ -6,16 +8,20 @@ KNOWINGLY_MUTABLE_DEFAULT = []
|
||||
@dataclass()
|
||||
class A:
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: typing.Sequence[int] = []
|
||||
without_annotation = []
|
||||
ignored_via_comment: list[int] = [] # noqa: RUF008
|
||||
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
|
||||
perfectly_fine: list[int] = field(default_factory=list)
|
||||
class_variable: typing.ClassVar[list[int]] = []
|
||||
|
||||
|
||||
@dataclass
|
||||
class B:
|
||||
mutable_default: list[int] = []
|
||||
immutable_annotation: Sequence[int] = []
|
||||
without_annotation = []
|
||||
ignored_via_comment: list[int] = [] # noqa: RUF008
|
||||
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
|
||||
perfectly_fine: list[int] = field(default_factory=list)
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import NamedTuple
|
||||
import datetime
|
||||
import re
|
||||
import typing
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, NamedTuple
|
||||
|
||||
|
||||
def default_function() -> list[int]:
|
||||
@@ -13,6 +17,14 @@ class ImmutableType(NamedTuple):
|
||||
@dataclass()
|
||||
class A:
|
||||
hidden_mutable_default: list[int] = default_function()
|
||||
class_variable: typing.ClassVar[list[int]] = default_function()
|
||||
another_class_var: ClassVar[list[int]] = default_function()
|
||||
|
||||
fine_path: Path = Path()
|
||||
fine_date: datetime.date = datetime.date(2042, 1, 1)
|
||||
fine_timedelta: datetime.timedelta = datetime.timedelta(hours=7)
|
||||
fine_tuple: tuple[int] = tuple([1])
|
||||
fine_regex: re.Pattern = re.compile(r".*")
|
||||
|
||||
|
||||
DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES = ImmutableType(40)
|
||||
@@ -26,3 +38,5 @@ class B:
|
||||
not_optimal: ImmutableType = ImmutableType(20)
|
||||
good_variant: ImmutableType = DEFAULT_IMMUTABLETYPE_FOR_ALL_DATACLASSES
|
||||
okay_variant: A = DEFAULT_A_FOR_ALL_DATACLASSES
|
||||
|
||||
fine_dataclass_function: list[int] = field(default_factory=list)
|
||||
|
||||
@@ -45,3 +45,10 @@ def good():
|
||||
logger.exception("a failed")
|
||||
except Exception:
|
||||
logger.exception("something failed")
|
||||
|
||||
|
||||
def fine():
|
||||
try:
|
||||
a = process() # This throws the exception now
|
||||
finally:
|
||||
print("finally")
|
||||
|
||||
@@ -4,12 +4,12 @@ use itertools::Itertools;
|
||||
use libcst_native::{
|
||||
Codegen, CodegenState, ImportNames, ParenthesizableWhitespace, SmallStatement, Statement,
|
||||
};
|
||||
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use rustpython_parser::ast::{self, ExcepthandlerKind, Expr, Keyword, Stmt, StmtKind};
|
||||
use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::helpers;
|
||||
use ruff_python_ast::helpers::to_absolute;
|
||||
use ruff_python_ast::imports::{AnyImport, Import};
|
||||
use ruff_python_ast::newlines::NewlineWithTrailingNewline;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
@@ -28,21 +28,21 @@ fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
|
||||
/// Determine if a child is the only statement in its body.
|
||||
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
||||
match &parent.node {
|
||||
StmtKind::FunctionDef { body, .. }
|
||||
| StmtKind::AsyncFunctionDef { body, .. }
|
||||
| StmtKind::ClassDef { body, .. }
|
||||
| StmtKind::With { body, .. }
|
||||
| StmtKind::AsyncWith { body, .. } => {
|
||||
StmtKind::FunctionDef(ast::StmtFunctionDef { body, .. })
|
||||
| StmtKind::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
|
||||
| StmtKind::ClassDef(ast::StmtClassDef { body, .. })
|
||||
| StmtKind::With(ast::StmtWith { body, .. })
|
||||
| StmtKind::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
bail!("Unable to find child in parent body")
|
||||
}
|
||||
}
|
||||
StmtKind::For { body, orelse, .. }
|
||||
| StmtKind::AsyncFor { body, orelse, .. }
|
||||
| StmtKind::While { body, orelse, .. }
|
||||
| StmtKind::If { body, orelse, .. } => {
|
||||
StmtKind::For(ast::StmtFor { body, orelse, .. })
|
||||
| StmtKind::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
|
||||
| StmtKind::While(ast::StmtWhile { body, orelse, .. })
|
||||
| StmtKind::If(ast::StmtIf { body, orelse, .. }) => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
@@ -51,18 +51,18 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
|
||||
bail!("Unable to find child in parent body")
|
||||
}
|
||||
}
|
||||
StmtKind::Try {
|
||||
StmtKind::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
}
|
||||
| StmtKind::TryStar {
|
||||
})
|
||||
| StmtKind::TryStar(ast::StmtTryStar {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => {
|
||||
}) => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
@@ -70,7 +70,9 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
|
||||
} else if finalbody.iter().contains(child) {
|
||||
Ok(has_single_child(finalbody, deleted))
|
||||
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
|
||||
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
||||
ExcepthandlerKind::ExceptHandler(ast::ExcepthandlerExceptHandler {
|
||||
body, ..
|
||||
}) => {
|
||||
if body.iter().contains(child) {
|
||||
Some(body)
|
||||
} else {
|
||||
@@ -83,7 +85,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
|
||||
bail!("Unable to find child in parent body")
|
||||
}
|
||||
}
|
||||
StmtKind::Match { cases, .. } => {
|
||||
StmtKind::Match(ast::StmtMatch { cases, .. }) => {
|
||||
if let Some(body) = cases.iter().find_map(|case| {
|
||||
if case.body.iter().contains(child) {
|
||||
Some(&case.body)
|
||||
@@ -102,20 +104,17 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
|
||||
|
||||
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
|
||||
/// of a multi-statement line.
|
||||
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
|
||||
let contents = locator.after(stmt.end_location.unwrap());
|
||||
for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() {
|
||||
let trimmed = line.trim();
|
||||
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<TextSize> {
|
||||
let contents = locator.after(stmt.end());
|
||||
|
||||
for line in NewlineWithTrailingNewline::from(contents) {
|
||||
let trimmed = line.trim_start();
|
||||
|
||||
if trimmed.starts_with(';') {
|
||||
let column = line
|
||||
.char_indices()
|
||||
.find_map(|(column, char)| if char == ';' { Some(column) } else { None })
|
||||
.unwrap();
|
||||
return Some(to_absolute(
|
||||
Location::new(row + 1, column),
|
||||
stmt.end_location.unwrap(),
|
||||
));
|
||||
let colon_offset = line.text_len() - trimmed.text_len();
|
||||
return Some(stmt.end() + line.start() + colon_offset);
|
||||
}
|
||||
|
||||
if !trimmed.starts_with('\\') {
|
||||
break;
|
||||
}
|
||||
@@ -124,42 +123,36 @@ fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
|
||||
}
|
||||
|
||||
/// Find the next valid break for a `Stmt` after a semicolon.
|
||||
fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
|
||||
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
|
||||
let contents = locator.after(start_location);
|
||||
for (row, line) in NewlineWithTrailingNewline::from(contents).enumerate() {
|
||||
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
|
||||
let start_location = semicolon + TextSize::from(1);
|
||||
|
||||
let contents = &locator.contents()[usize::from(start_location)..];
|
||||
for line in NewlineWithTrailingNewline::from(contents) {
|
||||
let trimmed = line.trim();
|
||||
// Skip past any continuations.
|
||||
if trimmed.starts_with('\\') {
|
||||
continue;
|
||||
}
|
||||
return if trimmed.is_empty() {
|
||||
// If the line is empty, then despite the previous statement ending in a
|
||||
// semicolon, we know that it's not a multi-statement line.
|
||||
to_absolute(Location::new(row + 1, 0), start_location)
|
||||
} else {
|
||||
// Otherwise, find the start of the next statement. (Or, anything that isn't
|
||||
// whitespace.)
|
||||
let column = line
|
||||
.char_indices()
|
||||
.find_map(|(column, char)| {
|
||||
if char.is_whitespace() {
|
||||
None
|
||||
} else {
|
||||
Some(column)
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
to_absolute(Location::new(row + 1, column), start_location)
|
||||
};
|
||||
|
||||
return start_location
|
||||
+ if trimmed.is_empty() {
|
||||
// If the line is empty, then despite the previous statement ending in a
|
||||
// semicolon, we know that it's not a multi-statement line.
|
||||
line.start()
|
||||
} else {
|
||||
// Otherwise, find the start of the next statement. (Or, anything that isn't
|
||||
// whitespace.)
|
||||
let relative_offset = line.find(|c: char| !c.is_whitespace()).unwrap();
|
||||
line.start() + TextSize::try_from(relative_offset).unwrap()
|
||||
};
|
||||
}
|
||||
Location::new(start_location.row() + 1, 0)
|
||||
|
||||
locator.line_end(start_location)
|
||||
}
|
||||
|
||||
/// Return `true` if a `Stmt` occurs at the end of a file.
|
||||
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
let contents = locator.after(stmt.end_location.unwrap());
|
||||
contents.is_empty()
|
||||
stmt.end() == locator.contents().text_len()
|
||||
}
|
||||
|
||||
/// Return the `Fix` to use when deleting a `Stmt`.
|
||||
@@ -175,7 +168,7 @@ fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
/// remove the entire start and end lines.
|
||||
/// - If the `Stmt` is the last statement in its parent body, replace it with a
|
||||
/// `pass` instead.
|
||||
pub fn delete_stmt(
|
||||
pub(crate) fn delete_stmt(
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
@@ -190,39 +183,29 @@ pub fn delete_stmt(
|
||||
{
|
||||
// If removing this node would lead to an invalid syntax tree, replace
|
||||
// it with a `pass`.
|
||||
Ok(Edit::replacement(
|
||||
"pass".to_string(),
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
))
|
||||
Ok(Edit::range_replacement("pass".to_string(), stmt.range()))
|
||||
} else {
|
||||
Ok(if let Some(semicolon) = trailing_semicolon(stmt, locator) {
|
||||
let next = next_stmt_break(semicolon, locator);
|
||||
Edit::deletion(stmt.location, next)
|
||||
} else if helpers::match_leading_content(stmt, locator) {
|
||||
Edit::deletion(stmt.location, stmt.end_location.unwrap())
|
||||
} else if helpers::preceded_by_continuation(stmt, indexer) {
|
||||
if is_end_of_file(stmt, locator) && stmt.location.column() == 0 {
|
||||
Edit::deletion(stmt.start(), next)
|
||||
} else if helpers::has_leading_content(stmt, locator) {
|
||||
Edit::range_deletion(stmt.range())
|
||||
} else if helpers::preceded_by_continuation(stmt, indexer, locator) {
|
||||
if is_end_of_file(stmt, locator) && locator.is_at_start_of_line(stmt.start()) {
|
||||
// Special-case: a file can't end in a continuation.
|
||||
Edit::replacement(
|
||||
stylist.line_ending().to_string(),
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
)
|
||||
Edit::range_replacement(stylist.line_ending().to_string(), stmt.range())
|
||||
} else {
|
||||
Edit::deletion(stmt.location, stmt.end_location.unwrap())
|
||||
Edit::range_deletion(stmt.range())
|
||||
}
|
||||
} else {
|
||||
Edit::deletion(
|
||||
Location::new(stmt.location.row(), 0),
|
||||
Location::new(stmt.end_location.unwrap().row() + 1, 0),
|
||||
)
|
||||
let range = locator.full_lines_range(stmt.range());
|
||||
Edit::range_deletion(range)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a `Fix` to remove any unused imports from an `import` statement.
|
||||
pub fn remove_unused_imports<'a>(
|
||||
pub(crate) fn remove_unused_imports<'a>(
|
||||
unused_imports: impl Iterator<Item = &'a str>,
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
@@ -231,7 +214,7 @@ pub fn remove_unused_imports<'a>(
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
) -> Result<Edit> {
|
||||
let module_text = locator.slice(stmt);
|
||||
let module_text = locator.slice(stmt.range());
|
||||
let mut tree = match_module(module_text)?;
|
||||
|
||||
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
|
||||
@@ -337,11 +320,7 @@ pub fn remove_unused_imports<'a>(
|
||||
};
|
||||
tree.codegen(&mut state);
|
||||
|
||||
Ok(Edit::replacement(
|
||||
state.to_string(),
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
))
|
||||
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,11 +330,10 @@ pub fn remove_unused_imports<'a>(
|
||||
///
|
||||
/// Supports the removal of parentheses when this is the only (kw)arg left.
|
||||
/// For this behavior, set `remove_parentheses` to `true`.
|
||||
pub fn remove_argument(
|
||||
pub(crate) fn remove_argument(
|
||||
locator: &Locator,
|
||||
call_at: Location,
|
||||
expr_at: Location,
|
||||
expr_end: Location,
|
||||
call_at: TextSize,
|
||||
expr_range: TextRange,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
remove_parentheses: bool,
|
||||
@@ -374,13 +352,13 @@ pub fn remove_argument(
|
||||
if n_arguments == 1 {
|
||||
// Case 1: there is only one argument.
|
||||
let mut count: usize = 0;
|
||||
for (start, tok, end) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
|
||||
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() {
|
||||
if matches!(tok, Tok::Lpar) {
|
||||
if count == 0 {
|
||||
fix_start = Some(if remove_parentheses {
|
||||
start
|
||||
range.start()
|
||||
} else {
|
||||
Location::new(start.row(), start.column() + 1)
|
||||
range.start() + TextSize::from(1)
|
||||
});
|
||||
}
|
||||
count += 1;
|
||||
@@ -390,9 +368,9 @@ pub fn remove_argument(
|
||||
count -= 1;
|
||||
if count == 0 {
|
||||
fix_end = Some(if remove_parentheses {
|
||||
end
|
||||
range.end()
|
||||
} else {
|
||||
Location::new(end.row(), end.column() - 1)
|
||||
range.end() - TextSize::from(1)
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -400,27 +378,27 @@ pub fn remove_argument(
|
||||
}
|
||||
} else if args
|
||||
.iter()
|
||||
.map(|node| node.location)
|
||||
.chain(keywords.iter().map(|node| node.location))
|
||||
.any(|location| location > expr_at)
|
||||
.map(Expr::start)
|
||||
.chain(keywords.iter().map(Keyword::start))
|
||||
.any(|location| location > expr_range.start())
|
||||
{
|
||||
// Case 2: argument or keyword is _not_ the last node.
|
||||
let mut seen_comma = false;
|
||||
for (start, tok, end) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
|
||||
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() {
|
||||
if seen_comma {
|
||||
if matches!(tok, Tok::NonLogicalNewline) {
|
||||
// Also delete any non-logical newlines after the comma.
|
||||
continue;
|
||||
}
|
||||
fix_end = Some(if matches!(tok, Tok::Newline) {
|
||||
end
|
||||
range.end()
|
||||
} else {
|
||||
start
|
||||
range.start()
|
||||
});
|
||||
break;
|
||||
}
|
||||
if start == expr_at {
|
||||
fix_start = Some(start);
|
||||
if range.start() == expr_range.start() {
|
||||
fix_start = Some(range.start());
|
||||
}
|
||||
if fix_start.is_some() && matches!(tok, Tok::Comma) {
|
||||
seen_comma = true;
|
||||
@@ -429,13 +407,13 @@ pub fn remove_argument(
|
||||
} else {
|
||||
// Case 3: argument or keyword is the last node, so we have to find the last
|
||||
// comma in the stmt.
|
||||
for (start, tok, _) in lexer::lex_located(contents, Mode::Module, call_at).flatten() {
|
||||
if start == expr_at {
|
||||
fix_end = Some(expr_end);
|
||||
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() {
|
||||
if range.start() == expr_range.start() {
|
||||
fix_end = Some(expr_range.end());
|
||||
break;
|
||||
}
|
||||
if matches!(tok, Tok::Comma) {
|
||||
fix_start = Some(start);
|
||||
fix_start = Some(range.start());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,17 +434,29 @@ pub fn remove_argument(
|
||||
/// name on which the `lru_cache` symbol would be made available (`"functools.lru_cache"`).
|
||||
///
|
||||
/// Attempts to reuse existing imports when possible.
|
||||
pub fn get_or_import_symbol(
|
||||
pub(crate) fn get_or_import_symbol(
|
||||
module: &str,
|
||||
member: &str,
|
||||
at: TextSize,
|
||||
context: &Context,
|
||||
importer: &Importer,
|
||||
locator: &Locator,
|
||||
) -> Result<(Edit, String)> {
|
||||
if let Some((source, binding)) = context.resolve_qualified_import_name(module, member) {
|
||||
// If the symbol is already available in the current scope, use it.
|
||||
//
|
||||
// We also add a no-nop edit to force conflicts with any other fixes that might try to
|
||||
|
||||
// The exception: the symbol source (i.e., the import statement) comes after the current
|
||||
// location. For example, we could be generating an edit within a function, and the import
|
||||
// could be defined in the module scope, but after the function definition. In this case,
|
||||
// it's unclear whether we can use the symbol (the function could be called between the
|
||||
// import and the current location, and thus the symbol would not be available). It's also
|
||||
// unclear whether should add an import statement at the top of the file, since it could
|
||||
// be shadowed between the import and the current location.
|
||||
if source.start() > at {
|
||||
bail!("Unable to use existing symbol `{binding}` due to late-import");
|
||||
}
|
||||
|
||||
// We also add a no-op edit to force conflicts with any other fixes that might try to
|
||||
// remove the import. Consider:
|
||||
//
|
||||
// ```py
|
||||
@@ -482,14 +472,11 @@ pub fn get_or_import_symbol(
|
||||
//
|
||||
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
|
||||
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
|
||||
let import_edit = Edit::replacement(
|
||||
locator.slice(source).to_string(),
|
||||
source.location,
|
||||
source.end_location.unwrap(),
|
||||
);
|
||||
let import_edit =
|
||||
Edit::range_replacement(locator.slice(source.range()).to_string(), source.range());
|
||||
Ok((import_edit, binding))
|
||||
} else {
|
||||
if let Some(stmt) = importer.get_import_from(module) {
|
||||
if let Some(stmt) = importer.find_import_from(module, at) {
|
||||
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
|
||||
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
|
||||
// bound name.
|
||||
@@ -500,10 +487,7 @@ pub fn get_or_import_symbol(
|
||||
let import_edit = importer.add_member(stmt, member)?;
|
||||
Ok((import_edit, member.to_string()))
|
||||
} else {
|
||||
bail!(
|
||||
"Unable to insert `{}` into scope due to name conflict",
|
||||
member
|
||||
)
|
||||
bail!("Unable to insert `{member}` into scope due to name conflict")
|
||||
}
|
||||
} else {
|
||||
// Case 2: No `functools` import is in scope; thus, we add `import functools`, and
|
||||
@@ -512,13 +496,11 @@ pub fn get_or_import_symbol(
|
||||
.find_binding(module)
|
||||
.map_or(true, |binding| binding.kind.is_builtin())
|
||||
{
|
||||
let import_edit = importer.add_import(&AnyImport::Import(Import::module(module)));
|
||||
let import_edit =
|
||||
importer.add_import(&AnyImport::Import(Import::module(module)), at);
|
||||
Ok((import_edit, format!("{module}.{member}")))
|
||||
} else {
|
||||
bail!(
|
||||
"Unable to insert `{}` into scope due to name conflict",
|
||||
module
|
||||
)
|
||||
bail!("Unable to insert `{module}` into scope due to name conflict")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -527,8 +509,8 @@ pub fn get_or_import_symbol(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use ruff_text_size::TextSize;
|
||||
use rustpython_parser as parser;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
@@ -546,19 +528,13 @@ mod tests {
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
trailing_semicolon(stmt, &locator),
|
||||
Some(Location::new(1, 5))
|
||||
);
|
||||
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(5)));
|
||||
|
||||
let contents = "x = 1 ; y = 1";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
trailing_semicolon(stmt, &locator),
|
||||
Some(Location::new(1, 6))
|
||||
);
|
||||
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(6)));
|
||||
|
||||
let contents = r#"
|
||||
x = 1 \
|
||||
@@ -568,10 +544,7 @@ x = 1 \
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
trailing_semicolon(stmt, &locator),
|
||||
Some(Location::new(2, 2))
|
||||
);
|
||||
assert_eq!(trailing_semicolon(stmt, &locator), Some(TextSize::from(10)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -581,15 +554,15 @@ x = 1 \
|
||||
let contents = "x = 1; y = 1";
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
next_stmt_break(Location::new(1, 4), &locator),
|
||||
Location::new(1, 5)
|
||||
next_stmt_break(TextSize::from(4), &locator),
|
||||
TextSize::from(5)
|
||||
);
|
||||
|
||||
let contents = "x = 1 ; y = 1";
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
next_stmt_break(Location::new(1, 5), &locator),
|
||||
Location::new(1, 6)
|
||||
next_stmt_break(TextSize::from(5), &locator),
|
||||
TextSize::from(6)
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -599,8 +572,8 @@ x = 1 \
|
||||
.trim();
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
next_stmt_break(Location::new(2, 2), &locator),
|
||||
Location::new(2, 4)
|
||||
next_stmt_break(TextSize::from(10), &locator),
|
||||
TextSize::from(12)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,25 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::types::Range;
|
||||
|
||||
use crate::linter::FixTable;
|
||||
use crate::registry::{AsRule, Rule};
|
||||
|
||||
pub mod actions;
|
||||
pub(crate) mod actions;
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<(String, FixTable)> {
|
||||
pub(crate) fn fix_file(
|
||||
diagnostics: &[Diagnostic],
|
||||
locator: &Locator,
|
||||
) -> Option<(String, FixTable)> {
|
||||
let mut with_fixes = diagnostics
|
||||
.iter()
|
||||
.filter(|diag| !diag.fix.is_empty())
|
||||
.filter(|diag| diag.fix.is_some())
|
||||
.peekable();
|
||||
|
||||
if with_fixes.peek().is_none() {
|
||||
@@ -33,17 +35,16 @@ fn apply_fixes<'a>(
|
||||
locator: &'a Locator<'a>,
|
||||
) -> (String, FixTable) {
|
||||
let mut output = String::with_capacity(locator.len());
|
||||
let mut last_pos: Option<Location> = None;
|
||||
let mut last_pos: Option<TextSize> = None;
|
||||
let mut applied: BTreeSet<&Edit> = BTreeSet::default();
|
||||
let mut fixed = FxHashMap::default();
|
||||
|
||||
for (rule, fix) in diagnostics
|
||||
.filter_map(|diagnostic| {
|
||||
if diagnostic.fix.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((diagnostic.kind.rule(), &diagnostic.fix))
|
||||
}
|
||||
diagnostic
|
||||
.fix
|
||||
.as_ref()
|
||||
.map(|fix| (diagnostic.kind.rule(), fix))
|
||||
})
|
||||
.sorted_by(|(rule1, fix1), (rule2, fix2)| cmp_fix(*rule1, *rule2, fix1, fix2))
|
||||
{
|
||||
@@ -57,7 +58,7 @@ fn apply_fixes<'a>(
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied,
|
||||
// skip it.
|
||||
if last_pos.map_or(false, |last_pos| {
|
||||
fix.min_location()
|
||||
fix.min_start()
|
||||
.map_or(false, |fix_location| last_pos >= fix_location)
|
||||
}) {
|
||||
continue;
|
||||
@@ -65,14 +66,14 @@ fn apply_fixes<'a>(
|
||||
|
||||
for edit in fix.edits() {
|
||||
// Add all contents from `last_pos` to `fix.location`.
|
||||
let slice = locator.slice(Range::new(last_pos.unwrap_or_default(), edit.location()));
|
||||
let slice = locator.slice(TextRange::new(last_pos.unwrap_or_default(), edit.start()));
|
||||
output.push_str(slice);
|
||||
|
||||
// Add the patch itself.
|
||||
output.push_str(edit.content().unwrap_or_default());
|
||||
|
||||
// Track that the edit was applied.
|
||||
last_pos = Some(edit.end_location());
|
||||
last_pos = Some(edit.end());
|
||||
applied.insert(edit);
|
||||
}
|
||||
|
||||
@@ -88,8 +89,8 @@ fn apply_fixes<'a>(
|
||||
|
||||
/// Compare two fixes.
|
||||
fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
|
||||
fix1.min_location()
|
||||
.cmp(&fix2.min_location())
|
||||
fix1.min_start()
|
||||
.cmp(&fix2.min_start())
|
||||
.then_with(|| match (&rule1, &rule2) {
|
||||
// Apply `EndsInPeriod` fixes before `NewLineAfterLastParagraph` fixes.
|
||||
(Rule::EndsInPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less,
|
||||
@@ -100,23 +101,24 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustpython_parser::ast::Location;
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
use crate::autofix::apply_fixes;
|
||||
use crate::rules::pycodestyle::rules::MissingNewlineAtEndOfFile;
|
||||
|
||||
#[allow(deprecated)]
|
||||
fn create_diagnostics(edit: impl IntoIterator<Item = Edit>) -> Vec<Diagnostic> {
|
||||
edit.into_iter()
|
||||
.map(|edit| Diagnostic {
|
||||
// The choice of rule here is arbitrary.
|
||||
kind: MissingNewlineAtEndOfFile.into(),
|
||||
location: edit.location(),
|
||||
end_location: edit.end_location(),
|
||||
fix: edit.into(),
|
||||
range: edit.range(),
|
||||
fix: Some(Fix::unspecified(edit)),
|
||||
parent: None,
|
||||
})
|
||||
.collect()
|
||||
@@ -142,8 +144,8 @@ class A(object):
|
||||
);
|
||||
let diagnostics = create_diagnostics([Edit::replacement(
|
||||
"Bar".to_string(),
|
||||
Location::new(1, 8),
|
||||
Location::new(1, 14),
|
||||
TextSize::new(8),
|
||||
TextSize::new(14),
|
||||
)]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
assert_eq!(
|
||||
@@ -166,8 +168,7 @@ class A(object):
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let diagnostics =
|
||||
create_diagnostics([Edit::deletion(Location::new(1, 7), Location::new(1, 15))]);
|
||||
let diagnostics = create_diagnostics([Edit::deletion(TextSize::new(7), TextSize::new(15))]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
@@ -190,8 +191,8 @@ class A(object, object, object):
|
||||
.trim(),
|
||||
);
|
||||
let diagnostics = create_diagnostics([
|
||||
Edit::deletion(Location::new(1, 8), Location::new(1, 16)),
|
||||
Edit::deletion(Location::new(1, 22), Location::new(1, 30)),
|
||||
Edit::deletion(TextSize::from(8), TextSize::from(16)),
|
||||
Edit::deletion(TextSize::from(22), TextSize::from(30)),
|
||||
]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
|
||||
@@ -216,12 +217,8 @@ class A(object):
|
||||
.trim(),
|
||||
);
|
||||
let diagnostics = create_diagnostics([
|
||||
Edit::deletion(Location::new(1, 7), Location::new(1, 15)),
|
||||
Edit::replacement(
|
||||
"ignored".to_string(),
|
||||
Location::new(1, 9),
|
||||
Location::new(1, 11),
|
||||
),
|
||||
Edit::deletion(TextSize::from(7), TextSize::from(15)),
|
||||
Edit::replacement("ignored".to_string(), TextSize::from(9), TextSize::from(11)),
|
||||
]);
|
||||
let (contents, fixed) = apply_fixes(diagnostics.iter(), &locator);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
use ruff_python_semantic::scope::ScopeStack;
|
||||
use rustpython_parser::ast::{Expr, Stmt};
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_python_ast::types::Range;
|
||||
use ruff_python_ast::types::RefEquality;
|
||||
use ruff_python_semantic::analyze::visibility::{Visibility, VisibleScope};
|
||||
|
||||
use crate::checkers::ast::AnnotationContext;
|
||||
use crate::docstrings::definition::Definition;
|
||||
|
||||
type Context<'a> = (ScopeStack, Vec<RefEquality<'a, Stmt>>);
|
||||
use ruff_python_semantic::context::Snapshot;
|
||||
|
||||
/// A collection of AST nodes that are deferred for later analysis.
|
||||
/// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all
|
||||
/// module-level definitions have been analyzed.
|
||||
#[derive(Default)]
|
||||
pub struct Deferred<'a> {
|
||||
pub definitions: Vec<(Definition<'a>, Visibility, Context<'a>)>,
|
||||
pub string_type_definitions: Vec<(Range, &'a str, AnnotationContext, Context<'a>)>,
|
||||
pub type_definitions: Vec<(&'a Expr, AnnotationContext, Context<'a>)>,
|
||||
pub functions: Vec<(&'a Stmt, Context<'a>, VisibleScope)>,
|
||||
pub lambdas: Vec<(&'a Expr, Context<'a>)>,
|
||||
pub for_loops: Vec<(&'a Stmt, Context<'a>)>,
|
||||
pub assignments: Vec<Context<'a>>,
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Deferred<'a> {
|
||||
pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>,
|
||||
pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) functions: Vec<Snapshot>,
|
||||
pub(crate) lambdas: Vec<(&'a Expr, Snapshot)>,
|
||||
pub(crate) for_loops: Vec<Snapshot>,
|
||||
pub(crate) assignments: Vec<Snapshot>,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ use crate::rules::flake8_no_pep420::rules::implicit_namespace_package;
|
||||
use crate::rules::pep8_naming::rules::invalid_module_name;
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub fn check_file_path(
|
||||
pub(crate) fn check_file_path(
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
settings: &Settings,
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
use std::borrow::Cow;
|
||||
use std::path::Path;
|
||||
|
||||
use rustpython_parser::ast::{StmtKind, Suite};
|
||||
use rustpython_parser::ast::{self, StmtKind, Suite};
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::helpers::to_module_path;
|
||||
use ruff_python_ast::imports::{ImportMap, ModuleImport};
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::statement_visitor::StatementVisitor;
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
|
||||
use crate::directives::IsortDirectives;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::isort;
|
||||
use crate::rules::isort::track::{Block, ImportTracker};
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::settings::Settings;
|
||||
|
||||
fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) -> Option<ImportMap> {
|
||||
let Some(package) = package else {
|
||||
@@ -29,22 +29,21 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
|
||||
let mut module_imports = Vec::with_capacity(num_imports);
|
||||
for stmt in blocks.iter().flat_map(|block| &block.imports) {
|
||||
match &stmt.node {
|
||||
StmtKind::Import { names } => {
|
||||
module_imports.extend(names.iter().map(|name| {
|
||||
ModuleImport::new(
|
||||
name.node.name.clone(),
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
)
|
||||
}));
|
||||
StmtKind::Import(ast::StmtImport { names }) => {
|
||||
module_imports.extend(
|
||||
names
|
||||
.iter()
|
||||
.map(|name| ModuleImport::new(name.node.name.to_string(), stmt.range())),
|
||||
);
|
||||
}
|
||||
StmtKind::ImportFrom {
|
||||
StmtKind::ImportFrom(ast::StmtImportFrom {
|
||||
module,
|
||||
names,
|
||||
level,
|
||||
} => {
|
||||
let level = level.unwrap_or(0);
|
||||
}) => {
|
||||
let level = level.map_or(0, |level| level.to_usize());
|
||||
let module = if let Some(module) = module {
|
||||
let module: &String = module.as_ref();
|
||||
if level == 0 {
|
||||
Cow::Borrowed(module)
|
||||
} else {
|
||||
@@ -61,11 +60,7 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
|
||||
Cow::Owned(module_path[..module_path.len() - level].join("."))
|
||||
};
|
||||
module_imports.extend(names.iter().map(|name| {
|
||||
ModuleImport::new(
|
||||
format!("{}.{}", module, name.node.name),
|
||||
name.location,
|
||||
name.end_location.unwrap(),
|
||||
)
|
||||
ModuleImport::new(format!("{}.{}", module, name.node.name), name.range())
|
||||
}));
|
||||
}
|
||||
_ => panic!("Expected StmtKind::Import | StmtKind::ImportFrom"),
|
||||
@@ -78,14 +73,13 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn check_imports(
|
||||
pub(crate) fn check_imports(
|
||||
python_ast: &Suite,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
directives: &IsortDirectives,
|
||||
settings: &Settings,
|
||||
stylist: &Stylist,
|
||||
autofix: flags::Autofix,
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
) -> (Vec<Diagnostic>, Option<ImportMap>) {
|
||||
@@ -105,7 +99,7 @@ pub fn check_imports(
|
||||
for block in &blocks {
|
||||
if !block.imports.is_empty() {
|
||||
if let Some(diagnostic) = isort::rules::organize_imports(
|
||||
block, locator, stylist, indexer, settings, autofix, package,
|
||||
block, locator, stylist, indexer, settings, package,
|
||||
) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -114,7 +108,7 @@ pub fn check_imports(
|
||||
}
|
||||
if settings.rules.enabled(Rule::MissingRequiredImport) {
|
||||
diagnostics.extend(isort::rules::add_required_imports(
|
||||
&blocks, python_ast, locator, stylist, settings, autofix, is_stub,
|
||||
&blocks, python_ast, locator, stylist, settings, is_stub,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use rustpython_parser::ast::Location;
|
||||
use ruff_text_size::TextRange;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Fix};
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
use ruff_python_ast::types::Range;
|
||||
use ruff_python_ast::token_kind::TokenKind;
|
||||
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rules::pycodestyle::rules::logical_lines::{
|
||||
@@ -12,7 +12,7 @@ use crate::rules::pycodestyle::rules::logical_lines::{
|
||||
whitespace_around_named_parameter_equals, whitespace_before_comment,
|
||||
whitespace_before_parameters, LogicalLines, TokenFlags,
|
||||
};
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// Return the amount of indentation, expanding tabs to the next multiple of 8.
|
||||
fn expand_indent(line: &str) -> usize {
|
||||
@@ -30,25 +30,23 @@ fn expand_indent(line: &str) -> usize {
|
||||
indent
|
||||
}
|
||||
|
||||
pub fn check_logical_lines(
|
||||
pub(crate) fn check_logical_lines(
|
||||
tokens: &[LexResult],
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics = vec![];
|
||||
let mut context = LogicalLinesContext::new(settings);
|
||||
|
||||
#[cfg(feature = "logical_lines")]
|
||||
let should_fix_missing_whitespace =
|
||||
autofix.into() && settings.rules.should_fix(Rule::MissingWhitespace);
|
||||
let should_fix_missing_whitespace = settings.rules.should_fix(Rule::MissingWhitespace);
|
||||
|
||||
#[cfg(not(feature = "logical_lines"))]
|
||||
let should_fix_missing_whitespace = false;
|
||||
|
||||
#[cfg(feature = "logical_lines")]
|
||||
let should_fix_whitespace_before_parameters =
|
||||
autofix.into() && settings.rules.should_fix(Rule::WhitespaceBeforeParameters);
|
||||
settings.rules.should_fix(Rule::WhitespaceBeforeParameters);
|
||||
|
||||
#[cfg(not(feature = "logical_lines"))]
|
||||
let should_fix_whitespace_before_parameters = false;
|
||||
@@ -59,120 +57,51 @@ pub fn check_logical_lines(
|
||||
|
||||
for line in &LogicalLines::from_tokens(tokens, locator) {
|
||||
if line.flags().contains(TokenFlags::OPERATOR) {
|
||||
for (location, kind) in space_around_operator(&line) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location,
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (location, kind) in whitespace_around_named_parameter_equals(&line.tokens()) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location,
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
for (location, kind) in missing_whitespace_around_operator(&line.tokens()) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location,
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for diagnostic in missing_whitespace(&line, should_fix_missing_whitespace) {
|
||||
if settings.rules.enabled(diagnostic.kind.rule()) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
space_around_operator(&line, &mut context);
|
||||
whitespace_around_named_parameter_equals(&line, &mut context);
|
||||
missing_whitespace_around_operator(&line, &mut context);
|
||||
missing_whitespace(&line, should_fix_missing_whitespace, &mut context);
|
||||
}
|
||||
|
||||
if line
|
||||
.flags()
|
||||
.contains(TokenFlags::OPERATOR | TokenFlags::PUNCTUATION)
|
||||
{
|
||||
for (location, kind) in extraneous_whitespace(&line) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location,
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
extraneous_whitespace(&line, &mut context);
|
||||
}
|
||||
if line.flags().contains(TokenFlags::KEYWORD) {
|
||||
for (location, kind) in whitespace_around_keywords(&line) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location,
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (location, kind) in missing_whitespace_after_keyword(&line.tokens()) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location,
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
whitespace_around_keywords(&line, &mut context);
|
||||
missing_whitespace_after_keyword(&line, &mut context);
|
||||
}
|
||||
|
||||
if line.flags().contains(TokenFlags::COMMENT) {
|
||||
for (range, kind) in whitespace_before_comment(&line.tokens(), locator) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location: range.location,
|
||||
end_location: range.end_location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
whitespace_before_comment(&line, locator, prev_line.is_none(), &mut context);
|
||||
}
|
||||
|
||||
if line.flags().contains(TokenFlags::BRACKET) {
|
||||
for diagnostic in whitespace_before_parameters(
|
||||
&line.tokens(),
|
||||
whitespace_before_parameters(
|
||||
&line,
|
||||
should_fix_whitespace_before_parameters,
|
||||
) {
|
||||
if settings.rules.enabled(diagnostic.kind.rule()) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
&mut context,
|
||||
);
|
||||
}
|
||||
|
||||
// Extract the indentation level.
|
||||
let Some(start_loc) = line.first_token_location() else { continue; };
|
||||
let start_line = locator.slice(Range::new(Location::new(start_loc.row(), 0), start_loc));
|
||||
let indent_level = expand_indent(start_line);
|
||||
let Some(first_token) = line.first_token() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let range = if first_token.kind() == TokenKind::Indent {
|
||||
first_token.range()
|
||||
} else {
|
||||
TextRange::new(locator.line_start(first_token.start()), first_token.start())
|
||||
};
|
||||
|
||||
let indent_level = expand_indent(locator.slice(range));
|
||||
|
||||
let indent_size = 4;
|
||||
|
||||
for (location, kind) in indentation(
|
||||
for kind in indentation(
|
||||
&line,
|
||||
prev_line.as_ref(),
|
||||
indent_char,
|
||||
@@ -181,13 +110,7 @@ pub fn check_logical_lines(
|
||||
indent_size,
|
||||
) {
|
||||
if settings.rules.enabled(kind.rule()) {
|
||||
diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
location: Location::new(start_loc.row(), 0),
|
||||
end_location: location,
|
||||
fix: Fix::empty(),
|
||||
parent: None,
|
||||
});
|
||||
context.push(kind, range);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +119,40 @@ pub fn check_logical_lines(
|
||||
prev_indent_level = Some(indent_level);
|
||||
}
|
||||
}
|
||||
diagnostics
|
||||
context.diagnostics
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LogicalLinesContext<'a> {
|
||||
settings: &'a Settings,
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
impl<'a> LogicalLinesContext<'a> {
|
||||
fn new(settings: &'a Settings) -> Self {
|
||||
Self {
|
||||
settings,
|
||||
diagnostics: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn push<K: Into<DiagnosticKind>>(&mut self, kind: K, range: TextRange) {
|
||||
let kind = kind.into();
|
||||
if self.settings.rules.enabled(kind.rule()) {
|
||||
self.diagnostics.push(Diagnostic {
|
||||
kind,
|
||||
range,
|
||||
fix: None,
|
||||
parent: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
|
||||
if self.settings.rules.enabled(diagnostic.kind.rule()) {
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pub mod ast;
|
||||
pub mod filesystem;
|
||||
pub mod imports;
|
||||
pub(crate) mod ast;
|
||||
pub(crate) mod filesystem;
|
||||
pub(crate) mod imports;
|
||||
#[cfg(feature = "logical_lines")]
|
||||
pub(crate) mod logical_lines;
|
||||
pub mod noqa;
|
||||
pub mod physical_lines;
|
||||
pub mod tokens;
|
||||
pub(crate) mod noqa;
|
||||
pub(crate) mod physical_lines;
|
||||
pub(crate) mod tokens;
|
||||
|
||||
@@ -1,53 +1,38 @@
|
||||
//! `NoQA` enforcement and validation.
|
||||
|
||||
use nohash_hasher::IntMap;
|
||||
use rustpython_parser::ast::Location;
|
||||
use itertools::Itertools;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit};
|
||||
use ruff_python_ast::newlines::StrExt;
|
||||
use ruff_python_ast::types::Range;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
use crate::codes::NoqaCode;
|
||||
use crate::noqa;
|
||||
use crate::noqa::{Directive, FileExemption};
|
||||
use crate::noqa::{Directive, FileExemption, NoqaDirectives, NoqaMapping};
|
||||
use crate::registry::{AsRule, Rule};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub fn check_noqa(
|
||||
pub(crate) fn check_noqa(
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
contents: &str,
|
||||
commented_lines: &[usize],
|
||||
noqa_line_for: &IntMap<usize, usize>,
|
||||
locator: &Locator,
|
||||
comment_ranges: &[TextRange],
|
||||
noqa_line_for: &NoqaMapping,
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Vec<usize> {
|
||||
let enforce_noqa = settings.rules.enabled(Rule::UnusedNOQA);
|
||||
|
||||
let lines: Vec<&str> = contents.universal_newlines().collect();
|
||||
|
||||
// Identify any codes that are globally exempted (within the current file).
|
||||
let exemption = noqa::file_exemption(&lines, commented_lines);
|
||||
|
||||
// Map from line number to `noqa` directive on that line, along with any codes
|
||||
// that were matched by the directive.
|
||||
let mut noqa_directives: IntMap<usize, (Directive, Vec<NoqaCode>)> = IntMap::default();
|
||||
let exemption = noqa::file_exemption(locator.contents(), comment_ranges);
|
||||
|
||||
// Extract all `noqa` directives.
|
||||
if enforce_noqa {
|
||||
for lineno in commented_lines {
|
||||
noqa_directives
|
||||
.entry(lineno - 1)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[lineno - 1]), vec![]));
|
||||
}
|
||||
}
|
||||
let mut noqa_directives = NoqaDirectives::from_commented_ranges(comment_ranges, locator);
|
||||
|
||||
// Indices of diagnostics that were ignored by a `noqa` directive.
|
||||
let mut ignored_diagnostics = vec![];
|
||||
|
||||
// Remove any ignored diagnostics.
|
||||
for (index, diagnostic) in diagnostics.iter().enumerate() {
|
||||
'outer: for (index, diagnostic) in diagnostics.iter().enumerate() {
|
||||
if matches!(diagnostic.kind.rule(), Rule::BlanketNOQA) {
|
||||
continue;
|
||||
}
|
||||
@@ -68,92 +53,66 @@ pub fn check_noqa(
|
||||
FileExemption::None => {}
|
||||
}
|
||||
|
||||
let diagnostic_lineno = diagnostic.location.row();
|
||||
let noqa_offsets = diagnostic
|
||||
.parent
|
||||
.into_iter()
|
||||
.chain(std::iter::once(diagnostic.start()))
|
||||
.map(|position| noqa_line_for.resolve(position))
|
||||
.unique();
|
||||
|
||||
// Is the violation ignored by a `noqa` directive on the parent line?
|
||||
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
|
||||
if parent_lineno != diagnostic_lineno {
|
||||
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
let noqa = noqa_directives.entry(noqa_lineno - 1).or_insert_with(|| {
|
||||
(noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![])
|
||||
});
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
(Directive::Codes(.., codes, _), matches) => {
|
||||
if noqa::includes(diagnostic.kind.rule(), codes) {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
(Directive::None, ..) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is the diagnostic ignored by a `noqa` directive on the same line?
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(&diagnostic_lineno)
|
||||
.unwrap_or(&diagnostic_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno - 1)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![]));
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
(Directive::Codes(.., codes, _), matches) => {
|
||||
if noqa::includes(diagnostic.kind.rule(), codes) {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
for noqa_offset in noqa_offsets {
|
||||
if let Some(directive_line) = noqa_directives.find_line_with_directive_mut(noqa_offset)
|
||||
{
|
||||
let suppressed = match &directive_line.directive {
|
||||
Directive::All(..) => {
|
||||
directive_line
|
||||
.matches
|
||||
.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
true
|
||||
}
|
||||
Directive::Codes(.., codes, _) => {
|
||||
if noqa::includes(diagnostic.kind.rule(), codes) {
|
||||
directive_line
|
||||
.matches
|
||||
.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored_diagnostics.push(index);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Directive::None => unreachable!(),
|
||||
};
|
||||
|
||||
if suppressed {
|
||||
continue 'outer;
|
||||
}
|
||||
(Directive::None, ..) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce that the noqa directive was actually used (RUF100).
|
||||
if enforce_noqa {
|
||||
for (row, (directive, matches)) in noqa_directives {
|
||||
match directive {
|
||||
Directive::All(leading_spaces, start_byte, end_byte, trailing_spaces) => {
|
||||
if matches.is_empty() {
|
||||
let start_char = lines[row][..start_byte].chars().count();
|
||||
let end_char =
|
||||
start_char + lines[row][start_byte..end_byte].chars().count();
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnusedNOQA { codes: None },
|
||||
Range::new(
|
||||
Location::new(row + 1, start_char),
|
||||
Location::new(row + 1, end_char),
|
||||
),
|
||||
);
|
||||
if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
diagnostic.set_fix(delete_noqa(
|
||||
row,
|
||||
lines[row],
|
||||
leading_spaces,
|
||||
start_byte,
|
||||
end_byte,
|
||||
trailing_spaces,
|
||||
for line in noqa_directives.lines() {
|
||||
match &line.directive {
|
||||
Directive::All(leading_spaces, noqa_range, trailing_spaces) => {
|
||||
if line.matches.is_empty() {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(UnusedNOQA { codes: None }, *noqa_range);
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix_from_edit(delete_noqa(
|
||||
*leading_spaces,
|
||||
*noqa_range,
|
||||
*trailing_spaces,
|
||||
locator,
|
||||
));
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
Directive::Codes(leading_spaces, start_byte, end_byte, codes, trailing_spaces) => {
|
||||
Directive::Codes(leading_spaces, range, codes, trailing_spaces) => {
|
||||
let mut disabled_codes = vec![];
|
||||
let mut unknown_codes = vec![];
|
||||
let mut unmatched_codes = vec![];
|
||||
@@ -166,7 +125,9 @@ pub fn check_noqa(
|
||||
break;
|
||||
}
|
||||
|
||||
if matches.iter().any(|m| *m == code) || settings.external.contains(code) {
|
||||
if line.matches.iter().any(|m| *m == code)
|
||||
|| settings.external.contains(code)
|
||||
{
|
||||
valid_codes.push(code);
|
||||
} else {
|
||||
if let Ok(rule) = Rule::from_code(code) {
|
||||
@@ -189,10 +150,6 @@ pub fn check_noqa(
|
||||
&& unknown_codes.is_empty()
|
||||
&& unmatched_codes.is_empty())
|
||||
{
|
||||
let start_char = lines[row][..start_byte].chars().count();
|
||||
let end_char =
|
||||
start_char + lines[row][start_byte..end_byte].chars().count();
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnusedNOQA {
|
||||
codes: Some(UnusedCodes {
|
||||
@@ -210,27 +167,23 @@ pub fn check_noqa(
|
||||
.collect(),
|
||||
}),
|
||||
},
|
||||
Range::new(
|
||||
Location::new(row + 1, start_char),
|
||||
Location::new(row + 1, end_char),
|
||||
),
|
||||
*range,
|
||||
);
|
||||
if autofix.into() && settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||
if valid_codes.is_empty() {
|
||||
diagnostic.set_fix(delete_noqa(
|
||||
row,
|
||||
lines[row],
|
||||
leading_spaces,
|
||||
start_byte,
|
||||
end_byte,
|
||||
trailing_spaces,
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix_from_edit(delete_noqa(
|
||||
*leading_spaces,
|
||||
*range,
|
||||
*trailing_spaces,
|
||||
locator,
|
||||
));
|
||||
} else {
|
||||
diagnostic.set_fix(Edit::replacement(
|
||||
#[allow(deprecated)]
|
||||
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
|
||||
format!("# noqa: {}", valid_codes.join(", ")),
|
||||
Location::new(row + 1, start_char),
|
||||
Location::new(row + 1, end_char),
|
||||
));
|
||||
*range,
|
||||
)));
|
||||
}
|
||||
}
|
||||
diagnostics.push(diagnostic);
|
||||
@@ -247,39 +200,37 @@ pub fn check_noqa(
|
||||
|
||||
/// Generate a [`Edit`] to delete a `noqa` directive.
|
||||
fn delete_noqa(
|
||||
row: usize,
|
||||
line: &str,
|
||||
leading_spaces: usize,
|
||||
start_byte: usize,
|
||||
end_byte: usize,
|
||||
trailing_spaces: usize,
|
||||
leading_spaces: TextSize,
|
||||
noqa_range: TextRange,
|
||||
trailing_spaces: TextSize,
|
||||
locator: &Locator,
|
||||
) -> Edit {
|
||||
if start_byte - leading_spaces == 0 && end_byte == line.len() {
|
||||
// Ex) `# noqa`
|
||||
Edit::deletion(Location::new(row + 1, 0), Location::new(row + 2, 0))
|
||||
} else if end_byte == line.len() {
|
||||
// Ex) `x = 1 # noqa`
|
||||
let start_char = line[..start_byte].chars().count();
|
||||
let end_char = start_char + line[start_byte..end_byte].chars().count();
|
||||
Edit::deletion(
|
||||
Location::new(row + 1, start_char - leading_spaces),
|
||||
Location::new(row + 1, end_char + trailing_spaces),
|
||||
let line_range = locator.line_range(noqa_range.start());
|
||||
|
||||
// Ex) `# noqa`
|
||||
if line_range
|
||||
== TextRange::new(
|
||||
noqa_range.start() - leading_spaces,
|
||||
noqa_range.end() + trailing_spaces,
|
||||
)
|
||||
} else if line[end_byte..].trim_start().starts_with('#') {
|
||||
// Ex) `x = 1 # noqa # type: ignore`
|
||||
let start_char = line[..start_byte].chars().count();
|
||||
let end_char = start_char + line[start_byte..end_byte].chars().count();
|
||||
{
|
||||
let full_line_end = locator.full_line_end(line_range.end());
|
||||
Edit::deletion(line_range.start(), full_line_end)
|
||||
}
|
||||
// Ex) `x = 1 # noqa`
|
||||
else if noqa_range.end() + trailing_spaces == line_range.end() {
|
||||
Edit::deletion(noqa_range.start() - leading_spaces, line_range.end())
|
||||
}
|
||||
// Ex) `x = 1 # noqa # type: ignore`
|
||||
else if locator.contents()[usize::from(noqa_range.end() + trailing_spaces)..].starts_with('#')
|
||||
{
|
||||
Edit::deletion(noqa_range.start(), noqa_range.end() + trailing_spaces)
|
||||
}
|
||||
// Ex) `x = 1 # noqa here`
|
||||
else {
|
||||
Edit::deletion(
|
||||
Location::new(row + 1, start_char),
|
||||
Location::new(row + 1, end_char + trailing_spaces),
|
||||
)
|
||||
} else {
|
||||
// Ex) `x = 1 # noqa here`
|
||||
let start_char = line[..start_byte].chars().count();
|
||||
let end_char = start_char + line[start_byte..end_byte].chars().count();
|
||||
Edit::deletion(
|
||||
Location::new(row + 1, start_char + 1 + 1),
|
||||
Location::new(row + 1, end_char + trailing_spaces),
|
||||
noqa_range.start() + "# ".text_len(),
|
||||
noqa_range.end() + trailing_spaces,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Lint rules based on checking physical lines.
|
||||
|
||||
use ruff_text_size::TextSize;
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
@@ -18,16 +19,15 @@ use crate::rules::pycodestyle::rules::{
|
||||
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
|
||||
use crate::rules::pylint;
|
||||
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub fn check_physical_lines(
|
||||
pub(crate) fn check_physical_lines(
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
indexer: &Indexer,
|
||||
doc_lines: &[usize],
|
||||
doc_lines: &[TextSize],
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
let mut has_any_shebang = false;
|
||||
@@ -50,25 +50,22 @@ pub fn check_physical_lines(
|
||||
settings.rules.enabled(Rule::BlankLineWithWhitespace);
|
||||
let enforce_tab_indentation = settings.rules.enabled(Rule::TabIndentation);
|
||||
|
||||
let fix_unnecessary_coding_comment =
|
||||
autofix.into() && settings.rules.should_fix(Rule::UTF8EncodingDeclaration);
|
||||
let fix_shebang_whitespace =
|
||||
autofix.into() && settings.rules.should_fix(Rule::ShebangLeadingWhitespace);
|
||||
let fix_unnecessary_coding_comment = settings.rules.should_fix(Rule::UTF8EncodingDeclaration);
|
||||
let fix_shebang_whitespace = settings.rules.should_fix(Rule::ShebangLeadingWhitespace);
|
||||
|
||||
let mut commented_lines_iter = indexer.commented_lines().iter().peekable();
|
||||
let mut commented_lines_iter = indexer.comment_ranges().iter().peekable();
|
||||
let mut doc_lines_iter = doc_lines.iter().peekable();
|
||||
|
||||
let string_lines = indexer.string_ranges();
|
||||
let string_lines = indexer.triple_quoted_string_ranges();
|
||||
|
||||
for (index, line) in locator.contents().universal_newlines().enumerate() {
|
||||
while commented_lines_iter
|
||||
.next_if(|lineno| &(index + 1) == *lineno)
|
||||
.next_if(|comment_range| line.range().contains_range(**comment_range))
|
||||
.is_some()
|
||||
{
|
||||
if enforce_unnecessary_coding_comment {
|
||||
if index < 2 {
|
||||
if let Some(diagnostic) =
|
||||
unnecessary_coding_comment(index, line, fix_unnecessary_coding_comment)
|
||||
unnecessary_coding_comment(&line, fix_unnecessary_coding_comment)
|
||||
{
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -76,11 +73,11 @@ pub fn check_physical_lines(
|
||||
}
|
||||
|
||||
if enforce_blanket_type_ignore {
|
||||
blanket_type_ignore(&mut diagnostics, index, line);
|
||||
blanket_type_ignore(&mut diagnostics, &line);
|
||||
}
|
||||
|
||||
if enforce_blanket_noqa {
|
||||
blanket_noqa(&mut diagnostics, index, line);
|
||||
blanket_noqa(&mut diagnostics, &line);
|
||||
}
|
||||
|
||||
if enforce_shebang_missing
|
||||
@@ -89,31 +86,31 @@ pub fn check_physical_lines(
|
||||
|| enforce_shebang_newline
|
||||
|| enforce_shebang_python
|
||||
{
|
||||
let shebang = extract_shebang(line);
|
||||
let shebang = extract_shebang(&line);
|
||||
if enforce_shebang_not_executable {
|
||||
if let Some(diagnostic) = shebang_not_executable(path, index, &shebang) {
|
||||
if let Some(diagnostic) = shebang_not_executable(path, line.range(), &shebang) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if enforce_shebang_missing {
|
||||
if !has_any_shebang && matches!(shebang, ShebangDirective::Match(_, _, _, _)) {
|
||||
if !has_any_shebang && matches!(shebang, ShebangDirective::Match(..)) {
|
||||
has_any_shebang = true;
|
||||
}
|
||||
}
|
||||
if enforce_shebang_whitespace {
|
||||
if let Some(diagnostic) =
|
||||
shebang_whitespace(index, &shebang, fix_shebang_whitespace)
|
||||
shebang_whitespace(line.range(), &shebang, fix_shebang_whitespace)
|
||||
{
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if enforce_shebang_newline {
|
||||
if let Some(diagnostic) = shebang_newline(index, &shebang) {
|
||||
if let Some(diagnostic) = shebang_newline(line.range(), &shebang, index == 0) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if enforce_shebang_python {
|
||||
if let Some(diagnostic) = shebang_python(index, &shebang) {
|
||||
if let Some(diagnostic) = shebang_python(line.range(), &shebang) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -121,40 +118,40 @@ pub fn check_physical_lines(
|
||||
}
|
||||
|
||||
while doc_lines_iter
|
||||
.next_if(|lineno| &(index + 1) == *lineno)
|
||||
.next_if(|doc_line_start| line.range().contains_inclusive(**doc_line_start))
|
||||
.is_some()
|
||||
{
|
||||
if enforce_doc_line_too_long {
|
||||
if let Some(diagnostic) = doc_line_too_long(index, line, settings) {
|
||||
if let Some(diagnostic) = doc_line_too_long(&line, settings) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_mixed_spaces_and_tabs {
|
||||
if let Some(diagnostic) = mixed_spaces_and_tabs(index, line) {
|
||||
if let Some(diagnostic) = mixed_spaces_and_tabs(&line) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_line_too_long {
|
||||
if let Some(diagnostic) = line_too_long(index, line, settings) {
|
||||
if let Some(diagnostic) = line_too_long(&line, settings) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_bidirectional_unicode {
|
||||
diagnostics.extend(pylint::rules::bidirectional_unicode(index, line));
|
||||
diagnostics.extend(pylint::rules::bidirectional_unicode(&line));
|
||||
}
|
||||
|
||||
if enforce_trailing_whitespace || enforce_blank_line_contains_whitespace {
|
||||
if let Some(diagnostic) = trailing_whitespace(index, line, settings, autofix) {
|
||||
if let Some(diagnostic) = trailing_whitespace(&line, settings) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_tab_indentation {
|
||||
if let Some(diagnostic) = tab_indentation(index + 1, line, string_lines) {
|
||||
if let Some(diagnostic) = tab_indentation(&line, string_lines) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -164,7 +161,7 @@ pub fn check_physical_lines(
|
||||
if let Some(diagnostic) = no_newline_at_end_of_file(
|
||||
locator,
|
||||
stylist,
|
||||
autofix.into() && settings.rules.should_fix(Rule::MissingNewlineAtEndOfFile),
|
||||
settings.rules.should_fix(Rule::MissingNewlineAtEndOfFile),
|
||||
) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -188,7 +185,7 @@ mod tests {
|
||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::settings::Settings;
|
||||
|
||||
use super::check_physical_lines;
|
||||
|
||||
@@ -197,7 +194,7 @@ mod tests {
|
||||
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
|
||||
let locator = Locator::new(line);
|
||||
let tokens: Vec<_> = lex(line, Mode::Module).collect();
|
||||
let indexer: Indexer = tokens.as_slice().into();
|
||||
let indexer = Indexer::from_tokens(&tokens, &locator);
|
||||
let stylist = Stylist::from_tokens(&tokens, &locator);
|
||||
|
||||
let check_with_max_line_length = |line_length: usize| {
|
||||
@@ -211,7 +208,6 @@ mod tests {
|
||||
line_length,
|
||||
..Settings::for_rule(Rule::LineTooLong)
|
||||
},
|
||||
flags::Autofix::Enabled,
|
||||
)
|
||||
};
|
||||
assert_eq!(check_with_max_line_length(8), vec![]);
|
||||
|
||||
@@ -10,15 +10,14 @@ use crate::rules::{
|
||||
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_pyi, flake8_quotes, pycodestyle,
|
||||
pylint, pyupgrade, ruff,
|
||||
};
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::settings::Settings;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
|
||||
pub fn check_tokens(
|
||||
pub(crate) fn check_tokens(
|
||||
locator: &Locator,
|
||||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
is_stub: bool,
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
@@ -64,7 +63,7 @@ pub fn check_tokens(
|
||||
// RUF001, RUF002, RUF003
|
||||
if enforce_ambiguous_unicode_character {
|
||||
let mut state_machine = StateMachine::default();
|
||||
for &(start, ref tok, end) in tokens.iter().flatten() {
|
||||
for &(ref tok, range) in tokens.iter().flatten() {
|
||||
let is_docstring = if enforce_ambiguous_unicode_character {
|
||||
state_machine.consume(tok)
|
||||
} else {
|
||||
@@ -74,8 +73,7 @@ pub fn check_tokens(
|
||||
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
|
||||
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
|
||||
locator,
|
||||
start,
|
||||
end,
|
||||
range,
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
if is_docstring {
|
||||
Context::Docstring
|
||||
@@ -86,7 +84,6 @@ pub fn check_tokens(
|
||||
Context::Comment
|
||||
},
|
||||
settings,
|
||||
autofix,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -94,10 +91,10 @@ pub fn check_tokens(
|
||||
|
||||
// ERA001
|
||||
if enforce_commented_out_code {
|
||||
for (start, tok, end) in tokens.iter().flatten() {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if matches!(tok, Tok::Comment(_)) {
|
||||
if let Some(diagnostic) =
|
||||
eradicate::rules::commented_out_code(locator, *start, *end, settings, autofix)
|
||||
eradicate::rules::commented_out_code(locator, *range, settings)
|
||||
{
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -107,23 +104,22 @@ pub fn check_tokens(
|
||||
|
||||
// W605
|
||||
if enforce_invalid_escape_sequence {
|
||||
for (start, tok, end) in tokens.iter().flatten() {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
|
||||
locator,
|
||||
*start,
|
||||
*end,
|
||||
autofix.into() && settings.rules.should_fix(Rule::InvalidEscapeSequence),
|
||||
*range,
|
||||
settings.rules.should_fix(Rule::InvalidEscapeSequence),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// PLE2510, PLE2512, PLE2513
|
||||
if enforce_invalid_string_character {
|
||||
for (start, tok, end) in tokens.iter().flatten() {
|
||||
for (tok, range) in tokens.iter().flatten() {
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
diagnostics.extend(
|
||||
pylint::rules::invalid_string_characters(locator, *start, *end, autofix.into())
|
||||
pylint::rules::invalid_string_characters(locator, *range)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
);
|
||||
@@ -134,7 +130,7 @@ pub fn check_tokens(
|
||||
// E701, E702, E703
|
||||
if enforce_compound_statements {
|
||||
diagnostics.extend(
|
||||
pycodestyle::rules::compound_statements(tokens, settings, autofix)
|
||||
pycodestyle::rules::compound_statements(tokens, settings)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
);
|
||||
@@ -143,7 +139,7 @@ pub fn check_tokens(
|
||||
// Q001, Q002, Q003
|
||||
if enforce_quotes {
|
||||
diagnostics.extend(
|
||||
flake8_quotes::rules::from_tokens(tokens, locator, settings, autofix)
|
||||
flake8_quotes::rules::from_tokens(tokens, locator, settings)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
);
|
||||
@@ -155,6 +151,7 @@ pub fn check_tokens(
|
||||
flake8_implicit_str_concat::rules::implicit(
|
||||
tokens,
|
||||
&settings.flake8_implicit_str_concat,
|
||||
locator,
|
||||
)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
@@ -164,7 +161,7 @@ pub fn check_tokens(
|
||||
// COM812, COM818, COM819
|
||||
if enforce_trailing_comma {
|
||||
diagnostics.extend(
|
||||
flake8_commas::rules::trailing_commas(tokens, locator, settings, autofix)
|
||||
flake8_commas::rules::trailing_commas(tokens, locator, settings)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
);
|
||||
@@ -173,8 +170,7 @@ pub fn check_tokens(
|
||||
// UP034
|
||||
if enforce_extraneous_parenthesis {
|
||||
diagnostics.extend(
|
||||
pyupgrade::rules::extraneous_parentheses(tokens, locator, settings, autofix)
|
||||
.into_iter(),
|
||||
pyupgrade::rules::extraneous_parentheses(tokens, locator, settings).into_iter(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
use std::fmt::Formatter;
|
||||
|
||||
use crate::registry::{Linter, Rule};
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct NoqaCode(&'static str, &'static str);
|
||||
|
||||
impl std::fmt::Debug for NoqaCode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for NoqaCode {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(f, "{}{}", self.0, self.1)
|
||||
@@ -197,11 +205,14 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Pylint, "R5501") => Rule::CollapsibleElseIf,
|
||||
(Pylint, "W0120") => Rule::UselessElseOnLoop,
|
||||
(Pylint, "W0129") => Rule::AssertOnStringLiteral,
|
||||
(Pylint, "W0406") => Rule::ImportSelf,
|
||||
(Pylint, "W0602") => Rule::GlobalVariableNotAssigned,
|
||||
(Pylint, "W0603") => Rule::GlobalStatement,
|
||||
(Pylint, "W0711") => Rule::BinaryOpException,
|
||||
(Pylint, "W1508") => Rule::InvalidEnvvarDefault,
|
||||
(Pylint, "W2901") => Rule::RedefinedLoopName,
|
||||
(Pylint, "E0302") => Rule::UnexpectedSpecialMethodSignature,
|
||||
(Pylint, "W3301") => Rule::NestedMinMax,
|
||||
|
||||
// flake8-builtins
|
||||
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,
|
||||
@@ -534,6 +545,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
// flake8-import-conventions
|
||||
(Flake8ImportConventions, "001") => Rule::UnconventionalImportAlias,
|
||||
(Flake8ImportConventions, "002") => Rule::BannedImportAlias,
|
||||
(Flake8ImportConventions, "003") => Rule::BannedImportFrom,
|
||||
|
||||
// flake8-datetimez
|
||||
(Flake8Datetimez, "001") => Rule::CallDatetimeWithoutTzinfo,
|
||||
@@ -551,6 +563,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(PygrepHooks, "002") => Rule::DeprecatedLogWarn,
|
||||
(PygrepHooks, "003") => Rule::BlanketTypeIgnore,
|
||||
(PygrepHooks, "004") => Rule::BlanketNOQA,
|
||||
(PygrepHooks, "005") => Rule::InvalidMockAccess,
|
||||
|
||||
// pandas-vet
|
||||
(PandasVet, "002") => Rule::PandasUseOfInplaceArgument,
|
||||
@@ -583,8 +596,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Pyi, "014") => Rule::ArgumentDefaultInStub,
|
||||
(Flake8Pyi, "015") => Rule::AssignmentDefaultInStub,
|
||||
(Flake8Pyi, "016") => Rule::DuplicateUnionMember,
|
||||
(Flake8Pyi, "020") => Rule::QuotedAnnotationInStub,
|
||||
(Flake8Pyi, "021") => Rule::DocstringInStub,
|
||||
(Flake8Pyi, "033") => Rule::TypeCommentInStub,
|
||||
(Flake8Pyi, "042") => Rule::SnakeCaseTypeAlias,
|
||||
(Flake8Pyi, "043") => Rule::TSuffixedTypeAlias,
|
||||
|
||||
// flake8-pytest-style
|
||||
(Flake8PytestStyle, "001") => Rule::PytestFixtureIncorrectParenthesesStyle,
|
||||
@@ -722,6 +738,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Django, "012") => Rule::DjangoUnorderedBodyContentInModel,
|
||||
(Flake8Django, "013") => Rule::DjangoNonLeadingReceiverDecorator,
|
||||
|
||||
// flynt
|
||||
// Reserved: (Flynt, "001") => Rule::StringConcatenationToFString,
|
||||
(Flynt, "002") => Rule::StaticJoinToFString,
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_call_path(expr: &Expression) -> Option<String> {
|
||||
pub(crate) fn compose_call_path(expr: &Expression) -> Option<String> {
|
||||
let mut segments = vec![];
|
||||
compose_call_path_inner(expr, &mut segments);
|
||||
if segments.is_empty() {
|
||||
@@ -26,7 +26,7 @@ pub fn compose_call_path(expr: &Expression) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_module_path(module: &NameOrAttribute) -> String {
|
||||
pub(crate) fn compose_module_path(module: &NameOrAttribute) -> String {
|
||||
match module {
|
||||
NameOrAttribute::N(name) => name.value.to_string(),
|
||||
NameOrAttribute::A(attr) => {
|
||||
|
||||
@@ -4,21 +4,21 @@ use libcst_native::{
|
||||
ImportNames, Module, SimpleString, SmallStatement, Statement,
|
||||
};
|
||||
|
||||
pub fn match_module(module_text: &str) -> Result<Module> {
|
||||
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
|
||||
match libcst_native::parse_module(module_text, None) {
|
||||
Ok(module) => Ok(module),
|
||||
Err(_) => bail!("Failed to extract CST from source"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_expression(expression_text: &str) -> Result<Expression> {
|
||||
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
|
||||
match libcst_native::parse_expression(expression_text) {
|
||||
Ok(expression) => Ok(expression),
|
||||
Err(_) => bail!("Failed to extract CST from source"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
|
||||
pub(crate) fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
|
||||
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
|
||||
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
|
||||
Ok(expr)
|
||||
@@ -30,7 +30,7 @@ pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Import<'b>> {
|
||||
pub(crate) fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Import<'b>> {
|
||||
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
|
||||
if let Some(SmallStatement::Import(expr)) = expr.body.first_mut() {
|
||||
Ok(expr)
|
||||
@@ -42,7 +42,9 @@ pub fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Import
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut ImportFrom<'b>> {
|
||||
pub(crate) fn match_import_from<'a, 'b>(
|
||||
module: &'a mut Module<'b>,
|
||||
) -> Result<&'a mut ImportFrom<'b>> {
|
||||
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
|
||||
if let Some(SmallStatement::ImportFrom(expr)) = expr.body.first_mut() {
|
||||
Ok(expr)
|
||||
@@ -54,7 +56,7 @@ pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut I
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_aliases<'a, 'b>(
|
||||
pub(crate) fn match_aliases<'a, 'b>(
|
||||
import_from: &'a mut ImportFrom<'b>,
|
||||
) -> Result<&'a mut Vec<ImportAlias<'b>>> {
|
||||
if let ImportNames::Aliases(aliases) = &mut import_from.names {
|
||||
@@ -64,7 +66,7 @@ pub fn match_aliases<'a, 'b>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Call<'b>> {
|
||||
pub(crate) fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Call<'b>> {
|
||||
if let Expression::Call(call) = expression {
|
||||
Ok(call)
|
||||
} else {
|
||||
@@ -72,7 +74,7 @@ pub fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_comparison<'a, 'b>(
|
||||
pub(crate) fn match_comparison<'a, 'b>(
|
||||
expression: &'a mut Expression<'b>,
|
||||
) -> Result<&'a mut Comparison<'b>> {
|
||||
if let Expression::Comparison(comparison) = expression {
|
||||
@@ -82,7 +84,7 @@ pub fn match_comparison<'a, 'b>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_dict<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Dict<'b>> {
|
||||
pub(crate) fn match_dict<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Dict<'b>> {
|
||||
if let Expression::Dict(dict) = expression {
|
||||
Ok(dict)
|
||||
} else {
|
||||
@@ -90,7 +92,7 @@ pub fn match_dict<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_attribute<'a, 'b>(
|
||||
pub(crate) fn match_attribute<'a, 'b>(
|
||||
expression: &'a mut Expression<'b>,
|
||||
) -> Result<&'a mut Attribute<'b>> {
|
||||
if let Expression::Attribute(attribute) = expression {
|
||||
@@ -100,7 +102,7 @@ pub fn match_attribute<'a, 'b>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_simple_string<'a, 'b>(
|
||||
pub(crate) fn match_simple_string<'a, 'b>(
|
||||
expression: &'a mut Expression<'b>,
|
||||
) -> Result<&'a mut SimpleString<'b>> {
|
||||
if let Expression::SimpleString(simple_string) = expression {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
pub mod helpers;
|
||||
pub mod matchers;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod matchers;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
//! Extract `# noqa` and `# isort: skip` directives from tokenized source.
|
||||
|
||||
use crate::noqa::NoqaMapping;
|
||||
use bitflags::bitflags;
|
||||
use nohash_hasher::{IntMap, IntSet};
|
||||
use rustpython_parser::ast::Location;
|
||||
use ruff_python_ast::source_code::{Indexer, Locator};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::Tok;
|
||||
|
||||
use crate::settings::Settings;
|
||||
|
||||
bitflags! {
|
||||
pub struct Flags: u32 {
|
||||
const NOQA = 0b0000_0001;
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Flags: u8 {
|
||||
const NOQA = 0b0000_0001;
|
||||
const ISORT = 0b0000_0010;
|
||||
}
|
||||
}
|
||||
@@ -29,27 +31,50 @@ impl Flags {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct IsortDirectives {
|
||||
pub exclusions: IntSet<usize>,
|
||||
pub splits: Vec<usize>,
|
||||
/// Ranges for which sorting is disabled
|
||||
pub exclusions: Vec<TextRange>,
|
||||
/// Text positions at which splits should be inserted
|
||||
pub splits: Vec<TextSize>,
|
||||
pub skip_file: bool,
|
||||
}
|
||||
|
||||
impl IsortDirectives {
|
||||
pub fn is_excluded(&self, offset: TextSize) -> bool {
|
||||
for range in &self.exclusions {
|
||||
if range.contains(offset) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if range.start() > offset {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Directives {
|
||||
pub noqa_line_for: IntMap<usize, usize>,
|
||||
pub noqa_line_for: NoqaMapping,
|
||||
pub isort: IsortDirectives,
|
||||
}
|
||||
|
||||
pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives {
|
||||
pub fn extract_directives(
|
||||
lxr: &[LexResult],
|
||||
flags: Flags,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
) -> Directives {
|
||||
Directives {
|
||||
noqa_line_for: if flags.contains(Flags::NOQA) {
|
||||
extract_noqa_line_for(lxr)
|
||||
extract_noqa_line_for(lxr, locator, indexer)
|
||||
} else {
|
||||
IntMap::default()
|
||||
NoqaMapping::default()
|
||||
},
|
||||
isort: if flags.contains(Flags::ISORT) {
|
||||
extract_isort_directives(lxr)
|
||||
extract_isort_directives(lxr, locator)
|
||||
} else {
|
||||
IsortDirectives::default()
|
||||
},
|
||||
@@ -57,48 +82,92 @@ pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives {
|
||||
}
|
||||
|
||||
/// Extract a mapping from logical line to noqa line.
|
||||
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
|
||||
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();
|
||||
let mut prev_non_newline: Option<(&Location, &Tok, &Location)> = None;
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
if matches!(tok, Tok::EndOfFile) {
|
||||
break;
|
||||
}
|
||||
// For multi-line strings, we expect `noqa` directives on the last line of the
|
||||
// string.
|
||||
if matches!(tok, Tok::String { .. }) && end.row() > start.row() {
|
||||
for i in start.row()..end.row() {
|
||||
noqa_line_for.insert(i, end.row());
|
||||
pub fn extract_noqa_line_for(
|
||||
lxr: &[LexResult],
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
) -> NoqaMapping {
|
||||
let mut string_mappings = Vec::new();
|
||||
|
||||
for (tok, range) in lxr.iter().flatten() {
|
||||
match tok {
|
||||
Tok::EndOfFile => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// For continuations, we expect `noqa` directives on the last line of the
|
||||
// continuation.
|
||||
if matches!(
|
||||
tok,
|
||||
Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..)
|
||||
) {
|
||||
if let Some((.., end)) = prev_non_newline {
|
||||
for i in end.row()..start.row() {
|
||||
noqa_line_for.insert(i, start.row());
|
||||
|
||||
// For multi-line strings, we expect `noqa` directives on the last line of the
|
||||
// string.
|
||||
Tok::String {
|
||||
triple_quoted: true,
|
||||
..
|
||||
} => {
|
||||
if locator.contains_line_break(*range) {
|
||||
string_mappings.push(*range);
|
||||
}
|
||||
}
|
||||
prev_non_newline = None;
|
||||
} else if prev_non_newline.is_none() {
|
||||
prev_non_newline = Some((start, tok, end));
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
noqa_line_for
|
||||
|
||||
let mut continuation_mappings = Vec::new();
|
||||
|
||||
// For continuations, we expect `noqa` directives on the last line of the
|
||||
// continuation.
|
||||
let mut last: Option<TextRange> = None;
|
||||
for continuation_line in indexer.continuation_line_starts() {
|
||||
let line_end = locator.full_line_end(*continuation_line);
|
||||
if let Some(last_range) = last.take() {
|
||||
if last_range.end() == *continuation_line {
|
||||
last = Some(TextRange::new(last_range.start(), line_end));
|
||||
continue;
|
||||
}
|
||||
// new continuation
|
||||
continuation_mappings.push(last_range);
|
||||
}
|
||||
|
||||
last = Some(TextRange::new(*continuation_line, line_end));
|
||||
}
|
||||
|
||||
if let Some(last_range) = last.take() {
|
||||
continuation_mappings.push(last_range);
|
||||
}
|
||||
|
||||
// Merge the mappings in sorted order
|
||||
let mut mappings =
|
||||
NoqaMapping::with_capacity(continuation_mappings.len() + string_mappings.len());
|
||||
|
||||
let mut continuation_mappings = continuation_mappings.into_iter().peekable();
|
||||
let mut string_mappings = string_mappings.into_iter().peekable();
|
||||
|
||||
while let (Some(continuation), Some(string)) =
|
||||
(continuation_mappings.peek(), string_mappings.peek())
|
||||
{
|
||||
if continuation.start() <= string.start() {
|
||||
mappings.push_mapping(continuation_mappings.next().unwrap());
|
||||
} else {
|
||||
mappings.push_mapping(string_mappings.next().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
for mapping in continuation_mappings {
|
||||
mappings.push_mapping(mapping);
|
||||
}
|
||||
|
||||
for mapping in string_mappings {
|
||||
mappings.push_mapping(mapping);
|
||||
}
|
||||
|
||||
mappings
|
||||
}
|
||||
|
||||
/// Extract a set of lines over which to disable isort.
|
||||
pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
let mut exclusions: IntSet<usize> = IntSet::default();
|
||||
let mut splits: Vec<usize> = Vec::default();
|
||||
let mut off: Option<Location> = None;
|
||||
let mut last: Option<Location> = None;
|
||||
for &(start, ref tok, end) in lxr.iter().flatten() {
|
||||
last = Some(end);
|
||||
/// Extract a set of ranges over which to disable isort.
|
||||
pub fn extract_isort_directives(lxr: &[LexResult], locator: &Locator) -> IsortDirectives {
|
||||
let mut exclusions: Vec<TextRange> = Vec::default();
|
||||
let mut splits: Vec<TextSize> = Vec::default();
|
||||
let mut off: Option<TextSize> = None;
|
||||
|
||||
for &(ref tok, range) in lxr.iter().flatten() {
|
||||
let Tok::Comment(comment_text) = tok else {
|
||||
continue;
|
||||
};
|
||||
@@ -108,7 +177,7 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
// required to include the space, and must appear on their own lines.
|
||||
let comment_text = comment_text.trim_end();
|
||||
if matches!(comment_text, "# isort: split" | "# ruff: isort: split") {
|
||||
splits.push(start.row());
|
||||
splits.push(range.start());
|
||||
} else if matches!(
|
||||
comment_text,
|
||||
"# isort: skip_file"
|
||||
@@ -122,30 +191,25 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
};
|
||||
} else if off.is_some() {
|
||||
if comment_text == "# isort: on" || comment_text == "# ruff: isort: on" {
|
||||
if let Some(start) = off {
|
||||
for row in start.row() + 1..=end.row() {
|
||||
exclusions.insert(row);
|
||||
}
|
||||
if let Some(exclusion_start) = off {
|
||||
exclusions.push(TextRange::new(exclusion_start, range.start()));
|
||||
}
|
||||
off = None;
|
||||
}
|
||||
} else {
|
||||
if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") {
|
||||
exclusions.insert(start.row());
|
||||
exclusions.push(locator.line_range(range.start()));
|
||||
} else if comment_text == "# isort: off" || comment_text == "# ruff: isort: off" {
|
||||
off = Some(start);
|
||||
off = Some(range.start());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(start) = off {
|
||||
// Enforce unterminated `isort: off`.
|
||||
if let Some(end) = last {
|
||||
for row in start.row() + 1..=end.row() {
|
||||
exclusions.insert(row);
|
||||
}
|
||||
}
|
||||
exclusions.push(TextRange::new(start, locator.contents().text_len()));
|
||||
}
|
||||
|
||||
IsortDirectives {
|
||||
exclusions,
|
||||
splits,
|
||||
@@ -155,120 +219,98 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nohash_hasher::{IntMap, IntSet};
|
||||
use ruff_python_ast::source_code::{Indexer, Locator};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::{lexer, Mode};
|
||||
|
||||
use crate::directives::{extract_isort_directives, extract_noqa_line_for};
|
||||
use crate::noqa::NoqaMapping;
|
||||
|
||||
fn noqa_mappings(contents: &str) -> NoqaMapping {
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
let locator = Locator::new(contents);
|
||||
let indexer = Indexer::from_tokens(&lxr, &locator);
|
||||
|
||||
extract_noqa_line_for(&lxr, &locator, &indexer)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_extraction() {
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
"x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
|
||||
let contents = "x = 1
|
||||
y = 2 \
|
||||
+ 1
|
||||
z = x + 1";
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
"
|
||||
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
|
||||
|
||||
let contents = "
|
||||
x = 1
|
||||
y = 2
|
||||
z = x + 1",
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
|
||||
z = x + 1";
|
||||
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
"x = 1
|
||||
let contents = "x = 1
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
|
||||
";
|
||||
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
"x = 1
|
||||
let contents = "x = 1
|
||||
|
||||
y = 2
|
||||
z = x + 1
|
||||
",
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
|
||||
";
|
||||
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
"x = '''abc
|
||||
let contents = "x = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
y = 2
|
||||
z = x + 1",
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
z = x + 1";
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(1, 4), (2, 4), (3, 4)])
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(4), TextSize::from(22)),])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
"x = 1
|
||||
let contents = "x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''
|
||||
z = 2",
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
z = 2";
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(10), TextSize::from(28))])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
"x = 1
|
||||
let contents = "x = 1
|
||||
y = '''abc
|
||||
def
|
||||
ghi
|
||||
'''",
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
'''";
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(10), TextSize::from(28))])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
r#"x = \
|
||||
1"#,
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
assert_eq!(extract_noqa_line_for(&lxr), IntMap::from_iter([(1, 2)]));
|
||||
let contents = r#"x = \
|
||||
1"#;
|
||||
assert_eq!(
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(6))])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
r#"from foo import \
|
||||
let contents = r#"from foo import \
|
||||
bar as baz, \
|
||||
qux as quux"#,
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
qux as quux"#;
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(1, 3), (2, 3)])
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(36))])
|
||||
);
|
||||
|
||||
let lxr: Vec<LexResult> = lexer::lex(
|
||||
r#"
|
||||
let contents = r#"
|
||||
# Foo
|
||||
from foo import \
|
||||
bar as baz, \
|
||||
@@ -276,13 +318,14 @@ from foo import \
|
||||
x = \
|
||||
1
|
||||
y = \
|
||||
2"#,
|
||||
Mode::Module,
|
||||
)
|
||||
.collect();
|
||||
2"#;
|
||||
assert_eq!(
|
||||
extract_noqa_line_for(&lxr),
|
||||
IntMap::from_iter([(3, 5), (4, 5), (6, 7), (8, 9)])
|
||||
noqa_mappings(contents),
|
||||
NoqaMapping::from_iter([
|
||||
TextRange::new(TextSize::from(7), TextSize::from(43)),
|
||||
TextRange::new(TextSize::from(65), TextSize::from(71)),
|
||||
TextRange::new(TextSize::from(77), TextSize::from(83)),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -292,7 +335,10 @@ y = \
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
Vec::default()
|
||||
);
|
||||
|
||||
let contents = "# isort: off
|
||||
x = 1
|
||||
@@ -301,8 +347,8 @@ y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr).exclusions,
|
||||
IntSet::from_iter([2, 3, 4])
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(25))])
|
||||
);
|
||||
|
||||
let contents = "# isort: off
|
||||
@@ -314,8 +360,8 @@ z = x + 1
|
||||
# isort: on";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr).exclusions,
|
||||
IntSet::from_iter([2, 3, 4, 5])
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
Vec::from_iter([TextRange::new(TextSize::from(0), TextSize::from(38))])
|
||||
);
|
||||
|
||||
let contents = "# isort: off
|
||||
@@ -324,8 +370,8 @@ y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr).exclusions,
|
||||
IntSet::from_iter([2, 3, 4])
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
Vec::from_iter([TextRange::at(TextSize::from(0), contents.text_len())])
|
||||
);
|
||||
|
||||
let contents = "# isort: skip_file
|
||||
@@ -333,7 +379,10 @@ x = 1
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
Vec::default()
|
||||
);
|
||||
|
||||
let contents = "# isort: off
|
||||
x = 1
|
||||
@@ -342,7 +391,10 @@ y = 2
|
||||
# isort: skip_file
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).exclusions,
|
||||
Vec::default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -351,19 +403,28 @@ z = x + 1";
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(extract_isort_directives(&lxr).splits, Vec::<usize>::new());
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
|
||||
Vec::new()
|
||||
);
|
||||
|
||||
let contents = "x = 1
|
||||
y = 2
|
||||
# isort: split
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(extract_isort_directives(&lxr).splits, vec![3]);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
|
||||
vec![TextSize::from(12)]
|
||||
);
|
||||
|
||||
let contents = "x = 1
|
||||
y = 2 # isort: split
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::lex(contents, Mode::Module).collect();
|
||||
assert_eq!(extract_isort_directives(&lxr).splits, vec![2]);
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr, &Locator::new(contents)).splits,
|
||||
vec![TextSize::from(13)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,84 +3,112 @@
|
||||
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use rustpython_parser::ast::{Constant, ExprKind, Stmt, StmtKind, Suite};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustpython_parser::ast::{self, Constant, ExprKind, Stmt, StmtKind, Suite};
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
use rustpython_parser::Tok;
|
||||
|
||||
use ruff_python_ast::visitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::newlines::UniversalNewlineIterator;
|
||||
use ruff_python_ast::source_code::Locator;
|
||||
use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor};
|
||||
|
||||
/// Extract doc lines (standalone comments) from a token sequence.
|
||||
pub fn doc_lines_from_tokens(lxr: &[LexResult]) -> DocLines {
|
||||
DocLines::new(lxr)
|
||||
pub(crate) fn doc_lines_from_tokens<'a>(
|
||||
lxr: &'a [LexResult],
|
||||
locator: &'a Locator<'a>,
|
||||
) -> DocLines<'a> {
|
||||
DocLines::new(lxr, locator)
|
||||
}
|
||||
|
||||
pub struct DocLines<'a> {
|
||||
pub(crate) struct DocLines<'a> {
|
||||
inner: std::iter::Flatten<core::slice::Iter<'a, LexResult>>,
|
||||
prev: Option<usize>,
|
||||
locator: &'a Locator<'a>,
|
||||
prev: TextSize,
|
||||
}
|
||||
|
||||
impl<'a> DocLines<'a> {
|
||||
fn new(lxr: &'a [LexResult]) -> Self {
|
||||
fn new(lxr: &'a [LexResult], locator: &'a Locator) -> Self {
|
||||
Self {
|
||||
inner: lxr.iter().flatten(),
|
||||
prev: None,
|
||||
locator,
|
||||
prev: TextSize::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for DocLines<'_> {
|
||||
type Item = usize;
|
||||
type Item = TextSize;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut at_start_of_line = true;
|
||||
loop {
|
||||
let (start, tok, end) = self.inner.next()?;
|
||||
let (tok, range) = self.inner.next()?;
|
||||
|
||||
match tok {
|
||||
Tok::Indent | Tok::Dedent | Tok::Newline => continue,
|
||||
Tok::Comment(..) => {
|
||||
if let Some(prev) = self.prev {
|
||||
if start.row() > prev {
|
||||
break Some(start.row());
|
||||
}
|
||||
} else {
|
||||
break Some(start.row());
|
||||
if at_start_of_line
|
||||
|| self
|
||||
.locator
|
||||
.contains_line_break(TextRange::new(self.prev, range.start()))
|
||||
{
|
||||
break Some(range.start());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
Tok::Newline => {
|
||||
at_start_of_line = true;
|
||||
}
|
||||
Tok::Indent | Tok::Dedent => {
|
||||
// ignore
|
||||
}
|
||||
_ => {
|
||||
at_start_of_line = false;
|
||||
}
|
||||
}
|
||||
|
||||
self.prev = Some(end.row());
|
||||
self.prev = range.end();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for DocLines<'_> {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StringLinesVisitor {
|
||||
string_lines: Vec<usize>,
|
||||
struct StringLinesVisitor<'a> {
|
||||
string_lines: Vec<TextSize>,
|
||||
locator: &'a Locator<'a>,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for StringLinesVisitor {
|
||||
impl StatementVisitor<'_> for StringLinesVisitor<'_> {
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
if let StmtKind::Expr { value } = &stmt.node {
|
||||
if let ExprKind::Constant {
|
||||
if let StmtKind::Expr(ast::StmtExpr { value }) = &stmt.node {
|
||||
if let ExprKind::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(..),
|
||||
..
|
||||
} = &value.node
|
||||
}) = &value.node
|
||||
{
|
||||
self.string_lines
|
||||
.extend(value.location.row()..=value.end_location.unwrap().row());
|
||||
for line in UniversalNewlineIterator::with_offset(
|
||||
self.locator.slice(value.range()),
|
||||
value.start(),
|
||||
) {
|
||||
self.string_lines.push(line.start());
|
||||
}
|
||||
}
|
||||
}
|
||||
visitor::walk_stmt(self, stmt);
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract doc lines (standalone strings) from an AST.
|
||||
pub fn doc_lines_from_ast(python_ast: &Suite) -> Vec<usize> {
|
||||
let mut visitor = StringLinesVisitor::default();
|
||||
impl<'a> StringLinesVisitor<'a> {
|
||||
fn new(locator: &'a Locator<'a>) -> Self {
|
||||
Self {
|
||||
string_lines: Vec::new(),
|
||||
locator,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract doc lines (standalone strings) start positions from an AST.
|
||||
pub(crate) fn doc_lines_from_ast(python_ast: &Suite, locator: &Locator) -> Vec<TextSize> {
|
||||
let mut visitor = StringLinesVisitor::new(locator);
|
||||
visitor.visit_body(python_ast);
|
||||
visitor.string_lines
|
||||
}
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
use rustpython_parser::ast::{Expr, Stmt};
|
||||
|
||||
use ruff_python_semantic::analyze::visibility::{
|
||||
class_visibility, function_visibility, method_visibility, Modifier, Visibility, VisibleScope,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DefinitionKind<'a> {
|
||||
Module,
|
||||
Package,
|
||||
Class(&'a Stmt),
|
||||
NestedClass(&'a Stmt),
|
||||
Function(&'a Stmt),
|
||||
NestedFunction(&'a Stmt),
|
||||
Method(&'a Stmt),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Definition<'a> {
|
||||
pub kind: DefinitionKind<'a>,
|
||||
pub docstring: Option<&'a Expr>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Docstring<'a> {
|
||||
pub kind: DefinitionKind<'a>,
|
||||
pub expr: &'a Expr,
|
||||
pub contents: &'a str,
|
||||
pub body: &'a str,
|
||||
pub indentation: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub enum Documentable {
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
pub fn transition_scope(scope: VisibleScope, stmt: &Stmt, kind: Documentable) -> VisibleScope {
|
||||
match kind {
|
||||
Documentable::Function => VisibleScope {
|
||||
modifier: Modifier::Function,
|
||||
visibility: match scope {
|
||||
VisibleScope {
|
||||
modifier: Modifier::Module,
|
||||
visibility: Visibility::Public,
|
||||
} => function_visibility(stmt),
|
||||
VisibleScope {
|
||||
modifier: Modifier::Class,
|
||||
visibility: Visibility::Public,
|
||||
} => method_visibility(stmt),
|
||||
_ => Visibility::Private,
|
||||
},
|
||||
},
|
||||
Documentable::Class => VisibleScope {
|
||||
modifier: Modifier::Class,
|
||||
visibility: match scope {
|
||||
VisibleScope {
|
||||
modifier: Modifier::Module,
|
||||
visibility: Visibility::Public,
|
||||
} => class_visibility(stmt),
|
||||
VisibleScope {
|
||||
modifier: Modifier::Class,
|
||||
visibility: Visibility::Public,
|
||||
} => class_visibility(stmt),
|
||||
_ => Visibility::Private,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,84 +1,91 @@
|
||||
//! Extract docstrings from an AST.
|
||||
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
|
||||
use rustpython_parser::ast::{self, Constant, Expr, ExprKind, Stmt, StmtKind};
|
||||
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
|
||||
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
|
||||
use ruff_python_semantic::definition::{Definition, DefinitionId, Definitions, Member, MemberKind};
|
||||
|
||||
/// Extract a docstring from a function or class body.
|
||||
pub fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
|
||||
pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
|
||||
let stmt = suite.first()?;
|
||||
// Require the docstring to be a standalone expression.
|
||||
let StmtKind::Expr { value } = &stmt.node else {
|
||||
let StmtKind::Expr(ast::StmtExpr { value }) = &stmt.node else {
|
||||
return None;
|
||||
};
|
||||
// Only match strings.
|
||||
if !matches!(
|
||||
&value.node,
|
||||
ExprKind::Constant {
|
||||
ExprKind::Constant(ast::ExprConstant {
|
||||
value: Constant::Str(_),
|
||||
..
|
||||
}
|
||||
})
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
Some(value)
|
||||
}
|
||||
|
||||
/// Extract a docstring from a `Definition`.
|
||||
pub(crate) fn extract_docstring<'a>(definition: &'a Definition<'a>) -> Option<&'a Expr> {
|
||||
match definition {
|
||||
Definition::Module(module) => docstring_from(module.python_ast),
|
||||
Definition::Member(member) => {
|
||||
if let StmtKind::ClassDef(ast::StmtClassDef { body, .. })
|
||||
| StmtKind::FunctionDef(ast::StmtFunctionDef { body, .. })
|
||||
| StmtKind::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. }) =
|
||||
&member.stmt.node
|
||||
{
|
||||
docstring_from(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) enum ExtractionTarget {
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
/// Extract a `Definition` from the AST node defined by a `Stmt`.
|
||||
pub fn extract<'a>(
|
||||
scope: visibility::VisibleScope,
|
||||
pub(crate) fn extract_definition<'a>(
|
||||
target: ExtractionTarget,
|
||||
stmt: &'a Stmt,
|
||||
body: &'a [Stmt],
|
||||
kind: Documentable,
|
||||
) -> Definition<'a> {
|
||||
let expr = docstring_from(body);
|
||||
match kind {
|
||||
Documentable::Function => match scope {
|
||||
visibility::VisibleScope {
|
||||
modifier: visibility::Modifier::Module,
|
||||
..
|
||||
} => Definition {
|
||||
kind: DefinitionKind::Function(stmt),
|
||||
docstring: expr,
|
||||
parent: DefinitionId,
|
||||
definitions: &Definitions<'a>,
|
||||
) -> Member<'a> {
|
||||
match target {
|
||||
ExtractionTarget::Function => match &definitions[parent] {
|
||||
Definition::Module(..) => Member {
|
||||
parent,
|
||||
kind: MemberKind::Function,
|
||||
stmt,
|
||||
},
|
||||
visibility::VisibleScope {
|
||||
modifier: visibility::Modifier::Class,
|
||||
Definition::Member(Member {
|
||||
kind: MemberKind::Class | MemberKind::NestedClass,
|
||||
..
|
||||
} => Definition {
|
||||
kind: DefinitionKind::Method(stmt),
|
||||
docstring: expr,
|
||||
}) => Member {
|
||||
parent,
|
||||
kind: MemberKind::Method,
|
||||
stmt,
|
||||
},
|
||||
visibility::VisibleScope {
|
||||
modifier: visibility::Modifier::Function,
|
||||
..
|
||||
} => Definition {
|
||||
kind: DefinitionKind::NestedFunction(stmt),
|
||||
docstring: expr,
|
||||
Definition::Member(..) => Member {
|
||||
parent,
|
||||
kind: MemberKind::NestedFunction,
|
||||
stmt,
|
||||
},
|
||||
},
|
||||
Documentable::Class => match scope {
|
||||
visibility::VisibleScope {
|
||||
modifier: visibility::Modifier::Module,
|
||||
..
|
||||
} => Definition {
|
||||
kind: DefinitionKind::Class(stmt),
|
||||
docstring: expr,
|
||||
ExtractionTarget::Class => match &definitions[parent] {
|
||||
Definition::Module(..) => Member {
|
||||
parent,
|
||||
kind: MemberKind::Class,
|
||||
stmt,
|
||||
},
|
||||
visibility::VisibleScope {
|
||||
modifier: visibility::Modifier::Class,
|
||||
..
|
||||
} => Definition {
|
||||
kind: DefinitionKind::NestedClass(stmt),
|
||||
docstring: expr,
|
||||
},
|
||||
visibility::VisibleScope {
|
||||
modifier: visibility::Modifier::Function,
|
||||
..
|
||||
} => Definition {
|
||||
kind: DefinitionKind::NestedClass(stmt),
|
||||
docstring: expr,
|
||||
Definition::Member(..) => Member {
|
||||
parent,
|
||||
kind: MemberKind::NestedClass,
|
||||
stmt,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ pub(crate) static GOOGLE_SECTIONS: &[SectionKind] = &[
|
||||
SectionKind::KeywordArguments,
|
||||
SectionKind::Note,
|
||||
SectionKind::Notes,
|
||||
SectionKind::OtherArgs,
|
||||
SectionKind::OtherArguments,
|
||||
SectionKind::Return,
|
||||
SectionKind::Tip,
|
||||
SectionKind::Todo,
|
||||
|
||||
@@ -1,6 +1,89 @@
|
||||
pub mod definition;
|
||||
pub mod extraction;
|
||||
pub mod google;
|
||||
pub mod numpy;
|
||||
pub mod sections;
|
||||
pub mod styles;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use ruff_python_semantic::definition::Definition;
|
||||
|
||||
pub(crate) mod extraction;
|
||||
pub(crate) mod google;
|
||||
pub(crate) mod numpy;
|
||||
pub(crate) mod sections;
|
||||
pub(crate) mod styles;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Docstring<'a> {
|
||||
pub(crate) definition: &'a Definition<'a>,
|
||||
pub(crate) expr: &'a Expr,
|
||||
/// The content of the docstring, including the leading and trailing quotes.
|
||||
pub(crate) contents: &'a str,
|
||||
|
||||
/// The range of the docstring body (without the quotes). The range is relative to [`Self::contents`].
|
||||
pub(crate) body_range: TextRange,
|
||||
pub(crate) indentation: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> Docstring<'a> {
|
||||
pub(crate) fn body(&self) -> DocstringBody {
|
||||
DocstringBody { docstring: self }
|
||||
}
|
||||
|
||||
pub(crate) const fn start(&self) -> TextSize {
|
||||
self.expr.start()
|
||||
}
|
||||
|
||||
pub(crate) const fn end(&self) -> TextSize {
|
||||
self.expr.end()
|
||||
}
|
||||
|
||||
pub(crate) const fn range(&self) -> TextRange {
|
||||
self.expr.range()
|
||||
}
|
||||
|
||||
pub(crate) fn leading_quote(&self) -> &'a str {
|
||||
&self.contents[TextRange::up_to(self.body_range.start())]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub(crate) struct DocstringBody<'a> {
|
||||
docstring: &'a Docstring<'a>,
|
||||
}
|
||||
|
||||
impl<'a> DocstringBody<'a> {
|
||||
#[inline]
|
||||
pub(crate) fn start(self) -> TextSize {
|
||||
self.range().start()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn end(self) -> TextSize {
|
||||
self.range().end()
|
||||
}
|
||||
|
||||
pub(crate) fn range(self) -> TextRange {
|
||||
self.docstring.body_range + self.docstring.start()
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(self) -> &'a str {
|
||||
&self.docstring.contents[self.docstring.body_range]
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DocstringBody<'_> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for DocstringBody<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("DocstringBody")
|
||||
.field("text", &self.as_str())
|
||||
.field("range", &self.range())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ pub(crate) static NUMPY_SECTIONS: &[SectionKind] = &[
|
||||
SectionKind::Yields,
|
||||
// NumPy-only
|
||||
SectionKind::ExtendedSummary,
|
||||
SectionKind::OtherParams,
|
||||
SectionKind::OtherParameters,
|
||||
SectionKind::Parameters,
|
||||
SectionKind::ShortSummary,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use ruff_python_ast::newlines::{StrExt, UniversalNewlineIterator};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::iter::FusedIterator;
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
use crate::docstrings::styles::SectionStyle;
|
||||
use crate::docstrings::{Docstring, DocstringBody};
|
||||
use ruff_python_ast::whitespace;
|
||||
|
||||
#[derive(EnumIter, PartialEq, Eq, Debug, Clone, Copy)]
|
||||
@@ -22,6 +27,9 @@ pub enum SectionKind {
|
||||
Methods,
|
||||
Note,
|
||||
Notes,
|
||||
OtherArgs,
|
||||
OtherArguments,
|
||||
OtherParams,
|
||||
OtherParameters,
|
||||
Parameters,
|
||||
Raises,
|
||||
@@ -59,6 +67,9 @@ impl SectionKind {
|
||||
"methods" => Some(Self::Methods),
|
||||
"note" => Some(Self::Note),
|
||||
"notes" => Some(Self::Notes),
|
||||
"other args" => Some(Self::OtherArgs),
|
||||
"other arguments" => Some(Self::OtherArguments),
|
||||
"other params" => Some(Self::OtherParams),
|
||||
"other parameters" => Some(Self::OtherParameters),
|
||||
"parameters" => Some(Self::Parameters),
|
||||
"raises" => Some(Self::Raises),
|
||||
@@ -97,6 +108,9 @@ impl SectionKind {
|
||||
Self::Methods => "Methods",
|
||||
Self::Note => "Note",
|
||||
Self::Notes => "Notes",
|
||||
Self::OtherArgs => "Other Args",
|
||||
Self::OtherArguments => "Other Arguments",
|
||||
Self::OtherParams => "Other Params",
|
||||
Self::OtherParameters => "Other Parameters",
|
||||
Self::Parameters => "Parameters",
|
||||
Self::Raises => "Raises",
|
||||
@@ -116,17 +130,259 @@ impl SectionKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SectionContexts<'a> {
|
||||
contexts: Vec<SectionContextData>,
|
||||
docstring: &'a Docstring<'a>,
|
||||
}
|
||||
|
||||
impl<'a> SectionContexts<'a> {
|
||||
/// Extract all `SectionContext` values from a docstring.
|
||||
pub(crate) fn from_docstring(docstring: &'a Docstring<'a>, style: SectionStyle) -> Self {
|
||||
let contents = docstring.body();
|
||||
|
||||
let mut contexts = Vec::new();
|
||||
let mut last: Option<SectionContextData> = None;
|
||||
let mut previous_line = None;
|
||||
|
||||
for line in contents.universal_newlines() {
|
||||
if previous_line.is_none() {
|
||||
// skip the first line
|
||||
previous_line = Some(line.as_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(section_kind) = suspected_as_section(&line, style) {
|
||||
let indent = whitespace::leading_space(&line);
|
||||
let section_name = whitespace::leading_words(&line);
|
||||
|
||||
let section_name_range = TextRange::at(indent.text_len(), section_name.text_len());
|
||||
|
||||
if is_docstring_section(
|
||||
&line,
|
||||
section_name_range,
|
||||
previous_line.unwrap_or_default(),
|
||||
) {
|
||||
if let Some(mut last) = last.take() {
|
||||
last.range = TextRange::new(last.range.start(), line.start());
|
||||
contexts.push(last);
|
||||
}
|
||||
|
||||
last = Some(SectionContextData {
|
||||
kind: section_kind,
|
||||
name_range: section_name_range + line.start(),
|
||||
range: TextRange::empty(line.start()),
|
||||
summary_full_end: line.full_end(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
previous_line = Some(line.as_str());
|
||||
}
|
||||
|
||||
if let Some(mut last) = last.take() {
|
||||
last.range = TextRange::new(last.range.start(), contents.text_len());
|
||||
contexts.push(last);
|
||||
}
|
||||
|
||||
Self {
|
||||
contexts,
|
||||
docstring,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.contexts.len()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> SectionContextsIter {
|
||||
SectionContextsIter {
|
||||
docstring_body: self.docstring.body(),
|
||||
inner: self.contexts.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a SectionContexts<'a> {
|
||||
type Item = SectionContext<'a>;
|
||||
type IntoIter = SectionContextsIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SectionContexts<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_list().entries(self.iter()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SectionContextsIter<'a> {
|
||||
docstring_body: DocstringBody<'a>,
|
||||
inner: std::slice::Iter<'a, SectionContextData>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SectionContextsIter<'a> {
|
||||
type Item = SectionContext<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let next = self.inner.next()?;
|
||||
|
||||
Some(SectionContext {
|
||||
data: next,
|
||||
docstring_body: self.docstring_body,
|
||||
})
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.inner.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> DoubleEndedIterator for SectionContextsIter<'a> {
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let back = self.inner.next_back()?;
|
||||
Some(SectionContext {
|
||||
data: back,
|
||||
docstring_body: self.docstring_body,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for SectionContextsIter<'_> {}
|
||||
impl ExactSizeIterator for SectionContextsIter<'_> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SectionContext<'a> {
|
||||
/// The "kind" of the section, e.g. "SectionKind::Args" or "SectionKind::Returns".
|
||||
pub(crate) kind: SectionKind,
|
||||
struct SectionContextData {
|
||||
kind: SectionKind,
|
||||
|
||||
/// Range of the section name, relative to the [`Docstring::body`]
|
||||
name_range: TextRange,
|
||||
|
||||
/// Range from the start to the end of the section, relative to the [`Docstring::body`]
|
||||
range: TextRange,
|
||||
|
||||
/// End of the summary, relative to the [`Docstring::body`]
|
||||
summary_full_end: TextSize,
|
||||
}
|
||||
|
||||
pub struct SectionContext<'a> {
|
||||
data: &'a SectionContextData,
|
||||
docstring_body: DocstringBody<'a>,
|
||||
}
|
||||
|
||||
impl<'a> SectionContext<'a> {
|
||||
pub fn is_last(&self) -> bool {
|
||||
self.range().end() == self.docstring_body.end()
|
||||
}
|
||||
|
||||
/// The `kind` of the section, e.g. [`SectionKind::Args`] or [`SectionKind::Returns`].
|
||||
pub const fn kind(&self) -> SectionKind {
|
||||
self.data.kind
|
||||
}
|
||||
|
||||
/// The name of the section as it appears in the docstring, e.g. "Args" or "Returns".
|
||||
pub(crate) section_name: &'a str,
|
||||
pub(crate) previous_line: &'a str,
|
||||
pub(crate) line: &'a str,
|
||||
pub(crate) following_lines: &'a [&'a str],
|
||||
pub(crate) is_last_section: bool,
|
||||
pub(crate) original_index: usize,
|
||||
pub fn section_name(&self) -> &'a str {
|
||||
&self.docstring_body.as_str()[self.data.name_range]
|
||||
}
|
||||
|
||||
/// Returns the rest of the summary line after the section name.
|
||||
pub fn summary_after_section_name(&self) -> &'a str {
|
||||
&self.summary_line()[usize::from(self.data.name_range.end() - self.data.range.start())..]
|
||||
}
|
||||
|
||||
fn offset(&self) -> TextSize {
|
||||
self.docstring_body.start()
|
||||
}
|
||||
|
||||
/// The absolute range of the section name
|
||||
pub fn section_name_range(&self) -> TextRange {
|
||||
self.data.name_range + self.offset()
|
||||
}
|
||||
|
||||
/// Summary range relative to the start of the document. Includes the trailing newline.
|
||||
pub fn summary_full_range(&self) -> TextRange {
|
||||
self.summary_full_range_relative() + self.offset()
|
||||
}
|
||||
|
||||
/// The absolute range of the summary line, excluding any trailing newline character.
|
||||
pub fn summary_range(&self) -> TextRange {
|
||||
TextRange::at(self.range().start(), self.summary_line().text_len())
|
||||
}
|
||||
|
||||
/// Range of the summary line relative to [`Docstring::body`], including the trailing newline character.
|
||||
fn summary_full_range_relative(&self) -> TextRange {
|
||||
TextRange::new(self.range_relative().start(), self.data.summary_full_end)
|
||||
}
|
||||
|
||||
/// Returns the range of this section relative to [`Docstring::body`]
|
||||
const fn range_relative(&self) -> TextRange {
|
||||
self.data.range
|
||||
}
|
||||
|
||||
/// The absolute range of the full-section.
|
||||
pub fn range(&self) -> TextRange {
|
||||
self.range_relative() + self.offset()
|
||||
}
|
||||
|
||||
/// Summary line without the trailing newline characters
|
||||
pub fn summary_line(&self) -> &'a str {
|
||||
let full_summary = &self.docstring_body.as_str()[self.summary_full_range_relative()];
|
||||
|
||||
let mut bytes = full_summary.bytes().rev();
|
||||
|
||||
let newline_width = match bytes.next() {
|
||||
Some(b'\n') => {
|
||||
if bytes.next() == Some(b'\r') {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
Some(b'\r') => 1,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
&full_summary[..full_summary.len() - newline_width]
|
||||
}
|
||||
|
||||
/// Returns the text of the last line of the previous section or an empty string if it is the first section.
|
||||
pub fn previous_line(&self) -> Option<&'a str> {
|
||||
let previous =
|
||||
&self.docstring_body.as_str()[TextRange::up_to(self.range_relative().start())];
|
||||
previous.universal_newlines().last().map(|l| l.as_str())
|
||||
}
|
||||
|
||||
/// Returns the lines belonging to this section after the summary line.
|
||||
pub fn following_lines(&self) -> UniversalNewlineIterator<'a> {
|
||||
let lines = self.following_lines_str();
|
||||
UniversalNewlineIterator::with_offset(lines, self.offset() + self.data.summary_full_end)
|
||||
}
|
||||
|
||||
fn following_lines_str(&self) -> &'a str {
|
||||
&self.docstring_body.as_str()[self.following_range_relative()]
|
||||
}
|
||||
|
||||
/// Returns the range to the following lines relative to [`Docstring::body`].
|
||||
const fn following_range_relative(&self) -> TextRange {
|
||||
TextRange::new(self.data.summary_full_end, self.range_relative().end())
|
||||
}
|
||||
|
||||
/// Returns the absolute range of the following lines.
|
||||
pub fn following_range(&self) -> TextRange {
|
||||
self.following_range_relative() + self.offset()
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SectionContext<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("SectionContext")
|
||||
.field("kind", &self.kind())
|
||||
.field("section_name", &self.section_name())
|
||||
.field("summary_line", &self.summary_line())
|
||||
.field("following_lines", &&self.following_lines_str())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind> {
|
||||
@@ -139,20 +395,15 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
|
||||
}
|
||||
|
||||
/// Check if the suspected context is really a section header.
|
||||
fn is_docstring_section(context: &SectionContext) -> bool {
|
||||
let section_name_suffix = context
|
||||
.line
|
||||
.trim()
|
||||
.strip_prefix(context.section_name)
|
||||
.unwrap()
|
||||
.trim();
|
||||
fn is_docstring_section(line: &str, section_name_range: TextRange, previous_lines: &str) -> bool {
|
||||
let section_name_suffix = line[usize::from(section_name_range.end())..].trim();
|
||||
let this_looks_like_a_section_name =
|
||||
section_name_suffix == ":" || section_name_suffix.is_empty();
|
||||
if !this_looks_like_a_section_name {
|
||||
return false;
|
||||
}
|
||||
|
||||
let prev_line = context.previous_line.trim();
|
||||
let prev_line = previous_lines.trim();
|
||||
let prev_line_ends_with_punctuation = [',', ';', '.', '-', '\\', '/', ']', '}', ')']
|
||||
.into_iter()
|
||||
.any(|char| prev_line.ends_with(char));
|
||||
@@ -164,50 +415,3 @@ fn is_docstring_section(context: &SectionContext) -> bool {
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Extract all `SectionContext` values from a docstring.
|
||||
pub(crate) fn section_contexts<'a>(
|
||||
lines: &'a [&'a str],
|
||||
style: SectionStyle,
|
||||
) -> Vec<SectionContext<'a>> {
|
||||
let mut contexts = vec![];
|
||||
for (kind, lineno) in lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(1)
|
||||
.filter_map(|(lineno, line)| suspected_as_section(line, style).map(|kind| (kind, lineno)))
|
||||
{
|
||||
let context = SectionContext {
|
||||
kind,
|
||||
section_name: whitespace::leading_words(lines[lineno]),
|
||||
previous_line: lines[lineno - 1],
|
||||
line: lines[lineno],
|
||||
following_lines: &lines[lineno + 1..],
|
||||
original_index: lineno,
|
||||
is_last_section: false,
|
||||
};
|
||||
if is_docstring_section(&context) {
|
||||
contexts.push(context);
|
||||
}
|
||||
}
|
||||
|
||||
let mut truncated_contexts = Vec::with_capacity(contexts.len());
|
||||
let mut end: Option<usize> = None;
|
||||
for context in contexts.into_iter().rev() {
|
||||
let next_end = context.original_index;
|
||||
truncated_contexts.push(SectionContext {
|
||||
kind: context.kind,
|
||||
section_name: context.section_name,
|
||||
previous_line: context.previous_line,
|
||||
line: context.line,
|
||||
following_lines: end.map_or(context.following_lines, |end| {
|
||||
&lines[context.original_index + 1..end]
|
||||
}),
|
||||
original_index: context.original_index,
|
||||
is_last_section: end.is_none(),
|
||||
});
|
||||
end = Some(next_end);
|
||||
}
|
||||
truncated_contexts.reverse();
|
||||
truncated_contexts
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ static COMMA_SEPARATED_LIST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").
|
||||
|
||||
/// Parse a comma-separated list of `RuleSelector` values (e.g.,
|
||||
/// "F401,E501").
|
||||
pub fn parse_prefix_codes(value: &str) -> Vec<RuleSelector> {
|
||||
pub(crate) fn parse_prefix_codes(value: &str) -> Vec<RuleSelector> {
|
||||
let mut codes: Vec<RuleSelector> = vec![];
|
||||
for code in COMMA_SEPARATED_LIST_RE.split(value) {
|
||||
let code = code.trim();
|
||||
@@ -30,7 +30,7 @@ pub fn parse_prefix_codes(value: &str) -> Vec<RuleSelector> {
|
||||
}
|
||||
|
||||
/// Parse a comma-separated list of strings (e.g., "__init__.py,__main__.py").
|
||||
pub fn parse_strings(value: &str) -> Vec<String> {
|
||||
pub(crate) fn parse_strings(value: &str) -> Vec<String> {
|
||||
COMMA_SEPARATED_LIST_RE
|
||||
.split(value)
|
||||
.map(str::trim)
|
||||
@@ -40,7 +40,7 @@ pub fn parse_strings(value: &str) -> Vec<String> {
|
||||
}
|
||||
|
||||
/// Parse a boolean.
|
||||
pub fn parse_bool(value: &str) -> Result<bool> {
|
||||
pub(crate) fn parse_bool(value: &str) -> Result<bool> {
|
||||
match value.trim() {
|
||||
"true" => Ok(true),
|
||||
"false" => Ok(false),
|
||||
@@ -138,7 +138,7 @@ fn tokenize_files_to_codes_mapping(value: &str) -> Vec<Token> {
|
||||
|
||||
/// Parse a 'files-to-codes' mapping, mimicking Flake8's internal logic.
|
||||
/// See: <https://github.com/PyCQA/flake8/blob/7dfe99616fc2f07c0017df2ba5fa884158f3ea8a/src/flake8/utils.py#L45>
|
||||
pub fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair>> {
|
||||
pub(crate) fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair>> {
|
||||
if value.trim().is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
@@ -178,7 +178,7 @@ pub fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair
|
||||
}
|
||||
|
||||
/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`.
|
||||
pub fn collect_per_file_ignores(
|
||||
pub(crate) fn collect_per_file_ignores(
|
||||
pairs: Vec<PatternPrefixPair>,
|
||||
) -> FxHashMap<String, Vec<RuleSelector>> {
|
||||
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();
|
||||
|
||||
@@ -175,7 +175,7 @@ impl From<&Plugin> for Linter {
|
||||
///
|
||||
/// For example, if the user specified a `mypy-init-return` setting, we should
|
||||
/// infer that `flake8-annotations` is active.
|
||||
pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> Vec<Plugin> {
|
||||
pub(crate) fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> Vec<Plugin> {
|
||||
let mut plugins = BTreeSet::new();
|
||||
for key in flake8.keys() {
|
||||
match key.as_str() {
|
||||
@@ -292,7 +292,7 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
|
||||
///
|
||||
/// For example, if the user ignores `ANN101`, we should infer that
|
||||
/// `flake8-annotations` is active.
|
||||
pub fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec<Plugin> {
|
||||
pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec<Plugin> {
|
||||
// Ignore cases in which we've knowingly changed rule prefixes.
|
||||
[
|
||||
Plugin::Flake82020,
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute};
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{Location, Stmt, StmtKind, Suite};
|
||||
use ruff_text_size::TextSize;
|
||||
use rustpython_parser::ast::{self, Stmt, StmtKind, Suite};
|
||||
use rustpython_parser::{lexer, Mode, Tok};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
@@ -17,10 +17,7 @@ pub struct Importer<'a> {
|
||||
python_ast: &'a Suite,
|
||||
locator: &'a Locator<'a>,
|
||||
stylist: &'a Stylist<'a>,
|
||||
/// A map from module name to top-level `StmtKind::ImportFrom` statements.
|
||||
import_from_map: FxHashMap<&'a str, &'a Stmt>,
|
||||
/// The last top-level import statement.
|
||||
trailing_import: Option<&'a Stmt>,
|
||||
ordered_imports: Vec<&'a Stmt>,
|
||||
}
|
||||
|
||||
impl<'a> Importer<'a> {
|
||||
@@ -29,34 +26,21 @@ impl<'a> Importer<'a> {
|
||||
python_ast,
|
||||
locator,
|
||||
stylist,
|
||||
import_from_map: FxHashMap::default(),
|
||||
trailing_import: None,
|
||||
ordered_imports: Vec::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Visit a top-level import statement.
|
||||
pub fn visit_import(&mut self, import: &'a Stmt) {
|
||||
// Store a reference to the import statement in the appropriate map.
|
||||
match &import.node {
|
||||
StmtKind::Import { .. } => {
|
||||
// Nothing to do here, we don't extend top-level `import` statements at all, so
|
||||
// no need to track them.
|
||||
}
|
||||
StmtKind::ImportFrom { module, level, .. } => {
|
||||
// Store a reverse-map from module name to `import ... from` statement.
|
||||
if level.map_or(true, |level| level == 0) {
|
||||
if let Some(module) = module {
|
||||
self.import_from_map.insert(module.as_str(), import);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected StmtKind::Import | StmtKind::ImportFrom");
|
||||
}
|
||||
}
|
||||
self.ordered_imports.push(import);
|
||||
}
|
||||
|
||||
// Store a reference to the last top-level import statement.
|
||||
self.trailing_import = Some(import);
|
||||
/// Return the import statement that precedes the given position, if any.
|
||||
fn preceding_import(&self, at: TextSize) -> Option<&Stmt> {
|
||||
self.ordered_imports
|
||||
.partition_point(|stmt| stmt.start() < at)
|
||||
.checked_sub(1)
|
||||
.map(|idx| self.ordered_imports[idx])
|
||||
}
|
||||
|
||||
/// Add an import statement to import the given module.
|
||||
@@ -64,9 +48,9 @@ impl<'a> Importer<'a> {
|
||||
/// If there are no existing imports, the new import will be added at the top
|
||||
/// of the file. Otherwise, it will be added after the most recent top-level
|
||||
/// import statement.
|
||||
pub fn add_import(&self, import: &AnyImport) -> Edit {
|
||||
pub fn add_import(&self, import: &AnyImport, at: TextSize) -> Edit {
|
||||
let required_import = import.to_string();
|
||||
if let Some(stmt) = self.trailing_import {
|
||||
if let Some(stmt) = self.preceding_import(at) {
|
||||
// Insert after the last top-level import.
|
||||
let Insertion {
|
||||
prefix,
|
||||
@@ -87,15 +71,33 @@ impl<'a> Importer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the top-level [`Stmt`] that imports the given module using `StmtKind::ImportFrom`.
|
||||
/// if it exists.
|
||||
pub fn get_import_from(&self, module: &str) -> Option<&Stmt> {
|
||||
self.import_from_map.get(module).copied()
|
||||
/// Return the top-level [`Stmt`] that imports the given module using `StmtKind::ImportFrom`
|
||||
/// preceding the given position, if any.
|
||||
pub fn find_import_from(&self, module: &str, at: TextSize) -> Option<&Stmt> {
|
||||
let mut import_from = None;
|
||||
for stmt in &self.ordered_imports {
|
||||
if stmt.start() >= at {
|
||||
break;
|
||||
}
|
||||
if let StmtKind::ImportFrom(ast::StmtImportFrom {
|
||||
module: name,
|
||||
level,
|
||||
..
|
||||
}) = &stmt.node
|
||||
{
|
||||
if level.map_or(true, |level| level.to_u32() == 0)
|
||||
&& name.as_ref().map_or(false, |name| name == module)
|
||||
{
|
||||
import_from = Some(*stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
import_from
|
||||
}
|
||||
|
||||
/// Add the given member to an existing `StmtKind::ImportFrom` statement.
|
||||
pub fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
|
||||
let mut tree = match_module(self.locator.slice(stmt))?;
|
||||
let mut tree = match_module(self.locator.slice(stmt.range()))?;
|
||||
let import_from = match_import_from(&mut tree)?;
|
||||
let aliases = match_aliases(import_from)?;
|
||||
aliases.push(ImportAlias {
|
||||
@@ -113,11 +115,7 @@ impl<'a> Importer<'a> {
|
||||
..CodegenState::default()
|
||||
};
|
||||
tree.codegen(&mut state);
|
||||
Ok(Edit::replacement(
|
||||
state.to_string(),
|
||||
stmt.location,
|
||||
stmt.end_location.unwrap(),
|
||||
))
|
||||
Ok(Edit::range_replacement(state.to_string(), stmt.range()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,13 +124,13 @@ struct Insertion {
|
||||
/// The content to add before the insertion.
|
||||
prefix: &'static str,
|
||||
/// The location at which to insert.
|
||||
location: Location,
|
||||
location: TextSize,
|
||||
/// The content to add after the insertion.
|
||||
suffix: &'static str,
|
||||
}
|
||||
|
||||
impl Insertion {
|
||||
fn new(prefix: &'static str, location: Location, suffix: &'static str) -> Self {
|
||||
fn new(prefix: &'static str, location: TextSize, suffix: &'static str) -> Self {
|
||||
Self {
|
||||
prefix,
|
||||
location,
|
||||
@@ -142,7 +140,7 @@ impl Insertion {
|
||||
}
|
||||
|
||||
/// Find the end of the last docstring.
|
||||
fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
|
||||
fn match_docstring_end(body: &[Stmt]) -> Option<TextSize> {
|
||||
let mut iter = body.iter();
|
||||
let Some(mut stmt) = iter.next() else {
|
||||
return None;
|
||||
@@ -156,10 +154,10 @@ fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
|
||||
}
|
||||
stmt = next;
|
||||
}
|
||||
Some(stmt.end_location.unwrap())
|
||||
Some(stmt.end())
|
||||
}
|
||||
|
||||
/// Find the location at which a "top-of-file" import should be inserted,
|
||||
/// Find the location at which an "end-of-statement" import should be inserted,
|
||||
/// along with a prefix and suffix to use for the insertion.
|
||||
///
|
||||
/// For example, given the following code:
|
||||
@@ -168,22 +166,29 @@ fn match_docstring_end(body: &[Stmt]) -> Option<Location> {
|
||||
/// """Hello, world!"""
|
||||
///
|
||||
/// import os
|
||||
/// import math
|
||||
///
|
||||
///
|
||||
/// def foo():
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// The location returned will be the start of the `import os` statement,
|
||||
/// The location returned will be the start of new line after the last
|
||||
/// import statement, which in this case is the line after `import math`,
|
||||
/// along with a trailing newline suffix.
|
||||
fn end_of_statement_insertion(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> Insertion {
|
||||
let location = stmt.end_location.unwrap();
|
||||
let mut tokens = lexer::lex_located(locator.after(location), Mode::Module, location).flatten();
|
||||
if let Some((.., Tok::Semi, end)) = tokens.next() {
|
||||
let location = stmt.end();
|
||||
let mut tokens =
|
||||
lexer::lex_starts_at(locator.after(location), Mode::Module, location).flatten();
|
||||
if let Some((Tok::Semi, range)) = tokens.next() {
|
||||
// If the first token after the docstring is a semicolon, insert after the semicolon as an
|
||||
// inline statement;
|
||||
Insertion::new(" ", end, ";")
|
||||
Insertion::new(" ", range.end(), ";")
|
||||
} else {
|
||||
// Otherwise, insert on the next line.
|
||||
Insertion::new(
|
||||
"",
|
||||
Location::new(location.row() + 1, 0),
|
||||
locator.full_line_end(location),
|
||||
stylist.line_ending().as_str(),
|
||||
)
|
||||
}
|
||||
@@ -207,25 +212,25 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) ->
|
||||
let mut location = if let Some(location) = match_docstring_end(body) {
|
||||
// If the first token after the docstring is a semicolon, insert after the semicolon as an
|
||||
// inline statement;
|
||||
let first_token = lexer::lex_located(locator.after(location), Mode::Module, location)
|
||||
let first_token = lexer::lex_starts_at(locator.after(location), Mode::Module, location)
|
||||
.flatten()
|
||||
.next();
|
||||
if let Some((.., Tok::Semi, end)) = first_token {
|
||||
return Insertion::new(" ", end, ";");
|
||||
if let Some((Tok::Semi, range)) = first_token {
|
||||
return Insertion::new(" ", range.end(), ";");
|
||||
}
|
||||
|
||||
// Otherwise, advance to the next row.
|
||||
Location::new(location.row() + 1, 0)
|
||||
locator.full_line_end(location)
|
||||
} else {
|
||||
Location::default()
|
||||
TextSize::default()
|
||||
};
|
||||
|
||||
// Skip over any comments and empty lines.
|
||||
for (.., tok, end) in
|
||||
lexer::lex_located(locator.after(location), Mode::Module, location).flatten()
|
||||
for (tok, range) in
|
||||
lexer::lex_starts_at(locator.after(location), Mode::Module, location).flatten()
|
||||
{
|
||||
if matches!(tok, Tok::Comment(..) | Tok::Newline) {
|
||||
location = Location::new(end.row() + 1, 0);
|
||||
location = locator.full_line_end(range.end());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -237,11 +242,12 @@ fn top_of_file_insertion(body: &[Stmt], locator: &Locator, stylist: &Stylist) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use ruff_text_size::TextSize;
|
||||
use rustpython_parser as parser;
|
||||
use rustpython_parser::ast::Location;
|
||||
use rustpython_parser::lexer::LexResult;
|
||||
|
||||
use ruff_python_ast::source_code::{LineEnding, Locator, Stylist};
|
||||
use ruff_python_ast::newlines::LineEnding;
|
||||
use ruff_python_ast::source_code::{Locator, Stylist};
|
||||
|
||||
use crate::importer::{top_of_file_insertion, Insertion};
|
||||
|
||||
@@ -258,7 +264,7 @@ mod tests {
|
||||
let contents = "";
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(1, 0), LineEnding::default().as_str())
|
||||
Insertion::new("", TextSize::from(0), LineEnding::default().as_str())
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -266,7 +272,7 @@ mod tests {
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(2, 0), LineEnding::default().as_str())
|
||||
Insertion::new("", TextSize::from(19), LineEnding::default().as_str())
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -275,7 +281,7 @@ mod tests {
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(2, 0), "\n")
|
||||
Insertion::new("", TextSize::from(20), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -285,7 +291,7 @@ mod tests {
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(3, 0), "\n")
|
||||
Insertion::new("", TextSize::from(40), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -294,7 +300,7 @@ x = 1
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(1, 0), "\n")
|
||||
Insertion::new("", TextSize::from(0), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -303,7 +309,7 @@ x = 1
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(2, 0), "\n")
|
||||
Insertion::new("", TextSize::from(23), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -313,7 +319,7 @@ x = 1
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(3, 0), "\n")
|
||||
Insertion::new("", TextSize::from(43), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -323,7 +329,7 @@ x = 1
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(3, 0), "\n")
|
||||
Insertion::new("", TextSize::from(43), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -332,7 +338,7 @@ x = 1
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new("", Location::new(1, 0), "\n")
|
||||
Insertion::new("", TextSize::from(0), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -341,7 +347,7 @@ x = 1
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new(" ", Location::new(1, 20), ";")
|
||||
Insertion::new(" ", TextSize::from(20), ";")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
@@ -351,7 +357,7 @@ x = 1
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::new(" ", Location::new(1, 20), ";")
|
||||
Insertion::new(" ", TextSize::from(20), ";")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_text_size::TextRange;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, BufWriter};
|
||||
use std::iter;
|
||||
@@ -7,7 +8,6 @@ use serde::Serialize;
|
||||
use serde_json::error::Category;
|
||||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_ast::types::Range;
|
||||
|
||||
use crate::jupyter::{CellType, JupyterNotebook, SourceValue};
|
||||
use crate::rules::pycodestyle::rules::SyntaxError;
|
||||
@@ -18,7 +18,7 @@ pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
|
||||
/// Jupyter Notebook indexing table
|
||||
///
|
||||
/// When we lint a jupyter notebook, we have to translate the row/column based on
|
||||
/// [`crate::message::Location`]
|
||||
/// [`ruff_text_size::TextSize`]
|
||||
/// to jupyter notebook cell/row/column.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct JupyterIndex {
|
||||
@@ -46,7 +46,7 @@ impl JupyterNotebook {
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
Range::default(),
|
||||
TextRange::default(),
|
||||
)
|
||||
})?);
|
||||
let notebook: JupyterNotebook = match serde_json::from_reader(reader) {
|
||||
@@ -59,7 +59,7 @@ impl JupyterNotebook {
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
Range::default(),
|
||||
TextRange::default(),
|
||||
),
|
||||
Category::Syntax | Category::Eof => {
|
||||
// Maybe someone saved the python sources (those with the `# %%` separator)
|
||||
@@ -69,7 +69,7 @@ impl JupyterNotebook {
|
||||
IOError {
|
||||
message: format!("{err}"),
|
||||
},
|
||||
Range::default(),
|
||||
TextRange::default(),
|
||||
)
|
||||
})?;
|
||||
// Check if tokenizing was successful and the file is non-empty
|
||||
@@ -84,7 +84,7 @@ impl JupyterNotebook {
|
||||
but this file isn't valid JSON: {err}"
|
||||
),
|
||||
},
|
||||
Range::default(),
|
||||
TextRange::default(),
|
||||
)
|
||||
} else {
|
||||
Diagnostic::new(
|
||||
@@ -95,7 +95,7 @@ impl JupyterNotebook {
|
||||
but found a Python source file: {err}"
|
||||
),
|
||||
},
|
||||
Range::default(),
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ impl JupyterNotebook {
|
||||
"This file does not match the schema expected of Jupyter Notebooks: {err}"
|
||||
),
|
||||
},
|
||||
Range::default(),
|
||||
TextRange::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -126,7 +126,7 @@ impl JupyterNotebook {
|
||||
notebook.nbformat
|
||||
),
|
||||
},
|
||||
Range::default(),
|
||||
TextRange::default(),
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ enum State {
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StateMachine {
|
||||
pub(crate) struct StateMachine {
|
||||
state: State,
|
||||
bracket_count: usize,
|
||||
}
|
||||
|
||||
impl StateMachine {
|
||||
pub fn consume(&mut self, tok: &Tok) -> bool {
|
||||
pub(crate) fn consume(&mut self, tok: &Tok) -> bool {
|
||||
match tok {
|
||||
Tok::NonLogicalNewline
|
||||
| Tok::Newline
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod docstring_detection;
|
||||
pub(crate) mod docstring_detection;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
//! [Ruff]: https://github.com/charliermarsh/ruff
|
||||
|
||||
pub use ruff_python_ast::source_code::round_trip;
|
||||
pub use ruff_python_ast::types::Range;
|
||||
pub use rule_selector::RuleSelector;
|
||||
pub use rules::pycodestyle::rules::IOError;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user