Compare commits
24 Commits
break-befo
...
tracing-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f16822c56 | ||
|
|
04183b0299 | ||
|
|
36fa1fe359 | ||
|
|
6e625bd93d | ||
|
|
ebd1b296fd | ||
|
|
1373e1c395 | ||
|
|
4bff397318 | ||
|
|
5347df4728 | ||
|
|
ebe9c03545 | ||
|
|
b0cbcd3dfa | ||
|
|
4df9e07a79 | ||
|
|
f0f7ea7502 | ||
|
|
4f26002dd5 | ||
|
|
d1a9c198e3 | ||
|
|
7a4f699fba | ||
|
|
3fb5418c2c | ||
|
|
9fcc009a0c | ||
|
|
bf8e5a167b | ||
|
|
8a001dfc3d | ||
|
|
0823394525 | ||
|
|
e15047815c | ||
|
|
7531bb3b21 | ||
|
|
2d9b39871f | ||
|
|
e122a96d27 |
34
.github/workflows/release.yaml
vendored
34
.github/workflows/release.yaml
vendored
@@ -7,12 +7,15 @@ on:
|
||||
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
|
||||
type: string
|
||||
sha:
|
||||
description: "Optionally, the full sha of the commit to be released"
|
||||
description: "The full sha of the commit to be released. If omitted, the latest commit on the default branch will be used."
|
||||
default: ""
|
||||
type: string
|
||||
pull_request:
|
||||
paths:
|
||||
# When we change pyproject.toml, we want to ensure that the maturin builds still work
|
||||
- pyproject.toml
|
||||
# And when we change this workflow itself...
|
||||
- .github/workflows/release.yaml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
@@ -31,6 +34,8 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -57,6 +62,8 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -95,6 +102,8 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -141,6 +150,8 @@ jobs:
|
||||
arch: x64
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -187,6 +198,8 @@ jobs:
|
||||
- i686-unknown-linux-gnu
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -244,6 +257,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -297,6 +312,8 @@ jobs:
|
||||
- i686-unknown-linux-musl
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -351,6 +368,8 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
@@ -399,6 +418,8 @@ jobs:
|
||||
if: ${{ inputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- name: Check tag consistency
|
||||
run: |
|
||||
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
|
||||
@@ -410,6 +431,15 @@ jobs:
|
||||
else
|
||||
echo "Releasing ${version}"
|
||||
fi
|
||||
- name: Check main branch
|
||||
if: ${{ inputs.sha }}
|
||||
run: |
|
||||
# Fetch the main branch since a shallow checkout is used by default
|
||||
git fetch origin main --unshallow
|
||||
if ! git branch --contains ${{ inputs.sha }} | grep -E '(^|\s)main$'; then
|
||||
echo "The specified sha is not on the main branch" >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: Check SHA consistency
|
||||
if: ${{ inputs.sha }}
|
||||
run: |
|
||||
@@ -465,6 +495,8 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.sha }}
|
||||
- name: git tag
|
||||
run: |
|
||||
git config user.email "hey@astral.sh"
|
||||
|
||||
144
Cargo.lock
generated
144
Cargo.lock
generated
@@ -128,10 +128,11 @@ checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
|
||||
|
||||
[[package]]
|
||||
name = "argfile"
|
||||
version = "0.1.5"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "265f5108974489a217d5098cd81666b60480c8dd67302acbbe7cbdd8aa09d638"
|
||||
checksum = "1287c4f82a41c5085e65ee337c7934d71ab43d5187740a81fb69129013f6a5f6"
|
||||
dependencies = [
|
||||
"fs-err",
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
@@ -279,8 +280,7 @@ dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"time 0.1.45",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
@@ -624,15 +624,6 @@ dependencies = [
|
||||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -792,15 +783,6 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764"
|
||||
|
||||
[[package]]
|
||||
name = "fern"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.22"
|
||||
@@ -945,12 +927,6 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hexf-parse"
|
||||
version = "0.2.1"
|
||||
@@ -1039,17 +1015,6 @@ dependencies = [
|
||||
"rust-stemmers",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"hashbrown 0.12.3",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.0.0"
|
||||
@@ -1140,15 +1105,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "is-macro"
|
||||
version = "0.2.2"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7d079e129b77477a49c5c4f1cfe9ce6c2c909ef52520693e8e811a714c7b20"
|
||||
checksum = "f4467ed1321b310c2625c5aa6c1b1ffc5de4d9e42668cf697a08fb033ee8265e"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"pmutil",
|
||||
"pmutil 0.6.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1356,9 +1321,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.2"
|
||||
version = "2.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
|
||||
checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
|
||||
|
||||
[[package]]
|
||||
name = "memoffset"
|
||||
@@ -1582,18 +1547,18 @@ checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
|
||||
[[package]]
|
||||
name = "path-absolutize"
|
||||
version = "3.1.0"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43eb3595c63a214e1b37b44f44b0a84900ef7ae0b4c5efce59e123d246d7a0de"
|
||||
checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5"
|
||||
dependencies = [
|
||||
"path-dedot",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "path-dedot"
|
||||
version = "3.1.0"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d55e486337acb9973cdea3ec5638c1b3bcb22e573b2b7b41969e0c744d5a15e"
|
||||
checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
@@ -1672,7 +1637,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
|
||||
dependencies = [
|
||||
"fixedbitset",
|
||||
"indexmap 2.0.0",
|
||||
"indexmap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1739,6 +1704,17 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pmutil"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.4.3"
|
||||
@@ -1830,11 +1806,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyproject-toml"
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee79feaa9d31e1c417e34219e610b67db4e786ce9b49d77dda549640abb9dc5f"
|
||||
checksum = "569e259cd132eb8cec5df8b672d187c5260f82ad352156b5da9549d4472e64b0"
|
||||
dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"indexmap",
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"serde",
|
||||
@@ -1848,7 +1824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bf780b59d590c25f8c59b44c124166a2a93587868b619fb8f5b47fb15e9ed6d"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"indexmap 2.0.0",
|
||||
"indexmap",
|
||||
"nextest-workspace-hack",
|
||||
"quick-xml",
|
||||
"thiserror",
|
||||
@@ -2013,7 +1989,7 @@ version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fabf0a2e54f711c68c50d49f648a1a8a37adcb57353f518ac4df374f0788f42"
|
||||
dependencies = [
|
||||
"pmutil",
|
||||
"pmutil 0.5.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
@@ -2045,7 +2021,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
"colored",
|
||||
"fern",
|
||||
"glob",
|
||||
"globset",
|
||||
"imperative",
|
||||
@@ -2095,6 +2070,8 @@ dependencies = [
|
||||
"test-case",
|
||||
"thiserror",
|
||||
"toml",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"typed-arena",
|
||||
"unicode-width",
|
||||
"unicode_names2",
|
||||
@@ -2722,9 +2699,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.105"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
|
||||
checksum = "2cc66a619ed80bf7a0f6b17dd063a84b88f6dea1813737cf469aef1d081142c2"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
@@ -2755,15 +2732,8 @@ version = "3.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.0.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time 0.3.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2853,24 +2823,24 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.24.1"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
|
||||
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.24.3"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
|
||||
checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.29",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3052,34 +3022,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572"
|
||||
dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
@@ -3141,7 +3083,7 @@ version = "0.19.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
|
||||
dependencies = [
|
||||
"indexmap 2.0.0",
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
@@ -3431,9 +3373,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.3"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
|
||||
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
|
||||
12
Cargo.toml
12
Cargo.toml
@@ -22,27 +22,27 @@ glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.10" }
|
||||
ignore = { version = "0.4.20" }
|
||||
insta = { version = "1.31.0", feature = ["filters", "glob"] }
|
||||
is-macro = { version = "0.2.2" }
|
||||
is-macro = { version = "0.3.0" }
|
||||
itertools = { version = "0.10.5" }
|
||||
log = { version = "0.4.17" }
|
||||
memchr = "2.5.0"
|
||||
memchr = "2.6.3"
|
||||
num-bigint = { version = "0.4.3" }
|
||||
num-traits = { version = "0.2.15" }
|
||||
once_cell = { version = "1.17.1" }
|
||||
path-absolutize = { version = "3.0.14" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
proc-macro2 = { version = "1.0.51" }
|
||||
quote = { version = "1.0.23" }
|
||||
regex = { version = "1.7.1" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
schemars = { version = "0.8.12" }
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = { version = "1.0.93" }
|
||||
serde_json = { version = "1.0.106" }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.2.1", features = ["inline"] }
|
||||
smallvec = { version = "1.10.0" }
|
||||
static_assertions = "1.1.0"
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.24.3" }
|
||||
strum = { version = "0.25.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.25.2" }
|
||||
syn = { version = "2.0.15" }
|
||||
test-case = { version = "3.0.0" }
|
||||
thiserror = { version = "1.0.43" }
|
||||
|
||||
@@ -37,7 +37,6 @@ bitflags = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true, features = ["derive", "string"], optional = true }
|
||||
colored = { workspace = true }
|
||||
fern = { version = "0.6.1" }
|
||||
glob = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
imperative = { version = "1.0.4" }
|
||||
@@ -56,7 +55,7 @@ path-absolutize = { workspace = true, features = [
|
||||
] }
|
||||
pathdiff = { version = "0.2.1" }
|
||||
pep440_rs = { version = "0.3.1", features = ["serde"] }
|
||||
pyproject-toml = { version = "0.6.0" }
|
||||
pyproject-toml = { version = "0.7.0" }
|
||||
quick-junit = { version = "0.3.2" }
|
||||
regex = { workspace = true }
|
||||
result-like = { version = "0.4.6" }
|
||||
@@ -71,6 +70,8 @@ strum = { workspace = true }
|
||||
strum_macros = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unicode-width = { workspace = true }
|
||||
unicode_names2 = { version = "0.6.0", git = "https://github.com/youknowone/unicode_names2.git", rev = "4ce16aa85cbcdd9cc830410f1a72ef9a235f2fde" }
|
||||
|
||||
@@ -7,6 +7,13 @@ reversed(sorted(x, reverse=True))
|
||||
reversed(sorted(x, key=lambda e: e, reverse=True))
|
||||
reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
reversed(sorted(x, reverse=False))
|
||||
reversed(sorted(x, reverse=x))
|
||||
reversed(sorted(x, reverse=not x))
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
reversed(sorted(i for i in range(42)))
|
||||
reversed(sorted((i for i in range(42)), reverse=True))
|
||||
|
||||
|
||||
def reversed(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@@ -529,3 +529,16 @@ def replace_equals_with_dash2():
|
||||
Parameters
|
||||
===========
|
||||
"""
|
||||
|
||||
|
||||
@expect(_D213)
|
||||
def non_empty_blank_line_before_section(): # noqa: D416
|
||||
"""Toggle the gizmo.
|
||||
|
||||
The function's description.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A value of some sort.
|
||||
|
||||
"""
|
||||
|
||||
60
crates/ruff/resources/test/fixtures/pylint/too_many_public_methods.py
vendored
Normal file
60
crates/ruff/resources/test/fixtures/pylint/too_many_public_methods.py
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
class Everything:
|
||||
foo = 1
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _private(self):
|
||||
pass
|
||||
|
||||
def method1(self):
|
||||
pass
|
||||
|
||||
def method2(self):
|
||||
pass
|
||||
|
||||
def method3(self):
|
||||
pass
|
||||
|
||||
def method4(self):
|
||||
pass
|
||||
|
||||
def method5(self):
|
||||
pass
|
||||
|
||||
def method6(self):
|
||||
pass
|
||||
|
||||
def method7(self):
|
||||
pass
|
||||
|
||||
def method8(self):
|
||||
pass
|
||||
|
||||
def method9(self):
|
||||
pass
|
||||
|
||||
class Small:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def _private(self):
|
||||
pass
|
||||
|
||||
def method1(self):
|
||||
pass
|
||||
|
||||
def method2(self):
|
||||
pass
|
||||
|
||||
def method3(self):
|
||||
pass
|
||||
|
||||
def method4(self):
|
||||
pass
|
||||
|
||||
def method5(self):
|
||||
pass
|
||||
|
||||
def method6(self):
|
||||
pass
|
||||
@@ -74,6 +74,8 @@ from typing import Collection
|
||||
from typing import AsyncGenerator
|
||||
from typing import Reversible
|
||||
from typing import Generator
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
|
||||
# OK
|
||||
from a import b
|
||||
|
||||
21
crates/ruff/resources/test/fixtures/refurb/FURB145.py
vendored
Normal file
21
crates/ruff/resources/test/fixtures/refurb/FURB145.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
l = [1, 2, 3, 4, 5]
|
||||
|
||||
# Errors.
|
||||
a = l[:]
|
||||
b, c = 1, l[:]
|
||||
d, e = l[:], 1
|
||||
m = l[::]
|
||||
l[:]
|
||||
print(l[:])
|
||||
|
||||
# False negatives.
|
||||
aa = a[:] # Type inference.
|
||||
|
||||
# OK.
|
||||
t = (1, 2, 3, 4, 5)
|
||||
f = t[:] # t.copy() is not supported.
|
||||
g = l[1:3]
|
||||
h = l[1:]
|
||||
i = l[:3]
|
||||
j = l[1:3:2]
|
||||
k = l[::2]
|
||||
@@ -16,7 +16,7 @@ use crate::rules::{
|
||||
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging_format,
|
||||
flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self, flake8_simplify,
|
||||
flake8_tidy_imports, flake8_use_pathlib, flynt, numpy, pandas_vet, pep8_naming, pycodestyle,
|
||||
pyflakes, pygrep_hooks, pylint, pyupgrade, ruff,
|
||||
pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
|
||||
};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
@@ -113,10 +113,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnnecessaryIterableAllocationForFirstElement) {
|
||||
ruff::rules::unnecessary_iterable_allocation_for_first_element(checker, subscript);
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::InvalidIndexType) {
|
||||
ruff::rules::invalid_index_type(checker, subscript);
|
||||
}
|
||||
if checker.settings.rules.enabled(Rule::SliceCopy) {
|
||||
refurb::rules::slice_copy(checker, subscript);
|
||||
}
|
||||
|
||||
pandas_vet::rules::subscript(checker, value, expr);
|
||||
}
|
||||
|
||||
@@ -411,6 +411,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::EqWithoutHash) {
|
||||
pylint::rules::object_without_hash_method(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::TooManyPublicMethods) {
|
||||
pylint::rules::too_many_public_methods(
|
||||
checker,
|
||||
class_def,
|
||||
checker.settings.pylint.max_public_methods,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::GlobalStatement) {
|
||||
pylint::rules::global_statement(checker, name);
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W1510") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessRunWithoutCheck),
|
||||
#[allow(deprecated)]
|
||||
(Pylint, "W1641") => (RuleGroup::Nursery, rules::pylint::rules::EqWithoutHash),
|
||||
(Pylint, "R0904") => (RuleGroup::Preview, rules::pylint::rules::TooManyPublicMethods),
|
||||
(Pylint, "W2901") => (RuleGroup::Unspecified, rules::pylint::rules::RedefinedLoopName),
|
||||
#[allow(deprecated)]
|
||||
(Pylint, "W3201") => (RuleGroup::Nursery, rules::pylint::rules::BadDunderMethodName),
|
||||
@@ -916,6 +917,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "132") => (RuleGroup::Nursery, rules::refurb::rules::CheckAndRemoveFromSet),
|
||||
(Refurb, "145") => (RuleGroup::Preview, rules::refurb::rules::SliceCopy),
|
||||
|
||||
_ => return None,
|
||||
})
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use libcst_native::{Expression, NameOrAttribute, ParenthesizableWhitespace, SimpleWhitespace};
|
||||
use libcst_native::{
|
||||
Expression, Name, NameOrAttribute, ParenthesizableWhitespace, SimpleWhitespace, UnaryOperation,
|
||||
};
|
||||
|
||||
fn compose_call_path_inner<'a>(expr: &'a Expression, parts: &mut Vec<&'a str>) {
|
||||
match expr {
|
||||
@@ -50,3 +52,41 @@ pub(crate) fn or_space(whitespace: ParenthesizableWhitespace) -> Parenthesizable
|
||||
whitespace
|
||||
}
|
||||
}
|
||||
|
||||
/// Negate a condition, i.e., `a` => `not a` and `not a` => `a`.
|
||||
pub(crate) fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> {
|
||||
if let Expression::UnaryOperation(ref expression) = expression {
|
||||
if matches!(expression.operator, libcst_native::UnaryOp::Not { .. }) {
|
||||
return *expression.expression.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if let Expression::Name(ref expression) = expression {
|
||||
match expression.value {
|
||||
"True" => {
|
||||
return Expression::Name(Box::new(Name {
|
||||
value: "False",
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
"False" => {
|
||||
return Expression::Name(Box::new(Name {
|
||||
value: "True",
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Expression::UnaryOperation(Box::new(UnaryOperation {
|
||||
operator: libcst_native::UnaryOp::Not {
|
||||
whitespace_after: space(),
|
||||
},
|
||||
expression: Box::new(expression.clone()),
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -4,16 +4,17 @@ use std::sync::Mutex;
|
||||
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
use fern;
|
||||
use log::Level;
|
||||
use once_cell::sync::Lazy;
|
||||
use ruff_python_parser::{ParseError, ParseErrorType};
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_parser::{ParseError, ParseErrorType};
|
||||
use ruff_source_file::{OneIndexed, SourceCode, SourceLocation};
|
||||
|
||||
use crate::fs;
|
||||
use crate::source_kind::SourceKind;
|
||||
use ruff_notebook::Notebook;
|
||||
|
||||
pub static WARNINGS: Lazy<Mutex<Vec<&'static str>>> = Lazy::new(Mutex::default);
|
||||
|
||||
@@ -90,49 +91,27 @@ pub enum LogLevel {
|
||||
|
||||
impl LogLevel {
|
||||
#[allow(clippy::trivially_copy_pass_by_ref)]
|
||||
const fn level_filter(&self) -> log::LevelFilter {
|
||||
const fn tracing_level(&self) -> tracing::Level {
|
||||
match self {
|
||||
LogLevel::Default => log::LevelFilter::Info,
|
||||
LogLevel::Verbose => log::LevelFilter::Debug,
|
||||
LogLevel::Quiet => log::LevelFilter::Off,
|
||||
LogLevel::Silent => log::LevelFilter::Off,
|
||||
LogLevel::Default => tracing::Level::INFO,
|
||||
LogLevel::Verbose => tracing::Level::DEBUG,
|
||||
LogLevel::Quiet => tracing::Level::WARN,
|
||||
LogLevel::Silent => tracing::Level::ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Log level priorities: 1. `RUST_LOG=`, 2. explicit CLI log level, 3. default to info
|
||||
pub fn set_up_logging(level: &LogLevel) -> Result<()> {
|
||||
fern::Dispatch::new()
|
||||
.format(|out, message, record| match record.level() {
|
||||
Level::Error => {
|
||||
out.finish(format_args!(
|
||||
"{}{} {}",
|
||||
"error".red().bold(),
|
||||
":".bold(),
|
||||
message
|
||||
));
|
||||
}
|
||||
Level::Warn => {
|
||||
out.finish(format_args!(
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
message
|
||||
));
|
||||
}
|
||||
Level::Info | Level::Debug | Level::Trace => {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
|
||||
record.target(),
|
||||
record.level(),
|
||||
message
|
||||
));
|
||||
}
|
||||
})
|
||||
.level(level.level_filter())
|
||||
.level_for("globset", log::LevelFilter::Warn)
|
||||
.chain(std::io::stderr())
|
||||
.apply()?;
|
||||
let filter_layer = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
EnvFilter::builder()
|
||||
.with_default_directive(level.tracing_level().into())
|
||||
.parse_lossy("")
|
||||
});
|
||||
tracing_subscriber::registry()
|
||||
.with(filter_layer)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ impl RuleSelector {
|
||||
// Always include rules that are not in preview or the nursery
|
||||
!(rule.is_preview() || rule.is_nursery())
|
||||
// Backwards compatibility allows selection of nursery rules by exact code or dedicated group
|
||||
|| (matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. }) && rule.is_nursery())
|
||||
|| ((matches!(self, RuleSelector::Rule { .. }) || matches!(self, RuleSelector::Nursery { .. })) && rule.is_nursery())
|
||||
// Enabling preview includes all preview or nursery rules
|
||||
|| preview.is_enabled()
|
||||
})
|
||||
|
||||
@@ -7,17 +7,17 @@ use libcst_native::{
|
||||
RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
|
||||
TrailingWhitespace, Tuple,
|
||||
};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::autofix::codemods::CodegenStylist;
|
||||
use crate::autofix::edits::pad;
|
||||
use crate::cst::helpers::space;
|
||||
use crate::cst::helpers::{negate, space};
|
||||
use crate::rules::flake8_comprehensions::rules::ObjectType;
|
||||
use crate::{
|
||||
checkers::ast::Checker,
|
||||
@@ -718,7 +718,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
|
||||
if outer_name.value == "list" {
|
||||
tree = Expression::Call(Box::new((*inner_call).clone()));
|
||||
} else {
|
||||
// If the `reverse` argument is used
|
||||
// If the `reverse` argument is used...
|
||||
let args = if inner_call.args.iter().any(|arg| {
|
||||
matches!(
|
||||
arg.keyword,
|
||||
@@ -728,7 +728,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
|
||||
})
|
||||
)
|
||||
}) {
|
||||
// Negate the `reverse` argument
|
||||
// Negate the `reverse` argument.
|
||||
inner_call
|
||||
.args
|
||||
.clone()
|
||||
@@ -741,35 +741,35 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
|
||||
..
|
||||
})
|
||||
) {
|
||||
if let Expression::Name(ref val) = arg.value {
|
||||
if val.value == "True" {
|
||||
// TODO: even better would be to drop the argument, as False is the default
|
||||
arg.value = Expression::Name(Box::new(Name {
|
||||
value: "False",
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
arg
|
||||
} else if val.value == "False" {
|
||||
arg.value = Expression::Name(Box::new(Name {
|
||||
value: "True",
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}));
|
||||
arg
|
||||
} else {
|
||||
arg
|
||||
}
|
||||
} else {
|
||||
arg
|
||||
}
|
||||
} else {
|
||||
arg
|
||||
arg.value = negate(&arg.value);
|
||||
}
|
||||
arg
|
||||
})
|
||||
.collect_vec()
|
||||
} else {
|
||||
let mut args = inner_call.args.clone();
|
||||
|
||||
// If necessary, parenthesize a generator expression, as a generator expression must
|
||||
// be parenthesized if it's not a solitary argument. For example, given:
|
||||
// ```python
|
||||
// reversed(sorted(i for i in range(42)))
|
||||
// ```
|
||||
// Rewrite as:
|
||||
// ```python
|
||||
// sorted((i for i in range(42)), reverse=True)
|
||||
// ```
|
||||
if let [arg] = args.as_mut_slice() {
|
||||
if matches!(arg.value, Expression::GeneratorExp(_)) {
|
||||
if arg.value.lpar().is_empty() && arg.value.rpar().is_empty() {
|
||||
arg.value = arg
|
||||
.value
|
||||
.clone()
|
||||
.with_parens(LeftParen::default(), RightParen::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the `reverse=True` argument.
|
||||
args.push(Arg {
|
||||
value: Expression::Name(Box::new(Name {
|
||||
value: "True",
|
||||
|
||||
@@ -103,17 +103,18 @@ C413.py:7:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
7 |+sorted(x, key=lambda e: e, reverse=False)
|
||||
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
9 9 | reversed(sorted(x, reverse=False))
|
||||
10 10 |
|
||||
10 10 | reversed(sorted(x, reverse=x))
|
||||
|
||||
C413.py:8:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
6 | reversed(sorted(x, reverse=True))
|
||||
7 | reversed(sorted(x, key=lambda e: e, reverse=True))
|
||||
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
9 | reversed(sorted(x, reverse=False))
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
|
||||
6 | reversed(sorted(x, reverse=True))
|
||||
7 | reversed(sorted(x, key=lambda e: e, reverse=True))
|
||||
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
9 | reversed(sorted(x, reverse=False))
|
||||
10 | reversed(sorted(x, reverse=x))
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 | reversed(sorted(x, key=lambda e: e))
|
||||
@@ -122,8 +123,8 @@ C413.py:8:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
8 |-reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
8 |+sorted(x, reverse=False, key=lambda e: e)
|
||||
9 9 | reversed(sorted(x, reverse=False))
|
||||
10 10 |
|
||||
11 11 | def reversed(*args, **kwargs):
|
||||
10 10 | reversed(sorted(x, reverse=x))
|
||||
11 11 | reversed(sorted(x, reverse=not x))
|
||||
|
||||
C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
@@ -131,8 +132,8 @@ C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
9 | reversed(sorted(x, reverse=False))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
10 |
|
||||
11 | def reversed(*args, **kwargs):
|
||||
10 | reversed(sorted(x, reverse=x))
|
||||
11 | reversed(sorted(x, reverse=not x))
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
@@ -142,8 +143,87 @@ C413.py:9:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
9 |-reversed(sorted(x, reverse=False))
|
||||
9 |+sorted(x, reverse=True)
|
||||
10 10 |
|
||||
11 11 | def reversed(*args, **kwargs):
|
||||
12 12 | return None
|
||||
10 10 | reversed(sorted(x, reverse=x))
|
||||
11 11 | reversed(sorted(x, reverse=not x))
|
||||
12 12 |
|
||||
|
||||
C413.py:10:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
9 | reversed(sorted(x, reverse=False))
|
||||
10 | reversed(sorted(x, reverse=x))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
11 | reversed(sorted(x, reverse=not x))
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
ℹ Suggested fix
|
||||
7 7 | reversed(sorted(x, key=lambda e: e, reverse=True))
|
||||
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
9 9 | reversed(sorted(x, reverse=False))
|
||||
10 |-reversed(sorted(x, reverse=x))
|
||||
10 |+sorted(x, reverse=not x)
|
||||
11 11 | reversed(sorted(x, reverse=not x))
|
||||
12 12 |
|
||||
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
|
||||
C413.py:11:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
9 | reversed(sorted(x, reverse=False))
|
||||
10 | reversed(sorted(x, reverse=x))
|
||||
11 | reversed(sorted(x, reverse=not x))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
12 |
|
||||
13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
ℹ Suggested fix
|
||||
8 8 | reversed(sorted(x, reverse=True, key=lambda e: e))
|
||||
9 9 | reversed(sorted(x, reverse=False))
|
||||
10 10 | reversed(sorted(x, reverse=x))
|
||||
11 |-reversed(sorted(x, reverse=not x))
|
||||
11 |+sorted(x, reverse=x)
|
||||
12 12 |
|
||||
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
14 14 | reversed(sorted(i for i in range(42)))
|
||||
|
||||
C413.py:14:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
14 | reversed(sorted(i for i in range(42)))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
15 | reversed(sorted((i for i in range(42)), reverse=True))
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
ℹ Suggested fix
|
||||
11 11 | reversed(sorted(x, reverse=not x))
|
||||
12 12 |
|
||||
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
14 |-reversed(sorted(i for i in range(42)))
|
||||
14 |+sorted((i for i in range(42)), reverse=True)
|
||||
15 15 | reversed(sorted((i for i in range(42)), reverse=True))
|
||||
16 16 |
|
||||
17 17 |
|
||||
|
||||
C413.py:15:1: C413 [*] Unnecessary `reversed` call around `sorted()`
|
||||
|
|
||||
13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
14 | reversed(sorted(i for i in range(42)))
|
||||
15 | reversed(sorted((i for i in range(42)), reverse=True))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C413
|
||||
|
|
||||
= help: Remove unnecessary `reversed` call
|
||||
|
||||
ℹ Suggested fix
|
||||
12 12 |
|
||||
13 13 | # Regression test for: https://github.com/astral-sh/ruff/issues/7289
|
||||
14 14 | reversed(sorted(i for i in range(42)))
|
||||
15 |-reversed(sorted((i for i in range(42)), reverse=True))
|
||||
15 |+sorted((i for i in range(42)), reverse=False)
|
||||
16 16 |
|
||||
17 17 |
|
||||
18 18 | def reversed(*args, **kwargs):
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use anyhow::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use libcst_native::{
|
||||
self, Assert, BooleanOp, CompoundStatement, Expression, ParenthesizedNode, SimpleStatementLine,
|
||||
SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, UnaryOperation,
|
||||
SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace,
|
||||
};
|
||||
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
@@ -21,7 +21,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::autofix::codemods::CodegenStylist;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::cst::helpers::space;
|
||||
use crate::cst::helpers::negate;
|
||||
use crate::cst::matchers::match_indented_block;
|
||||
use crate::cst::matchers::match_module;
|
||||
use crate::importer::ImportRequest;
|
||||
@@ -567,23 +567,6 @@ fn is_composite_condition(test: &Expr) -> CompositionKind {
|
||||
CompositionKind::None
|
||||
}
|
||||
|
||||
/// Negate a condition, i.e., `a` => `not a` and `not a` => `a`.
|
||||
fn negate<'a>(expression: &Expression<'a>) -> Expression<'a> {
|
||||
if let Expression::UnaryOperation(ref expression) = expression {
|
||||
if matches!(expression.operator, libcst_native::UnaryOp::Not { .. }) {
|
||||
return *expression.expression.clone();
|
||||
}
|
||||
}
|
||||
Expression::UnaryOperation(Box::new(UnaryOperation {
|
||||
operator: libcst_native::UnaryOp::Not {
|
||||
whitespace_after: space(),
|
||||
},
|
||||
expression: Box::new(expression.clone()),
|
||||
lpar: vec![],
|
||||
rpar: vec![],
|
||||
}))
|
||||
}
|
||||
|
||||
/// Propagate parentheses from a parent to a child expression, if necessary.
|
||||
///
|
||||
/// For example, when splitting:
|
||||
|
||||
@@ -1691,7 +1691,10 @@ fn common_section(
|
||||
}
|
||||
|
||||
if checker.enabled(Rule::NoBlankLineBeforeSection) {
|
||||
if !context.previous_line().is_some_and(str::is_empty) {
|
||||
if !context
|
||||
.previous_line()
|
||||
.is_some_and(|line| line.trim().is_empty())
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
NoBlankLineBeforeSection {
|
||||
name: context.section_name().to_string(),
|
||||
|
||||
@@ -496,5 +496,6 @@ sections.py:527:5: D407 [*] Missing dashed underline after section ("Parameters"
|
||||
530 |+ ----------
|
||||
530 531 | ===========
|
||||
531 532 | """
|
||||
532 533 |
|
||||
|
||||
|
||||
|
||||
@@ -256,4 +256,20 @@ mod tests {
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn too_many_public_methods() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
Path::new("pylint/too_many_public_methods.py"),
|
||||
&Settings {
|
||||
pylint: pylint::settings::Settings {
|
||||
max_public_methods: 7,
|
||||
..pylint::settings::Settings::default()
|
||||
},
|
||||
..Settings::for_rules(vec![Rule::TooManyPublicMethods])
|
||||
},
|
||||
)?;
|
||||
assert_messages!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(crate) use subprocess_run_without_check::*;
|
||||
pub(crate) use sys_exit_alias::*;
|
||||
pub(crate) use too_many_arguments::*;
|
||||
pub(crate) use too_many_branches::*;
|
||||
pub(crate) use too_many_public_methods::*;
|
||||
pub(crate) use too_many_return_statements::*;
|
||||
pub(crate) use too_many_statements::*;
|
||||
pub(crate) use type_bivariance::*;
|
||||
@@ -101,6 +102,7 @@ mod subprocess_run_without_check;
|
||||
mod sys_exit_alias;
|
||||
mod too_many_arguments;
|
||||
mod too_many_branches;
|
||||
mod too_many_public_methods;
|
||||
mod too_many_return_statements;
|
||||
mod too_many_statements;
|
||||
mod type_bivariance;
|
||||
|
||||
126
crates/ruff/src/rules/pylint/rules/too_many_public_methods.rs
Normal file
126
crates/ruff/src/rules/pylint/rules/too_many_public_methods.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::analyze::visibility::{self, Visibility::Public};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for classes with too many public methods
|
||||
///
|
||||
/// By default, this rule allows up to 20 statements, as configured by the
|
||||
/// [`pylint.max-public-methods`] option.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Classes with many public methods are harder to understand
|
||||
/// and maintain.
|
||||
///
|
||||
/// Instead, consider refactoring the class into separate classes.
|
||||
///
|
||||
/// ## Example
|
||||
/// Assuming that `pylint.max-public-settings` is set to 5:
|
||||
/// ```python
|
||||
/// class Linter:
|
||||
/// def __init__(self):
|
||||
/// pass
|
||||
///
|
||||
/// def pylint(self):
|
||||
/// pass
|
||||
///
|
||||
/// def pylint_settings(self):
|
||||
/// pass
|
||||
///
|
||||
/// def flake8(self):
|
||||
/// pass
|
||||
///
|
||||
/// def flake8_settings(self):
|
||||
/// pass
|
||||
///
|
||||
/// def pydocstyle(self):
|
||||
/// pass
|
||||
///
|
||||
/// def pydocstyle_settings(self):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// class Linter:
|
||||
/// def __init__(self):
|
||||
/// self.pylint = Pylint()
|
||||
/// self.flake8 = Flake8()
|
||||
/// self.pydocstyle = Pydocstyle()
|
||||
///
|
||||
/// def lint(self):
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// class Pylint:
|
||||
/// def lint(self):
|
||||
/// pass
|
||||
///
|
||||
/// def settings(self):
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// class Flake8:
|
||||
/// def lint(self):
|
||||
/// pass
|
||||
///
|
||||
/// def settings(self):
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// class Pydocstyle:
|
||||
/// def lint(self):
|
||||
/// pass
|
||||
///
|
||||
/// def settings(self):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `pylint.max-public-methods`
|
||||
#[violation]
|
||||
pub struct TooManyPublicMethods {
|
||||
methods: usize,
|
||||
max_methods: usize,
|
||||
}
|
||||
|
||||
impl Violation for TooManyPublicMethods {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let TooManyPublicMethods {
|
||||
methods,
|
||||
max_methods,
|
||||
} = self;
|
||||
format!("Too many public methods ({methods} > {max_methods})")
|
||||
}
|
||||
}
|
||||
|
||||
/// R0904
|
||||
pub(crate) fn too_many_public_methods(
|
||||
checker: &mut Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
max_methods: usize,
|
||||
) {
|
||||
let methods = class_def
|
||||
.body
|
||||
.iter()
|
||||
.filter(|stmt| {
|
||||
stmt.as_function_def_stmt()
|
||||
.is_some_and(|node| matches!(visibility::method_visibility(node), Public))
|
||||
})
|
||||
.count();
|
||||
|
||||
if methods > max_methods {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TooManyPublicMethods {
|
||||
methods,
|
||||
max_methods,
|
||||
},
|
||||
class_def.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ pub struct Settings {
|
||||
pub max_returns: usize,
|
||||
pub max_branches: usize,
|
||||
pub max_statements: usize,
|
||||
pub max_public_methods: usize,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
@@ -52,6 +53,7 @@ impl Default for Settings {
|
||||
max_returns: 6,
|
||||
max_branches: 12,
|
||||
max_statements: 50,
|
||||
max_public_methods: 20,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
too_many_public_methods.py:1:1: PLR0904 Too many public methods (10 > 7)
|
||||
|
|
||||
1 | / class Everything:
|
||||
2 | | foo = 1
|
||||
3 | |
|
||||
4 | | def __init__(self):
|
||||
5 | | pass
|
||||
6 | |
|
||||
7 | | def _private(self):
|
||||
8 | | pass
|
||||
9 | |
|
||||
10 | | def method1(self):
|
||||
11 | | pass
|
||||
12 | |
|
||||
13 | | def method2(self):
|
||||
14 | | pass
|
||||
15 | |
|
||||
16 | | def method3(self):
|
||||
17 | | pass
|
||||
18 | |
|
||||
19 | | def method4(self):
|
||||
20 | | pass
|
||||
21 | |
|
||||
22 | | def method5(self):
|
||||
23 | | pass
|
||||
24 | |
|
||||
25 | | def method6(self):
|
||||
26 | | pass
|
||||
27 | |
|
||||
28 | | def method7(self):
|
||||
29 | | pass
|
||||
30 | |
|
||||
31 | | def method8(self):
|
||||
32 | | pass
|
||||
33 | |
|
||||
34 | | def method9(self):
|
||||
35 | | pass
|
||||
| |____________^ PLR0904
|
||||
36 |
|
||||
37 | class Small:
|
||||
|
|
||||
|
||||
|
||||
@@ -138,16 +138,52 @@ const PIPES_TO_SHLEX: &[&str] = &["quote"];
|
||||
|
||||
// Members of `typing_extensions` that were moved to `typing`.
|
||||
const TYPING_EXTENSIONS_TO_TYPING: &[&str] = &[
|
||||
"AbstractSet",
|
||||
"AnyStr",
|
||||
"AsyncIterable",
|
||||
"AsyncIterator",
|
||||
"Awaitable",
|
||||
"BinaryIO",
|
||||
"Callable",
|
||||
"ClassVar",
|
||||
"Collection",
|
||||
"Container",
|
||||
"ContextManager",
|
||||
"Coroutine",
|
||||
"DefaultDict",
|
||||
"Dict",
|
||||
"FrozenSet",
|
||||
"Generator",
|
||||
"Generic",
|
||||
"Hashable",
|
||||
"IO",
|
||||
"ItemsView",
|
||||
"Iterable",
|
||||
"Iterator",
|
||||
"KeysView",
|
||||
"List",
|
||||
"Mapping",
|
||||
"MappingView",
|
||||
"Match",
|
||||
"MutableMapping",
|
||||
"MutableSequence",
|
||||
"MutableSet",
|
||||
"Optional",
|
||||
"Pattern",
|
||||
"Reversible",
|
||||
"Sequence",
|
||||
"Set",
|
||||
"Sized",
|
||||
"TYPE_CHECKING",
|
||||
"Text",
|
||||
"TextIO",
|
||||
"Tuple",
|
||||
"Type",
|
||||
"Union",
|
||||
"ValuesView",
|
||||
"cast",
|
||||
"no_type_check",
|
||||
"no_type_check_decorator",
|
||||
// Introduced in Python 3.5.2, but `typing_extensions` contains backported bugfixes and
|
||||
// optimizations,
|
||||
// "NewType",
|
||||
@@ -165,6 +201,7 @@ const TYPING_EXTENSIONS_TO_TYPING_37: &[&str] = &[
|
||||
"ChainMap",
|
||||
"Counter",
|
||||
"Deque",
|
||||
"ForwardRef",
|
||||
"NoReturn",
|
||||
];
|
||||
|
||||
@@ -287,6 +324,18 @@ const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[
|
||||
|
||||
// Members of `typing_extensions` that were moved to `typing`.
|
||||
const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[
|
||||
"NamedTuple",
|
||||
// Introduced in Python 3.8, but `typing_extensions` backports a ton of optimizations that were
|
||||
// added in Python 3.12.
|
||||
"Protocol",
|
||||
"SupportsAbs",
|
||||
"SupportsBytes",
|
||||
"SupportsComplex",
|
||||
"SupportsFloat",
|
||||
"SupportsInt",
|
||||
"SupportsRound",
|
||||
"TypedDict",
|
||||
"Unpack",
|
||||
// Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument,
|
||||
// which was introduced in Python 3.12.
|
||||
"dataclass_transform",
|
||||
|
||||
@@ -931,7 +931,7 @@ UP035.py:74:1: UP035 [*] Import from `collections.abc` instead: `AsyncGenerator`
|
||||
74 |+from collections.abc import AsyncGenerator
|
||||
75 75 | from typing import Reversible
|
||||
76 76 | from typing import Generator
|
||||
77 77 |
|
||||
77 77 | from typing import Callable
|
||||
|
||||
UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
|
||||
|
|
||||
@@ -940,6 +940,7 @@ UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
|
||||
75 | from typing import Reversible
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
76 | from typing import Generator
|
||||
77 | from typing import Callable
|
||||
|
|
||||
= help: Import from `collections.abc`
|
||||
|
||||
@@ -950,8 +951,8 @@ UP035.py:75:1: UP035 [*] Import from `collections.abc` instead: `Reversible`
|
||||
75 |-from typing import Reversible
|
||||
75 |+from collections.abc import Reversible
|
||||
76 76 | from typing import Generator
|
||||
77 77 |
|
||||
78 78 | # OK
|
||||
77 77 | from typing import Callable
|
||||
78 78 | from typing import cast
|
||||
|
||||
UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
|
||||
|
|
||||
@@ -959,8 +960,8 @@ UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
|
||||
75 | from typing import Reversible
|
||||
76 | from typing import Generator
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
77 |
|
||||
78 | # OK
|
||||
77 | from typing import Callable
|
||||
78 | from typing import cast
|
||||
|
|
||||
= help: Import from `collections.abc`
|
||||
|
||||
@@ -970,23 +971,63 @@ UP035.py:76:1: UP035 [*] Import from `collections.abc` instead: `Generator`
|
||||
75 75 | from typing import Reversible
|
||||
76 |-from typing import Generator
|
||||
76 |+from collections.abc import Generator
|
||||
77 77 |
|
||||
78 78 | # OK
|
||||
79 79 | from a import b
|
||||
77 77 | from typing import Callable
|
||||
78 78 | from typing import cast
|
||||
79 79 |
|
||||
|
||||
UP035.py:88:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
|
||||
UP035.py:77:1: UP035 [*] Import from `collections.abc` instead: `Callable`
|
||||
|
|
||||
87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
88 | from typing_extensions import dataclass_transform
|
||||
75 | from typing import Reversible
|
||||
76 | from typing import Generator
|
||||
77 | from typing import Callable
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
78 | from typing import cast
|
||||
|
|
||||
= help: Import from `collections.abc`
|
||||
|
||||
ℹ Suggested fix
|
||||
74 74 | from typing import AsyncGenerator
|
||||
75 75 | from typing import Reversible
|
||||
76 76 | from typing import Generator
|
||||
77 |-from typing import Callable
|
||||
77 |+from collections.abc import Callable
|
||||
78 78 | from typing import cast
|
||||
79 79 |
|
||||
80 80 | # OK
|
||||
|
||||
UP035.py:87:1: UP035 [*] Import from `typing` instead: `NamedTuple`
|
||||
|
|
||||
86 | # Ok: `typing_extensions` contains backported improvements.
|
||||
87 | from typing_extensions import NamedTuple
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
88 |
|
||||
89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
|
|
||||
= help: Import from `typing`
|
||||
|
||||
ℹ Suggested fix
|
||||
84 84 | from typing_extensions import SupportsIndex
|
||||
85 85 |
|
||||
86 86 | # Ok: `typing_extensions` contains backported improvements.
|
||||
87 |-from typing_extensions import NamedTuple
|
||||
87 |+from typing import NamedTuple
|
||||
88 88 |
|
||||
89 89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
90 90 | from typing_extensions import dataclass_transform
|
||||
|
||||
UP035.py:90:1: UP035 [*] Import from `typing` instead: `dataclass_transform`
|
||||
|
|
||||
89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
90 | from typing_extensions import dataclass_transform
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP035
|
||||
|
|
||||
= help: Import from `typing`
|
||||
|
||||
ℹ Suggested fix
|
||||
85 85 | from typing_extensions import NamedTuple
|
||||
86 86 |
|
||||
87 87 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
88 |-from typing_extensions import dataclass_transform
|
||||
88 |+from typing import dataclass_transform
|
||||
87 87 | from typing_extensions import NamedTuple
|
||||
88 88 |
|
||||
89 89 | # Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
|
||||
90 |-from typing_extensions import dataclass_transform
|
||||
90 |+from typing import dataclass_transform
|
||||
|
||||
|
||||
|
||||
36
crates/ruff/src/rules/refurb/helpers.rs
Normal file
36
crates/ruff/src/rules/refurb/helpers.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
/// Format a code snippet to call `name.method()`.
|
||||
pub(super) fn generate_method_call(name: &str, method: &str, generator: Generator) -> String {
|
||||
// Construct `name`.
|
||||
let var = ast::ExprName {
|
||||
id: name.to_string(),
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// Construct `name.method`.
|
||||
let attr = ast::ExprAttribute {
|
||||
value: Box::new(var.into()),
|
||||
attr: ast::Identifier::new(method.to_string(), TextRange::default()),
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// Make it into a call `name.method()`
|
||||
let call = ast::ExprCall {
|
||||
func: Box::new(attr.into()),
|
||||
arguments: ast::Arguments {
|
||||
args: vec![],
|
||||
keywords: vec![],
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// And finally, turn it into a statement.
|
||||
let stmt = ast::StmtExpr {
|
||||
value: Box::new(call.into()),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
generator.stmt(&stmt.into())
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Rules from [refurb](https://pypi.org/project/refurb/)/
|
||||
|
||||
mod helpers;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -16,6 +17,7 @@ mod tests {
|
||||
#[test_case(Rule::RepeatedAppend, Path::new("FURB113.py"))]
|
||||
#[test_case(Rule::DeleteFullSlice, Path::new("FURB131.py"))]
|
||||
#[test_case(Rule::CheckAndRemoveFromSet, Path::new("FURB132.py"))]
|
||||
#[test_case(Rule::SliceCopy, Path::new("FURB145.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -18,6 +18,11 @@ use crate::registry::AsRule;
|
||||
/// If an element should be removed from a set if it is present, it is more
|
||||
/// succinct and idiomatic to use `discard`.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// This rule is prone to false negatives due to type inference limitations,
|
||||
/// as it will only detect sets that are instantiated as literals or annotated
|
||||
/// with a type annotation.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// nums = {123, 456}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_python_semantic::analyze::typing::{is_dict, is_list};
|
||||
use ruff_python_semantic::{Binding, SemanticModel};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::refurb::helpers::generate_method_call;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `del` statements that delete the entire slice of a list or
|
||||
@@ -17,6 +17,11 @@ use crate::registry::AsRule;
|
||||
/// It's is faster and more succinct to remove all items via the `clear()`
|
||||
/// method.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// This rule is prone to false negatives due to type inference limitations,
|
||||
/// as it will only detect lists and dictionaries that are instantiated as
|
||||
/// literals or annotated with a type annotation.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// names = {"key": "value"}
|
||||
@@ -65,7 +70,7 @@ pub(crate) fn delete_full_slice(checker: &mut Checker, delete: &ast::StmtDelete)
|
||||
|
||||
// Fix is only supported for single-target deletions.
|
||||
if checker.patch(diagnostic.kind.rule()) && delete.targets.len() == 1 {
|
||||
let replacement = make_suggestion(name, checker.generator());
|
||||
let replacement = generate_method_call(name, "clear", checker.generator());
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
replacement,
|
||||
delete.start(),
|
||||
@@ -118,38 +123,3 @@ fn match_full_slice<'a>(expr: &'a Expr, semantic: &SemanticModel) -> Option<&'a
|
||||
// Name is needed for the fix suggestion.
|
||||
Some(name)
|
||||
}
|
||||
|
||||
/// Make fix suggestion for the given name, ie `name.clear()`.
|
||||
fn make_suggestion(name: &str, generator: Generator) -> String {
|
||||
// Here we construct `var.clear()`
|
||||
//
|
||||
// And start with construction of `var`
|
||||
let var = ast::ExprName {
|
||||
id: name.to_string(),
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// Make `var.clear`.
|
||||
let attr = ast::ExprAttribute {
|
||||
value: Box::new(var.into()),
|
||||
attr: ast::Identifier::new("clear".to_string(), TextRange::default()),
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// Make it into a call `var.clear()`
|
||||
let call = ast::ExprCall {
|
||||
func: Box::new(attr.into()),
|
||||
arguments: ast::Arguments {
|
||||
args: vec![],
|
||||
keywords: vec![],
|
||||
range: TextRange::default(),
|
||||
},
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// And finally, turn it into a statement.
|
||||
let stmt = ast::StmtExpr {
|
||||
value: Box::new(call.into()),
|
||||
range: TextRange::default(),
|
||||
};
|
||||
generator.stmt(&stmt.into())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pub(crate) use check_and_remove_from_set::*;
|
||||
pub(crate) use delete_full_slice::*;
|
||||
pub(crate) use repeated_append::*;
|
||||
pub(crate) use slice_copy::*;
|
||||
|
||||
mod check_and_remove_from_set;
|
||||
mod delete_full_slice;
|
||||
mod repeated_append;
|
||||
mod slice_copy;
|
||||
|
||||
@@ -21,6 +21,11 @@ use crate::registry::AsRule;
|
||||
/// a single `extend`. Each `append` resizes the list individually, whereas an
|
||||
/// `extend` can resize the list once for all elements.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// This rule is prone to false negatives due to type inference limitations,
|
||||
/// as it will only detect lists that are instantiated as literals or annotated
|
||||
/// with a type annotation.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// nums = [1, 2, 3]
|
||||
|
||||
109
crates/ruff/src/rules/refurb/rules/slice_copy.rs
Normal file
109
crates/ruff/src/rules/refurb/rules/slice_copy.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::analyze::typing::is_list;
|
||||
use ruff_python_semantic::{Binding, SemanticModel};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
use crate::rules::refurb::helpers::generate_method_call;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for unbounded slice expressions to copy a list.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `list#copy` method is more readable and consistent with copying other
|
||||
/// types.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// This rule is prone to false negatives due to type inference limitations,
|
||||
/// as it will only detect lists that are instantiated as literals or annotated
|
||||
/// with a type annotation.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// a = [1, 2, 3]
|
||||
/// b = a[:]
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// a = [1, 2, 3]
|
||||
/// b = a.copy()
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Mutable Sequence Types](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types)
|
||||
#[violation]
|
||||
pub struct SliceCopy;
|
||||
|
||||
impl Violation for SliceCopy {
|
||||
const AUTOFIX: AutofixKind = AutofixKind::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Prefer `copy` method over slicing")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> Option<String> {
|
||||
Some("Replace with `copy()`".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB145
|
||||
pub(crate) fn slice_copy(checker: &mut Checker, subscript: &ast::ExprSubscript) {
|
||||
if subscript.ctx.is_store() || subscript.ctx.is_del() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(name) = match_list_full_slice(subscript, checker.semantic()) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = Diagnostic::new(SliceCopy, subscript.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
let replacement = generate_method_call(name, "copy", checker.generator());
|
||||
diagnostic.set_fix(Fix::suggested(Edit::replacement(
|
||||
replacement,
|
||||
subscript.start(),
|
||||
subscript.end(),
|
||||
)));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// Matches `obj[:]` where `obj` is a list.
|
||||
fn match_list_full_slice<'a>(
|
||||
subscript: &'a ast::ExprSubscript,
|
||||
semantic: &SemanticModel,
|
||||
) -> Option<&'a str> {
|
||||
// Check that it is `obj[:]`.
|
||||
if !matches!(
|
||||
subscript.slice.as_ref(),
|
||||
Expr::Slice(ast::ExprSlice {
|
||||
lower: None,
|
||||
upper: None,
|
||||
step: None,
|
||||
range: _,
|
||||
})
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ast::ExprName { id, .. } = subscript.value.as_name_expr()?;
|
||||
|
||||
// Check that `obj` is a list.
|
||||
let scope = semantic.current_scope();
|
||||
let bindings: Vec<&Binding> = scope
|
||||
.get_all(id)
|
||||
.map(|binding_id| semantic.binding(binding_id))
|
||||
.collect();
|
||||
let [binding] = bindings.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
if !is_list(binding, semantic) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(id)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB145.py:4:5: FURB145 [*] Prefer `copy` method over slicing
|
||||
|
|
||||
3 | # Errors.
|
||||
4 | a = l[:]
|
||||
| ^^^^ FURB145
|
||||
5 | b, c = 1, l[:]
|
||||
6 | d, e = l[:], 1
|
||||
|
|
||||
= help: Replace with `copy()`
|
||||
|
||||
ℹ Suggested fix
|
||||
1 1 | l = [1, 2, 3, 4, 5]
|
||||
2 2 |
|
||||
3 3 | # Errors.
|
||||
4 |-a = l[:]
|
||||
4 |+a = l.copy()
|
||||
5 5 | b, c = 1, l[:]
|
||||
6 6 | d, e = l[:], 1
|
||||
7 7 | m = l[::]
|
||||
|
||||
FURB145.py:5:11: FURB145 [*] Prefer `copy` method over slicing
|
||||
|
|
||||
3 | # Errors.
|
||||
4 | a = l[:]
|
||||
5 | b, c = 1, l[:]
|
||||
| ^^^^ FURB145
|
||||
6 | d, e = l[:], 1
|
||||
7 | m = l[::]
|
||||
|
|
||||
= help: Replace with `copy()`
|
||||
|
||||
ℹ Suggested fix
|
||||
2 2 |
|
||||
3 3 | # Errors.
|
||||
4 4 | a = l[:]
|
||||
5 |-b, c = 1, l[:]
|
||||
5 |+b, c = 1, l.copy()
|
||||
6 6 | d, e = l[:], 1
|
||||
7 7 | m = l[::]
|
||||
8 8 | l[:]
|
||||
|
||||
FURB145.py:6:8: FURB145 [*] Prefer `copy` method over slicing
|
||||
|
|
||||
4 | a = l[:]
|
||||
5 | b, c = 1, l[:]
|
||||
6 | d, e = l[:], 1
|
||||
| ^^^^ FURB145
|
||||
7 | m = l[::]
|
||||
8 | l[:]
|
||||
|
|
||||
= help: Replace with `copy()`
|
||||
|
||||
ℹ Suggested fix
|
||||
3 3 | # Errors.
|
||||
4 4 | a = l[:]
|
||||
5 5 | b, c = 1, l[:]
|
||||
6 |-d, e = l[:], 1
|
||||
6 |+d, e = l.copy(), 1
|
||||
7 7 | m = l[::]
|
||||
8 8 | l[:]
|
||||
9 9 | print(l[:])
|
||||
|
||||
FURB145.py:7:5: FURB145 [*] Prefer `copy` method over slicing
|
||||
|
|
||||
5 | b, c = 1, l[:]
|
||||
6 | d, e = l[:], 1
|
||||
7 | m = l[::]
|
||||
| ^^^^^ FURB145
|
||||
8 | l[:]
|
||||
9 | print(l[:])
|
||||
|
|
||||
= help: Replace with `copy()`
|
||||
|
||||
ℹ Suggested fix
|
||||
4 4 | a = l[:]
|
||||
5 5 | b, c = 1, l[:]
|
||||
6 6 | d, e = l[:], 1
|
||||
7 |-m = l[::]
|
||||
7 |+m = l.copy()
|
||||
8 8 | l[:]
|
||||
9 9 | print(l[:])
|
||||
10 10 |
|
||||
|
||||
FURB145.py:8:1: FURB145 [*] Prefer `copy` method over slicing
|
||||
|
|
||||
6 | d, e = l[:], 1
|
||||
7 | m = l[::]
|
||||
8 | l[:]
|
||||
| ^^^^ FURB145
|
||||
9 | print(l[:])
|
||||
|
|
||||
= help: Replace with `copy()`
|
||||
|
||||
ℹ Suggested fix
|
||||
5 5 | b, c = 1, l[:]
|
||||
6 6 | d, e = l[:], 1
|
||||
7 7 | m = l[::]
|
||||
8 |-l[:]
|
||||
8 |+l.copy()
|
||||
9 9 | print(l[:])
|
||||
10 10 |
|
||||
11 11 | # False negatives.
|
||||
|
||||
FURB145.py:9:7: FURB145 [*] Prefer `copy` method over slicing
|
||||
|
|
||||
7 | m = l[::]
|
||||
8 | l[:]
|
||||
9 | print(l[:])
|
||||
| ^^^^ FURB145
|
||||
10 |
|
||||
11 | # False negatives.
|
||||
|
|
||||
= help: Replace with `copy()`
|
||||
|
||||
ℹ Suggested fix
|
||||
6 6 | d, e = l[:], 1
|
||||
7 7 | m = l[::]
|
||||
8 8 | l[:]
|
||||
9 |-print(l[:])
|
||||
9 |+print(l.copy())
|
||||
10 10 |
|
||||
11 11 | # False negatives.
|
||||
12 12 | aa = a[:] # Type inference.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ ruff_text_size = { path = "../ruff_text_size" }
|
||||
|
||||
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
||||
anyhow = { workspace = true }
|
||||
argfile = { version = "0.1.5" }
|
||||
argfile = { version = "0.1.6" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { workspace = true }
|
||||
cachedir = { version = "0.3.0" }
|
||||
@@ -64,7 +64,7 @@ shellexpand = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
strum = { workspace = true, features = [] }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true, features = ["log"] }
|
||||
tracing = { workspace = true }
|
||||
walkdir = { version = "2.3.2" }
|
||||
wild = { version = "2" }
|
||||
|
||||
|
||||
@@ -300,6 +300,195 @@ fn nursery_direct() {
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
warning: Selection of nursery rule `E225` without the `--preview` flag is deprecated.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nursery_group_selector() {
|
||||
// Only nursery rules should be detected e.g. E225 and a warning should be displayed
|
||||
let args = ["--select", "NURSERY"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: CPY001 Missing copyright notice at top of file
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `NURSERY` selector has been deprecated. Use the `--preview` flag instead.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nursery_group_selector_preview_enabled() {
|
||||
// Only nursery rules should be detected e.g. E225 and a warning should be displayed
|
||||
let args = ["--select", "NURSERY", "--preview"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: CPY001 Missing copyright notice at top of file
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
warning: The `NURSERY` selector has been deprecated. Use the `PREVIEW` selector instead.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_enabled_prefix() {
|
||||
// E741 and E225 (preview) should both be detected
|
||||
let args = ["--select", "E", "--preview"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `I`
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_enabled_all() {
|
||||
let args = ["--select", "ALL", "--preview"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `I`
|
||||
-:1:1: D100 Missing docstring in public module
|
||||
-:1:1: CPY001 Missing copyright notice at top of file
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 4 errors.
|
||||
|
||||
----- stderr -----
|
||||
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
|
||||
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_enabled_direct() {
|
||||
// E225 should be detected without warning
|
||||
let args = ["--select", "E225", "--preview"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_disabled_direct() {
|
||||
// FURB145 is preview not nursery so selecting should be empty
|
||||
let args = ["--select", "FURB145"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("a = l[:]\n"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Selection `FURB145` has no effect because the `--preview` flag was not included.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_disabled_prefix_empty() {
|
||||
// Warns that the selection is empty since all of the CPY rules are in preview
|
||||
let args = ["--select", "CPY"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Selection `CPY` has no effect because the `--preview` flag was not included.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_disabled_group_selector() {
|
||||
// `--select PREVIEW` should warn without the `--preview` flag
|
||||
let args = ["--select", "PREVIEW"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
warning: Selection `PREVIEW` has no effect because the `--preview` flag was not included.
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_enabled_group_selector() {
|
||||
// `--select PREVIEW` is okay with the `--preview` flag and shouldn't warn
|
||||
let args = ["--select", "PREVIEW", "--preview"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: CPY001 Missing copyright notice at top of file
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preview_enabled_group_ignore() {
|
||||
// `--select E --ignore PREVIEW` should detect E741 and E225, which is in preview but "E" is more specific.
|
||||
let args = ["--select", "E", "--ignore", "PREVIEW", "--preview"];
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(args)
|
||||
.pass_stdin("I=42\n"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `I`
|
||||
-:1:2: E225 Missing whitespace around operator
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
}
|
||||
AutofixKind::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
|
||||
};
|
||||
let preview_token = if rule.is_preview() {
|
||||
let preview_token = if rule.is_preview() || rule.is_nursery() {
|
||||
format!("<span style='opacity: 1'>{PREVIEW_SYMBOL}</span>")
|
||||
} else {
|
||||
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")
|
||||
|
||||
@@ -361,7 +361,7 @@ where
|
||||
|
||||
f.write_element(FormatElement::Text {
|
||||
text: self.text.to_string().into_boxed_str(),
|
||||
text_width: TextWidth::from_text(self.text, f.options().tab_width()),
|
||||
text_width: TextWidth::from_text(self.text, f.options().indent_width()),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -393,8 +393,10 @@ where
|
||||
let slice = source_code.slice(self.range);
|
||||
debug_assert_no_newlines(slice.text(source_code));
|
||||
|
||||
let text_width =
|
||||
TextWidth::from_text(slice.text(source_code), f.context().options().tab_width());
|
||||
let text_width = TextWidth::from_text(
|
||||
slice.text(source_code),
|
||||
f.context().options().indent_width(),
|
||||
);
|
||||
|
||||
f.write_element(FormatElement::SourceCodeSlice { slice, text_width });
|
||||
|
||||
@@ -917,8 +919,10 @@ where
|
||||
/// use ruff_formatter::prelude::*;
|
||||
///
|
||||
/// # fn main() -> FormatResult<()> {
|
||||
/// use ruff_formatter::IndentWidth;
|
||||
/// let context = SimpleFormatContext::new(SimpleFormatOptions {
|
||||
/// indent_style: IndentStyle::Space(4),
|
||||
/// indent_style: IndentStyle::Space,
|
||||
/// indent_width: IndentWidth::try_from(4).unwrap(),
|
||||
/// ..SimpleFormatOptions::default()
|
||||
/// });
|
||||
///
|
||||
|
||||
@@ -10,7 +10,7 @@ use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use crate::format_element::tag::{GroupMode, LabelId, Tag};
|
||||
use crate::source_code::SourceCodeSlice;
|
||||
use crate::{TabWidth, TagKind};
|
||||
use crate::{IndentWidth, TagKind};
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
/// Language agnostic IR for formatting source code.
|
||||
@@ -432,12 +432,12 @@ pub enum TextWidth {
|
||||
}
|
||||
|
||||
impl TextWidth {
|
||||
pub fn from_text(text: &str, tab_width: TabWidth) -> TextWidth {
|
||||
pub fn from_text(text: &str, indent_width: IndentWidth) -> TextWidth {
|
||||
let mut width = 0u32;
|
||||
|
||||
for c in text.chars() {
|
||||
let char_width = match c {
|
||||
'\t' => tab_width.value(),
|
||||
'\t' => indent_width.value(),
|
||||
'\n' => return TextWidth::Multiline,
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
c => c.width().unwrap_or(0) as u32,
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::prelude::*;
|
||||
use crate::source_code::SourceCode;
|
||||
use crate::{
|
||||
format, write, BufferExtensions, Format, FormatContext, FormatElement, FormatOptions,
|
||||
FormatResult, Formatter, IndentStyle, LineWidth, PrinterOptions, TabWidth,
|
||||
FormatResult, Formatter, IndentStyle, IndentWidth, LineWidth, PrinterOptions,
|
||||
};
|
||||
|
||||
use super::tag::Tag;
|
||||
@@ -213,11 +213,11 @@ struct IrFormatOptions;
|
||||
|
||||
impl FormatOptions for IrFormatOptions {
|
||||
fn indent_style(&self) -> IndentStyle {
|
||||
IndentStyle::Space(2)
|
||||
IndentStyle::Space
|
||||
}
|
||||
|
||||
fn tab_width(&self) -> TabWidth {
|
||||
TabWidth::default()
|
||||
fn indent_width(&self) -> IndentWidth {
|
||||
IndentWidth::default()
|
||||
}
|
||||
|
||||
fn line_width(&self) -> LineWidth {
|
||||
@@ -227,7 +227,7 @@ impl FormatOptions for IrFormatOptions {
|
||||
fn as_print_options(&self) -> PrinterOptions {
|
||||
PrinterOptions {
|
||||
line_width: self.line_width(),
|
||||
indent_style: IndentStyle::Space(2),
|
||||
indent_style: IndentStyle::Space,
|
||||
..PrinterOptions::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,23 +52,20 @@ pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, Pri
|
||||
pub use format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS};
|
||||
pub use group_id::GroupId;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
#[derive(Default)]
|
||||
pub enum IndentStyle {
|
||||
/// Tab
|
||||
/// Use tabs to indent code.
|
||||
#[default]
|
||||
Tab,
|
||||
/// Space, with its quantity
|
||||
Space(u8),
|
||||
/// Use [`IndentWidth`] spaces to indent code.
|
||||
Space,
|
||||
}
|
||||
|
||||
impl IndentStyle {
|
||||
pub const DEFAULT_SPACES: u8 = 2;
|
||||
|
||||
/// Returns `true` if this is an [`IndentStyle::Tab`].
|
||||
pub const fn is_tab(&self) -> bool {
|
||||
matches!(self, IndentStyle::Tab)
|
||||
@@ -76,58 +73,42 @@ impl IndentStyle {
|
||||
|
||||
/// Returns `true` if this is an [`IndentStyle::Space`].
|
||||
pub const fn is_space(&self) -> bool {
|
||||
matches!(self, IndentStyle::Space(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for IndentStyle {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"tab" | "Tabs" => Ok(Self::Tab),
|
||||
"space" | "Spaces" => Ok(Self::Space(IndentStyle::DEFAULT_SPACES)),
|
||||
// TODO: replace this error with a diagnostic
|
||||
v => {
|
||||
let v = v.strip_prefix("Spaces, size: ").unwrap_or(v);
|
||||
|
||||
u8::from_str(v)
|
||||
.map(Self::Space)
|
||||
.map_err(|_| "Value not supported for IndentStyle")
|
||||
}
|
||||
}
|
||||
matches!(self, IndentStyle::Space)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for IndentStyle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
IndentStyle::Tab => std::write!(f, "Tab"),
|
||||
IndentStyle::Space(size) => std::write!(f, "Spaces, size: {size}"),
|
||||
IndentStyle::Tab => std::write!(f, "tab"),
|
||||
IndentStyle::Space => std::write!(f, "space"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The visual width of a `\t` character.
|
||||
/// The visual width of a indentation.
|
||||
///
|
||||
/// Determines the visual width of a tab character (`\t`) and the number of
|
||||
/// spaces per indent when using [`IndentStyle::Space`].
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct TabWidth(NonZeroU8);
|
||||
pub struct IndentWidth(NonZeroU8);
|
||||
|
||||
impl TabWidth {
|
||||
impl IndentWidth {
|
||||
/// Return the numeric value for this [`LineWidth`]
|
||||
pub const fn value(&self) -> u32 {
|
||||
self.0.get() as u32
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TabWidth {
|
||||
impl Default for IndentWidth {
|
||||
fn default() -> Self {
|
||||
Self(NonZeroU8::new(2).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for TabWidth {
|
||||
impl TryFrom<u8> for IndentWidth {
|
||||
type Error = TryFromIntError;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
@@ -196,16 +177,8 @@ pub trait FormatOptions {
|
||||
/// The indent style.
|
||||
fn indent_style(&self) -> IndentStyle;
|
||||
|
||||
/// The visual width of a tab character.
|
||||
fn tab_width(&self) -> TabWidth;
|
||||
|
||||
/// The visual width of an indent
|
||||
fn indent_width(&self) -> u32 {
|
||||
match self.indent_style() {
|
||||
IndentStyle::Tab => self.tab_width().value(),
|
||||
IndentStyle::Space(spaces) => u32::from(spaces),
|
||||
}
|
||||
}
|
||||
fn indent_width(&self) -> IndentWidth;
|
||||
|
||||
/// What's the max width of a line. Defaults to 80.
|
||||
fn line_width(&self) -> LineWidth;
|
||||
@@ -250,6 +223,7 @@ impl FormatContext for SimpleFormatContext {
|
||||
#[derive(Debug, Default, Eq, PartialEq, Clone)]
|
||||
pub struct SimpleFormatOptions {
|
||||
pub indent_style: IndentStyle,
|
||||
pub indent_width: IndentWidth,
|
||||
pub line_width: LineWidth,
|
||||
}
|
||||
|
||||
@@ -258,8 +232,8 @@ impl FormatOptions for SimpleFormatOptions {
|
||||
self.indent_style
|
||||
}
|
||||
|
||||
fn tab_width(&self) -> TabWidth {
|
||||
TabWidth::default()
|
||||
fn indent_width(&self) -> IndentWidth {
|
||||
self.indent_width
|
||||
}
|
||||
|
||||
fn line_width(&self) -> LineWidth {
|
||||
@@ -270,6 +244,7 @@ impl FormatOptions for SimpleFormatOptions {
|
||||
PrinterOptions {
|
||||
line_width: self.line_width,
|
||||
indent_style: self.indent_style,
|
||||
indent_width: self.indent_width,
|
||||
source_map_generation: SourceMapGeneration::Enabled,
|
||||
..PrinterOptions::default()
|
||||
}
|
||||
|
||||
@@ -367,7 +367,7 @@ impl<'a> Printer<'a> {
|
||||
if !self.state.pending_indent.is_empty() {
|
||||
let (indent_char, repeat_count) = match self.options.indent_style() {
|
||||
IndentStyle::Tab => ('\t', 1),
|
||||
IndentStyle::Space(count) => (' ', count),
|
||||
IndentStyle::Space => (' ', self.options.indent_width()),
|
||||
};
|
||||
|
||||
let indent = std::mem::take(&mut self.state.pending_indent);
|
||||
@@ -764,7 +764,7 @@ impl<'a> Printer<'a> {
|
||||
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
let char_width = if char == '\t' {
|
||||
self.options.tab_width.value()
|
||||
self.options.indent_width.value()
|
||||
} else {
|
||||
// SAFETY: A u32 is sufficient to represent the width of a file <= 4GB
|
||||
char.width().unwrap_or(0) as u32
|
||||
@@ -1347,7 +1347,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
} else {
|
||||
for c in text.chars() {
|
||||
let char_width = match c {
|
||||
'\t' => self.options().tab_width.value(),
|
||||
'\t' => self.options().indent_width.value(),
|
||||
'\n' => {
|
||||
if self.must_be_flat {
|
||||
return Fits::No;
|
||||
@@ -1501,7 +1501,7 @@ mod tests {
|
||||
use crate::printer::{LineEnding, Printer, PrinterOptions};
|
||||
use crate::source_code::SourceCode;
|
||||
use crate::{
|
||||
format_args, write, Document, FormatState, IndentStyle, LineWidth, Printed, TabWidth,
|
||||
format_args, write, Document, FormatState, IndentStyle, IndentWidth, LineWidth, Printed,
|
||||
VecBuffer,
|
||||
};
|
||||
|
||||
@@ -1509,7 +1509,7 @@ mod tests {
|
||||
format_with_options(
|
||||
root,
|
||||
PrinterOptions {
|
||||
indent_style: IndentStyle::Space(2),
|
||||
indent_style: IndentStyle::Space,
|
||||
..PrinterOptions::default()
|
||||
},
|
||||
)
|
||||
@@ -1653,7 +1653,7 @@ two lines`,
|
||||
fn it_use_the_indent_character_specified_in_the_options() {
|
||||
let options = PrinterOptions {
|
||||
indent_style: IndentStyle::Tab,
|
||||
tab_width: TabWidth::try_from(4).unwrap(),
|
||||
indent_width: IndentWidth::try_from(4).unwrap(),
|
||||
line_width: LineWidth::try_from(19).unwrap(),
|
||||
..PrinterOptions::default()
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
use crate::{FormatOptions, IndentStyle, LineWidth, TabWidth};
|
||||
use crate::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
|
||||
|
||||
/// Options that affect how the [`crate::Printer`] prints the format tokens
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||
pub struct PrinterOptions {
|
||||
/// Width of a single tab character (does it equal 2, 4, ... spaces?)
|
||||
pub tab_width: TabWidth,
|
||||
pub indent_width: IndentWidth,
|
||||
|
||||
/// Whether the printer should use tabs or spaces to indent code.
|
||||
pub indent_style: IndentStyle,
|
||||
|
||||
/// What's the max width of a line. Defaults to 80
|
||||
pub line_width: LineWidth,
|
||||
@@ -12,9 +15,6 @@ pub struct PrinterOptions {
|
||||
/// The type of line ending to apply to the printed input
|
||||
pub line_ending: LineEnding,
|
||||
|
||||
/// Whether the printer should use tabs or spaces to indent code and if spaces, by how many.
|
||||
pub indent_style: IndentStyle,
|
||||
|
||||
/// Whether the printer should build a source map that allows mapping positions in the source document
|
||||
/// to positions in the formatted document.
|
||||
pub source_map_generation: SourceMapGeneration,
|
||||
@@ -46,8 +46,8 @@ impl PrinterOptions {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_tab_width(mut self, width: TabWidth) -> Self {
|
||||
self.tab_width = width;
|
||||
pub fn with_tab_width(mut self, width: IndentWidth) -> Self {
|
||||
self.indent_width = width;
|
||||
|
||||
self
|
||||
}
|
||||
@@ -58,10 +58,7 @@ impl PrinterOptions {
|
||||
|
||||
/// Width of an indent in characters.
|
||||
pub(super) const fn indent_width(&self) -> u32 {
|
||||
match self.indent_style {
|
||||
IndentStyle::Tab => self.tab_width.value(),
|
||||
IndentStyle::Space(count) => count as u32,
|
||||
}
|
||||
self.indent_width.value()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ itertools = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
serde_with = { version = "3.0.0" }
|
||||
serde_with = { version = "3.0.0", default-features = false, features = ["macros"] }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"tab_width": 8
|
||||
"indent_width": 4
|
||||
}
|
||||
|
||||
@@ -1,22 +1,18 @@
|
||||
[
|
||||
{
|
||||
"indent_style": {
|
||||
"Space": 4
|
||||
},
|
||||
"tab_width": 8
|
||||
"indent_style": "Space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": {
|
||||
"Space": 2
|
||||
},
|
||||
"tab_width": 8
|
||||
"indent_style": "Space",
|
||||
"indent_width": 2
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab",
|
||||
"tab_width": 8
|
||||
"indent_width": 8
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab",
|
||||
"tab_width": 4
|
||||
"indent_width": 4
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
[
|
||||
{
|
||||
"indent_style": { "Space": 4 }
|
||||
"indent_style": "Space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": { "Space": 2 }
|
||||
"indent_style": "Space",
|
||||
"indent_width": 2
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
[
|
||||
{
|
||||
"indent_style": { "Space": 4 }
|
||||
"indent_style": "Space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": { "Space": 1 }
|
||||
"indent_style": "Space",
|
||||
"indent_width": 1
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab"
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
[
|
||||
{
|
||||
"indent_style": { "Space": 4 }
|
||||
"indent_style": "Space",
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_style": { "Space": 2 }
|
||||
"indent_style": "Space",
|
||||
"indent_width": 2
|
||||
},
|
||||
{
|
||||
"indent_style": "Tab"
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
[
|
||||
{
|
||||
"tab_width": 2
|
||||
"indent_width": 2
|
||||
},
|
||||
{
|
||||
"tab_width": 4
|
||||
"indent_width": 4
|
||||
},
|
||||
{
|
||||
"indent_width": 8
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Fits with tab width 2
|
||||
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567")
|
||||
|
||||
# Fits with tab width 4
|
||||
1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
|
||||
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
|
||||
|
||||
# Fits with tab width 8
|
||||
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
|
||||
|
||||
@@ -353,7 +353,7 @@ impl Format<PyFormatContext<'_>> for FormatTrailingEndOfLineComment<'_> {
|
||||
} else {
|
||||
// Start with 2 because of the two leading spaces.
|
||||
let width = 2u32.saturating_add(
|
||||
TextWidth::from_text(&normalized_comment, f.options().tab_width())
|
||||
TextWidth::from_text(&normalized_comment, f.options().indent_width())
|
||||
.width()
|
||||
.expect("Expected comment not to contain any newlines")
|
||||
.value(),
|
||||
|
||||
@@ -47,7 +47,7 @@ where
|
||||
text_len > 5
|
||||
&& text_len
|
||||
<= context.options().line_width().value() as usize
|
||||
- context.options().indent_width() as usize
|
||||
- context.options().indent_width().value() as usize
|
||||
}
|
||||
|
||||
pub(crate) trait NeedsParentheses {
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::borrow::Cow;
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
use ruff_formatter::{format_args, write, FormatError, FormatOptions, TabWidth};
|
||||
use ruff_formatter::{format_args, write, FormatError};
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::{self as ast, Constant, ExprConstant, ExprFString, ExpressionRef};
|
||||
use ruff_python_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType};
|
||||
@@ -727,22 +727,20 @@ fn normalize_string(input: &str, quotes: StringQuotes, is_raw: bool) -> Cow<str>
|
||||
/// For docstring indentation, black counts spaces as 1 and tabs by increasing the indentation up
|
||||
/// to the next multiple of 8. This is effectively a port of
|
||||
/// [`str.expandtabs`](https://docs.python.org/3/library/stdtypes.html#str.expandtabs),
|
||||
/// which black [calls with the default tab width of 8](https://github.com/psf/black/blob/c36e468794f9256d5e922c399240d49782ba04f1/src/black/strings.py#L61)
|
||||
fn count_indentation_like_black(line: &str, tab_width: TabWidth) -> TextSize {
|
||||
let mut indentation = TextSize::default();
|
||||
/// which black [calls with the default tab width of 8](https://github.com/psf/black/blob/c36e468794f9256d5e922c399240d49782ba04f1/src/black/strings.py#L61).
|
||||
fn indentation_length(line: &str) -> TextSize {
|
||||
let mut indentation = 0u32;
|
||||
for char in line.chars() {
|
||||
if char == '\t' {
|
||||
// Pad to the next multiple of tab_width
|
||||
indentation += TextSize::from(
|
||||
tab_width.value() - (indentation.to_u32().rem_euclid(tab_width.value())),
|
||||
);
|
||||
indentation += 8 - (indentation.rem_euclid(8));
|
||||
} else if char.is_whitespace() {
|
||||
indentation += char.text_len();
|
||||
indentation += u32::from(char.text_len());
|
||||
} else {
|
||||
return indentation;
|
||||
break;
|
||||
}
|
||||
}
|
||||
indentation
|
||||
TextSize::new(indentation)
|
||||
}
|
||||
|
||||
/// Format a docstring by trimming whitespace and adjusting the indentation.
|
||||
@@ -910,7 +908,7 @@ fn format_docstring(normalized: &NormalizedString, f: &mut PyFormatter) -> Forma
|
||||
.clone()
|
||||
// We don't want to count whitespace-only lines as miss-indented
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| count_indentation_like_black(line, f.options().tab_width()))
|
||||
.map(indentation_length)
|
||||
.min()
|
||||
.unwrap_or_default();
|
||||
|
||||
@@ -952,7 +950,7 @@ fn format_docstring_line(
|
||||
line: &str,
|
||||
is_last: bool,
|
||||
offset: TextSize,
|
||||
stripped_indentation: TextSize,
|
||||
stripped_indentation_length: TextSize,
|
||||
already_normalized: bool,
|
||||
f: &mut PyFormatter,
|
||||
) -> FormatResult<()> {
|
||||
@@ -979,21 +977,20 @@ fn format_docstring_line(
|
||||
// overindented, in which case we strip the additional whitespace (see example in
|
||||
// [`format_docstring`] doc comment). We then prepend the in-docstring indentation to the
|
||||
// string.
|
||||
let indent_len =
|
||||
count_indentation_like_black(trim_end, f.options().tab_width()) - stripped_indentation;
|
||||
let in_docstring_indent = " ".repeat(indent_len.to_usize()) + trim_end.trim_start();
|
||||
let indent_len = indentation_length(trim_end) - stripped_indentation_length;
|
||||
let in_docstring_indent = " ".repeat(usize::from(indent_len)) + trim_end.trim_start();
|
||||
text(&in_docstring_indent, Some(offset)).fmt(f)?;
|
||||
} else {
|
||||
// Take the string with the trailing whitespace removed, then also skip the leading
|
||||
// whitespace
|
||||
let trimmed_line_range =
|
||||
TextRange::at(offset, trim_end.text_len()).add_start(stripped_indentation);
|
||||
TextRange::at(offset, trim_end.text_len()).add_start(stripped_indentation_length);
|
||||
if already_normalized {
|
||||
source_text_slice(trimmed_line_range).fmt(f)?;
|
||||
} else {
|
||||
// All indents are ascii spaces, so the slicing is correct
|
||||
text(
|
||||
&trim_end[stripped_indentation.to_usize()..],
|
||||
&trim_end[usize::from(stripped_indentation_length)..],
|
||||
Some(trimmed_line_range.start()),
|
||||
)
|
||||
.fmt(f)?;
|
||||
@@ -1012,25 +1009,14 @@ fn format_docstring_line(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_formatter::TabWidth;
|
||||
|
||||
use crate::expression::string::count_indentation_like_black;
|
||||
use crate::expression::string::indentation_length;
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
#[test]
|
||||
fn test_indentation_like_black() {
|
||||
let tab_width = TabWidth::try_from(8).unwrap();
|
||||
assert_eq!(
|
||||
count_indentation_like_black("\t \t \t", tab_width).to_u32(),
|
||||
24
|
||||
);
|
||||
assert_eq!(
|
||||
count_indentation_like_black("\t \t", tab_width).to_u32(),
|
||||
24
|
||||
);
|
||||
assert_eq!(
|
||||
count_indentation_like_black("\t\t\t", tab_width).to_u32(),
|
||||
24
|
||||
);
|
||||
assert_eq!(count_indentation_like_black(" ", tab_width).to_u32(), 4);
|
||||
assert_eq!(indentation_length("\t \t \t"), TextSize::new(24));
|
||||
assert_eq!(indentation_length("\t \t"), TextSize::new(24));
|
||||
assert_eq!(indentation_length("\t\t\t"), TextSize::new(24));
|
||||
assert_eq!(indentation_length(" "), TextSize::new(4));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_formatter::printer::{LineEnding, PrinterOptions, SourceMapGeneration};
|
||||
use ruff_formatter::{FormatOptions, IndentStyle, LineWidth, TabWidth};
|
||||
use ruff_formatter::{FormatOptions, IndentStyle, IndentWidth, LineWidth};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
@@ -25,8 +25,8 @@ pub struct PyFormatOptions {
|
||||
line_width: LineWidth,
|
||||
|
||||
/// The visual width of a tab character.
|
||||
#[cfg_attr(feature = "serde", serde(default = "default_tab_width"))]
|
||||
tab_width: TabWidth,
|
||||
#[cfg_attr(feature = "serde", serde(default = "default_indent_width"))]
|
||||
indent_width: IndentWidth,
|
||||
|
||||
line_ending: LineEnding,
|
||||
|
||||
@@ -49,11 +49,11 @@ fn default_line_width() -> LineWidth {
|
||||
}
|
||||
|
||||
fn default_indent_style() -> IndentStyle {
|
||||
IndentStyle::Space(4)
|
||||
IndentStyle::Space
|
||||
}
|
||||
|
||||
fn default_tab_width() -> TabWidth {
|
||||
TabWidth::try_from(4).unwrap()
|
||||
fn default_indent_width() -> IndentWidth {
|
||||
IndentWidth::try_from(4).unwrap()
|
||||
}
|
||||
|
||||
impl Default for PyFormatOptions {
|
||||
@@ -62,7 +62,7 @@ impl Default for PyFormatOptions {
|
||||
source_type: PySourceType::default(),
|
||||
indent_style: default_indent_style(),
|
||||
line_width: default_line_width(),
|
||||
tab_width: default_tab_width(),
|
||||
indent_width: default_indent_width(),
|
||||
quote_style: QuoteStyle::default(),
|
||||
line_ending: LineEnding::default(),
|
||||
magic_trailing_comma: MagicTrailingComma::default(),
|
||||
@@ -110,8 +110,8 @@ impl PyFormatOptions {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_tab_width(mut self, tab_width: TabWidth) -> Self {
|
||||
self.tab_width = tab_width;
|
||||
pub fn with_indent_width(mut self, indent_width: IndentWidth) -> Self {
|
||||
self.indent_width = indent_width;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -157,8 +157,8 @@ impl FormatOptions for PyFormatOptions {
|
||||
self.indent_style
|
||||
}
|
||||
|
||||
fn tab_width(&self) -> TabWidth {
|
||||
self.tab_width
|
||||
fn indent_width(&self) -> IndentWidth {
|
||||
self.indent_width
|
||||
}
|
||||
|
||||
fn line_width(&self) -> LineWidth {
|
||||
@@ -167,7 +167,7 @@ impl FormatOptions for PyFormatOptions {
|
||||
|
||||
fn as_print_options(&self) -> PrinterOptions {
|
||||
PrinterOptions {
|
||||
tab_width: self.tab_width,
|
||||
indent_width: self.indent_width,
|
||||
line_width: self.line_width,
|
||||
line_ending: self.line_ending,
|
||||
indent_style: self.indent_style,
|
||||
|
||||
@@ -253,11 +253,11 @@ impl fmt::Display for DisplayPyOptions<'_> {
|
||||
f,
|
||||
r#"indent-style = {indent_style}
|
||||
line-width = {line_width}
|
||||
tab-width = {tab_width}
|
||||
indent-width = {indent_width}
|
||||
quote-style = {quote_style:?}
|
||||
magic-trailing-comma = {magic_trailing_comma:?}"#,
|
||||
indent_style = self.0.indent_style(),
|
||||
tab_width = self.0.tab_width().value(),
|
||||
indent_width = self.0.indent_width().value(),
|
||||
line_width = self.0.line_width().value(),
|
||||
quote_style = self.0.quote_style(),
|
||||
magic_trailing_comma = self.0.magic_trailing_comma()
|
||||
|
||||
@@ -111,9 +111,9 @@ class TabbedIndent:
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 8
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -224,9 +224,9 @@ class TabbedIndent:
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 2
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 8
|
||||
indent-width = 2
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -337,9 +337,9 @@ class TabbedIndent:
|
||||
|
||||
### Output 3
|
||||
```
|
||||
indent-style = Tab
|
||||
indent-style = tab
|
||||
line-width = 88
|
||||
tab-width = 8
|
||||
indent-width = 8
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -450,9 +450,9 @@ class TabbedIndent:
|
||||
|
||||
### Output 4
|
||||
```
|
||||
indent-style = Tab
|
||||
indent-style = tab
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -556,7 +556,7 @@ class TabbedIndent:
|
||||
"""check for correct tabbed formatting
|
||||
^^^^^^^^^^
|
||||
Normal indented line
|
||||
- autor
|
||||
- autor
|
||||
"""
|
||||
```
|
||||
|
||||
|
||||
@@ -129,9 +129,9 @@ test_particular = [
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -277,9 +277,9 @@ test_particular = [
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Single
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
@@ -141,9 +141,9 @@ x = (b"""aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa""" b"""bbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -310,9 +310,9 @@ x = (
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Single
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
@@ -28,9 +28,9 @@ def test():
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -60,9 +60,9 @@ def test():
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 2
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 2
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
@@ -9,9 +9,9 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/fmt_on_off
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -22,9 +22,9 @@ magic-trailing-comma = Respect
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 1
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 1
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -35,9 +35,9 @@ magic-trailing-comma = Respect
|
||||
|
||||
### Output 3
|
||||
```
|
||||
indent-style = Tab
|
||||
indent-style = tab
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
@@ -24,9 +24,9 @@ not_fixed
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -53,9 +53,9 @@ not_fixed
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 2
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 2
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -82,9 +82,9 @@ not_fixed
|
||||
|
||||
### Output 3
|
||||
```
|
||||
indent-style = Tab
|
||||
indent-style = tab
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
@@ -42,9 +42,9 @@ with (a,): # magic trailing comma
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -94,9 +94,9 @@ with (
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Ignore
|
||||
```
|
||||
|
||||
@@ -5,45 +5,42 @@ input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/tab_width.
|
||||
## Input
|
||||
```py
|
||||
# Fits with tab width 2
|
||||
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567")
|
||||
|
||||
# Fits with tab width 4
|
||||
1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
|
||||
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
|
||||
|
||||
# Fits with tab width 8
|
||||
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
|
||||
```
|
||||
|
||||
## Outputs
|
||||
### Output 1
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 2
|
||||
indent-width = 2
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
# Fits with tab width 2
|
||||
(
|
||||
1
|
||||
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
)
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567")
|
||||
|
||||
# Fits with tab width 4
|
||||
1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
|
||||
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
|
||||
|
||||
# Fits with tab width 8
|
||||
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
|
||||
```
|
||||
|
||||
|
||||
### Output 2
|
||||
```
|
||||
indent-style = Spaces, size: 4
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
tab-width = 4
|
||||
indent-width = 4
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
@@ -52,17 +49,41 @@ magic-trailing-comma = Respect
|
||||
# Fits with tab width 2
|
||||
(
|
||||
1
|
||||
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567"
|
||||
)
|
||||
|
||||
# Fits with tab width 4
|
||||
(1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345")
|
||||
|
||||
# Fits with tab width 8
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
|
||||
```
|
||||
|
||||
|
||||
### Output 3
|
||||
```
|
||||
indent-style = space
|
||||
line-width = 88
|
||||
indent-width = 8
|
||||
quote-style = Double
|
||||
magic-trailing-comma = Respect
|
||||
```
|
||||
|
||||
```py
|
||||
# Fits with tab width 2
|
||||
(
|
||||
1
|
||||
+ " 012345678901234567890123456789012345678901234567890123456789012345678901234567"
|
||||
)
|
||||
|
||||
# Fits with tab width 4
|
||||
(
|
||||
1
|
||||
+ " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"
|
||||
1
|
||||
+ " 0123456789012345678901234567890123456789012345678901234567890123456789012345"
|
||||
)
|
||||
|
||||
# Fits with tab width 8
|
||||
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
|
||||
(1 + " 012345678901234567890123456789012345678901234567890123456789012345678901")
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -184,7 +184,7 @@ pub(crate) fn function_visibility(function: &ast::StmtFunctionDef) -> Visibility
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn method_visibility(function: &ast::StmtFunctionDef) -> Visibility {
|
||||
pub fn method_visibility(function: &ast::StmtFunctionDef) -> Visibility {
|
||||
// Is this a setter or deleter?
|
||||
if function.decorator_list.iter().any(|decorator| {
|
||||
collect_call_path(&decorator.expression).is_some_and(|call_path| {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::num::NonZeroU16;
|
||||
use std::path::Path;
|
||||
|
||||
use js_sys::Error;
|
||||
@@ -8,10 +9,10 @@ use ruff::directives;
|
||||
use ruff::line_width::{LineLength, TabSize};
|
||||
use ruff::linter::{check_path, LinterResult};
|
||||
use ruff::registry::AsRule;
|
||||
use ruff::settings::types::PythonVersion;
|
||||
use ruff::settings::types::{PreviewMode, PythonVersion};
|
||||
use ruff::settings::{defaults, flags, Settings};
|
||||
use ruff::source_kind::SourceKind;
|
||||
use ruff_formatter::{FormatResult, Formatted};
|
||||
use ruff_formatter::{FormatResult, Formatted, LineWidth};
|
||||
use ruff_python_ast::{Mod, PySourceType};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_formatter::{format_node, pretty_comments, PyFormatContext, PyFormatOptions};
|
||||
@@ -237,7 +238,7 @@ impl Workspace {
|
||||
|
||||
pub fn format(&self, contents: &str) -> Result<String, Error> {
|
||||
let parsed = ParsedModule::from_source(contents)?;
|
||||
let formatted = parsed.format().map_err(into_error)?;
|
||||
let formatted = parsed.format(&self.settings).map_err(into_error)?;
|
||||
let printed = formatted.print().map_err(into_error)?;
|
||||
|
||||
Ok(printed.into_code())
|
||||
@@ -245,7 +246,7 @@ impl Workspace {
|
||||
|
||||
pub fn format_ir(&self, contents: &str) -> Result<String, Error> {
|
||||
let parsed = ParsedModule::from_source(contents)?;
|
||||
let formatted = parsed.format().map_err(into_error)?;
|
||||
let formatted = parsed.format(&self.settings).map_err(into_error)?;
|
||||
|
||||
Ok(format!("{formatted}"))
|
||||
}
|
||||
@@ -298,9 +299,14 @@ impl<'a> ParsedModule<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
fn format(&self) -> FormatResult<Formatted<PyFormatContext>> {
|
||||
fn format(&self, settings: &Settings) -> FormatResult<Formatted<PyFormatContext>> {
|
||||
// TODO(konstin): Add an options for py/pyi to the UI (2/2)
|
||||
let options = PyFormatOptions::from_source_type(PySourceType::default());
|
||||
let options = PyFormatOptions::from_source_type(PySourceType::default())
|
||||
.with_preview(match settings.preview {
|
||||
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
|
||||
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
|
||||
})
|
||||
.with_line_width(LineWidth::from(NonZeroU16::from(settings.line_length)));
|
||||
|
||||
format_node(
|
||||
&self.module,
|
||||
|
||||
@@ -27,7 +27,7 @@ use ruff::settings::types::{
|
||||
Version,
|
||||
};
|
||||
use ruff::settings::{defaults, resolve_per_file_ignores, AllSettings, CliSettings, Settings};
|
||||
use ruff::{fs, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION};
|
||||
use ruff::{fs, warn_user, warn_user_once, warn_user_once_by_id, RuleSelector, RUFF_PKG_VERSION};
|
||||
use ruff_cache::cache_dir;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use shellexpand;
|
||||
@@ -460,7 +460,10 @@ impl Configuration {
|
||||
let mut carryover_ignores: Option<&[RuleSelector]> = None;
|
||||
let mut carryover_unfixables: Option<&[RuleSelector]> = None;
|
||||
|
||||
// Store selectors for displaying warnings
|
||||
let mut redirects = FxHashMap::default();
|
||||
let mut deprecated_nursery_selectors = FxHashSet::default();
|
||||
let mut ignored_preview_selectors = FxHashSet::default();
|
||||
|
||||
for selection in &self.rule_selections {
|
||||
// If a selection only specifies extend-select we cannot directly
|
||||
@@ -571,8 +574,7 @@ impl Configuration {
|
||||
}
|
||||
}
|
||||
|
||||
// We insert redirects into the hashmap so that we
|
||||
// can warn the users about remapped rule codes.
|
||||
// Check for selections that require a warning
|
||||
for selector in selection
|
||||
.select
|
||||
.iter()
|
||||
@@ -583,6 +585,29 @@ impl Configuration {
|
||||
.chain(selection.unfixable.iter())
|
||||
.chain(selection.extend_fixable.iter())
|
||||
{
|
||||
#[allow(deprecated)]
|
||||
if matches!(selector, RuleSelector::Nursery) {
|
||||
let suggestion = if preview.is_disabled() {
|
||||
"Use the `--preview` flag instead."
|
||||
} else {
|
||||
"Use the `PREVIEW` selector instead."
|
||||
};
|
||||
warn_user_once!("The `NURSERY` selector has been deprecated. {suggestion}");
|
||||
}
|
||||
|
||||
if preview.is_disabled() {
|
||||
if let RuleSelector::Rule { prefix, .. } = selector {
|
||||
if prefix.rules().any(|rule| rule.is_nursery()) {
|
||||
deprecated_nursery_selectors.insert(selector);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the selector is empty because preview mode is disabled
|
||||
if selector.rules(PreviewMode::Disabled).next().is_none() {
|
||||
ignored_preview_selectors.insert(selector);
|
||||
}
|
||||
}
|
||||
|
||||
if let RuleSelector::Prefix {
|
||||
prefix,
|
||||
redirected_from: Some(redirect_from),
|
||||
@@ -603,6 +628,18 @@ impl Configuration {
|
||||
);
|
||||
}
|
||||
|
||||
for selection in deprecated_nursery_selectors {
|
||||
let (prefix, code) = selection.prefix_and_code();
|
||||
warn_user!("Selection of nursery rule `{prefix}{code}` without the `--preview` flag is deprecated.",);
|
||||
}
|
||||
|
||||
for selection in ignored_preview_selectors {
|
||||
let (prefix, code) = selection.prefix_and_code();
|
||||
warn_user!(
|
||||
"Selection `{prefix}{code}` has no effect because the `--preview` flag was not included.",
|
||||
);
|
||||
}
|
||||
|
||||
let mut rules = RuleTable::empty();
|
||||
|
||||
for rule in select_set {
|
||||
@@ -764,7 +801,7 @@ pub fn resolve_src(src: &[String], project_root: &Path) -> Result<Vec<PathBuf>>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::configuration::{Configuration, RuleSelection};
|
||||
use ruff::codes::{Flake8Copyright, Pycodestyle};
|
||||
use ruff::codes::{Flake8Copyright, Pycodestyle, Refurb};
|
||||
use ruff::registry::{Linter, Rule, RuleSet};
|
||||
use ruff::settings::types::PreviewMode;
|
||||
use ruff::RuleSelector;
|
||||
@@ -814,6 +851,8 @@ mod tests {
|
||||
Rule::QuadraticListSummation,
|
||||
];
|
||||
|
||||
const PREVIEW_RULES: &[Rule] = &[Rule::TooManyPublicMethods, Rule::SliceCopy];
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn resolve_rules(
|
||||
selections: impl IntoIterator<Item = RuleSelection>,
|
||||
@@ -1099,6 +1138,29 @@ mod tests {
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_rule_preview() {
|
||||
let actual = resolve_rules(
|
||||
[RuleSelection {
|
||||
select: Some(vec![Refurb::_145.into()]),
|
||||
..RuleSelection::default()
|
||||
}],
|
||||
Some(PreviewMode::Disabled),
|
||||
);
|
||||
let expected = RuleSet::empty();
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = resolve_rules(
|
||||
[RuleSelection {
|
||||
select: Some(vec![Refurb::_145.into()]),
|
||||
..RuleSelection::default()
|
||||
}],
|
||||
Some(PreviewMode::Enabled),
|
||||
);
|
||||
let expected = RuleSet::from_rule(Rule::SliceCopy);
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_preview() {
|
||||
let actual = resolve_rules(
|
||||
@@ -1118,7 +1180,9 @@ mod tests {
|
||||
}],
|
||||
Some(PreviewMode::Enabled),
|
||||
);
|
||||
let expected = RuleSet::from_rules(NURSERY_RULES);
|
||||
|
||||
let expected =
|
||||
RuleSet::from_rules(NURSERY_RULES).union(&RuleSet::from_rules(PREVIEW_RULES));
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
|
||||
@@ -2155,6 +2155,13 @@ pub struct PylintOptions {
|
||||
/// Maximum number of statements allowed for a function or method body (see:
|
||||
/// `PLR0915`).
|
||||
pub max_statements: Option<usize>,
|
||||
#[option(
|
||||
default = r"20",
|
||||
value_type = "int",
|
||||
example = r"max-public-methods = 20"
|
||||
)]
|
||||
/// Maximum number of public methods allowed for a class (see: `PLR0904`).
|
||||
pub max_public_methods: Option<usize>,
|
||||
}
|
||||
|
||||
impl PylintOptions {
|
||||
@@ -2168,6 +2175,9 @@ impl PylintOptions {
|
||||
max_returns: self.max_returns.unwrap_or(defaults.max_returns),
|
||||
max_branches: self.max_branches.unwrap_or(defaults.max_branches),
|
||||
max_statements: self.max_statements.unwrap_or(defaults.max_statements),
|
||||
max_public_methods: self
|
||||
.max_public_methods
|
||||
.unwrap_or(defaults.max_public_methods),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,8 +394,9 @@ i = 1 # noqa: E741, F841
|
||||
x = 1 # noqa
|
||||
```
|
||||
|
||||
Note that, for multi-line strings, the `noqa` directive should come at the end of the string, and
|
||||
will apply to the entire string, like so:
|
||||
For multi-line strings (like docstrings),
|
||||
the `noqa` directive should come at the end of the string (after the closing triple quote),
|
||||
and will apply to the entire string, like so:
|
||||
|
||||
```python
|
||||
"""Lorem ipsum dolor sit amet.
|
||||
|
||||
@@ -205,6 +205,11 @@ def sum_even_numbers(numbers: List[int]) -> int:
|
||||
return sum(num for num in numbers if num % 2 == 0)
|
||||
```
|
||||
|
||||
For more in-depth instructions on ignoring errors,
|
||||
please see [_Configuration_](configuration.md#error-suppression).
|
||||
|
||||
### Adding Rules
|
||||
|
||||
When enabling a new rule on an existing codebase, you may want to ignore all _existing_
|
||||
violations of that rule and instead focus on enforcing it going forward.
|
||||
|
||||
|
||||
13
ruff.schema.json
generated
13
ruff.schema.json
generated
@@ -1612,6 +1612,15 @@
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"max-public-methods": {
|
||||
"description": "Maximum number of public methods allowed for a class (see: `PLR0904`).",
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "uint",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"max-returns": {
|
||||
"description": "Maximum number of return statements allowed for a function or method body (see `PLR0911`)",
|
||||
"type": [
|
||||
@@ -2079,6 +2088,8 @@
|
||||
"FURB13",
|
||||
"FURB131",
|
||||
"FURB132",
|
||||
"FURB14",
|
||||
"FURB145",
|
||||
"G",
|
||||
"G0",
|
||||
"G00",
|
||||
@@ -2294,6 +2305,8 @@
|
||||
"PLR040",
|
||||
"PLR0402",
|
||||
"PLR09",
|
||||
"PLR090",
|
||||
"PLR0904",
|
||||
"PLR091",
|
||||
"PLR0911",
|
||||
"PLR0912",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
1.72
|
||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[toolchain]
|
||||
channel = "1.72"
|
||||
930
scripts/benchmarks/poetry.lock
generated
930
scripts/benchmarks/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -6,70 +6,31 @@ authors = ["Charles Marsh <charlie.r.marsh@gmail.com>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.10,<3.12"
|
||||
autoflake = "^2.0.0"
|
||||
flake8 = "^6.0.0"
|
||||
pycodestyle = "^2.10.0"
|
||||
pyflakes = "^3.0.1"
|
||||
pylint = "^2.15.10"
|
||||
black = "^22.12.0"
|
||||
isort = "^5.11.4"
|
||||
flake8-2020 = { version = "*", optional = true }
|
||||
flake8-annotations = { version = "*", optional = true }
|
||||
flake8-bandit = { version = "*", optional = true }
|
||||
flake8-blind-except = { version = "*", optional = true }
|
||||
# flake8-boolean-trap = { version = "*", optional = true }
|
||||
flake8-bugbear = { version = "*", optional = true }
|
||||
flake8-builtins = { version = "*", optional = true }
|
||||
flake8-commas = { version = "*", optional = true }
|
||||
flake8-comprehensions = { version = "*", optional = true }
|
||||
flake8-datetimez = { version = "*", optional = true }
|
||||
flake8-debugger = { version = "*", optional = true }
|
||||
flake8-docstrings = { version = "*", optional = true }
|
||||
# flake8-eradicate = { version = "*", optional = true }
|
||||
flake8-errmsg = { version = "*", optional = true }
|
||||
flake8-implicit-str-concat = { version = "*", optional = true }
|
||||
# flake8-import-conventions = { version = "*", optional = true }
|
||||
flake8-isort = { version = "*", optional = true }
|
||||
flake8-pie = { version = "*", optional = true }
|
||||
flake8-print = { version = "*", optional = true }
|
||||
flake8-quotes = { version = "*", optional = true }
|
||||
flake8-return = { version = "*", optional = true }
|
||||
flake8-simplify = { version = "*", optional = true }
|
||||
flake8-super = { version = "*", optional = true }
|
||||
flake8-tidy-imports = { version = "*", optional = true }
|
||||
pandas-vet = { version = "*", optional = true }
|
||||
pep8-naming = { version = "*", optional = true }
|
||||
autoflake = "*"
|
||||
autopep8 = "*"
|
||||
black = "*"
|
||||
flake8 = "*"
|
||||
isort = "*"
|
||||
pycodestyle = "*"
|
||||
pyflakes = "*"
|
||||
pylint = "*"
|
||||
yapf = "*"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[tool.poetry.extras]
|
||||
plugins = [
|
||||
"flake8-2020",
|
||||
"flake8-annotations",
|
||||
"flake8-bandit",
|
||||
"flake8-blind-except",
|
||||
# "flake8-boolean-trap",
|
||||
"flake8-bugbear",
|
||||
"flake8-builtins",
|
||||
"flake8-commas",
|
||||
"flake8-comprehensions",
|
||||
"flake8-datetimez",
|
||||
"flake8-debugger",
|
||||
"flake8-docstrings",
|
||||
# "flake8-eradicate",
|
||||
"flake8-errmsg",
|
||||
"flake8-implicit-str-concat",
|
||||
# "flake8-import-conventions",
|
||||
"flake8-isort",
|
||||
"flake8-pie",
|
||||
"flake8-print",
|
||||
"flake8-quotes",
|
||||
"flake8-return",
|
||||
"flake8-simplify",
|
||||
"flake8-super",
|
||||
"flake8-tidy-imports",
|
||||
"pandas-vet",
|
||||
"pep8-naming",
|
||||
formatter = [
|
||||
"black",
|
||||
"yapf",
|
||||
"autopep8",
|
||||
]
|
||||
linter = [
|
||||
"autoflake",
|
||||
"flake8",
|
||||
"pycodestyle",
|
||||
"pyflakes",
|
||||
"pylint",
|
||||
"isort",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
74
scripts/benchmarks/run_formatter.sh
Executable file
74
scripts/benchmarks/run_formatter.sh
Executable file
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
###
|
||||
# Benchmark the Ruff formatter's performance against a variety of similar tools.
|
||||
#
|
||||
# Expects to be run from the repo root after invoking `cargo build --release`,
|
||||
# in an environment with access to `black`, `autopep8`, and `yapf` (most recently:
|
||||
# `black` v23.9.1, `autopep8` v2.0.4, and `yapf` v0.40.1).
|
||||
#
|
||||
# Example usage:
|
||||
#
|
||||
# ./scripts/benchmarks/run_formatter.sh ~/workspace/zulip
|
||||
###
|
||||
|
||||
TARGET_DIR=${1}
|
||||
|
||||
# In each case, ensure that we format the code in-place before invoking a given tool. This ensures
|
||||
# a fair comparison across tools, since every tool is then running on a repository that already
|
||||
# matches that tool's desired formatting.
|
||||
#
|
||||
# For example, if we're benchmarking Black's preview style, we first run `black --preview` over the
|
||||
# target directory, thus ensuring that we're benchmarking preview style against a codebase that
|
||||
# already conforms to it. The same goes for yapf, autoepp8, etc.
|
||||
|
||||
# Benchmark 1: Write to disk.
|
||||
hyperfine --ignore-failure \
|
||||
--prepare "./target/release/ruff format ${TARGET_DIR}" \
|
||||
"./target/release/ruff format ${TARGET_DIR}" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
|
||||
--prepare "autopep8 ${TARGET_DIR} --recursive --in-place" \
|
||||
"autopep8 ${TARGET_DIR} --recursive --in-place" \
|
||||
--prepare "yapf ${TARGET_DIR} --parallel --recursive --in-place" \
|
||||
"yapf ${TARGET_DIR} --parallel --recursive --in-place"
|
||||
|
||||
# Benchmark 2: Write to disk, but only use one thread.
|
||||
hyperfine --ignore-failure \
|
||||
--prepare "./target/release/ruff format ${TARGET_DIR}" \
|
||||
"RAYON_NUM_THREADS=1 ./target/release/ruff format ${TARGET_DIR}" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --safe" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --fast" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --safe --preview" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --workers=1 --fast --preview" \
|
||||
--prepare "autopep8 ${TARGET_DIR} --recursive --in-place" \
|
||||
"autopep8 ${TARGET_DIR} --in-place --recursive --jobs=1" \
|
||||
--prepare "yapf ${TARGET_DIR} --parallel --recursive --in-place" \
|
||||
"yapf ${TARGET_DIR} --recursive --in-place"
|
||||
|
||||
# Benchmark 3: Check formatting, but don't write to disk.
|
||||
hyperfine --ignore-failure \
|
||||
--prepare "./target/release/ruff format ${TARGET_DIR}" \
|
||||
"./target/release/ruff format ${TARGET_DIR} --check" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --safe" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --fast" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --safe --preview" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --safe --preview" \
|
||||
--prepare "BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --fast --preview" \
|
||||
"BLACK_CACHE_DIR=/dev/null black ${TARGET_DIR} --check --fast --preview" \
|
||||
--prepare "autopep8 ${TARGET_DIR} --recursive --in-place" \
|
||||
"autopep8 ${TARGET_DIR} --recursive --diff" \
|
||||
--prepare "yapf ${TARGET_DIR} --parallel --recursive --in-place" \
|
||||
"yapf ${TARGET_DIR} --parallel --recursive --quiet"
|
||||
Reference in New Issue
Block a user