Compare commits

...

23 Commits

Author SHA1 Message Date
Charlie Marsh
9e704a7c63 Only fix true-false returns for return-bool-condition-directly (#2037)
Closes #2035.
2023-01-20 13:17:19 -05:00
Zeddicus414
c9da98e0b7 Fix D404 NoThisPrefix not working with whitespace. (#2036)
D404 should trigger for """ This is a docstring."""

Add a few tests to ensure the fix worked.
2023-01-20 13:01:31 -05:00
Charlie Marsh
5377d24507 Bump version to 0.0.228 2023-01-20 09:58:56 -05:00
Florian Best
db8e4500ee fix(pydocstyle): Avoid trimming docstring if starts with leading quote (#2027)
Fixes: #2017

looks like the other way round is also possible to break:

```""" "foo"""`
2023-01-20 09:57:48 -05:00
Aarni Koskela
bd2de5624e Move readme dev details to CONTRIBUTING.md and fix contradictions (#2030)
Following up on #2018/#2019 discussion, this moves the readme's development-related bits to `CONTRIBUTING.md` to avoid duplication, and fixes up the commands accordingly 😄
2023-01-20 09:23:28 -05:00
Aarni Koskela
3a81f893cc Bump terminfo to remove a whole bunch of unnecessary dependencies (#2022)
See 6281c6b8f7

```
$ cargo update -p terminfo
    Updating crates.io index
    Removing cfg-if v0.1.10
    Removing dirs v2.0.2
    Removing getrandom v0.1.16
    Removing phf v0.8.0
    Updating phf_codegen v0.8.0 -> v0.11.1
    Updating phf_generator v0.8.0 -> v0.11.1
    Removing phf_shared v0.8.0
    Removing rand v0.7.3
    Removing rand_chacha v0.2.2
    Removing rand_core v0.5.1
    Removing rand_hc v0.2.0
    Removing rand_pcg v0.2.1
    Updating terminfo v0.7.3 -> v0.7.5
    Removing wasi v0.9.0+wasi-snapshot-preview1
```
2023-01-20 09:09:02 -05:00
Charlie Marsh
fd6dc2a343 Use platform-appropriate newline character for LibCST embedding (#2028)
Closes #2026.
2023-01-20 09:08:04 -05:00
Martin Fischer
8693236f9e Make CI test add_*.py scripts 2023-01-20 08:09:54 -05:00
Martin Fischer
44e2b6208a fix: Update add_rule.py to create new files for rules 2023-01-20 08:09:54 -05:00
Martin Fischer
16c81f75c2 fix: Update add_rule.py to account for 16e79c8d 2023-01-20 08:09:54 -05:00
Martin Fischer
e1d6ac3265 fix: Update add_plugin.py to account for 9dc66b5a 2023-01-20 08:09:54 -05:00
Martin Fischer
3aec1100f5 fix: Update add_plugin.py to account for b78b6f27 2023-01-20 08:09:54 -05:00
Martin Fischer
c00df647e1 fix: Update add_rule.py to account for 81996f1bc 2023-01-20 08:09:54 -05:00
Martin Fischer
f012877be1 Add scripts/pyproject.toml to use ruff for ruff :) 2023-01-20 08:09:54 -05:00
Martin Fischer
ff6defc988 refactor: Introduce get_indent helper for scripts 2023-01-20 08:09:54 -05:00
Martin Fischer
67ca50e9f2 refactor: Reduce code duplication in scripts/ 2023-01-20 08:09:54 -05:00
Martin Fischer
6cc160bc2b Mark scripts/add_*.py as executable 2023-01-20 08:09:54 -05:00
Ville Skyttä
4bdf506d80 Grammar fixes (#2014) 2023-01-20 07:44:23 -05:00
Charlie Marsh
4af2353ef9 Avoid trimming docstring if ends in trailing quote (#2025)
Closes #2017.
2023-01-20 07:41:58 -05:00
Ville Skyttä
6072edf5bf Note .astimezone() in call-datetime-strptime-without-zone message (#2015) 2023-01-20 07:40:34 -05:00
Martin Fischer
4061eeeb32 Update CI to use MSRV for cargo test and build
As per Cargo.toml our minimal supported Rust version is 1.65.0, so we
should be using that version in our CI for cargo test and cargo build.

This was apparently accidentally changed in
79ca66ace5.
2023-01-20 07:39:40 -05:00
Aarni Koskela
bea6deb0c3 Port pydocstyle code 401 (ImperativeMood) (#1999)
This adds support for pydocstyle code D401 using the `imperative` crate.
2023-01-20 07:18:27 -05:00
Colin Delahunty
81db00a3c4 Pyupgrade: Extraneous parenthesis (#1926) 2023-01-20 00:04:07 -05:00
49 changed files with 1000 additions and 305 deletions

View File

@@ -70,7 +70,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
toolchain: 1.65.0
override: true
- uses: Swatinem/rust-cache@v1
- run: cargo install cargo-insta
@@ -84,6 +84,21 @@ jobs:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
- run: RUSTDOCFLAGS="-D warnings" cargo doc --all --no-deps
scripts:
name: "test scripts"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --origin pylint
- run: cargo check
- run: ./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/
- run: cargo check
# TODO(charlie): Re-enable the `wasm-pack` tests.
# See: https://github.com/charliermarsh/ruff/issues/1425
# wasm-pack-test:
@@ -122,7 +137,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
toolchain: 1.65.0
override: true
- uses: Swatinem/rust-cache@v1
- uses: actions/setup-python@v4

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.227
rev: v0.0.228
hooks:
- id: ruff

View File

@@ -37,13 +37,16 @@ After cloning the repository, run Ruff locally with:
cargo run resources/test/fixtures --no-cache
```
Prior to opening a pull request, ensure that your code has been auto-formatted, and that it passes
both the lint and test validation checks:
Prior to opening a pull request, ensure that your code has been auto-formatted,
and that it passes both the lint and test validation checks.
For rustfmt and Clippy, we use [nightly Rust][nightly], as it is stricter than stable Rust.
(However, tests and builds use stable Rust.)
```shell
cargo +nightly fmt --all # Auto-formatting...
cargo +nightly clippy --all # Linting...
cargo +nightly test --all # Testing...
cargo +nightly clippy --fix --workspace --all-targets --all-features -- -W clippy::pedantic # Linting...
cargo test --all # Testing...
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
@@ -127,3 +130,5 @@ them to [PyPI](https://pypi.org/project/ruff/).
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
[nightly]: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust

233
Cargo.lock generated
View File

@@ -14,7 +14,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.8",
"getrandom",
"once_cell",
"version_check",
]
@@ -197,12 +197,6 @@ version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -413,7 +407,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"wasm-bindgen",
]
@@ -439,7 +433,7 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -484,7 +478,7 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-utils",
]
@@ -494,7 +488,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
@@ -506,7 +500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
@@ -518,7 +512,7 @@ version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -592,16 +586,6 @@ dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
dependencies = [
"cfg-if 0.1.10",
"dirs-sys",
]
[[package]]
name = "dirs"
version = "4.0.0"
@@ -617,7 +601,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"dirs-sys-next",
]
@@ -721,7 +705,7 @@ version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"redox_syscall",
"windows-sys",
@@ -735,7 +719,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.227"
version = "0.0.228"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -786,24 +770,13 @@ dependencies = [
"libc",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
@@ -926,6 +899,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "imperative"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f92123bf2fe0d9f1b5df1964727b970ca3b2d0203d47cf97fb1f36d856b6398"
dependencies = [
"phf 0.11.1",
"rust-stemmers",
]
[[package]]
name = "indexmap"
version = "1.9.2"
@@ -976,7 +959,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -1186,7 +1169,7 @@ version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -1265,7 +1248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"libc",
"static_assertions",
]
@@ -1419,7 +1402,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
@@ -1493,15 +1476,6 @@ dependencies = [
"indexmap",
]
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared 0.8.0",
]
[[package]]
name = "phf"
version = "0.10.1"
@@ -1512,13 +1486,12 @@ dependencies = [
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
name = "phf"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [
"phf_generator 0.8.0",
"phf_shared 0.8.0",
"phf_shared 0.11.1",
]
[[package]]
@@ -1532,13 +1505,13 @@ dependencies = [
]
[[package]]
name = "phf_generator"
version = "0.8.0"
name = "phf_codegen"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
dependencies = [
"phf_shared 0.8.0",
"rand 0.7.3",
"phf_generator 0.11.1",
"phf_shared 0.11.1",
]
[[package]]
@@ -1548,16 +1521,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.5",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
name = "phf_generator"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
dependencies = [
"siphasher",
"phf_shared 0.11.1",
"rand",
]
[[package]]
@@ -1569,6 +1543,15 @@ dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.4.2"
@@ -1724,20 +1707,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
"rand_pcg",
]
[[package]]
name = "rand"
version = "0.8.5"
@@ -1745,18 +1714,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
"rand_chacha",
"rand_core",
]
[[package]]
@@ -1766,16 +1725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
"rand_core",
]
[[package]]
@@ -1784,25 +1734,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.8",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
"getrandom",
]
[[package]]
@@ -1842,7 +1774,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.8",
"getrandom",
"redox_syscall",
"thiserror",
]
@@ -1906,23 +1838,24 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.227"
version = "0.0.228"
dependencies = [
"anyhow",
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"chrono",
"clap 4.0.32",
"colored",
"console_error_panic_hook",
"console_log",
"criterion",
"dirs 4.0.0",
"dirs",
"fern",
"getrandom 0.2.8",
"getrandom",
"glob",
"globset",
"ignore",
"imperative",
"insta",
"itertools",
"js-sys",
@@ -1960,7 +1893,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.227"
version = "0.0.228"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1997,7 +1930,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.227"
version = "0.0.228"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -2018,7 +1951,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.227"
version = "0.0.228"
dependencies = [
"once_cell",
"proc-macro2",
@@ -2027,6 +1960,16 @@ dependencies = [
"textwrap",
]
[[package]]
name = "rust-stemmers"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54"
dependencies = [
"serde",
"serde_derive",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -2076,7 +2019,7 @@ source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8e
dependencies = [
"ascii",
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"hexf-parse",
"itertools",
"lexical-parse-float",
@@ -2087,7 +2030,7 @@ dependencies = [
"num-traits",
"once_cell",
"radium",
"rand 0.8.5",
"rand",
"siphasher",
"unic-ucd-category",
"volatile",
@@ -2274,7 +2217,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
dependencies = [
"dirs 4.0.0",
"dirs",
]
[[package]]
@@ -2377,7 +2320,7 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
@@ -2417,15 +2360,15 @@ dependencies = [
[[package]]
name = "terminfo"
version = "0.7.3"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
checksum = "da31aef70da0f6352dbcb462683eb4dd2bfad01cf3fc96cf204547b9a839a585"
dependencies = [
"dirs 2.0.2",
"dirs",
"fnv",
"nom",
"phf 0.8.0",
"phf_codegen 0.8.0",
"phf 0.11.1",
"phf_codegen 0.11.1",
]
[[package]]
@@ -2449,7 +2392,7 @@ version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45b7bf6e19353ddd832745c8fcf77a17a93171df7151187f26623f2b75b5b26"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"proc-macro-error",
"proc-macro2",
"quote",
@@ -2589,7 +2532,7 @@ version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"static_assertions",
]
@@ -2793,12 +2736,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@@ -2817,7 +2754,7 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"wasm-bindgen-macro",
]
@@ -2842,7 +2779,7 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.227"
version = "0.0.228"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -35,6 +35,7 @@ fern = { version = "0.6.1" }
glob = { version = "0.3.0" }
globset = { version = "0.4.9" }
ignore = { version = "0.4.18" }
imperative = { version = "1.0.3" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
log = { version = "0.4.17" }
@@ -46,7 +47,7 @@ once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.227", path = "ruff_macros" }
ruff_macros = { version = "0.0.228", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
@@ -59,9 +60,9 @@ smallvec = { version = "1.10.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }
thiserror = { version = "1.0" }
titlecase = { version = "2.2.1" }
toml_edit = { version = "0.17.1", features = ["easy"] }
thiserror = { version = "1.0" }
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
# For (future) wasm-pack support

View File

@@ -141,12 +141,11 @@ developer of [Zulip](https://github.com/zulip/zulip):
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
1. [Editor Integrations](#editor-integrations)
1. [FAQ](#faq)
1. [Development](#development)
1. [Contributing](#contributing)
1. [Releases](#releases)
1. [Benchmarks](#benchmarks)
1. [Reference](#reference)
1. [License](#license)
1. [Contributing](#contributing)
## Installation and Usage
@@ -199,7 +198,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.227'
rev: 'v0.0.228'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -672,6 +671,7 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
| D300 | uses-triple-quotes | Use """triple double quotes""" | |
| D301 | uses-r-prefix-for-backslashed-content | Use r""" if any backslashes in a docstring | |
| D400 | ends-in-period | First line should end with a period | 🛠 |
| D401 | non-imperative-mood | First line of docstring should be in imperative mood: "{first_line}" | |
| D402 | no-signature | First line should not be the function's signature | |
| D403 | first-line-capitalized | First word of the first line should be properly capitalized | |
| D404 | no-this-prefix | First word of the docstring should not be "This" | |
@@ -728,6 +728,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP030 | format-literals | Use implicit references for positional format fields | 🛠 |
| UP032 | f-string | Use f-string instead of `format` call | 🛠 |
| UP033 | functools-cache | Use `@functools.cache` instead of `@functools.lru_cache(maxsize=None)` | 🛠 |
| UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 |
### pep8-naming (N)
@@ -1062,7 +1063,7 @@ For more, see [flake8-datetimez](https://pypi.org/project/flake8-datetimez/20.10
| DTZ004 | call-datetime-utcfromtimestamp | The use of `datetime.datetime.utcfromtimestamp()` is not allowed | |
| DTZ005 | call-datetime-now-without-tzinfo | The use of `datetime.datetime.now()` without `tz` argument is not allowed | |
| DTZ006 | call-datetime-fromtimestamp | The use of `datetime.datetime.fromtimestamp()` without `tz` argument is not allowed | |
| DTZ007 | call-datetime-strptime-without-zone | The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)` | |
| DTZ007 | call-datetime-strptime-without-zone | The use of `datetime.datetime.strptime()` without %z must be followed by `.replace(tzinfo=)` or `.astimezone()` | |
| DTZ011 | call-date-today | The use of `datetime.date.today()` is not allowed. | |
| DTZ012 | call-date-fromtimestamp | The use of `datetime.date.fromtimestamp()` is not allowed | |
@@ -1639,24 +1640,10 @@ matter how they're provided, which avoids accidental incompatibilities and simpl
Run `ruff /path/to/code.py --show-settings` to view the resolved settings for a given file.
## Development
## Contributing
Ruff is written in Rust (1.65.0). You'll need to install the [Rust toolchain](https://www.rust-lang.org/tools/install)
for development.
Assuming you have `cargo` installed, you can run:
```shell
cargo run resources/test/fixtures
```
For development, we use [nightly Rust](https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust):
```shell
cargo +nightly fmt
cargo +nightly clippy --fix --workspace --all-targets --all-features -- -W clippy::pedantic
cargo +nightly test --all
```
Contributions are welcome and hugely appreciated. To get started, check out the
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTING.md).
## Releases
@@ -3456,8 +3443,3 @@ keep-runtime-typing = true
## License
MIT
## Contributing
Contributions are welcome and hugely appreciated. To get started, check out the
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTING.md).

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.227"
version = "0.0.228"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.227"
version = "0.0.228"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.227"
version = "0.0.228"
edition = "2021"
[dependencies]

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.227"
version = "0.0.228"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },

View File

@@ -383,7 +383,7 @@ s = (
s2 = (
'this'
'is a also a string'
'is also a string'
)
t = (

View File

@@ -42,3 +42,11 @@ def f():
return "foo"
else:
return False
def f():
# SIM103 (but not fixable)
if a:
return False
else:
return True

View File

@@ -606,3 +606,31 @@ def one_liner():
r"""Wrong.
"""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""Wrong."
"""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""
"Wrong."""
@expect('D404: First word of the docstring should not be "This"')
def starts_with_this():
"""This is a docstring."""
@expect('D404: First word of the docstring should not be "This"')
def starts_with_space_then_this():
""" This is a docstring that starts with a space.""" # noqa: D210

View File

@@ -0,0 +1,43 @@
# Bad examples
def bad_liouiwnlkjl():
"""Returns foo."""
def bad_sdgfsdg23245():
"""Constructor for a foo."""
def bad_sdgfsdg23245777():
"""
Constructor for a boa.
"""
def bad_run_something():
"""Runs something"""
pass
def multi_line():
"""Writes a logical line that
extends to two physical lines.
"""
# Good examples
def good_run_something():
"""Run away."""
def good_construct():
"""Construct a beautiful house."""
def good_multi_line():
"""Write a logical line that
extends to two physical lines.
"""

View File

@@ -0,0 +1,61 @@
# UP034
print(("foo"))
# UP034
print(("hell((goodybe))o"))
# UP034
print((("foo")))
# UP034
print((((1))))
# UP034
print(("foo{}".format(1)))
# UP034
print(
("foo{}".format(1))
)
# UP034
print(
(
"foo"
)
)
# UP034
def f():
x = int(((yield 1)))
# UP034
if True:
print(
("foo{}".format(1))
)
# UP034
print((x for x in range(3)))
# OK
print("foo")
# OK
print((1, 2, 3))
# OK
print(())
# OK
print((1,))
# OK
sum((block.code for block in blocks), [])
# OK
def f():
x = int((yield 1))
# OK
sum((i for i in range(3)), [])

View File

@@ -1277,6 +1277,7 @@
"D4",
"D40",
"D400",
"D401",
"D402",
"D403",
"D404",
@@ -1768,6 +1769,7 @@
"UP030",
"UP032",
"UP033",
"UP034",
"W",
"W2",
"W29",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.227"
version = "0.0.228"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.227"
version = "0.0.228"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.227"
version = "0.0.228"
edition = "2021"
[lib]

18
scripts/_utils.py Normal file
View File

@@ -0,0 +1,18 @@
import os
import re
from pathlib import Path
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(origin: str) -> str:
return origin.replace("-", "_")
def pascal_case(origin: str) -> str:
"""Convert from snake-case to PascalCase."""
return "".join(word.title() for word in origin.split("-"))
def get_indent(line: str) -> str:
return re.match(r"^\s*", line).group() # pyright: ignore[reportOptionalMemberAccess]

36
scripts/add_plugin.py Normal file → Executable file
View File

@@ -10,17 +10,8 @@ Example usage:
import argparse
import os
from pathlib import Path
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(plugin: str) -> str:
return plugin.replace("-", "_")
def pascal_case(plugin: str) -> str:
return "".join(word.title() for word in plugin.split("-"))
from _utils import ROOT_DIR, dir_name, get_indent, pascal_case
def main(*, plugin: str, url: str) -> None:
@@ -36,6 +27,7 @@ def main(*, plugin: str, url: str) -> None:
with open(rust_module / "rules.rs", "w+") as fp:
fp.write("use crate::checkers::ast::Checker;\n")
with open(rust_module / "mod.rs", "w+") as fp:
fp.write(f"//! Rules from [{plugin}]({url}).\n")
fp.write("pub(crate) mod rules;\n")
fp.write("\n")
fp.write(
@@ -76,35 +68,23 @@ mod tests {
with open(ROOT_DIR / "src/registry.rs", "w") as fp:
for line in content.splitlines():
indent = get_indent(line)
if line.strip() == "// Ruff":
indent = line.split("// Ruff")[0]
fp.write(f"{indent}// {plugin}")
fp.write("\n")
elif line.strip() == "Ruff,":
indent = line.split("Ruff,")[0]
fp.write(f"{indent}{pascal_case(plugin)},")
fp.write("\n")
elif line.strip() == 'RuleOrigin::Ruff => "Ruff-specific rules",':
indent = line.split('RuleOrigin::Ruff => "Ruff-specific rules",')[0]
fp.write(f'{indent}RuleOrigin::{pascal_case(plugin)} => "{plugin}",')
fp.write("\n")
elif line.strip() == "RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],":
indent = line.split("RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],")[0]
elif line.strip() == "RuleOrigin::Ruff => Prefixes::Single(RuleCodePrefix::RUF),":
prefix = 'todo!("Fill-in prefix after generating codes")'
fp.write(
f"{indent}RuleOrigin::{pascal_case(plugin)} => vec![\n"
f'{indent} todo!("Fill-in prefix after generating codes")\n'
f"{indent}],"
f"{indent}RuleOrigin::{pascal_case(plugin)} => Prefixes::Single({prefix}),"
)
fp.write("\n")
elif line.strip() == "RuleOrigin::Ruff => None,":
indent = line.split("RuleOrigin::Ruff => None,")[0]
fp.write(f"{indent}RuleOrigin::{pascal_case(plugin)} => " f'Some(("{url}", &Platform::PyPI)),')
fp.write("\n")
fp.write(line)
fp.write("\n")
@@ -114,7 +94,7 @@ mod tests {
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
for line in content.splitlines():
if line.strip() == "// Ruff":
indent = line.split("// Ruff")[0]
indent = get_indent(line)
fp.write(f"{indent}// {plugin}")
fp.write("\n")

33
scripts/add_rule.py Normal file → Executable file
View File

@@ -10,19 +10,8 @@ Example usage:
"""
import argparse
import os
from pathlib import Path
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(origin: str) -> str:
return origin.replace("-", "_")
def pascal_case(origin: str) -> str:
"""Convert from snake-case to PascalCase."""
return "".join(word.title() for word in origin.split("-"))
from _utils import ROOT_DIR, dir_name, get_indent
def snake_case(name: str) -> str:
@@ -45,7 +34,7 @@ def main(*, name: str, code: str, origin: str) -> None:
with open(mod_rs, "w") as fp:
for line in content.splitlines():
if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {":
indent = line.split("fn rules(rule_code: Rule, path: &Path) -> Result<()> {")[0]
indent = get_indent(line)
fp.write(f'{indent}#[test_case(Rule::{code}, Path::new("{code}.py"); "{code}")]')
fp.write("\n")
@@ -53,7 +42,7 @@ def main(*, name: str, code: str, origin: str) -> None:
fp.write("\n")
# Add the relevant rule function.
with open(ROOT_DIR / "src/rules" / dir_name(origin) / "rules.rs", "a") as fp:
with open(ROOT_DIR / "src/rules" / dir_name(origin) / (snake_case(name) + ".rs"), "w") as fp:
fp.write(
f"""
/// {code}
@@ -76,16 +65,14 @@ pub fn {snake_case(name)}(checker: &mut Checker) {{}}
pub struct %s;
);
impl Violation for %s {
#[derive_message_formats]
fn message(&self) -> String {
todo!("Implement message")
}
fn placeholder() -> Self {
%s
todo!("implement message");
format!("TODO: write message")
}
}
"""
% (name, name, name)
% (name, name)
)
fp.write("\n")
@@ -102,7 +89,7 @@ impl Violation for %s {
if has_written:
continue
if line.startswith("define_rule_mapping!"):
if line.startswith("ruff_macros::define_rule_mapping!"):
seen_macro = True
continue
@@ -110,11 +97,13 @@ impl Violation for %s {
continue
if line.strip() == f"// {origin}":
indent = line.split("//")[0]
indent = get_indent(line)
fp.write(f"{indent}{code} => violations::{name},")
fp.write("\n")
has_written = True
assert has_written
if __name__ == "__main__":
parser = argparse.ArgumentParser(

9
scripts/pyproject.toml Normal file
View File

@@ -0,0 +1,9 @@
[tool.ruff]
select = ["ALL"]
ignore = [
"S101", # assert-used
"PLR2004", # magic-value-comparison
]
[tool.ruff.pydocstyle]
convention = "pep257"

View File

@@ -4497,6 +4497,7 @@ impl<'a> Checker<'a> {
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
|| self.settings.rules.enabled(&Rule::EndsInPeriod)
|| self.settings.rules.enabled(&Rule::NonImperativeMood)
|| self.settings.rules.enabled(&Rule::NoSignature)
|| self.settings.rules.enabled(&Rule::FirstLineCapitalized)
|| self.settings.rules.enabled(&Rule::NoThisPrefix)
@@ -4645,6 +4646,9 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::EndsInPeriod) {
pydocstyle::rules::ends_with_period(self, &docstring);
}
if self.settings.rules.enabled(&Rule::NonImperativeMood) {
pydocstyle::rules::non_imperative_mood::non_imperative_mood(self, &docstring);
}
if self.settings.rules.enabled(&Rule::NoSignature) {
pydocstyle::rules::no_signature(self, &docstring);
}

View File

@@ -6,7 +6,8 @@ use crate::lex::docstring_detection::StateMachine;
use crate::registry::{Diagnostic, Rule};
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_quotes, pycodestyle, ruff,
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_quotes, pycodestyle, pyupgrade,
ruff,
};
use crate::settings::{flags, Settings};
use crate::source_code::Locator;
@@ -45,6 +46,7 @@ pub fn check_tokens(
.rules
.enabled(&Rule::TrailingCommaOnBareTupleProhibited)
|| settings.rules.enabled(&Rule::TrailingCommaProhibited);
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
@@ -137,5 +139,13 @@ pub fn check_tokens(
);
}
// UP034
if enforce_extraneous_parenthesis {
diagnostics.extend(
pyupgrade::rules::extraneous_parentheses(tokens, locator, settings, autofix)
.into_iter(),
);
}
diagnostics
}

View File

@@ -251,7 +251,8 @@ ruff_macros::define_rule_mapping!(
UP029 => violations::UnnecessaryBuiltinImport,
UP030 => violations::FormatLiterals,
UP032 => violations::FString,
UP033 => violations::FunctoolsCache,
UP033 => violations::FunctoolsCache,
UP034 => violations::ExtraneousParentheses,
// pydocstyle
D100 => violations::PublicModule,
D101 => violations::PublicClass,
@@ -280,6 +281,7 @@ ruff_macros::define_rule_mapping!(
D300 => violations::UsesTripleQuotes,
D301 => violations::UsesRPrefixForBackslashedContent,
D400 => violations::EndsInPeriod,
D401 => crate::rules::pydocstyle::rules::non_imperative_mood::NonImperativeMood,
D402 => violations::NoSignature,
D403 => violations::FirstLineCapitalized,
D404 => violations::NoThisPrefix,
@@ -555,20 +557,21 @@ impl Rule {
| Rule::PEP3120UnnecessaryCodingComment
| Rule::BlanketTypeIgnore
| Rule::BlanketNOQA => &LintSource::Lines,
Rule::CommentedOutCode
| Rule::SingleLineImplicitStringConcatenation
| Rule::MultiLineImplicitStringConcatenation
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
| Rule::AvoidQuoteEscape
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
| Rule::BadQuotesDocstring
| Rule::AvoidQuoteEscape
| Rule::CommentedOutCode
| Rule::ExtraneousParentheses
| Rule::InvalidEscapeSequence
| Rule::MultiLineImplicitStringConcatenation
| Rule::SingleLineImplicitStringConcatenation
| Rule::TrailingCommaMissing
| Rule::TrailingCommaOnBareTupleProhibited
| Rule::TrailingCommaProhibited
| Rule::AmbiguousUnicodeCharacterString
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterComment => &LintSource::Tokens,
| Rule::TrailingCommaProhibited => &LintSource::Tokens,
Rule::IOError => &LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports,
Rule::ImplicitNamespacePackage => &LintSource::Filesystem,

View File

@@ -75,7 +75,7 @@ pub fn nested_if_statements(checker: &mut Checker, stmt: &Stmt) {
Range::new(stmt.location, nested_if.location),
checker.locator,
) {
match fix_if::fix_nested_if_statements(checker.locator, stmt) {
match fix_if::fix_nested_if_statements(checker.locator, checker.stylist, stmt) {
Ok(fix) => {
if fix
.content
@@ -92,17 +92,35 @@ pub fn nested_if_statements(checker: &mut Checker, stmt: &Stmt) {
checker.diagnostics.push(diagnostic);
}
fn is_one_line_return_bool(stmts: &[Stmt]) -> bool {
enum Bool {
True,
False,
}
impl From<bool> for Bool {
fn from(value: bool) -> Self {
if value {
Bool::True
} else {
Bool::False
}
}
}
fn is_one_line_return_bool(stmts: &[Stmt]) -> Option<Bool> {
if stmts.len() != 1 {
return false;
return None;
}
let StmtKind::Return { value } = &stmts[0].node else {
return false;
return None;
};
let Some(ExprKind::Constant { value, .. }) = value.as_ref().map(|value| &value.node) else {
return false;
return None;
};
matches!(value, Constant::Bool(_))
let Constant::Bool(value) = value else {
return None;
};
Some((*value).into())
}
/// SIM103
@@ -110,17 +128,18 @@ pub fn return_bool_condition_directly(checker: &mut Checker, stmt: &Stmt) {
let StmtKind::If { test, body, orelse } = &stmt.node else {
return;
};
if !(is_one_line_return_bool(body) && is_one_line_return_bool(orelse)) {
let (Some(if_return), Some(else_return)) = (is_one_line_return_bool(body), is_one_line_return_bool(orelse)) else {
return;
}
};
let condition = unparse_expr(test, checker.stylist);
let mut diagnostic = Diagnostic::new(
violations::ReturnBoolConditionDirectly(condition),
Range::from_located(stmt),
);
if checker.patch(&Rule::ReturnBoolConditionDirectly)
&& !(has_comments_in(Range::from_located(stmt), checker.locator)
|| has_comments_in(Range::from_located(&orelse[0]), checker.locator))
&& matches!(if_return, Bool::True)
&& matches!(else_return, Bool::False)
&& !has_comments_in(Range::from_located(stmt), checker.locator)
{
let return_stmt = create_stmt(StmtKind::Return {
value: Some(test.clone()),

View File

@@ -12,7 +12,7 @@ use crate::ast::types::Range;
use crate::ast::whitespace;
use crate::cst::matchers::match_module;
use crate::fix::Fix;
use crate::source_code::Locator;
use crate::source_code::{Locator, Stylist};
fn parenthesize_and_operand(expr: Expression) -> Expression {
match &expr {
@@ -32,6 +32,7 @@ fn parenthesize_and_operand(expr: Expression) -> Expression {
/// (SIM102) Convert `if a: if b:` to `if a and b:`.
pub(crate) fn fix_nested_if_statements(
locator: &Locator,
stylist: &Stylist,
stmt: &rustpython_ast::Stmt,
) -> Result<Fix> {
// Infer the indentation of the outer block.
@@ -62,7 +63,10 @@ pub(crate) fn fix_nested_if_statements(
let module_text = if outer_indent.is_empty() {
module_text
} else {
Cow::Owned(format!("def f():\n{module_text}"))
Cow::Owned(format!(
"def f():{}{module_text}",
stylist.line_ending().as_str()
))
};
// Parse the CST.
@@ -113,7 +117,10 @@ pub(crate) fn fix_nested_if_statements(
}));
outer_if.body = inner_if.body.clone();
let mut state = CodegenState::default();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
..Default::default()
};
tree.codegen(&mut state);
// Reconstruct and reformat the code.
@@ -121,7 +128,9 @@ pub(crate) fn fix_nested_if_statements(
let module_text = if outer_indent.is_empty() {
&module_text
} else {
module_text.strip_prefix("def f():\n").unwrap()
module_text
.strip_prefix(&format!("def f():{}", stylist.line_ending().as_str()))
.unwrap()
};
let contents = if is_elif {
module_text.replacen("if", "elif", 1)

View File

@@ -53,4 +53,14 @@ expression: diagnostics
row: 27
column: 24
parent: ~
- kind:
ReturnBoolConditionDirectly: a
location:
row: 49
column: 4
end_location:
row: 52
column: 19
fix: ~
parent: ~

View File

@@ -55,3 +55,11 @@ pub fn logical_line(content: &str) -> Option<usize> {
}
logical_line
}
/// Normalize a word by removing all non-alphanumeric characters
/// and converting it to lowercase.
pub fn normalize_word(first_word: &str) -> String {
first_word
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase()
}

View File

@@ -44,6 +44,7 @@ mod tests {
#[test_case(Rule::UsesRPrefixForBackslashedContent, Path::new("D.py"); "D301")]
#[test_case(Rule::EndsInPeriod, Path::new("D.py"); "D400_0")]
#[test_case(Rule::EndsInPeriod, Path::new("D400.py"); "D400_1")]
#[test_case(Rule::NonImperativeMood, Path::new("D401.py"); "D401")]
#[test_case(Rule::NoSignature, Path::new("D.py"); "D402")]
#[test_case(Rule::FirstLineCapitalized, Path::new("D.py"); "D403")]
#[test_case(Rule::NoThisPrefix, Path::new("D.py"); "D404")]

View File

@@ -31,6 +31,7 @@ mod multi_line_summary_start;
mod newline_after_last_paragraph;
mod no_signature;
mod no_surrounding_whitespace;
pub mod non_imperative_mood;
mod not_empty;
mod not_missing;
mod one_liner;

View File

@@ -30,22 +30,22 @@ pub fn no_surrounding_whitespace(checker: &mut Checker, docstring: &Docstring) {
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(pattern) = leading_quote(contents) {
if let Some(quote) = pattern.chars().last() {
// If removing whitespace would lead to an invalid string of quote
// characters, avoid applying the fix.
if !trimmed.ends_with(quote) {
diagnostic.amend(Fix::replacement(
trimmed.to_string(),
Location::new(
docstring.expr.location.row(),
docstring.expr.location.column() + pattern.len(),
),
Location::new(
docstring.expr.location.row(),
docstring.expr.location.column() + pattern.len() + line.chars().count(),
),
));
}
// If removing whitespace would lead to an invalid string of quote
// characters, avoid applying the fix.
if !trimmed.ends_with(pattern.chars().last().unwrap())
&& !trimmed.starts_with(pattern.chars().last().unwrap())
{
diagnostic.amend(Fix::replacement(
trimmed.to_string(),
Location::new(
docstring.expr.location.row(),
docstring.expr.location.column() + pattern.len(),
),
Location::new(
docstring.expr.location.row(),
docstring.expr.location.column() + pattern.len() + line.chars().count(),
),
));
}
}
}

View File

@@ -0,0 +1,50 @@
use imperative::Mood;
use once_cell::sync::Lazy;
use ruff_macros::derive_message_formats;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::docstrings::definition::Docstring;
use crate::registry::Diagnostic;
use crate::rules::pydocstyle::helpers::normalize_word;
use crate::violation::Violation;
static MOOD: Lazy<Mood> = Lazy::new(Mood::new);
/// D401
pub fn non_imperative_mood(checker: &mut Checker, docstring: &Docstring) {
let body = docstring.body;
// Find first line, disregarding whitespace.
let line = match body.trim().lines().next() {
Some(line) => line.trim(),
None => return,
};
// Find the first word on that line and normalize it to lower-case.
let first_word_norm = match line.split_whitespace().next() {
Some(word) => normalize_word(word),
None => return,
};
if first_word_norm.is_empty() {
return;
}
if let Some(false) = MOOD.is_imperative(&first_word_norm) {
let diagnostic = Diagnostic::new(
NonImperativeMood(line.to_string()),
Range::from_located(docstring.expr),
);
checker.diagnostics.push(diagnostic);
}
}
define_violation!(
pub struct NonImperativeMood(pub String);
);
impl Violation for NonImperativeMood {
#[derive_message_formats]
fn message(&self) -> String {
let NonImperativeMood(first_line) = self;
format!("First line of docstring should be in imperative mood: \"{first_line}\"")
}
}

View File

@@ -31,11 +31,18 @@ pub fn one_liner(checker: &mut Checker, docstring: &Docstring) {
helpers::leading_quote(docstring.contents),
helpers::trailing_quote(docstring.contents),
) {
diagnostic.amend(Fix::replacement(
format!("{leading}{}{trailing}", docstring.body.trim()),
docstring.expr.location,
docstring.expr.end_location.unwrap(),
));
// If removing whitespace would lead to an invalid string of quote
// characters, avoid applying the fix.
let trimmed = docstring.body.trim();
if !trimmed.ends_with(trailing.chars().last().unwrap())
&& !trimmed.starts_with(leading.chars().last().unwrap())
{
diagnostic.amend(Fix::replacement(
format!("{leading}{trimmed}{trailing}"),
docstring.expr.location,
docstring.expr.end_location.unwrap(),
));
}
}
}
checker.diagnostics.push(diagnostic);

View File

@@ -2,6 +2,7 @@ use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::docstrings::definition::Docstring;
use crate::registry::Diagnostic;
use crate::rules::pydocstyle::helpers::normalize_word;
use crate::violations;
/// D404
@@ -13,14 +14,10 @@ pub fn starts_with_this(checker: &mut Checker, docstring: &Docstring) {
return;
}
let Some(first_word) = body.split(' ').next() else {
let Some(first_word) = trimmed.split(' ').next() else {
return
};
if first_word
.replace(|c: char| !c.is_alphanumeric(), "")
.to_lowercase()
!= "this"
{
if normalize_word(first_word) != "this" {
return;
}
checker.diagnostics.push(Diagnostic::new(

View File

@@ -53,4 +53,24 @@ expression: diagnostics
row: 608
column: 7
parent: ~
- kind:
FitsOnOneLine: ~
location:
row: 615
column: 4
end_location:
row: 617
column: 7
fix: ~
parent: ~
- kind:
FitsOnOneLine: ~
location:
row: 624
column: 4
end_location:
row: 626
column: 14
fix: ~
parent: ~

View File

@@ -22,4 +22,14 @@ expression: diagnostics
column: 13
fix: ~
parent: ~
- kind:
MultiLineSummaryFirstLine: ~
location:
row: 624
column: 4
end_location:
row: 626
column: 14
fix: ~
parent: ~

View File

@@ -212,4 +212,14 @@ expression: diagnostics
column: 7
fix: ~
parent: ~
- kind:
MultiLineSummarySecondLine: ~
location:
row: 615
column: 4
end_location:
row: 617
column: 7
fix: ~
parent: ~

View File

@@ -240,4 +240,21 @@ expression: diagnostics
row: 581
column: 47
parent: ~
- kind:
EndsInPeriod: ~
location:
row: 615
column: 4
end_location:
row: 617
column: 7
fix:
content: "."
location:
row: 615
column: 14
end_location:
row: 615
column: 14
parent: ~

View File

@@ -0,0 +1,55 @@
---
source: src/rules/pydocstyle/mod.rs
expression: diagnostics
---
- kind:
NonImperativeMood: Returns foo.
location:
row: 4
column: 4
end_location:
row: 4
column: 22
fix: ~
parent: ~
- kind:
NonImperativeMood: Constructor for a foo.
location:
row: 8
column: 4
end_location:
row: 8
column: 32
fix: ~
parent: ~
- kind:
NonImperativeMood: Constructor for a boa.
location:
row: 12
column: 4
end_location:
row: 16
column: 7
fix: ~
parent: ~
- kind:
NonImperativeMood: Runs something
location:
row: 20
column: 4
end_location:
row: 20
column: 24
fix: ~
parent: ~
- kind:
NonImperativeMood: Writes a logical line that
location:
row: 25
column: 4
end_location:
row: 27
column: 7
fix: ~
parent: ~

View File

@@ -2,5 +2,24 @@
source: src/rules/pydocstyle/mod.rs
expression: diagnostics
---
[]
- kind:
NoThisPrefix: ~
location:
row: 631
column: 4
end_location:
row: 631
column: 30
fix: ~
parent: ~
- kind:
NoThisPrefix: ~
location:
row: 636
column: 4
end_location:
row: 636
column: 56
fix: ~
parent: ~

View File

@@ -223,4 +223,21 @@ expression: diagnostics
row: 581
column: 47
parent: ~
- kind:
EndsInPunctuation: ~
location:
row: 615
column: 4
end_location:
row: 617
column: 7
fix:
content: "."
location:
row: 615
column: 14
end_location:
row: 615
column: 14
parent: ~

View File

@@ -54,6 +54,7 @@ mod tests {
#[test_case(Rule::FormatLiterals, Path::new("UP030_1.py"); "UP030_1")]
#[test_case(Rule::FString, Path::new("UP032.py"); "UP032")]
#[test_case(Rule::FunctoolsCache, Path::new("UP033.py"); "UP033")]
#[test_case(Rule::ExtraneousParentheses, Path::new("UP034.py"); "UP034")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,143 @@
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::settings::{flags, Settings};
use crate::source_code::Locator;
use crate::violations;
// See: https://github.com/asottile/pyupgrade/blob/97ed6fb3cf2e650d4f762ba231c3f04c41797710/pyupgrade/_main.py#L148
fn match_extraneous_parentheses(tokens: &[LexResult], mut i: usize) -> Option<(usize, usize)> {
i += 1;
loop {
if i >= tokens.len() {
return None;
}
let Ok((_, tok, _)) = &tokens[i] else {
return None;
};
match tok {
Tok::Comment(..) | Tok::NonLogicalNewline => {
i += 1;
}
Tok::Lpar => {
break;
}
_ => {
return None;
}
}
}
// Store the location of the extraneous opening parenthesis.
let start = i;
// Verify that we're not in a tuple or coroutine.
let mut depth = 1;
while depth > 0 {
i += 1;
if i >= tokens.len() {
return None;
}
let Ok((_, tok, _)) = &tokens[i] else {
return None;
};
// If we find a comma or a yield at depth 1 or 2, it's a tuple or coroutine.
if depth == 1 && matches!(tok, Tok::Comma | Tok::Yield) {
return None;
} else if matches!(tok, Tok::Lpar | Tok::Lbrace | Tok::Lsqb) {
depth += 1;
} else if matches!(tok, Tok::Rpar | Tok::Rbrace | Tok::Rsqb) {
depth -= 1;
}
}
// Store the location of the extraneous closing parenthesis.
let end = i;
// Verify that we're not in an empty tuple.
if (start + 1..i).all(|i| {
matches!(
tokens[i],
Ok((_, Tok::Comment(..) | Tok::NonLogicalNewline, _))
)
}) {
return None;
}
// Find the next non-coding token.
i += 1;
loop {
if i >= tokens.len() {
return None;
}
let Ok((_, tok, _)) = &tokens[i] else {
return None;
};
match tok {
Tok::Comment(..) | Tok::NonLogicalNewline => {
i += 1;
}
_ => {
break;
}
}
}
if i >= tokens.len() {
return None;
}
let Ok((_, tok, _)) = &tokens[i] else {
return None;
};
if matches!(tok, Tok::Rpar) {
Some((start, end))
} else {
None
}
}
/// UP034
pub fn extraneous_parentheses(
tokens: &[LexResult],
locator: &Locator,
settings: &Settings,
autofix: flags::Autofix,
) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
let mut i = 0;
while i < tokens.len() {
if matches!(tokens[i], Ok((_, Tok::Lpar, _))) {
if let Some((start, end)) = match_extraneous_parentheses(tokens, i) {
i = end + 1;
let Ok((start, ..)) = &tokens[start] else {
return diagnostics;
};
let Ok((.., end)) = &tokens[end] else {
return diagnostics;
};
let mut diagnostic =
Diagnostic::new(violations::ExtraneousParentheses, Range::new(*start, *end));
if matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::ExtraneousParentheses)
{
let contents = locator.slice_source_code_range(&Range::new(*start, *end));
diagnostic.amend(Fix::replacement(
contents[1..contents.len() - 1].to_string(),
*start,
*end,
));
}
diagnostics.push(diagnostic);
} else {
i += 1;
}
} else {
i += 1;
}
}
diagnostics
}

View File

@@ -2,6 +2,7 @@ pub(crate) use convert_named_tuple_functional_to_class::convert_named_tuple_func
pub(crate) use convert_typed_dict_functional_to_class::convert_typed_dict_functional_to_class;
pub(crate) use datetime_utc_alias::datetime_utc_alias;
pub(crate) use deprecated_unittest_alias::deprecated_unittest_alias;
pub(crate) use extraneous_parentheses::extraneous_parentheses;
pub(crate) use f_strings::f_strings;
pub(crate) use format_literals::format_literals;
pub(crate) use functools_cache::functools_cache;
@@ -43,6 +44,7 @@ mod convert_named_tuple_functional_to_class;
mod convert_typed_dict_functional_to_class;
mod datetime_utc_alias;
mod deprecated_unittest_alias;
mod extraneous_parentheses;
mod f_strings;
mod format_literals;
mod functools_cache;

View File

@@ -0,0 +1,175 @@
---
source: src/rules/pyupgrade/mod.rs
expression: diagnostics
---
- kind:
ExtraneousParentheses: ~
location:
row: 2
column: 6
end_location:
row: 2
column: 13
fix:
content: "\"foo\""
location:
row: 2
column: 6
end_location:
row: 2
column: 13
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 5
column: 6
end_location:
row: 5
column: 26
fix:
content: "\"hell((goodybe))o\""
location:
row: 5
column: 6
end_location:
row: 5
column: 26
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 8
column: 6
end_location:
row: 8
column: 15
fix:
content: "(\"foo\")"
location:
row: 8
column: 6
end_location:
row: 8
column: 15
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 11
column: 6
end_location:
row: 11
column: 13
fix:
content: ((1))
location:
row: 11
column: 6
end_location:
row: 11
column: 13
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 14
column: 6
end_location:
row: 14
column: 25
fix:
content: "\"foo{}\".format(1)"
location:
row: 14
column: 6
end_location:
row: 14
column: 25
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 18
column: 4
end_location:
row: 18
column: 23
fix:
content: "\"foo{}\".format(1)"
location:
row: 18
column: 4
end_location:
row: 18
column: 23
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 23
column: 4
end_location:
row: 25
column: 5
fix:
content: "\n \"foo\"\n "
location:
row: 23
column: 4
end_location:
row: 25
column: 5
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 30
column: 12
end_location:
row: 30
column: 23
fix:
content: (yield 1)
location:
row: 30
column: 12
end_location:
row: 30
column: 23
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 35
column: 8
end_location:
row: 35
column: 27
fix:
content: "\"foo{}\".format(1)"
location:
row: 35
column: 8
end_location:
row: 35
column: 27
parent: ~
- kind:
ExtraneousParentheses: ~
location:
row: 39
column: 6
end_location:
row: 39
column: 27
fix:
content: x for x in range(3)
location:
row: 39
column: 6
end_location:
row: 39
column: 27
parent: ~

View File

@@ -118,23 +118,38 @@ impl Deref for Indentation {
/// The line ending style used in Python source code.
/// See <https://docs.python.org/3/reference/lexical_analysis.html#physical-lines>
#[derive(Debug, Default, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub enum LineEnding {
#[default]
Lf,
Cr,
CrLf,
}
impl Default for LineEnding {
fn default() -> Self {
if cfg!(windows) {
LineEnding::CrLf
} else {
LineEnding::Lf
}
}
}
impl LineEnding {
pub fn as_str(&self) -> &'static str {
match self {
LineEnding::CrLf => "\r\n",
LineEnding::Lf => "\n",
LineEnding::Cr => "\r",
}
}
}
impl Deref for LineEnding {
type Target = str;
fn deref(&self) -> &Self::Target {
match &self {
LineEnding::CrLf => "\r\n",
LineEnding::Lf => "\n",
LineEnding::Cr => "\r",
}
self.as_str()
}
}

View File

@@ -3152,6 +3152,20 @@ impl AlwaysAutofixableViolation for FormatLiterals {
}
}
define_violation!(
pub struct ExtraneousParentheses;
);
impl AlwaysAutofixableViolation for ExtraneousParentheses {
#[derive_message_formats]
fn message(&self) -> String {
format!("Avoid extraneous parentheses")
}
fn autofix_title(&self) -> String {
"Remove extraneous parentheses".to_string()
}
}
define_violation!(
pub struct FString;
);
@@ -4366,7 +4380,7 @@ impl Violation for CallDatetimeStrptimeWithoutZone {
fn message(&self) -> String {
format!(
"The use of `datetime.datetime.strptime()` without %z must be followed by \
`.replace(tzinfo=)`"
`.replace(tzinfo=)` or `.astimezone()`"
)
}
}

View File

@@ -80,12 +80,12 @@ pub fn is_init(name: &str) -> bool {
name == "__init__"
}
/// Returns `true` if a function is an `__new__`.
/// Returns `true` if a function is a `__new__`.
pub fn is_new(name: &str) -> bool {
name == "__new__"
}
/// Returns `true` if a function is an `__call__`.
/// Returns `true` if a function is a `__call__`.
pub fn is_call(name: &str) -> bool {
name == "__call__"
}