Compare commits
46 Commits
v0.0.288
...
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 | ||
|
|
f4c7bff36b | ||
|
|
56440ad835 | ||
|
|
179128dc54 | ||
|
|
e7a2779402 | ||
|
|
008da95b29 | ||
|
|
5d4dd3e38e | ||
|
|
e561f5783b | ||
|
|
ee0f1270cf | ||
|
|
e7b7e4a18d | ||
|
|
b4419c34ea | ||
|
|
08f19226b9 | ||
|
|
1e6df19a35 | ||
|
|
c21b960fc7 | ||
|
|
73ad2affa1 | ||
|
|
40c936922e | ||
|
|
874db4fb86 | ||
|
|
a41bb2733f | ||
|
|
24b848a4ea | ||
|
|
773ba5f816 | ||
|
|
f5701fcc63 | ||
|
|
ff0feb191c | ||
|
|
6566d00295 |
3
.github/release.yml
vendored
3
.github/release.yml
vendored
@@ -20,6 +20,9 @@ changelog:
|
||||
- title: Bug Fixes
|
||||
labels:
|
||||
- bug
|
||||
- title: Preview
|
||||
labels:
|
||||
- preview
|
||||
- title: Other Changes
|
||||
labels:
|
||||
- "*"
|
||||
|
||||
10
.github/workflows/docs.yaml
vendored
10
.github/workflows/docs.yaml
vendored
@@ -2,6 +2,11 @@ name: mkdocs
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
description: "The commit SHA, tag, or branch to publish. Uses the default branch if not specified."
|
||||
default: ""
|
||||
type: string
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
@@ -13,6 +18,8 @@ jobs:
|
||||
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
- uses: actions/setup-python@v4
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
@@ -44,4 +51,5 @@ jobs:
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
command: pages publish site --project-name=ruff-docs --branch ${GITHUB_HEAD_REF} --commit-hash ${GITHUB_SHA}
|
||||
# `github.head_ref` is only set during pull requests and for manual runs or tags we use `main` to deploy to production
|
||||
command: pages deploy site --project-name=ruff-docs --branch ${{ github.head_ref || 'main' }} --commit-hash ${GITHUB_SHA}
|
||||
|
||||
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"
|
||||
|
||||
153
Cargo.lock
generated
153
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"
|
||||
@@ -821,7 +803,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.288"
|
||||
version = "0.0.289"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -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",
|
||||
@@ -2037,7 +2013,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.288"
|
||||
version = "0.0.289"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -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",
|
||||
@@ -2135,7 +2112,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.288"
|
||||
version = "0.0.289"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -2183,6 +2160,7 @@ dependencies = [
|
||||
"similar",
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror",
|
||||
"tikv-jemallocator",
|
||||
"tracing",
|
||||
@@ -2303,6 +2281,7 @@ dependencies = [
|
||||
"bitflags 2.4.0",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"itertools",
|
||||
"memchr",
|
||||
"num-bigint",
|
||||
"num-traits",
|
||||
@@ -2400,7 +2379,6 @@ dependencies = [
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"static_assertions",
|
||||
"test-case",
|
||||
"tiny-keccak",
|
||||
"unicode-ident",
|
||||
"unicode_names2",
|
||||
@@ -2721,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",
|
||||
@@ -2754,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]]
|
||||
@@ -2852,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]]
|
||||
@@ -3051,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"
|
||||
@@ -3140,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",
|
||||
@@ -3430,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" }
|
||||
|
||||
@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.0.288
|
||||
rev: v0.0.289
|
||||
hooks:
|
||||
- id: ruff
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.288"
|
||||
version = "0.0.289"
|
||||
description = """
|
||||
Convert Flake8 configuration files to Ruff configuration files.
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ruff::registry::Linter;
|
||||
use ruff::settings::types::PreviewMode;
|
||||
use ruff::RuleSelector;
|
||||
|
||||
#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
@@ -331,7 +332,7 @@ pub(crate) fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec
|
||||
.filter(|plugin| {
|
||||
for selector in selectors {
|
||||
if selector
|
||||
.into_iter()
|
||||
.rules(PreviewMode::Disabled)
|
||||
.any(|rule| Linter::from(plugin).rules().any(|r| r == rule))
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.288"
|
||||
version = "0.0.289"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -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.
|
||||
|
||||
"""
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from typing import override
|
||||
|
||||
|
||||
class Apples:
|
||||
def _init_(self): # [bad-dunder-name]
|
||||
pass
|
||||
@@ -21,6 +24,11 @@ class Apples:
|
||||
# author likely meant to call the invert dunder method
|
||||
pass
|
||||
|
||||
@override
|
||||
def _ignore__(self): # [bad-dunder-name]
|
||||
# overridden dunder methods should be ignored
|
||||
pass
|
||||
|
||||
def hello(self):
|
||||
print("hello")
|
||||
|
||||
|
||||
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
|
||||
|
||||
@@ -178,3 +178,9 @@ if True:
|
||||
if True:
|
||||
if sys.version_info > (3, 0): \
|
||||
expected_error = []
|
||||
|
||||
if sys.version_info < (3,12):
|
||||
print("py3")
|
||||
|
||||
if sys.version_info <= (3,12):
|
||||
print("py3")
|
||||
|
||||
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]
|
||||
@@ -3,7 +3,7 @@
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Stmt};
|
||||
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Stmt};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_trivia::{
|
||||
@@ -92,10 +92,8 @@ pub(crate) fn remove_argument<T: Ranged>(
|
||||
) -> Result<Edit> {
|
||||
// Partition into arguments before and after the argument to remove.
|
||||
let (before, after): (Vec<_>, Vec<_>) = arguments
|
||||
.args
|
||||
.iter()
|
||||
.map(Expr::range)
|
||||
.chain(arguments.keywords.iter().map(Keyword::range))
|
||||
.arguments_source_order()
|
||||
.map(|arg| arg.range())
|
||||
.filter(|range| argument.range() != *range)
|
||||
.partition(|range| range.start() < argument.start());
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use strum_macros::{AsRefStr, EnumIter};
|
||||
use ruff_diagnostics::Violation;
|
||||
|
||||
use crate::registry::{AsRule, Linter};
|
||||
use crate::rule_selector::is_single_rule_selector;
|
||||
use crate::rules;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
@@ -51,7 +52,10 @@ impl PartialEq<&str> for NoqaCode {
|
||||
pub enum RuleGroup {
|
||||
/// The rule has not been assigned to any specific group.
|
||||
Unspecified,
|
||||
/// The rule is still under development, and must be enabled explicitly.
|
||||
/// The rule is unstable, and preview mode must be enabled for usage.
|
||||
Preview,
|
||||
/// Legacy category for unstable rules, supports backwards compatible selection.
|
||||
#[deprecated(note = "Use `RuleGroup::Preview` for new rules instead")]
|
||||
Nursery,
|
||||
}
|
||||
|
||||
@@ -64,38 +68,71 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
Some(match (linter, code) {
|
||||
// pycodestyle errors
|
||||
(Pycodestyle, "E101") => (RuleGroup::Unspecified, rules::pycodestyle::rules::MixedSpacesAndTabs),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E111") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultiple),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E113") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentation),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E114") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::IndentationWithInvalidMultipleComment),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E115") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlockComment),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E116") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedIndentationComment),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E117") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::OverIndented),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E201") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceAfterOpenBracket),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E202") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeCloseBracket),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E203") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforePunctuation),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E211") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::WhitespaceBeforeParameters),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E221") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E222") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E223") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E224") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E225") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E226") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundArithmeticOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E227") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundBitwiseOrShiftOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E228") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundModuloOperator),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E231") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespace),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E241") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterComma),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E242") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterComma),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E251") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::UnexpectedSpacesAroundKeywordParameterEquals),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E252") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAroundParameterEquals),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E261") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TooFewSpacesBeforeInlineComment),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E262") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterInlineComment),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E265") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoSpaceAfterBlockComment),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E266") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleLeadingHashesForBlockComment),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E271") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesAfterKeyword),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E272") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MultipleSpacesBeforeKeyword),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E273") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabAfterKeyword),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E274") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::TabBeforeKeyword),
|
||||
#[allow(deprecated)]
|
||||
(Pycodestyle, "E275") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::MissingWhitespaceAfterKeyword),
|
||||
(Pycodestyle, "E401") => (RuleGroup::Unspecified, rules::pycodestyle::rules::MultipleImportsOnOneLine),
|
||||
(Pycodestyle, "E402") => (RuleGroup::Unspecified, rules::pycodestyle::rules::ModuleImportNotAtTopOfFile),
|
||||
@@ -176,6 +213,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "C0205") => (RuleGroup::Unspecified, rules::pylint::rules::SingleStringSlots),
|
||||
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
|
||||
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
|
||||
#[allow(deprecated)]
|
||||
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
|
||||
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
|
||||
(Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit),
|
||||
@@ -216,6 +254,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
|
||||
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
|
||||
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
|
||||
#[allow(deprecated)]
|
||||
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
|
||||
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
|
||||
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
|
||||
@@ -228,8 +267,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W1508") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarDefault),
|
||||
(Pylint, "W1509") => (RuleGroup::Unspecified, rules::pylint::rules::SubprocessPopenPreexecFn),
|
||||
(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),
|
||||
(Pylint, "W3301") => (RuleGroup::Unspecified, rules::pylint::rules::NestedMinMax),
|
||||
|
||||
@@ -403,6 +445,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault),
|
||||
|
||||
// flake8-copyright
|
||||
#[allow(deprecated)]
|
||||
(Flake8Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice),
|
||||
|
||||
// pyupgrade
|
||||
@@ -815,9 +858,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "012") => (RuleGroup::Unspecified, rules::ruff::rules::MutableClassDefault),
|
||||
(Ruff, "013") => (RuleGroup::Unspecified, rules::ruff::rules::ImplicitOptional),
|
||||
#[cfg(feature = "unreachable-code")] // When removing this feature gate, also update rules_selector.rs
|
||||
#[allow(deprecated)]
|
||||
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
|
||||
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
|
||||
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
|
||||
#[allow(deprecated)]
|
||||
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
|
||||
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),
|
||||
@@ -866,9 +911,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Slots, "002") => (RuleGroup::Unspecified, rules::flake8_slots::rules::NoSlotsInNamedtupleSubclass),
|
||||
|
||||
// refurb
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
#[allow(deprecated)]
|
||||
(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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ use std::num::NonZeroUsize;
|
||||
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
@@ -65,7 +65,7 @@ impl Emitter for GroupedEmitter {
|
||||
writer,
|
||||
"{}",
|
||||
DisplayGroupedMessage {
|
||||
jupyter_index: context.notebook(message.filename()).map(Notebook::index),
|
||||
notebook_index: context.notebook_index(message.filename()),
|
||||
message,
|
||||
show_fix_status: self.show_fix_status,
|
||||
show_source: self.show_source,
|
||||
@@ -92,7 +92,7 @@ struct DisplayGroupedMessage<'a> {
|
||||
show_source: bool,
|
||||
row_length: NonZeroUsize,
|
||||
column_length: NonZeroUsize,
|
||||
jupyter_index: Option<&'a NotebookIndex>,
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
}
|
||||
|
||||
impl Display for DisplayGroupedMessage<'_> {
|
||||
@@ -110,7 +110,7 @@ impl Display for DisplayGroupedMessage<'_> {
|
||||
)?;
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let (row, col) = if let Some(jupyter_index) = self.jupyter_index {
|
||||
let (row, col) = if let Some(jupyter_index) = self.notebook_index {
|
||||
write!(
|
||||
f,
|
||||
"cell {cell}{sep}",
|
||||
@@ -150,7 +150,7 @@ impl Display for DisplayGroupedMessage<'_> {
|
||||
"{}",
|
||||
MessageCodeFrame {
|
||||
message,
|
||||
jupyter_index: self.jupyter_index
|
||||
notebook_index: self.notebook_index
|
||||
}
|
||||
)?;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub use json_lines::JsonLinesEmitter;
|
||||
pub use junit::JunitEmitter;
|
||||
pub use pylint::PylintEmitter;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{SourceFile, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
pub use text::TextEmitter;
|
||||
@@ -127,21 +127,21 @@ pub trait Emitter {
|
||||
|
||||
/// Context passed to [`Emitter`].
|
||||
pub struct EmitterContext<'a> {
|
||||
notebooks: &'a FxHashMap<String, Notebook>,
|
||||
notebook_indexes: &'a FxHashMap<String, NotebookIndex>,
|
||||
}
|
||||
|
||||
impl<'a> EmitterContext<'a> {
|
||||
pub fn new(notebooks: &'a FxHashMap<String, Notebook>) -> Self {
|
||||
Self { notebooks }
|
||||
pub fn new(notebook_indexes: &'a FxHashMap<String, NotebookIndex>) -> Self {
|
||||
Self { notebook_indexes }
|
||||
}
|
||||
|
||||
/// Tests if the file with `name` is a jupyter notebook.
|
||||
pub fn is_notebook(&self, name: &str) -> bool {
|
||||
self.notebooks.contains_key(name)
|
||||
self.notebook_indexes.contains_key(name)
|
||||
}
|
||||
|
||||
pub fn notebook(&self, name: &str) -> Option<&Notebook> {
|
||||
self.notebooks.get(name)
|
||||
pub fn notebook_index(&self, name: &str) -> Option<&NotebookIndex> {
|
||||
self.notebook_indexes.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,8 +225,8 @@ def fibonacci(n):
|
||||
emitter: &mut dyn Emitter,
|
||||
messages: &[Message],
|
||||
) -> String {
|
||||
let source_kinds = FxHashMap::default();
|
||||
let context = EmitterContext::new(&source_kinds);
|
||||
let notebook_indexes = FxHashMap::default();
|
||||
let context = EmitterContext::new(¬ebook_indexes);
|
||||
let mut output: Vec<u8> = Vec::new();
|
||||
emitter.emit(&mut output, messages, &context).unwrap();
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, Sou
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{OneIndexed, SourceLocation};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
@@ -71,14 +71,14 @@ impl Emitter for TextEmitter {
|
||||
)?;
|
||||
|
||||
let start_location = message.compute_start_location();
|
||||
let jupyter_index = context.notebook(message.filename()).map(Notebook::index);
|
||||
let notebook_index = context.notebook_index(message.filename());
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let diagnostic_location = if let Some(jupyter_index) = jupyter_index {
|
||||
let diagnostic_location = if let Some(notebook_index) = notebook_index {
|
||||
write!(
|
||||
writer,
|
||||
"cell {cell}{sep}",
|
||||
cell = jupyter_index
|
||||
cell = notebook_index
|
||||
.cell(start_location.row.get())
|
||||
.unwrap_or_default(),
|
||||
sep = ":".cyan(),
|
||||
@@ -86,7 +86,7 @@ impl Emitter for TextEmitter {
|
||||
|
||||
SourceLocation {
|
||||
row: OneIndexed::new(
|
||||
jupyter_index
|
||||
notebook_index
|
||||
.cell_row(start_location.row.get())
|
||||
.unwrap_or(1) as usize,
|
||||
)
|
||||
@@ -115,7 +115,7 @@ impl Emitter for TextEmitter {
|
||||
"{}",
|
||||
MessageCodeFrame {
|
||||
message,
|
||||
jupyter_index
|
||||
notebook_index
|
||||
}
|
||||
)?;
|
||||
}
|
||||
@@ -161,7 +161,7 @@ impl Display for RuleCodeAndBody<'_> {
|
||||
|
||||
pub(super) struct MessageCodeFrame<'a> {
|
||||
pub(crate) message: &'a Message,
|
||||
pub(crate) jupyter_index: Option<&'a NotebookIndex>,
|
||||
pub(crate) notebook_index: Option<&'a NotebookIndex>,
|
||||
}
|
||||
|
||||
impl Display for MessageCodeFrame<'_> {
|
||||
@@ -186,14 +186,12 @@ impl Display for MessageCodeFrame<'_> {
|
||||
let content_start_index = source_code.line_index(range.start());
|
||||
let mut start_index = content_start_index.saturating_sub(2);
|
||||
|
||||
// If we're working on a jupyter notebook, skip the lines which are
|
||||
// If we're working with a Jupyter Notebook, skip the lines which are
|
||||
// outside of the cell containing the diagnostic.
|
||||
if let Some(jupyter_index) = self.jupyter_index {
|
||||
let content_start_cell = jupyter_index
|
||||
.cell(content_start_index.get())
|
||||
.unwrap_or_default();
|
||||
if let Some(index) = self.notebook_index {
|
||||
let content_start_cell = index.cell(content_start_index.get()).unwrap_or_default();
|
||||
while start_index < content_start_index {
|
||||
if jupyter_index.cell(start_index.get()).unwrap_or_default() == content_start_cell {
|
||||
if index.cell(start_index.get()).unwrap_or_default() == content_start_cell {
|
||||
break;
|
||||
}
|
||||
start_index = start_index.saturating_add(1);
|
||||
@@ -213,14 +211,12 @@ impl Display for MessageCodeFrame<'_> {
|
||||
.saturating_add(2)
|
||||
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
|
||||
|
||||
// If we're working on a jupyter notebook, skip the lines which are
|
||||
// If we're working with a Jupyter Notebook, skip the lines which are
|
||||
// outside of the cell containing the diagnostic.
|
||||
if let Some(jupyter_index) = self.jupyter_index {
|
||||
let content_end_cell = jupyter_index
|
||||
.cell(content_end_index.get())
|
||||
.unwrap_or_default();
|
||||
if let Some(index) = self.notebook_index {
|
||||
let content_end_cell = index.cell(content_end_index.get()).unwrap_or_default();
|
||||
while end_index > content_end_index {
|
||||
if jupyter_index.cell(end_index.get()).unwrap_or_default() == content_end_cell {
|
||||
if index.cell(end_index.get()).unwrap_or_default() == content_end_cell {
|
||||
break;
|
||||
}
|
||||
end_index = end_index.saturating_sub(1);
|
||||
@@ -256,10 +252,10 @@ impl Display for MessageCodeFrame<'_> {
|
||||
title: None,
|
||||
slices: vec![Slice {
|
||||
source: &source.text,
|
||||
line_start: self.jupyter_index.map_or_else(
|
||||
line_start: self.notebook_index.map_or_else(
|
||||
|| start_index.get(),
|
||||
|jupyter_index| {
|
||||
jupyter_index
|
||||
|notebook_index| {
|
||||
notebook_index
|
||||
.cell_row(start_index.get())
|
||||
.unwrap_or_default() as usize
|
||||
},
|
||||
|
||||
@@ -9,12 +9,16 @@ use crate::codes::RuleCodePrefix;
|
||||
use crate::codes::RuleIter;
|
||||
use crate::registry::{Linter, Rule, RuleNamespace};
|
||||
use crate::rule_redirects::get_redirect;
|
||||
use crate::settings::types::PreviewMode;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum RuleSelector {
|
||||
/// Select all stable rules.
|
||||
/// Select all rules (includes rules in preview if enabled)
|
||||
All,
|
||||
/// Select all nursery rules.
|
||||
/// Category to select all rules in preview (includes legacy nursery rules)
|
||||
Preview,
|
||||
/// Legacy category to select all rules in the "nursery" which predated preview mode
|
||||
#[deprecated(note = "Use `RuleSelector::Preview` for new rules instead")]
|
||||
Nursery,
|
||||
/// Legacy category to select both the `mccabe` and `flake8-comprehensions` linters
|
||||
/// via a single selector.
|
||||
@@ -29,6 +33,11 @@ pub enum RuleSelector {
|
||||
prefix: RuleCodePrefix,
|
||||
redirected_from: Option<&'static str>,
|
||||
},
|
||||
/// Select an individual rule with a given prefix.
|
||||
Rule {
|
||||
prefix: RuleCodePrefix,
|
||||
redirected_from: Option<&'static str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Linter> for RuleSelector {
|
||||
@@ -43,7 +52,9 @@ impl FromStr for RuleSelector {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"ALL" => Ok(Self::All),
|
||||
#[allow(deprecated)]
|
||||
"NURSERY" => Ok(Self::Nursery),
|
||||
"PREVIEW" => Ok(Self::Preview),
|
||||
"C" => Ok(Self::C),
|
||||
"T" => Ok(Self::T),
|
||||
_ => {
|
||||
@@ -59,16 +70,43 @@ impl FromStr for RuleSelector {
|
||||
return Ok(Self::Linter(linter));
|
||||
}
|
||||
|
||||
Ok(Self::Prefix {
|
||||
prefix: RuleCodePrefix::parse(&linter, code)
|
||||
.map_err(|_| ParseError::Unknown(s.to_string()))?,
|
||||
redirected_from,
|
||||
})
|
||||
// Does the selector select a single rule?
|
||||
let prefix = RuleCodePrefix::parse(&linter, code)
|
||||
.map_err(|_| ParseError::Unknown(s.to_string()))?;
|
||||
|
||||
if is_single_rule_selector(&prefix) {
|
||||
Ok(Self::Rule {
|
||||
prefix,
|
||||
redirected_from,
|
||||
})
|
||||
} else {
|
||||
Ok(Self::Prefix {
|
||||
prefix,
|
||||
redirected_from,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the [`RuleCodePrefix`] matches a single rule exactly
|
||||
/// (e.g., `E225`, as opposed to `E2`).
|
||||
pub(crate) fn is_single_rule_selector(prefix: &RuleCodePrefix) -> bool {
|
||||
let mut rules = prefix.rules();
|
||||
|
||||
// The selector must match a single rule.
|
||||
let Some(rule) = rules.next() else {
|
||||
return false;
|
||||
};
|
||||
if rules.next().is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The rule must match the selector exactly.
|
||||
rule.noqa_code().suffix() == prefix.short_code()
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ParseError {
|
||||
#[error("Unknown rule selector: `{0}`")]
|
||||
@@ -81,10 +119,12 @@ impl RuleSelector {
|
||||
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
|
||||
match self {
|
||||
RuleSelector::All => ("", "ALL"),
|
||||
#[allow(deprecated)]
|
||||
RuleSelector::Nursery => ("", "NURSERY"),
|
||||
RuleSelector::Preview => ("", "PREVIEW"),
|
||||
RuleSelector::C => ("", "C"),
|
||||
RuleSelector::T => ("", "T"),
|
||||
RuleSelector::Prefix { prefix, .. } => {
|
||||
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
|
||||
(prefix.linter().common_prefix(), prefix.short_code())
|
||||
}
|
||||
RuleSelector::Linter(l) => (l.common_prefix(), ""),
|
||||
@@ -135,27 +175,19 @@ impl Visitor<'_> for SelectorVisitor {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RuleCodePrefix> for RuleSelector {
|
||||
fn from(prefix: RuleCodePrefix) -> Self {
|
||||
Self::Prefix {
|
||||
prefix,
|
||||
redirected_from: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for &RuleSelector {
|
||||
type IntoIter = RuleSelectorIter;
|
||||
type Item = Rule;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
impl RuleSelector {
|
||||
/// Return all matching rules, regardless of whether they're in preview.
|
||||
pub fn all_rules(&self) -> impl Iterator<Item = Rule> + '_ {
|
||||
match self {
|
||||
RuleSelector::All => {
|
||||
RuleSelectorIter::All(Rule::iter().filter(|rule| !rule.is_nursery()))
|
||||
}
|
||||
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
|
||||
|
||||
#[allow(deprecated)]
|
||||
RuleSelector::Nursery => {
|
||||
RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery))
|
||||
}
|
||||
RuleSelector::Preview => RuleSelectorIter::Nursery(
|
||||
Rule::iter().filter(|rule| rule.is_preview() || rule.is_nursery()),
|
||||
),
|
||||
RuleSelector::C => RuleSelectorIter::Chain(
|
||||
Linter::Flake8Comprehensions
|
||||
.rules()
|
||||
@@ -167,13 +199,28 @@ impl IntoIterator for &RuleSelector {
|
||||
.chain(Linter::Flake8Print.rules()),
|
||||
),
|
||||
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.rules()),
|
||||
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.clone().rules()),
|
||||
RuleSelector::Prefix { prefix, .. } | RuleSelector::Rule { prefix, .. } => {
|
||||
RuleSelectorIter::Vec(prefix.clone().rules())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns rules matching the selector, taking into account whether preview mode is enabled.
|
||||
pub fn rules(&self, preview: PreviewMode) -> impl Iterator<Item = Rule> + '_ {
|
||||
#[allow(deprecated)]
|
||||
self.all_rules().filter(move |rule| {
|
||||
// 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())
|
||||
// Enabling preview includes all preview or nursery rules
|
||||
|| preview.is_enabled()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RuleSelectorIter {
|
||||
All(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
|
||||
All(RuleIter),
|
||||
Nursery(std::iter::Filter<RuleIter, fn(&Rule) -> bool>),
|
||||
Chain(std::iter::Chain<std::vec::IntoIter<Rule>, std::vec::IntoIter<Rule>>),
|
||||
Vec(std::vec::IntoIter<Rule>),
|
||||
@@ -192,18 +239,6 @@ impl Iterator for RuleSelectorIter {
|
||||
}
|
||||
}
|
||||
|
||||
/// A const alternative to the `impl From<RuleCodePrefix> for RuleSelector`
|
||||
/// to let us keep the fields of [`RuleSelector`] private.
|
||||
// Note that Rust doesn't yet support `impl const From<RuleCodePrefix> for
|
||||
// RuleSelector` (see https://github.com/rust-lang/rust/issues/67792).
|
||||
// TODO(martin): Remove once RuleSelector is an enum with Linter & Rule variants
|
||||
pub(crate) const fn prefix_to_selector(prefix: RuleCodePrefix) -> RuleSelector {
|
||||
RuleSelector::Prefix {
|
||||
prefix,
|
||||
redirected_from: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
mod schema {
|
||||
use itertools::Itertools;
|
||||
@@ -266,18 +301,20 @@ impl RuleSelector {
|
||||
pub fn specificity(&self) -> Specificity {
|
||||
match self {
|
||||
RuleSelector::All => Specificity::All,
|
||||
RuleSelector::Preview => Specificity::All,
|
||||
#[allow(deprecated)]
|
||||
RuleSelector::Nursery => Specificity::All,
|
||||
RuleSelector::T => Specificity::LinterGroup,
|
||||
RuleSelector::C => Specificity::LinterGroup,
|
||||
RuleSelector::Linter(..) => Specificity::Linter,
|
||||
RuleSelector::Rule { .. } => Specificity::Rule,
|
||||
RuleSelector::Prefix { prefix, .. } => {
|
||||
let prefix: &'static str = prefix.short_code();
|
||||
match prefix.len() {
|
||||
1 => Specificity::Code1Char,
|
||||
2 => Specificity::Code2Chars,
|
||||
3 => Specificity::Code3Chars,
|
||||
4 => Specificity::Code4Chars,
|
||||
5 => Specificity::Code5Chars,
|
||||
1 => Specificity::Prefix1Char,
|
||||
2 => Specificity::Prefix2Chars,
|
||||
3 => Specificity::Prefix3Chars,
|
||||
4 => Specificity::Prefix4Chars,
|
||||
_ => panic!("RuleSelector::specificity doesn't yet support codes with so many characters"),
|
||||
}
|
||||
}
|
||||
@@ -285,16 +322,24 @@ impl RuleSelector {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
|
||||
#[derive(EnumIter, PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Debug)]
|
||||
pub enum Specificity {
|
||||
/// The specificity when selecting all rules (e.g., `--select ALL`).
|
||||
All,
|
||||
/// The specificity when selecting a legacy linter group (e.g., `--select C` or `--select T`).
|
||||
LinterGroup,
|
||||
/// The specificity when selecting a linter (e.g., `--select PLE` or `--select UP`).
|
||||
Linter,
|
||||
Code1Char,
|
||||
Code2Chars,
|
||||
Code3Chars,
|
||||
Code4Chars,
|
||||
Code5Chars,
|
||||
/// The specificity when selecting via a rule prefix with a one-character code (e.g., `--select PLE1`).
|
||||
Prefix1Char,
|
||||
/// The specificity when selecting via a rule prefix with a two-character code (e.g., `--select PLE12`).
|
||||
Prefix2Chars,
|
||||
/// The specificity when selecting via a rule prefix with a three-character code (e.g., `--select PLE123`).
|
||||
Prefix3Chars,
|
||||
/// The specificity when selecting via a rule prefix with a four-character code (e.g., `--select PLE1234`).
|
||||
Prefix4Chars,
|
||||
/// The specificity when selecting an individual rule (e.g., `--select PLE1205`).
|
||||
Rule,
|
||||
}
|
||||
|
||||
#[cfg(feature = "clap")]
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,24 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for duplicate union members.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Duplicate union members are redundant and should be removed.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// foo: str | str
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// foo: str
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `typing.Union`](https://docs.python.org/3/library/typing.html#typing.Union)
|
||||
#[violation]
|
||||
pub struct DuplicateUnionMember {
|
||||
duplicate_name: String,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,18 +2,12 @@ use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::Stmt;
|
||||
use ruff_python_semantic::analyze::visibility;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for any misspelled dunder name method and for any method
|
||||
/// defined with `_..._` that's not one of the pre-defined methods
|
||||
///
|
||||
/// The pre-defined methods encompass all of Python's standard dunder
|
||||
/// methods.
|
||||
///
|
||||
/// Note this includes all methods starting and ending with at least
|
||||
/// one underscore to detect mistakes.
|
||||
/// Checks for misspelled and unknown dunder names in method definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Misspelled dunder name methods may cause your code to not function
|
||||
@@ -24,6 +18,10 @@ use crate::checkers::ast::Checker;
|
||||
/// that diverges from standard Python dunder methods could potentially
|
||||
/// confuse someone reading the code.
|
||||
///
|
||||
/// This rule will detect all methods starting and ending with at least
|
||||
/// one underscore (e.g., `_str_`), but ignores known dunder methods (like
|
||||
/// `__init__`), as well as methods that are marked with `@override`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Foo:
|
||||
@@ -62,6 +60,9 @@ pub(crate) fn bad_dunder_method_name(checker: &mut Checker, class_body: &[Stmt])
|
||||
method.name.starts_with('_') && method.name.ends_with('_')
|
||||
})
|
||||
{
|
||||
if visibility::is_override(&method.decorator_list, checker.semantic()) {
|
||||
continue;
|
||||
}
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
BadDunderMethodName {
|
||||
name: method.name.to_string(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/pylint/mod.rs
|
||||
---
|
||||
bad_dunder_method_name.py:2:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
|
||||
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
|
||||
|
|
||||
1 | class Apples:
|
||||
2 | def _init_(self): # [bad-dunder-name]
|
||||
4 | class Apples:
|
||||
5 | def _init_(self): # [bad-dunder-name]
|
||||
| ^^^^^^ PLW3201
|
||||
3 | pass
|
||||
6 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `__hello__`. (bad-dunder-name)
|
||||
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__hello__`. (bad-dunder-name)
|
||||
|
|
||||
3 | pass
|
||||
4 |
|
||||
5 | def __hello__(self): # [bad-dunder-name]
|
||||
6 | pass
|
||||
7 |
|
||||
8 | def __hello__(self): # [bad-dunder-name]
|
||||
| ^^^^^^^^^ PLW3201
|
||||
6 | print("hello")
|
||||
9 | print("hello")
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__init_`. (bad-dunder-name)
|
||||
bad_dunder_method_name.py:11:9: PLW3201 Bad or misspelled dunder method name `__init_`. (bad-dunder-name)
|
||||
|
|
||||
6 | print("hello")
|
||||
7 |
|
||||
8 | def __init_(self): # [bad-dunder-name]
|
||||
9 | print("hello")
|
||||
10 |
|
||||
11 | def __init_(self): # [bad-dunder-name]
|
||||
| ^^^^^^^ PLW3201
|
||||
9 | # author likely unintentionally misspelled the correct init dunder.
|
||||
10 | pass
|
||||
12 | # author likely unintentionally misspelled the correct init dunder.
|
||||
13 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:12:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
|
||||
bad_dunder_method_name.py:15:9: PLW3201 Bad or misspelled dunder method name `_init_`. (bad-dunder-name)
|
||||
|
|
||||
10 | pass
|
||||
11 |
|
||||
12 | def _init_(self): # [bad-dunder-name]
|
||||
13 | pass
|
||||
14 |
|
||||
15 | def _init_(self): # [bad-dunder-name]
|
||||
| ^^^^^^ PLW3201
|
||||
13 | # author likely unintentionally misspelled the correct init dunder.
|
||||
14 | pass
|
||||
16 | # author likely unintentionally misspelled the correct init dunder.
|
||||
17 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:16:9: PLW3201 Bad or misspelled dunder method name `___neg__`. (bad-dunder-name)
|
||||
bad_dunder_method_name.py:19:9: PLW3201 Bad or misspelled dunder method name `___neg__`. (bad-dunder-name)
|
||||
|
|
||||
14 | pass
|
||||
15 |
|
||||
16 | def ___neg__(self): # [bad-dunder-name]
|
||||
17 | pass
|
||||
18 |
|
||||
19 | def ___neg__(self): # [bad-dunder-name]
|
||||
| ^^^^^^^^ PLW3201
|
||||
17 | # author likely accidentally added an additional `_`
|
||||
18 | pass
|
||||
20 | # author likely accidentally added an additional `_`
|
||||
21 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:20:9: PLW3201 Bad or misspelled dunder method name `__inv__`. (bad-dunder-name)
|
||||
bad_dunder_method_name.py:23:9: PLW3201 Bad or misspelled dunder method name `__inv__`. (bad-dunder-name)
|
||||
|
|
||||
18 | pass
|
||||
19 |
|
||||
20 | def __inv__(self): # [bad-dunder-name]
|
||||
21 | pass
|
||||
22 |
|
||||
23 | def __inv__(self): # [bad-dunder-name]
|
||||
| ^^^^^^^ PLW3201
|
||||
21 | # author likely meant to call the invert dunder method
|
||||
22 | pass
|
||||
24 | # author likely meant to call the invert dunder method
|
||||
25 | pass
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -60,66 +60,147 @@ impl AlwaysAutofixableViolation for OutdatedVersionBlock {
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a `BigInt` to a `u32`. If the number is negative, it will return 0.
|
||||
fn bigint_to_u32(number: &BigInt) -> u32 {
|
||||
let the_number = number.to_u32_digits();
|
||||
match the_number.0 {
|
||||
Sign::Minus | Sign::NoSign => 0,
|
||||
Sign::Plus => *the_number.1.first().unwrap(),
|
||||
}
|
||||
}
|
||||
/// UP036
|
||||
pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
for branch in if_elif_branches(stmt_if) {
|
||||
let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
range: _,
|
||||
}) = &branch.test
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
/// Gets the version from the tuple
|
||||
fn extract_version(elts: &[Expr]) -> Vec<u32> {
|
||||
let mut version: Vec<u32> = vec![];
|
||||
for elt in elts {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(item),
|
||||
..
|
||||
}) = &elt
|
||||
let ([op], [comparison]) = (ops.as_slice(), comparators.as_slice()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(left)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["sys", "version_info"]))
|
||||
{
|
||||
let number = bigint_to_u32(item);
|
||||
version.push(number);
|
||||
} else {
|
||||
return version;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
version
|
||||
}
|
||||
|
||||
/// Returns true if the `if_version` is less than the `PythonVersion`
|
||||
fn compare_version(if_version: &[u32], py_version: PythonVersion, or_equal: bool) -> bool {
|
||||
let mut if_version_iter = if_version.iter();
|
||||
if let Some(if_major) = if_version_iter.next() {
|
||||
let (py_major, py_minor) = py_version.as_tuple();
|
||||
match if_major.cmp(&py_major) {
|
||||
Ordering::Less => true,
|
||||
Ordering::Equal => {
|
||||
if let Some(if_minor) = if_version_iter.next() {
|
||||
// Check the if_minor number (the minor version).
|
||||
if or_equal {
|
||||
*if_minor <= py_minor
|
||||
} else {
|
||||
*if_minor < py_minor
|
||||
match comparison {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => match op {
|
||||
CmpOp::Lt | CmpOp::LtE => {
|
||||
let version = extract_version(elts);
|
||||
let target = checker.settings.target_version;
|
||||
if compare_version(&version, target, op == &CmpOp::LtE) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_always_false_branch(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
CmpOp::Gt | CmpOp::GtE => {
|
||||
let version = extract_version(elts);
|
||||
let target = checker.settings.target_version;
|
||||
if compare_version(&version, target, op == &CmpOp::GtE) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(number),
|
||||
..
|
||||
}) => {
|
||||
if op == &CmpOp::Eq {
|
||||
match bigint_to_u32(number) {
|
||||
2 => {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) =
|
||||
fix_always_false_branch(checker, stmt_if, &branch)
|
||||
{
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
3 => {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_always_true_branch(checker, stmt_if, &branch)
|
||||
{
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// Assume Python 3.0.
|
||||
true
|
||||
}
|
||||
}
|
||||
Ordering::Greater => false,
|
||||
_ => (),
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// For fixing, we have 4 cases:
|
||||
/// * Just an if: delete as statement (insert pass in parent if required)
|
||||
/// * If with an elif: delete, turn elif into if
|
||||
/// * If with an else: delete, dedent else
|
||||
/// * Just an elif: delete, `elif False` can always be removed
|
||||
fn fix_py2_block(checker: &Checker, stmt_if: &StmtIf, branch: &IfElifBranch) -> Option<Fix> {
|
||||
/// Returns true if the `target_version` is always less than the [`PythonVersion`].
|
||||
fn compare_version(target_version: &[u32], py_version: PythonVersion, or_equal: bool) -> bool {
|
||||
let mut target_version_iter = target_version.iter();
|
||||
|
||||
let Some(if_major) = target_version_iter.next() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let (py_major, py_minor) = py_version.as_tuple();
|
||||
|
||||
match if_major.cmp(&py_major) {
|
||||
Ordering::Less => true,
|
||||
Ordering::Greater => false,
|
||||
Ordering::Equal => {
|
||||
let Some(if_minor) = target_version_iter.next() else {
|
||||
return true;
|
||||
};
|
||||
if or_equal {
|
||||
// Ex) `sys.version_info <= 3.8`. If Python 3.8 is the minimum supported version,
|
||||
// the condition won't always evaluate to `false`, so we want to return `false`.
|
||||
*if_minor < py_minor
|
||||
} else {
|
||||
// Ex) `sys.version_info < 3.8`. If Python 3.8 is the minimum supported version,
|
||||
// the condition _will_ always evaluate to `false`, so we want to return `true`.
|
||||
*if_minor <= py_minor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fix a branch that is known to always evaluate to `false`.
|
||||
///
|
||||
/// For example, when running with a minimum supported version of Python 3.8, the following branch
|
||||
/// would be considered redundant:
|
||||
/// ```python
|
||||
/// if sys.version_info < (3, 7): ...
|
||||
/// ```
|
||||
///
|
||||
/// In this case, the fix would involve removing the branch; however, there are multiple cases to
|
||||
/// consider. For example, if the `if` has an `else`, then the `if` should be removed, and the
|
||||
/// `else` should be inlined at the top level.
|
||||
fn fix_always_false_branch(
|
||||
checker: &Checker,
|
||||
stmt_if: &StmtIf,
|
||||
branch: &IfElifBranch,
|
||||
) -> Option<Fix> {
|
||||
match branch.kind {
|
||||
BranchKind::If => match stmt_if.elif_else_clauses.first() {
|
||||
// If we have a lone `if`, delete as statement (insert pass in parent if required)
|
||||
@@ -210,8 +291,18 @@ fn fix_py2_block(checker: &Checker, stmt_if: &StmtIf, branch: &IfElifBranch) ->
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`Stmt::If`], removing the `else` block.
|
||||
fn fix_py3_block(checker: &mut Checker, stmt_if: &StmtIf, branch: &IfElifBranch) -> Option<Fix> {
|
||||
/// Fix a branch that is known to always evaluate to `true`.
|
||||
///
|
||||
/// For example, when running with a minimum supported version of Python 3.8, the following branch
|
||||
/// would be considered redundant, as it's known to always evaluate to `true`:
|
||||
/// ```python
|
||||
/// if sys.version_info >= (3, 8): ...
|
||||
/// ```
|
||||
fn fix_always_true_branch(
|
||||
checker: &mut Checker,
|
||||
stmt_if: &StmtIf,
|
||||
branch: &IfElifBranch,
|
||||
) -> Option<Fix> {
|
||||
match branch.kind {
|
||||
BranchKind::If => {
|
||||
// If the first statement is an `if`, use the body of this statement, and ignore
|
||||
@@ -262,85 +353,31 @@ fn fix_py3_block(checker: &mut Checker, stmt_if: &StmtIf, branch: &IfElifBranch)
|
||||
}
|
||||
}
|
||||
|
||||
/// UP036
|
||||
pub(crate) fn outdated_version_block(checker: &mut Checker, stmt_if: &StmtIf) {
|
||||
for branch in if_elif_branches(stmt_if) {
|
||||
let Expr::Compare(ast::ExprCompare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
range: _,
|
||||
}) = &branch.test
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
/// Converts a `BigInt` to a `u32`. If the number is negative, it will return 0.
|
||||
fn bigint_to_u32(number: &BigInt) -> u32 {
|
||||
let the_number = number.to_u32_digits();
|
||||
match the_number.0 {
|
||||
Sign::Minus | Sign::NoSign => 0,
|
||||
Sign::Plus => *the_number.1.first().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
let ([op], [comparison]) = (ops.as_slice(), comparators.as_slice()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_call_path(left)
|
||||
.is_some_and(|call_path| matches!(call_path.as_slice(), ["sys", "version_info"]))
|
||||
/// Gets the version from the tuple
|
||||
fn extract_version(elts: &[Expr]) -> Vec<u32> {
|
||||
let mut version: Vec<u32> = vec![];
|
||||
for elt in elts {
|
||||
if let Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(item),
|
||||
..
|
||||
}) = &elt
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
match comparison {
|
||||
Expr::Tuple(ast::ExprTuple { elts, .. }) => {
|
||||
let version = extract_version(elts);
|
||||
let target = checker.settings.target_version;
|
||||
if op == &CmpOp::Lt || op == &CmpOp::LtE {
|
||||
if compare_version(&version, target, op == &CmpOp::LtE) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_py2_block(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
} else if op == &CmpOp::Gt || op == &CmpOp::GtE {
|
||||
if compare_version(&version, target, op == &CmpOp::GtE) {
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_py3_block(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Constant(ast::ExprConstant {
|
||||
value: Constant::Int(number),
|
||||
..
|
||||
}) => {
|
||||
let version_number = bigint_to_u32(number);
|
||||
if version_number == 2 && op == &CmpOp::Eq {
|
||||
let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_py2_block(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
} else if version_number == 3 && op == &CmpOp::Eq {
|
||||
let mut diagnostic = Diagnostic::new(OutdatedVersionBlock, branch.test.range());
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(fix) = fix_py3_block(checker, stmt_if, &branch) {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
let number = bigint_to_u32(item);
|
||||
version.push(number);
|
||||
} else {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
version
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -355,8 +392,8 @@ mod tests {
|
||||
#[test_case(PythonVersion::Py37, &[3, 0], true, true; "compare-3.0-whole")]
|
||||
#[test_case(PythonVersion::Py37, &[3, 1], true, true; "compare-3.1")]
|
||||
#[test_case(PythonVersion::Py37, &[3, 5], true, true; "compare-3.5")]
|
||||
#[test_case(PythonVersion::Py37, &[3, 7], true, true; "compare-3.7")]
|
||||
#[test_case(PythonVersion::Py37, &[3, 7], false, false; "compare-3.7-not-equal")]
|
||||
#[test_case(PythonVersion::Py37, &[3, 7], true, false; "compare-3.7")]
|
||||
#[test_case(PythonVersion::Py37, &[3, 7], false, true; "compare-3.7-not-equal")]
|
||||
#[test_case(PythonVersion::Py37, &[3, 8], false , false; "compare-3.8")]
|
||||
#[test_case(PythonVersion::Py310, &[3,9], true, true; "compare-3.9")]
|
||||
#[test_case(PythonVersion::Py310, &[3, 11], true, false; "compare-3.11")]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -662,5 +662,27 @@ UP036_0.py:179:8: UP036 [*] Version block is outdated for minimum Python version
|
||||
178 178 | if True:
|
||||
179 |- if sys.version_info > (3, 0): \
|
||||
180 179 | expected_error = []
|
||||
181 180 |
|
||||
182 181 | if sys.version_info < (3,12):
|
||||
|
||||
UP036_0.py:182:4: UP036 [*] Version block is outdated for minimum Python version
|
||||
|
|
||||
180 | expected_error = []
|
||||
181 |
|
||||
182 | if sys.version_info < (3,12):
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP036
|
||||
183 | print("py3")
|
||||
|
|
||||
= help: Remove outdated version block
|
||||
|
||||
ℹ Suggested fix
|
||||
179 179 | if sys.version_info > (3, 0): \
|
||||
180 180 | expected_error = []
|
||||
181 181 |
|
||||
182 |-if sys.version_info < (3,12):
|
||||
183 |- print("py3")
|
||||
184 182 |
|
||||
185 183 | if sys.version_info <= (3,12):
|
||||
186 184 | print("py3")
|
||||
|
||||
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::Settings;
|
||||
use crate::codes::{self, RuleCodePrefix};
|
||||
use crate::line_width::{LineLength, TabSize};
|
||||
use crate::registry::Linter;
|
||||
use crate::rule_selector::{prefix_to_selector, RuleSelector};
|
||||
use crate::rule_selector::RuleSelector;
|
||||
use crate::rules::{
|
||||
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
|
||||
flake8_copyright, flake8_errmsg, flake8_gettext, flake8_implicit_str_concat,
|
||||
@@ -20,7 +20,10 @@ use crate::rules::{
|
||||
use crate::settings::types::FilePatternSet;
|
||||
|
||||
pub const PREFIXES: &[RuleSelector] = &[
|
||||
prefix_to_selector(RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E)),
|
||||
RuleSelector::Prefix {
|
||||
prefix: RuleCodePrefix::Pycodestyle(codes::Pycodestyle::E),
|
||||
redirected_from: None,
|
||||
},
|
||||
RuleSelector::Linter(Linter::Pyflakes),
|
||||
];
|
||||
|
||||
@@ -70,7 +73,10 @@ pub static INCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
rules: PREFIXES.iter().flat_map(IntoIterator::into_iter).collect(),
|
||||
rules: PREFIXES
|
||||
.iter()
|
||||
.flat_map(|selector| selector.rules(PreviewMode::default()))
|
||||
.collect(),
|
||||
allowed_confusables: FxHashSet::from_iter([]),
|
||||
builtins: vec![],
|
||||
dummy_variable_rgx: DUMMY_VARIABLE_RGX.clone(),
|
||||
|
||||
@@ -194,7 +194,8 @@ pub struct PerFileIgnore {
|
||||
|
||||
impl PerFileIgnore {
|
||||
pub fn new(pattern: String, prefixes: &[RuleSelector], project_root: Option<&Path>) -> Self {
|
||||
let rules: RuleSet = prefixes.iter().flat_map(IntoIterator::into_iter).collect();
|
||||
// Rules in preview are included here even if preview mode is disabled; it's safe to ignore disabled rules
|
||||
let rules: RuleSet = prefixes.iter().flat_map(RuleSelector::all_rules).collect();
|
||||
let path = Path::new(&pattern);
|
||||
let absolute = match project_root {
|
||||
Some(project_root) => fs::normalize_path_to(path, project_root),
|
||||
|
||||
@@ -297,7 +297,7 @@ pub(crate) fn print_jupyter_messages(
|
||||
messages,
|
||||
&EmitterContext::new(&FxHashMap::from_iter([(
|
||||
path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
notebook.clone(),
|
||||
notebook.index().clone(),
|
||||
)])),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -79,7 +79,7 @@ fn benchmark_default_rules(criterion: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn benchmark_all_rules(criterion: &mut Criterion) {
|
||||
let mut rules: RuleTable = RuleSelector::All.into_iter().collect();
|
||||
let mut rules: RuleTable = RuleSelector::All.all_rules().collect();
|
||||
|
||||
// Disable IO based rules because it is a source of flakiness
|
||||
rules.disable(Rule::ShebangMissingExecutableFile);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_cli"
|
||||
version = "0.0.288"
|
||||
version = "0.0.289"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -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" }
|
||||
|
||||
@@ -75,6 +75,7 @@ colored = { workspace = true, features = ["no-color"]}
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
insta-cmd = { version = "0.4.0" }
|
||||
tempfile = "3.6.0"
|
||||
test-case = { workspace = true }
|
||||
ureq = { version = "2.6.2", features = [] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
|
||||
@@ -116,7 +116,7 @@ pub struct CheckCommand {
|
||||
#[arg(long, value_enum)]
|
||||
pub target_version: Option<PythonVersion>,
|
||||
/// Enable preview mode; checks will include unstable rules and fixes.
|
||||
#[arg(long, overrides_with("no_preview"), hide = true)]
|
||||
#[arg(long, overrides_with("no_preview"))]
|
||||
preview: bool,
|
||||
#[clap(long, overrides_with("preview"), hide = true)]
|
||||
no_preview: bool,
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::sync::Mutex;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff::message::Message;
|
||||
@@ -15,6 +16,7 @@ use ruff::settings::Settings;
|
||||
use ruff::warn_user;
|
||||
use ruff_cache::{CacheKey, CacheKeyHasher};
|
||||
use ruff_diagnostics::{DiagnosticKind, Fix};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
@@ -193,6 +195,7 @@ impl Cache {
|
||||
key: T,
|
||||
messages: &[Message],
|
||||
imports: &ImportMap,
|
||||
notebook_index: Option<&NotebookIndex>,
|
||||
) {
|
||||
let source = if let Some(msg) = messages.first() {
|
||||
msg.file.source_text().to_owned()
|
||||
@@ -226,6 +229,7 @@ impl Cache {
|
||||
imports: imports.clone(),
|
||||
messages,
|
||||
source,
|
||||
notebook_index: notebook_index.cloned(),
|
||||
};
|
||||
self.new_files.lock().unwrap().insert(path, file);
|
||||
}
|
||||
@@ -263,6 +267,8 @@ pub(crate) struct FileCache {
|
||||
///
|
||||
/// This will be empty if `messages` is empty.
|
||||
source: String,
|
||||
/// Notebook index if this file is a Jupyter Notebook.
|
||||
notebook_index: Option<NotebookIndex>,
|
||||
}
|
||||
|
||||
impl FileCache {
|
||||
@@ -283,7 +289,12 @@ impl FileCache {
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
Diagnostics::new(messages, self.imports.clone())
|
||||
let notebook_indexes = if let Some(notebook_index) = self.notebook_index.as_ref() {
|
||||
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
|
||||
} else {
|
||||
FxHashMap::default()
|
||||
};
|
||||
Diagnostics::new(messages, self.imports.clone(), notebook_indexes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,16 +361,19 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
|
||||
#[test]
|
||||
fn same_results() {
|
||||
use test_case::test_case;
|
||||
|
||||
#[test_case("../ruff/resources/test/fixtures", "ruff_tests/cache_same_results_ruff"; "ruff_fixtures")]
|
||||
#[test_case("../ruff_notebook/resources/test/fixtures", "ruff_tests/cache_same_results_ruff_notebook"; "ruff_notebook_fixtures")]
|
||||
fn same_results(package_root: &str, cache_dir_path: &str) {
|
||||
let mut cache_dir = temp_dir();
|
||||
cache_dir.push("ruff_tests/cache_same_results");
|
||||
cache_dir.push(cache_dir_path);
|
||||
let _ = fs::remove_dir_all(&cache_dir);
|
||||
cache::init(&cache_dir).unwrap();
|
||||
|
||||
let settings = AllSettings::default();
|
||||
|
||||
let package_root = fs::canonicalize("../ruff/resources/test/fixtures").unwrap();
|
||||
let package_root = fs::canonicalize(package_root).unwrap();
|
||||
let cache = Cache::open(&cache_dir, package_root.clone(), &settings.lib);
|
||||
assert_eq!(cache.new_files.lock().unwrap().len(), 0);
|
||||
|
||||
@@ -444,9 +458,6 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Not stored in the cache.
|
||||
expected_diagnostics.notebooks.clear();
|
||||
got_diagnostics.notebooks.clear();
|
||||
assert_eq!(expected_diagnostics, got_diagnostics);
|
||||
}
|
||||
|
||||
@@ -614,6 +625,7 @@ mod tests {
|
||||
imports: ImportMap::new(),
|
||||
messages: Vec::new(),
|
||||
source: String::new(),
|
||||
notebook_index: None,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use itertools::Itertools;
|
||||
use log::{debug, error, warn};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use rayon::prelude::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff::message::Message;
|
||||
use ruff::registry::Rule;
|
||||
@@ -156,6 +157,7 @@ pub(crate) fn check(
|
||||
TextSize::default(),
|
||||
)],
|
||||
ImportMap::default(),
|
||||
FxHashMap::default(),
|
||||
)
|
||||
} else {
|
||||
warn!(
|
||||
|
||||
@@ -73,12 +73,14 @@ pub(crate) fn format(
|
||||
return None;
|
||||
};
|
||||
|
||||
let preview = match pyproject_config.settings.lib.preview {
|
||||
let resolved_settings = resolver.resolve(path, &pyproject_config);
|
||||
|
||||
let preview = match resolved_settings.preview {
|
||||
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
|
||||
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
|
||||
};
|
||||
let line_length = resolved_settings.line_length;
|
||||
|
||||
let line_length = resolver.resolve(path, &pyproject_config).line_length;
|
||||
let options = PyFormatOptions::from_source_type(source_type)
|
||||
.with_line_width(LineWidth::from(NonZeroU16::from(line_length)))
|
||||
.with_preview(preview);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::io::{stdout, Write};
|
||||
use std::num::NonZeroU16;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::warn;
|
||||
use ruff::settings::types::PreviewMode;
|
||||
use ruff_formatter::LineWidth;
|
||||
|
||||
use ruff_python_formatter::{format_module, PyFormatOptions};
|
||||
use ruff_workspace::resolver::python_file_at_path;
|
||||
@@ -35,9 +38,19 @@ pub(crate) fn format_stdin(cli: &FormatArguments, overrides: &Overrides) -> Resu
|
||||
|
||||
// Format the file.
|
||||
let path = cli.stdin_filename.as_deref();
|
||||
|
||||
let preview = match pyproject_config.settings.lib.preview {
|
||||
PreviewMode::Enabled => ruff_python_formatter::PreviewMode::Enabled,
|
||||
PreviewMode::Disabled => ruff_python_formatter::PreviewMode::Disabled,
|
||||
};
|
||||
let line_length = pyproject_config.settings.lib.line_length;
|
||||
|
||||
let options = path
|
||||
.map(PyFormatOptions::from_extension)
|
||||
.unwrap_or_default();
|
||||
.unwrap_or_default()
|
||||
.with_line_width(LineWidth::from(NonZeroU16::from(line_length)))
|
||||
.with_preview(preview);
|
||||
|
||||
match format_source(path, options, mode) {
|
||||
Ok(result) => match mode {
|
||||
FormatMode::Write => Ok(ExitStatus::Success),
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Explanation<'a> {
|
||||
message_formats: &'a [&'a str],
|
||||
autofix: String,
|
||||
explanation: Option<&'a str>,
|
||||
nursery: bool,
|
||||
preview: bool,
|
||||
}
|
||||
|
||||
impl<'a> Explanation<'a> {
|
||||
@@ -35,7 +35,7 @@ impl<'a> Explanation<'a> {
|
||||
message_formats: rule.message_formats(),
|
||||
autofix,
|
||||
explanation: rule.explanation(),
|
||||
nursery: rule.is_nursery(),
|
||||
preview: rule.is_preview(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,13 +58,10 @@ fn format_rule_text(rule: Rule) -> String {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
if rule.is_nursery() {
|
||||
output.push_str(&format!(
|
||||
r#"This rule is part of the **nursery**, a collection of newer lints that are
|
||||
still under development. As such, it must be enabled by explicitly selecting
|
||||
{}."#,
|
||||
rule.noqa_code()
|
||||
));
|
||||
if rule.is_preview() {
|
||||
output.push_str(
|
||||
r#"This rule is in preview and is not stable. The `--preview` flag is required for use."#,
|
||||
);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ use ruff::source_kind::SourceKind;
|
||||
use ruff::{fs, IOError, SyntaxError};
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::{Cell, Notebook, NotebookError};
|
||||
use ruff_notebook::{Cell, Notebook, NotebookError, NotebookIndex};
|
||||
use ruff_python_ast::imports::ImportMap;
|
||||
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
|
||||
use ruff_source_file::{LineIndex, SourceCode, SourceFileBuilder};
|
||||
@@ -64,16 +64,20 @@ pub(crate) struct Diagnostics {
|
||||
pub(crate) messages: Vec<Message>,
|
||||
pub(crate) fixed: FxHashMap<String, FixTable>,
|
||||
pub(crate) imports: ImportMap,
|
||||
pub(crate) notebooks: FxHashMap<String, Notebook>,
|
||||
pub(crate) notebook_indexes: FxHashMap<String, NotebookIndex>,
|
||||
}
|
||||
|
||||
impl Diagnostics {
|
||||
pub(crate) fn new(messages: Vec<Message>, imports: ImportMap) -> Self {
|
||||
pub(crate) fn new(
|
||||
messages: Vec<Message>,
|
||||
imports: ImportMap,
|
||||
notebook_indexes: FxHashMap<String, NotebookIndex>,
|
||||
) -> Self {
|
||||
Self {
|
||||
messages,
|
||||
fixed: FxHashMap::default(),
|
||||
imports,
|
||||
notebooks: FxHashMap::default(),
|
||||
notebook_indexes,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +98,7 @@ impl Diagnostics {
|
||||
TextSize::default(),
|
||||
)],
|
||||
ImportMap::default(),
|
||||
FxHashMap::default(),
|
||||
)
|
||||
} else {
|
||||
match path {
|
||||
@@ -130,7 +135,7 @@ impl AddAssign for Diagnostics {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.notebooks.extend(other.notebooks);
|
||||
self.notebook_indexes.extend(other.notebook_indexes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -341,7 +346,13 @@ pub(crate) fn lint_path(
|
||||
if let Some((cache, relative_path, key)) = caching {
|
||||
// We don't cache parsing errors.
|
||||
if parse_error.is_none() {
|
||||
cache.update(relative_path.to_owned(), key, &messages, &imports);
|
||||
cache.update(
|
||||
relative_path.to_owned(),
|
||||
key,
|
||||
&messages,
|
||||
&imports,
|
||||
source_kind.as_ipy_notebook().map(Notebook::index),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,12 +370,13 @@ pub(crate) fn lint_path(
|
||||
);
|
||||
}
|
||||
|
||||
let notebooks = if let SourceKind::IpyNotebook(notebook) = source_kind {
|
||||
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = source_kind {
|
||||
FxHashMap::from_iter([(
|
||||
path.to_str()
|
||||
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?
|
||||
.to_string(),
|
||||
notebook,
|
||||
// Index needs to be computed always to store in cache.
|
||||
notebook.index().clone(),
|
||||
)])
|
||||
} else {
|
||||
FxHashMap::default()
|
||||
@@ -374,7 +386,7 @@ pub(crate) fn lint_path(
|
||||
messages,
|
||||
fixed: FxHashMap::from_iter([(fs::relativize_path(path), fixed)]),
|
||||
imports,
|
||||
notebooks,
|
||||
notebook_indexes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -498,7 +510,7 @@ pub(crate) fn lint_stdin(
|
||||
fixed,
|
||||
)]),
|
||||
imports,
|
||||
notebooks: FxHashMap::default(),
|
||||
notebook_indexes: FxHashMap::default(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ impl Printer {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let context = EmitterContext::new(&diagnostics.notebooks);
|
||||
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
||||
|
||||
match self.format {
|
||||
SerializationFormat::Json => {
|
||||
@@ -364,7 +364,7 @@ impl Printer {
|
||||
writeln!(writer)?;
|
||||
}
|
||||
|
||||
let context = EmitterContext::new(&diagnostics.notebooks);
|
||||
let context = EmitterContext::new(&diagnostics.notebook_indexes);
|
||||
TextEmitter::default()
|
||||
.with_show_fix_status(show_fix_status(self.autofix_level))
|
||||
.with_show_source(self.flags.intersects(Flags::SHOW_SOURCE))
|
||||
|
||||
@@ -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 -----
|
||||
"###);
|
||||
}
|
||||
|
||||
@@ -43,13 +43,10 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
if rule.is_nursery() {
|
||||
output.push_str(&format!(
|
||||
r#"This rule is part of the **nursery**, a collection of newer lints that are
|
||||
still under development. As such, it must be enabled by explicitly selecting
|
||||
{}."#,
|
||||
rule.noqa_code()
|
||||
));
|
||||
if rule.is_preview() {
|
||||
output.push_str(
|
||||
r#"This rule is in preview and is not stable. The `--preview` flag is required for use."#,
|
||||
);
|
||||
output.push('\n');
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ use ruff::upstream_categories::UpstreamCategoryAndPrefix;
|
||||
use ruff_diagnostics::AutofixKind;
|
||||
use ruff_workspace::options::Options;
|
||||
|
||||
const FIX_SYMBOL: &str = "🛠";
|
||||
const NURSERY_SYMBOL: &str = "🌅";
|
||||
const FIX_SYMBOL: &str = "🛠️";
|
||||
const PREVIEW_SYMBOL: &str = "🧪";
|
||||
|
||||
fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>, linter: &Linter) {
|
||||
table_out.push_str("| Code | Name | Message | |");
|
||||
@@ -25,12 +25,12 @@ fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>,
|
||||
}
|
||||
AutofixKind::None => format!("<span style='opacity: 0.1'>{FIX_SYMBOL}</span>"),
|
||||
};
|
||||
let nursery_token = if rule.is_nursery() {
|
||||
format!("<span style='opacity: 1'>{NURSERY_SYMBOL}</span>")
|
||||
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'>{NURSERY_SYMBOL}</span>")
|
||||
format!("<span style='opacity: 0.1'>{PREVIEW_SYMBOL}</span>")
|
||||
};
|
||||
let status_token = format!("{fix_token} {nursery_token}");
|
||||
let status_token = format!("{fix_token} {preview_token}");
|
||||
|
||||
let rule_name = rule.as_ref();
|
||||
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn generate() -> String {
|
||||
table_out.push('\n');
|
||||
|
||||
table_out.push_str(&format!(
|
||||
"The {NURSERY_SYMBOL} emoji indicates that a rule is part of the [\"nursery\"](../faq/#what-is-the-nursery)."
|
||||
"The {PREVIEW_SYMBOL} emoji indicates that a rule in [\"preview\"](../faq/#what-is-preview)."
|
||||
));
|
||||
table_out.push('\n');
|
||||
table_out.push('\n');
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use syn::{
|
||||
Ident, ItemFn, LitStr, Pat, Path, Stmt, Token,
|
||||
};
|
||||
|
||||
use crate::rule_code_prefix::{get_prefix_ident, if_all_same, is_nursery};
|
||||
use crate::rule_code_prefix::{get_prefix_ident, if_all_same};
|
||||
|
||||
/// A rule entry in the big match statement such a
|
||||
/// `(Pycodestyle, "E112") => (RuleGroup::Nursery, rules::pycodestyle::rules::logical_lines::NoIndentedBlock),`
|
||||
@@ -113,9 +113,23 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
|
||||
Self::#linter(linter)
|
||||
}
|
||||
}
|
||||
|
||||
// Rust doesn't yet support `impl const From<RuleCodePrefix> for RuleSelector`
|
||||
// See https://github.com/rust-lang/rust/issues/67792
|
||||
impl From<#linter> for crate::rule_selector::RuleSelector {
|
||||
fn from(linter: #linter) -> Self {
|
||||
Self::Prefix{prefix: RuleCodePrefix::#linter(linter), redirected_from: None}
|
||||
let prefix = RuleCodePrefix::#linter(linter);
|
||||
if is_single_rule_selector(&prefix) {
|
||||
Self::Rule {
|
||||
prefix,
|
||||
redirected_from: None,
|
||||
}
|
||||
} else {
|
||||
Self::Prefix {
|
||||
prefix,
|
||||
redirected_from: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -156,7 +170,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
|
||||
|
||||
output.extend(quote! {
|
||||
impl #linter {
|
||||
pub fn rules(self) -> ::std::vec::IntoIter<Rule> {
|
||||
pub fn rules(&self) -> ::std::vec::IntoIter<Rule> {
|
||||
match self { #prefix_into_iter_match_arms }
|
||||
}
|
||||
}
|
||||
@@ -172,7 +186,7 @@ pub(crate) fn map_codes(func: &ItemFn) -> syn::Result<TokenStream> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rules(self) -> ::std::vec::IntoIter<Rule> {
|
||||
pub fn rules(&self) -> ::std::vec::IntoIter<Rule> {
|
||||
match self {
|
||||
#(RuleCodePrefix::#linter_idents(prefix) => prefix.clone().rules(),)*
|
||||
}
|
||||
@@ -195,26 +209,12 @@ fn rules_by_prefix(
|
||||
// TODO(charlie): Why do we do this here _and_ in `rule_code_prefix::expand`?
|
||||
let mut rules_by_prefix = BTreeMap::new();
|
||||
|
||||
for (code, rule) in rules {
|
||||
// Nursery rules have to be explicitly selected, so we ignore them when looking at
|
||||
// prefix-level selectors (e.g., `--select SIM10`), but add the rule itself under
|
||||
// its fully-qualified code (e.g., `--select SIM101`).
|
||||
if is_nursery(&rule.group) {
|
||||
rules_by_prefix.insert(code.clone(), vec![(rule.path.clone(), rule.attrs.clone())]);
|
||||
continue;
|
||||
}
|
||||
|
||||
for code in rules.keys() {
|
||||
for i in 1..=code.len() {
|
||||
let prefix = code[..i].to_string();
|
||||
let rules: Vec<_> = rules
|
||||
.iter()
|
||||
.filter_map(|(code, rule)| {
|
||||
// Nursery rules have to be explicitly selected, so we ignore them when
|
||||
// looking at prefixes.
|
||||
if is_nursery(&rule.group) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if code.starts_with(&prefix) {
|
||||
Some((rule.path.clone(), rule.attrs.clone()))
|
||||
} else {
|
||||
@@ -311,6 +311,11 @@ See also https://github.com/astral-sh/ruff/issues/2186.
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_preview(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Preview)
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub fn is_nursery(&self) -> bool {
|
||||
matches!(self.group(), RuleGroup::Nursery)
|
||||
}
|
||||
@@ -336,12 +341,10 @@ fn generate_iter_impl(
|
||||
let mut linter_rules_match_arms = quote!();
|
||||
let mut linter_all_rules_match_arms = quote!();
|
||||
for (linter, map) in linter_to_rules {
|
||||
let rule_paths = map.values().filter(|rule| !is_nursery(&rule.group)).map(
|
||||
|Rule { attrs, path, .. }| {
|
||||
let rule_name = path.segments.last().unwrap();
|
||||
quote!(#(#attrs)* Rule::#rule_name)
|
||||
},
|
||||
);
|
||||
let rule_paths = map.values().map(|Rule { attrs, path, .. }| {
|
||||
let rule_name = path.segments.last().unwrap();
|
||||
quote!(#(#attrs)* Rule::#rule_name)
|
||||
});
|
||||
linter_rules_match_arms.extend(quote! {
|
||||
Linter::#linter => vec![#(#rule_paths,)*].into_iter(),
|
||||
});
|
||||
|
||||
@@ -12,22 +12,14 @@ pub(crate) fn expand<'a>(
|
||||
let mut prefix_to_codes: BTreeMap<String, BTreeSet<String>> = BTreeMap::default();
|
||||
let mut code_to_attributes: BTreeMap<String, &[Attribute]> = BTreeMap::default();
|
||||
|
||||
for (variant, group, attr) in variants {
|
||||
for (variant, .., attr) in variants {
|
||||
let code_str = variant.to_string();
|
||||
// Nursery rules have to be explicitly selected, so we ignore them when looking at prefixes.
|
||||
if is_nursery(group) {
|
||||
for i in 1..=code_str.len() {
|
||||
let prefix = code_str[..i].to_string();
|
||||
prefix_to_codes
|
||||
.entry(code_str.clone())
|
||||
.entry(prefix)
|
||||
.or_default()
|
||||
.insert(code_str.clone());
|
||||
} else {
|
||||
for i in 1..=code_str.len() {
|
||||
let prefix = code_str[..i].to_string();
|
||||
prefix_to_codes
|
||||
.entry(prefix)
|
||||
.or_default()
|
||||
.insert(code_str.clone());
|
||||
}
|
||||
}
|
||||
|
||||
code_to_attributes.insert(code_str, attr);
|
||||
@@ -125,14 +117,3 @@ pub(crate) fn get_prefix_ident(prefix: &str) -> Ident {
|
||||
};
|
||||
Ident::new(&prefix, Span::call_site())
|
||||
}
|
||||
|
||||
/// Returns true if the given group is the "nursery" group.
|
||||
pub(crate) fn is_nursery(group: &Path) -> bool {
|
||||
let group = group
|
||||
.segments
|
||||
.iter()
|
||||
.map(|segment| segment.ident.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("::");
|
||||
group == "RuleGroup::Nursery"
|
||||
}
|
||||
|
||||
@@ -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,8 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Jupyter Notebook indexing table
|
||||
///
|
||||
/// When we lint a jupyter notebook, we have to translate the row/column based on
|
||||
/// [`ruff_text_size::TextSize`] to jupyter notebook cell/row/column.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct NotebookIndex {
|
||||
/// Enter a row (1-based), get back the cell (1-based)
|
||||
pub(super) row_to_cell: Vec<u32>,
|
||||
|
||||
@@ -19,6 +19,7 @@ ruff_text_size = { path = "../ruff_text_size" }
|
||||
|
||||
bitflags = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
num-bigint = { workspace = true }
|
||||
num-traits = { workspace = true }
|
||||
|
||||
@@ -207,6 +207,8 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
|
||||
range: _,
|
||||
}) => {
|
||||
any_over_expr(call_func, func)
|
||||
// Note that this is the evaluation order but not necessarily the declaration order
|
||||
// (e.g. for `f(*args, a=2, *args2, **kwargs)` it's not)
|
||||
|| args.iter().any(|expr| any_over_expr(expr, func))
|
||||
|| keywords
|
||||
.iter()
|
||||
@@ -347,6 +349,8 @@ pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool {
|
||||
decorator_list,
|
||||
..
|
||||
}) => {
|
||||
// Note that e.g. `class A(*args, a=2, *args2, **kwargs): pass` is a valid class
|
||||
// definition
|
||||
arguments
|
||||
.as_deref()
|
||||
.is_some_and(|Arguments { args, keywords, .. }| {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::visitor::preorder::PreorderVisitor;
|
||||
use crate::{
|
||||
self as ast, Alias, Arguments, Comprehension, Decorator, ExceptHandler, Expr, Keyword,
|
||||
MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern, PatternArguments,
|
||||
PatternKeyword, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
|
||||
TypeParams, WithItem,
|
||||
self as ast, Alias, ArgOrKeyword, Arguments, Comprehension, Decorator, ExceptHandler, Expr,
|
||||
Keyword, MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern,
|
||||
PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
|
||||
TypeParamTypeVarTuple, TypeParams, WithItem,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::ptr::NonNull;
|
||||
@@ -3549,18 +3549,11 @@ impl AstNode for Arguments {
|
||||
where
|
||||
V: PreorderVisitor<'a> + ?Sized,
|
||||
{
|
||||
let ast::Arguments {
|
||||
range: _,
|
||||
args,
|
||||
keywords,
|
||||
} = self;
|
||||
|
||||
for arg in args {
|
||||
visitor.visit_expr(arg);
|
||||
}
|
||||
|
||||
for keyword in keywords {
|
||||
visitor.visit_keyword(keyword);
|
||||
for arg_or_keyword in self.arguments_source_order() {
|
||||
match arg_or_keyword {
|
||||
ArgOrKeyword::Arg(arg) => visitor.visit_expr(arg),
|
||||
ArgOrKeyword::Keyword(keyword) => visitor.visit_keyword(keyword),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
||||
use itertools::Itertools;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
@@ -2177,6 +2178,34 @@ pub struct Arguments {
|
||||
pub keywords: Vec<Keyword>,
|
||||
}
|
||||
|
||||
/// An entry in the argument list of a function call.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ArgOrKeyword<'a> {
|
||||
Arg(&'a Expr),
|
||||
Keyword(&'a Keyword),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Expr> for ArgOrKeyword<'a> {
|
||||
fn from(arg: &'a Expr) -> Self {
|
||||
Self::Arg(arg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Keyword> for ArgOrKeyword<'a> {
|
||||
fn from(keyword: &'a Keyword) -> Self {
|
||||
Self::Keyword(keyword)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ArgOrKeyword<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::Arg(arg) => arg.range(),
|
||||
Self::Keyword(keyword) => keyword.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
/// Return the number of positional and keyword arguments.
|
||||
pub fn len(&self) -> usize {
|
||||
@@ -2212,6 +2241,46 @@ impl Arguments {
|
||||
.map(|keyword| &keyword.value)
|
||||
.or_else(|| self.find_positional(position))
|
||||
}
|
||||
|
||||
/// Return the positional and keyword arguments in the order of declaration.
|
||||
///
|
||||
/// Positional arguments are generally before keyword arguments, but star arguments are an
|
||||
/// exception:
|
||||
/// ```python
|
||||
/// class A(*args, a=2, *args2, **kwargs):
|
||||
/// pass
|
||||
///
|
||||
/// f(*args, a=2, *args2, **kwargs)
|
||||
/// ```
|
||||
/// where `*args` and `args2` are `args` while `a=1` and `kwargs` are `keywords`.
|
||||
///
|
||||
/// If you would just chain `args` and `keywords` the call would get reordered which we don't
|
||||
/// want. This function instead "merge sorts" them into the correct order.
|
||||
///
|
||||
/// Note that the order of evaluation is always first `args`, then `keywords`:
|
||||
/// ```python
|
||||
/// def f(*args, **kwargs):
|
||||
/// pass
|
||||
///
|
||||
/// def g(x):
|
||||
/// print(x)
|
||||
/// return x
|
||||
///
|
||||
///
|
||||
/// f(*g([1]), a=g(2), *g([3]), **g({"4": 5}))
|
||||
/// ```
|
||||
/// Output:
|
||||
/// ```text
|
||||
/// [1]
|
||||
/// [3]
|
||||
/// 2
|
||||
/// {'4': 5}
|
||||
/// ```
|
||||
pub fn arguments_source_order(&self) -> impl Iterator<Item = ArgOrKeyword<'_>> {
|
||||
let args = self.args.iter().map(ArgOrKeyword::Arg);
|
||||
let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword);
|
||||
args.merge_by(keywords, |left, right| left.start() < right.start())
|
||||
}
|
||||
}
|
||||
|
||||
/// An AST node used to represent a sequence of type parameters.
|
||||
|
||||
@@ -573,6 +573,9 @@ pub fn walk_format_spec<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, format_spe
|
||||
}
|
||||
|
||||
pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) {
|
||||
// Note that the there might be keywords before the last arg, e.g. in
|
||||
// f(*args, a=2, *args2, **kwargs)`, but we follow Python in evaluating first `args` and then
|
||||
// `keywords`. See also [Arguments::arguments_source_order`].
|
||||
for arg in &arguments.args {
|
||||
visitor.visit_expr(arg);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_python_ast::{
|
||||
self as ast, Alias, BoolOp, CmpOp, Comprehension, Constant, ConversionFlag, DebugText,
|
||||
ExceptHandler, Expr, Identifier, MatchCase, Operator, Parameter, Parameters, Pattern, Stmt,
|
||||
Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem,
|
||||
self as ast, Alias, ArgOrKeyword, BoolOp, CmpOp, Comprehension, Constant, ConversionFlag,
|
||||
DebugText, ExceptHandler, Expr, Identifier, MatchCase, Operator, Parameter, Parameters,
|
||||
Pattern, Stmt, Suite, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
|
||||
WithItem,
|
||||
};
|
||||
use ruff_python_ast::{ParameterWithDefault, TypeParams};
|
||||
use ruff_python_literal::escape::{AsciiEscape, Escape, UnicodeEscape};
|
||||
@@ -265,19 +266,23 @@ impl<'a> Generator<'a> {
|
||||
if let Some(arguments) = arguments {
|
||||
self.p("(");
|
||||
let mut first = true;
|
||||
for base in &arguments.args {
|
||||
self.p_delim(&mut first, ", ");
|
||||
self.unparse_expr(base, precedence::MAX);
|
||||
}
|
||||
for keyword in &arguments.keywords {
|
||||
self.p_delim(&mut first, ", ");
|
||||
if let Some(arg) = &keyword.arg {
|
||||
self.p_id(arg);
|
||||
self.p("=");
|
||||
} else {
|
||||
self.p("**");
|
||||
for arg_or_keyword in arguments.arguments_source_order() {
|
||||
match arg_or_keyword {
|
||||
ArgOrKeyword::Arg(arg) => {
|
||||
self.p_delim(&mut first, ", ");
|
||||
self.unparse_expr(arg, precedence::MAX);
|
||||
}
|
||||
ArgOrKeyword::Keyword(keyword) => {
|
||||
self.p_delim(&mut first, ", ");
|
||||
if let Some(arg) = &keyword.arg {
|
||||
self.p_id(arg);
|
||||
self.p("=");
|
||||
} else {
|
||||
self.p("**");
|
||||
}
|
||||
self.unparse_expr(&keyword.value, precedence::MAX);
|
||||
}
|
||||
}
|
||||
self.unparse_expr(&keyword.value, precedence::MAX);
|
||||
}
|
||||
self.p(")");
|
||||
}
|
||||
@@ -1045,19 +1050,24 @@ impl<'a> Generator<'a> {
|
||||
self.unparse_comp(generators);
|
||||
} else {
|
||||
let mut first = true;
|
||||
for arg in &arguments.args {
|
||||
self.p_delim(&mut first, ", ");
|
||||
self.unparse_expr(arg, precedence::COMMA);
|
||||
}
|
||||
for kw in &arguments.keywords {
|
||||
self.p_delim(&mut first, ", ");
|
||||
if let Some(arg) = &kw.arg {
|
||||
self.p_id(arg);
|
||||
self.p("=");
|
||||
self.unparse_expr(&kw.value, precedence::COMMA);
|
||||
} else {
|
||||
self.p("**");
|
||||
self.unparse_expr(&kw.value, precedence::MAX);
|
||||
|
||||
for arg_or_keyword in arguments.arguments_source_order() {
|
||||
match arg_or_keyword {
|
||||
ArgOrKeyword::Arg(arg) => {
|
||||
self.p_delim(&mut first, ", ");
|
||||
self.unparse_expr(arg, precedence::COMMA);
|
||||
}
|
||||
ArgOrKeyword::Keyword(keyword) => {
|
||||
self.p_delim(&mut first, ", ");
|
||||
if let Some(arg) = &keyword.arg {
|
||||
self.p_id(arg);
|
||||
self.p("=");
|
||||
self.unparse_expr(&keyword.value, precedence::COMMA);
|
||||
} else {
|
||||
self.p("**");
|
||||
self.unparse_expr(&keyword.value, precedence::MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1649,6 +1659,11 @@ class Foo:
|
||||
assert_round_trip!(r#"type Foo[*Ts] = ..."#);
|
||||
assert_round_trip!(r#"type Foo[**P] = ..."#);
|
||||
assert_round_trip!(r#"type Foo[T, U, *Ts, **P] = ..."#);
|
||||
// https://github.com/astral-sh/ruff/issues/6498
|
||||
assert_round_trip!(r#"f(a=1, *args, **kwargs)"#);
|
||||
assert_round_trip!(r#"f(*args, a=1, **kwargs)"#);
|
||||
assert_round_trip!(r#"f(*args, a=1, *args2, **kwargs)"#);
|
||||
assert_round_trip!("class A(*args, a=2, *args2, **kwargs):\n pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
# Ruff Formatter
|
||||
|
||||
The Ruff formatter is an extremely fast Python code formatter that ships as part of the `ruff`
|
||||
CLI (as of Ruff v0.0.287).
|
||||
CLI (as of Ruff v0.0.289).
|
||||
|
||||
The formatter is currently in an **alpha** state. As such, it's not yet recommended for production
|
||||
use, but it _is_ ready for experimentation and testing. _We'd love to have your feedback._
|
||||
The formatter is currently in an **Alpha** state. The Alpha is primarily intended for
|
||||
experimentation: our focus is on collecting feedback that we can address prior to a production-ready
|
||||
Beta release later this year. (While we're using the formatter in production on our own projects,
|
||||
the CLI, configuration options, and code style may change arbitrarily between the Alpha and Beta.)
|
||||
|
||||
[_We'd love to hear your feedback._](https://github.com/astral-sh/ruff/discussions/7310)
|
||||
|
||||
## Goals
|
||||
|
||||
@@ -26,7 +30,7 @@ For details, see [Black compatibility](#black-compatibility).
|
||||
|
||||
## Getting started
|
||||
|
||||
The Ruff formatter shipped in an alpha state as part of Ruff v0.0.287.
|
||||
The Ruff formatter shipped in an Alpha state as part of Ruff v0.0.289.
|
||||
|
||||
### CLI
|
||||
|
||||
@@ -69,8 +73,7 @@ instead exiting with a non-zero status code if any files are not already formatt
|
||||
|
||||
### VS Code
|
||||
|
||||
As of `v2023.34.0`,
|
||||
the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff)
|
||||
As of `v2023.36.0`, the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff)
|
||||
ships with support for the Ruff formatter. To enable formatting capabilities, set the
|
||||
`ruff.enableExperimentalFormatter` setting to `true` in your `settings.json`, and mark the Ruff
|
||||
extension as your default Python formatter:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
@@ -320,6 +320,14 @@ rowuses = [(1 << j) | # column ordinal
|
||||
(1 << (n + 2*n-1 + i+j)) # NE-SW ordinal
|
||||
for j in rangen]
|
||||
|
||||
rowuses = [((1 << j) # column ordinal
|
||||
)|
|
||||
(
|
||||
# comment
|
||||
(1 << (n + i-j + n-1))) | # NW-SE ordinal
|
||||
(1 << (n + 2*n-1 + i+j)) # NE-SW ordinal
|
||||
for j in rangen]
|
||||
|
||||
skip_bytes = (
|
||||
header.timecnt * 5 # Transition times and types
|
||||
+ header.typecnt * 6 # Local time type records
|
||||
@@ -328,3 +336,56 @@ skip_bytes = (
|
||||
+ header.isstdcnt # Standard/wall indicators
|
||||
+ header.isutcnt # UT/local indicators
|
||||
)
|
||||
|
||||
|
||||
if (
|
||||
(1 + 2) # test
|
||||
or (3 + 4) # other
|
||||
or (4 + 5) # more
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
if (
|
||||
(1 and 2) # test
|
||||
+ (3 and 4) # other
|
||||
+ (4 and 5) # more
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
if (
|
||||
(1 + 2) # test
|
||||
< (3 + 4) # other
|
||||
> (4 + 5) # more
|
||||
):
|
||||
pass
|
||||
|
||||
z = (
|
||||
a
|
||||
+
|
||||
# a: extracts this comment
|
||||
(
|
||||
# b: and this comment
|
||||
(
|
||||
# c: formats it as part of the expression
|
||||
x and y
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
z = (
|
||||
(
|
||||
|
||||
(
|
||||
|
||||
x and y
|
||||
# a: formats it as part of the expression
|
||||
|
||||
)
|
||||
# b: extracts this comment
|
||||
|
||||
)
|
||||
# c: and this comment
|
||||
+ a
|
||||
)
|
||||
|
||||
@@ -169,3 +169,23 @@ c = (a
|
||||
# test trailing operator comment
|
||||
b
|
||||
)
|
||||
|
||||
c = ("a" "b" +
|
||||
# test leading binary comment
|
||||
"a" "b"
|
||||
)
|
||||
|
||||
(
|
||||
b + c + d +
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbb" +
|
||||
"cccccccccccccccccccccccccc"
|
||||
"dddddddddddddddddddddddddd"
|
||||
% aaaaaaaaaaaa
|
||||
+ x
|
||||
)
|
||||
|
||||
"a" "b" "c" + "d" "e" + "f" "g" + "h" "i" "j"
|
||||
class EC2REPATH:
|
||||
f.write ("Pathway name" + "\t" "Database Identifier" + "\t" "Source database" + "\n")
|
||||
|
||||
|
||||
@@ -102,3 +102,86 @@ def test():
|
||||
and {k.lower(): v for k, v in self.items()}
|
||||
== {k.lower(): v for k, v in other.items()}
|
||||
)
|
||||
|
||||
|
||||
|
||||
if "_continue" in request.POST or (
|
||||
# Redirecting after "Save as new".
|
||||
"_saveasnew" in request.POST
|
||||
and self.save_as_continue
|
||||
and self.has_change_permission(request, obj)
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
if False:
|
||||
if True:
|
||||
if (
|
||||
self.validate_max
|
||||
and self.total_form_count() - len(self.deleted_forms) > self.max_num
|
||||
) or self.management_form.cleaned_data[
|
||||
TOTAL_FORM_COUNT
|
||||
] > self.absolute_max:
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
if (
|
||||
reference_field_name is None
|
||||
or
|
||||
# Unspecified to_field(s).
|
||||
to_fields is None
|
||||
or
|
||||
# Reference to primary key.
|
||||
(
|
||||
None in to_fields
|
||||
and (reference_field is None or reference_field.primary_key)
|
||||
)
|
||||
or
|
||||
# Reference to field.
|
||||
reference_field_name in to_fields
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
field = opts.get_field(name)
|
||||
if (
|
||||
field.is_relation
|
||||
and
|
||||
# Generic foreign keys OR reverse relations
|
||||
((field.many_to_one and not field.related_model) or field.one_to_many)
|
||||
):
|
||||
pass
|
||||
|
||||
|
||||
if True:
|
||||
return (
|
||||
filtered.exists()
|
||||
and
|
||||
# It may happen that the object is deleted from the DB right after
|
||||
# this check, causing the subsequent UPDATE to return zero matching
|
||||
# rows. The same result can occur in some rare cases when the
|
||||
# database returns zero despite the UPDATE being executed
|
||||
# successfully (a row is matched and updated). In order to
|
||||
# distinguish these two cases, the object's existence in the
|
||||
# database is again checked for if the UPDATE query returns 0.
|
||||
(filtered._update(values) > 0 or filtered.exists())
|
||||
)
|
||||
|
||||
|
||||
if (self._proc is not None
|
||||
# has the child process finished?
|
||||
and self._returncode is None
|
||||
# the child process has finished, but the
|
||||
# transport hasn't been notified yet?
|
||||
and self._proc.poll() is None):
|
||||
pass
|
||||
|
||||
if (self._proc
|
||||
# has the child process finished?
|
||||
* self._returncode
|
||||
# the child process has finished, but the
|
||||
# transport hasn't been notified yet?
|
||||
+ self._proc.poll()):
|
||||
pass
|
||||
|
||||
@@ -242,3 +242,26 @@ f(x=(
|
||||
# comment
|
||||
1
|
||||
))
|
||||
|
||||
args = [2]
|
||||
args2 = [3]
|
||||
kwargs = {"4": 5}
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/6498
|
||||
f(a=1, *args, **kwargs)
|
||||
f(*args, a=1, **kwargs)
|
||||
f(*args, a=1, *args2, **kwargs)
|
||||
f( # a
|
||||
* # b
|
||||
args
|
||||
# c
|
||||
, # d
|
||||
a=1,
|
||||
# e
|
||||
* # f
|
||||
args2
|
||||
# g
|
||||
** # h
|
||||
kwargs,
|
||||
)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -38,7 +38,7 @@ pub(super) fn place_comment<'a>(
|
||||
/// ):
|
||||
/// ...
|
||||
/// ```
|
||||
/// The parentheses enclose `True`, but the range of `True`doesn't include the `# comment`.
|
||||
/// The parentheses enclose `True`, but the range of `True` doesn't include the `# comment`.
|
||||
///
|
||||
/// Default handling can get parenthesized comments wrong in a number of ways. For example, the
|
||||
/// comment here is marked (by default) as a trailing comment of `x`, when it should be a leading
|
||||
@@ -120,10 +120,8 @@ fn handle_parenthesized_comment<'a>(
|
||||
// For now, we _can_ assert, but to do so, we stop lexing when we hit a token that precedes an
|
||||
// identifier.
|
||||
if comment.line_position().is_end_of_line() {
|
||||
let tokenizer = SimpleTokenizer::new(
|
||||
locator.contents(),
|
||||
TextRange::new(preceding.end(), comment.start()),
|
||||
);
|
||||
let range = TextRange::new(preceding.end(), comment.start());
|
||||
let tokenizer = SimpleTokenizer::new(locator.contents(), range);
|
||||
if tokenizer
|
||||
.skip_trivia()
|
||||
.take_while(|token| {
|
||||
@@ -136,7 +134,7 @@ fn handle_parenthesized_comment<'a>(
|
||||
debug_assert!(
|
||||
!matches!(token.kind, SimpleTokenKind::Bogus),
|
||||
"Unexpected token between nodes: `{:?}`",
|
||||
locator.slice(TextRange::new(preceding.end(), comment.start()),)
|
||||
locator.slice(range)
|
||||
);
|
||||
|
||||
token.kind() == SimpleTokenKind::LParen
|
||||
@@ -145,10 +143,8 @@ fn handle_parenthesized_comment<'a>(
|
||||
return CommentPlacement::leading(following, comment);
|
||||
}
|
||||
} else {
|
||||
let tokenizer = SimpleTokenizer::new(
|
||||
locator.contents(),
|
||||
TextRange::new(comment.end(), following.start()),
|
||||
);
|
||||
let range = TextRange::new(comment.end(), following.start());
|
||||
let tokenizer = SimpleTokenizer::new(locator.contents(), range);
|
||||
if tokenizer
|
||||
.skip_trivia()
|
||||
.take_while(|token| {
|
||||
@@ -161,7 +157,7 @@ fn handle_parenthesized_comment<'a>(
|
||||
debug_assert!(
|
||||
!matches!(token.kind, SimpleTokenKind::Bogus),
|
||||
"Unexpected token between nodes: `{:?}`",
|
||||
locator.slice(TextRange::new(comment.end(), following.start()))
|
||||
locator.slice(range)
|
||||
);
|
||||
token.kind() == SimpleTokenKind::RParen
|
||||
})
|
||||
@@ -205,6 +201,9 @@ fn handle_enclosed_comment<'a>(
|
||||
locator,
|
||||
)
|
||||
}
|
||||
AnyNodeRef::ExprBoolOp(_) | AnyNodeRef::ExprCompare(_) => {
|
||||
handle_trailing_binary_like_comment(comment, locator)
|
||||
}
|
||||
AnyNodeRef::Keyword(keyword) => handle_keyword_comment(comment, keyword, locator),
|
||||
AnyNodeRef::PatternKeyword(pattern_keyword) => {
|
||||
handle_pattern_keyword_comment(comment, pattern_keyword, locator)
|
||||
@@ -836,6 +835,47 @@ fn handle_trailing_binary_expression_left_or_operator_comment<'a>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches comments between two bool or compare expression operands to the preceding operand if the comment is before the operator.
|
||||
///
|
||||
/// ```python
|
||||
/// a = (
|
||||
/// 5 > 3
|
||||
/// # trailing comment
|
||||
/// and 3 == 3
|
||||
/// )
|
||||
/// ```
|
||||
fn handle_trailing_binary_like_comment<'a>(
|
||||
comment: DecoratedComment<'a>,
|
||||
locator: &Locator,
|
||||
) -> CommentPlacement<'a> {
|
||||
debug_assert!(
|
||||
comment.enclosing_node().is_expr_bool_op() || comment.enclosing_node().is_expr_compare()
|
||||
);
|
||||
|
||||
// Only if there's a preceding node (in which case, the preceding node is `left` or middle node).
|
||||
let (Some(left_operand), Some(right_operand)) =
|
||||
(comment.preceding_node(), comment.following_node())
|
||||
else {
|
||||
return CommentPlacement::Default(comment);
|
||||
};
|
||||
|
||||
let between_operands_range = TextRange::new(left_operand.end(), right_operand.start());
|
||||
|
||||
let mut tokens = SimpleTokenizer::new(locator.contents(), between_operands_range)
|
||||
.skip_trivia()
|
||||
.skip_while(|token| token.kind == SimpleTokenKind::RParen);
|
||||
let operator_offset = tokens
|
||||
.next()
|
||||
.expect("Expected a token for the operator")
|
||||
.start();
|
||||
|
||||
if comment.end() < operator_offset {
|
||||
CommentPlacement::trailing(left_operand, comment)
|
||||
} else {
|
||||
CommentPlacement::Default(comment)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles own line comments on the module level before a class or function statement.
|
||||
/// A comment only becomes the leading comment of a class or function if it isn't separated by an empty
|
||||
/// line from the class. Comments that are separated by at least one empty line from the header of the
|
||||
|
||||
@@ -5,14 +5,18 @@ use smallvec::SmallVec;
|
||||
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::{
|
||||
Constant, Expr, ExprAttribute, ExprBinOp, ExprCompare, ExprConstant, ExprUnaryOp, UnaryOp,
|
||||
Constant, Expr, ExprAttribute, ExprBinOp, ExprBoolOp, ExprCompare, ExprConstant, ExprUnaryOp,
|
||||
UnaryOp,
|
||||
};
|
||||
use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::comments::{leading_comments, trailing_comments, Comments, SourceComment};
|
||||
use crate::expression::parentheses::{
|
||||
in_parentheses_only_group, in_parentheses_only_soft_line_break,
|
||||
in_parentheses_only_soft_line_break_or_space, is_expression_parenthesized,
|
||||
write_in_parentheses_only_group_end_tag, write_in_parentheses_only_group_start_tag,
|
||||
Parentheses,
|
||||
};
|
||||
use crate::expression::string::{AnyString, FormatString, StringLayout};
|
||||
use crate::expression::OperatorPrecedence;
|
||||
@@ -20,8 +24,9 @@ use crate::prelude::*;
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(super) enum BinaryLike<'a> {
|
||||
BinaryExpression(&'a ExprBinOp),
|
||||
CompareExpression(&'a ExprCompare),
|
||||
Binary(&'a ExprBinOp),
|
||||
Compare(&'a ExprCompare),
|
||||
Bool(&'a ExprBoolOp),
|
||||
}
|
||||
|
||||
impl<'a> BinaryLike<'a> {
|
||||
@@ -84,6 +89,54 @@ impl<'a> BinaryLike<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn recurse_bool<'a>(
|
||||
bool_expression: &'a ExprBoolOp,
|
||||
leading_comments: &'a [SourceComment],
|
||||
trailing_comments: &'a [SourceComment],
|
||||
comments: &'a Comments,
|
||||
source: &str,
|
||||
parts: &mut SmallVec<[OperandOrOperator<'a>; 8]>,
|
||||
) {
|
||||
parts.reserve(bool_expression.values.len() * 2 - 1);
|
||||
|
||||
if let Some((left, rest)) = bool_expression.values.split_first() {
|
||||
rec(
|
||||
Operand::Left {
|
||||
expression: left,
|
||||
leading_comments,
|
||||
},
|
||||
comments,
|
||||
source,
|
||||
parts,
|
||||
);
|
||||
|
||||
parts.push(OperandOrOperator::Operator(Operator {
|
||||
symbol: OperatorSymbol::Bool(bool_expression.op),
|
||||
trailing_comments: &[],
|
||||
}));
|
||||
|
||||
if let Some((right, middle)) = rest.split_last() {
|
||||
for expression in middle {
|
||||
rec(Operand::Middle { expression }, comments, source, parts);
|
||||
parts.push(OperandOrOperator::Operator(Operator {
|
||||
symbol: OperatorSymbol::Bool(bool_expression.op),
|
||||
trailing_comments: &[],
|
||||
}));
|
||||
}
|
||||
|
||||
rec(
|
||||
Operand::Right {
|
||||
expression: right,
|
||||
trailing_comments,
|
||||
},
|
||||
comments,
|
||||
source,
|
||||
parts,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recurse_binary<'a>(
|
||||
binary: &'a ExprBinOp,
|
||||
leading_comments: &'a [SourceComment],
|
||||
@@ -164,6 +217,26 @@ impl<'a> BinaryLike<'a> {
|
||||
parts,
|
||||
);
|
||||
}
|
||||
Expr::BoolOp(bool_op)
|
||||
if !is_expression_parenthesized(expression.into(), source) =>
|
||||
{
|
||||
let leading_comments = operand
|
||||
.leading_binary_comments()
|
||||
.unwrap_or_else(|| comments.leading(bool_op));
|
||||
|
||||
let trailing_comments = operand
|
||||
.trailing_binary_comments()
|
||||
.unwrap_or_else(|| comments.trailing(bool_op));
|
||||
|
||||
recurse_bool(
|
||||
bool_op,
|
||||
leading_comments,
|
||||
trailing_comments,
|
||||
comments,
|
||||
source,
|
||||
parts,
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
parts.push(OperandOrOperator::Operand(operand));
|
||||
}
|
||||
@@ -172,18 +245,25 @@ impl<'a> BinaryLike<'a> {
|
||||
|
||||
let mut parts = SmallVec::new();
|
||||
match self {
|
||||
BinaryLike::BinaryExpression(binary) => {
|
||||
BinaryLike::Binary(binary) => {
|
||||
// Leading and trailing comments are handled by the binary's ``FormatNodeRule` implementation.
|
||||
recurse_binary(binary, &[], &[], comments, source, &mut parts);
|
||||
}
|
||||
BinaryLike::CompareExpression(compare) => {
|
||||
BinaryLike::Compare(compare) => {
|
||||
// Leading and trailing comments are handled by the compare's ``FormatNodeRule` implementation.
|
||||
recurse_compare(compare, &[], &[], comments, source, &mut parts);
|
||||
}
|
||||
BinaryLike::Bool(bool) => {
|
||||
recurse_bool(bool, &[], &[], comments, source, &mut parts);
|
||||
}
|
||||
}
|
||||
|
||||
FlatBinaryExpression(parts)
|
||||
}
|
||||
|
||||
const fn is_bool_op(self) -> bool {
|
||||
matches!(self, BinaryLike::Bool(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||
@@ -191,6 +271,10 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||
let comments = f.context().comments().clone();
|
||||
let flat_binary = self.flatten(&comments, f.context().source());
|
||||
|
||||
if self.is_bool_op() {
|
||||
return in_parentheses_only_group(&&*flat_binary).fmt(f);
|
||||
}
|
||||
|
||||
let source = f.context().source();
|
||||
let mut string_operands = flat_binary
|
||||
.operands()
|
||||
@@ -233,44 +317,58 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||
// ^^^^^^ this part or ^^^^^^^ this part
|
||||
// ```
|
||||
if let Some(left_operator_index) = index.left_operator() {
|
||||
// Everything between the last implicit concatenated string and the left operator
|
||||
// right before the implicit concatenated string:
|
||||
// Handles the case where the left and right side of a binary expression are both
|
||||
// implicit concatenated strings. In this case, the left operator has already been written
|
||||
// by the preceding implicit concatenated string. It is only necessary to finish the group,
|
||||
// wrapping the soft line break and operator.
|
||||
//
|
||||
// ```python
|
||||
// a + b + "c" "d"
|
||||
// ^--- left_operator
|
||||
// ^^^^^-- left
|
||||
// "a" "b" + "c" "d"
|
||||
// ```
|
||||
let left =
|
||||
flat_binary.between_operators(last_operator_index, left_operator_index);
|
||||
let left_operator = &flat_binary[left_operator_index];
|
||||
|
||||
if let Some(leading) = left.first_operand().leading_binary_comments() {
|
||||
leading_comments(leading).fmt(f)?;
|
||||
}
|
||||
|
||||
// Write the left, the left operator, and the space before the right side
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
left,
|
||||
left.last_operand()
|
||||
.trailing_binary_comments()
|
||||
.map(trailing_comments),
|
||||
in_parentheses_only_soft_line_break_or_space(),
|
||||
left_operator,
|
||||
]
|
||||
)?;
|
||||
|
||||
// Finish the left-side group (the group was started before the loop or by the
|
||||
// previous iteration)
|
||||
write_in_parentheses_only_group_end_tag(f);
|
||||
|
||||
if operand.has_leading_comments(f.context().comments())
|
||||
|| left_operator.has_trailing_comments()
|
||||
{
|
||||
hard_line_break().fmt(f)?;
|
||||
if last_operator_index == Some(left_operator_index) {
|
||||
write_in_parentheses_only_group_end_tag(f);
|
||||
} else {
|
||||
space().fmt(f)?;
|
||||
// Everything between the last implicit concatenated string and the left operator
|
||||
// right before the implicit concatenated string:
|
||||
// ```python
|
||||
// a + b + "c" "d"
|
||||
// ^--- left_operator
|
||||
// ^^^^^-- left
|
||||
// ```
|
||||
let left = flat_binary
|
||||
.between_operators(last_operator_index, left_operator_index);
|
||||
let left_operator = &flat_binary[left_operator_index];
|
||||
|
||||
if let Some(leading) = left.first_operand().leading_binary_comments() {
|
||||
leading_comments(leading).fmt(f)?;
|
||||
}
|
||||
|
||||
// Write the left, the left operator, and the space before the right side
|
||||
write!(
|
||||
f,
|
||||
[
|
||||
left,
|
||||
left.last_operand()
|
||||
.trailing_binary_comments()
|
||||
.map(trailing_comments),
|
||||
in_parentheses_only_soft_line_break_or_space(),
|
||||
left_operator,
|
||||
]
|
||||
)?;
|
||||
|
||||
// Finish the left-side group (the group was started before the loop or by the
|
||||
// previous iteration)
|
||||
write_in_parentheses_only_group_end_tag(f);
|
||||
|
||||
if operand.has_unparenthesized_leading_comments(
|
||||
f.context().comments(),
|
||||
f.context().source(),
|
||||
) || left_operator.has_trailing_comments()
|
||||
{
|
||||
hard_line_break().fmt(f)?;
|
||||
} else {
|
||||
space().fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
write!(
|
||||
@@ -314,8 +412,11 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||
if let Some(right_operator) = flat_binary.get_operator(index.right_operator()) {
|
||||
write_in_parentheses_only_group_start_tag(f);
|
||||
let right_operand = &flat_binary[right_operator_index.right_operand()];
|
||||
let right_operand_has_leading_comments =
|
||||
right_operand.has_leading_comments(f.context().comments());
|
||||
let right_operand_has_leading_comments = right_operand
|
||||
.has_unparenthesized_leading_comments(
|
||||
f.context().comments(),
|
||||
f.context().source(),
|
||||
);
|
||||
|
||||
// Keep the operator on the same line if the right side has leading comments (and thus, breaks)
|
||||
if right_operand_has_leading_comments {
|
||||
@@ -326,7 +427,11 @@ impl Format<PyFormatContext<'_>> for BinaryLike<'_> {
|
||||
|
||||
right_operator.fmt(f)?;
|
||||
|
||||
if right_operand_has_leading_comments
|
||||
if (right_operand_has_leading_comments
|
||||
&& !is_expression_parenthesized(
|
||||
right_operand.expression().into(),
|
||||
f.context().source(),
|
||||
))
|
||||
|| right_operator.has_trailing_comments()
|
||||
{
|
||||
hard_line_break().fmt(f)?;
|
||||
@@ -540,7 +645,7 @@ impl Format<PyFormatContext<'_>> for FlatBinaryExpressionSlice<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext>) -> FormatResult<()> {
|
||||
// Single operand slice
|
||||
if let [OperandOrOperator::Operand(operand)] = &self.0 {
|
||||
return operand.expression().format().fmt(f);
|
||||
return operand.fmt(f);
|
||||
}
|
||||
|
||||
let mut last_operator: Option<OperatorIndex> = None;
|
||||
@@ -577,10 +682,11 @@ impl Format<PyFormatContext<'_>> for FlatBinaryExpressionSlice<'_> {
|
||||
operator_part.fmt(f)?;
|
||||
|
||||
// Format the operator on its own line if the right side has any leading comments.
|
||||
if right
|
||||
.first_operand()
|
||||
.has_leading_comments(f.context().comments())
|
||||
|| operator_part.has_trailing_comments()
|
||||
if operator_part.has_trailing_comments()
|
||||
|| right.first_operand().has_unparenthesized_leading_comments(
|
||||
f.context().comments(),
|
||||
f.context().source(),
|
||||
)
|
||||
{
|
||||
hard_line_break().fmt(f)?;
|
||||
} else if !is_pow {
|
||||
@@ -682,13 +788,33 @@ impl<'a> Operand<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_leading_comments(&self, comments: &Comments) -> bool {
|
||||
/// Returns `true` if the operand has any leading comments that are not parenthesized.
|
||||
fn has_unparenthesized_leading_comments(&self, comments: &Comments, source: &str) -> bool {
|
||||
match self {
|
||||
Operand::Left {
|
||||
leading_comments, ..
|
||||
} => !leading_comments.is_empty(),
|
||||
Operand::Middle { expression } | Operand::Right { expression, .. } => {
|
||||
comments.has_leading(*expression)
|
||||
let leading = comments.leading(*expression);
|
||||
if is_expression_parenthesized((*expression).into(), source) {
|
||||
leading.iter().any(|comment| {
|
||||
!comment.is_formatted()
|
||||
&& matches!(
|
||||
SimpleTokenizer::new(
|
||||
source,
|
||||
TextRange::new(comment.end(), expression.start()),
|
||||
)
|
||||
.skip_trivia()
|
||||
.next(),
|
||||
Some(SimpleToken {
|
||||
kind: SimpleTokenKind::LParen,
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
} else {
|
||||
!leading.is_empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -713,6 +839,146 @@ impl<'a> Operand<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for Operand<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
|
||||
let expression = self.expression();
|
||||
|
||||
return if is_expression_parenthesized(expression.into(), f.context().source()) {
|
||||
let comments = f.context().comments().clone();
|
||||
let expression_comments = comments.leading_dangling_trailing(expression);
|
||||
|
||||
// Format leading comments that are before the inner most `(` outside of the expression's parentheses.
|
||||
// ```python
|
||||
// z = (
|
||||
// a
|
||||
// +
|
||||
// # a: extracts this comment
|
||||
// (
|
||||
// # b: and this comment
|
||||
// (
|
||||
// # c: formats it as part of the expression
|
||||
// x and y
|
||||
// )
|
||||
// )
|
||||
// )
|
||||
// ```
|
||||
//
|
||||
// Gets formatted as
|
||||
// ```python
|
||||
// z = (
|
||||
// a
|
||||
// +
|
||||
// # a: extracts this comment
|
||||
// # b: and this comment
|
||||
// (
|
||||
// # c: formats it as part of the expression
|
||||
// x and y
|
||||
// )
|
||||
// )
|
||||
// ```
|
||||
let leading = expression_comments.leading;
|
||||
let leading_before_parentheses_end = leading
|
||||
.iter()
|
||||
.rposition(|comment| {
|
||||
comment.is_unformatted()
|
||||
&& matches!(
|
||||
SimpleTokenizer::new(
|
||||
f.context().source(),
|
||||
TextRange::new(comment.end(), expression.start()),
|
||||
)
|
||||
.skip_trivia()
|
||||
.next(),
|
||||
Some(SimpleToken {
|
||||
kind: SimpleTokenKind::LParen,
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
.map_or(0, |position| position + 1);
|
||||
|
||||
let leading_before_parentheses = &leading[..leading_before_parentheses_end];
|
||||
|
||||
// Format trailing comments that are outside of the inner most `)` outside of the parentheses.
|
||||
// ```python
|
||||
// z = (
|
||||
// (
|
||||
//
|
||||
// (
|
||||
//
|
||||
// x and y
|
||||
// # a: extracts this comment
|
||||
// )
|
||||
// # b: and this comment
|
||||
// )
|
||||
// # c: formats it as part of the expression
|
||||
// + a
|
||||
// )
|
||||
// ```
|
||||
// Gets formatted as
|
||||
// ```python
|
||||
// z = (
|
||||
// (
|
||||
// x and y
|
||||
// # a: extracts this comment
|
||||
// )
|
||||
// # b: and this comment
|
||||
// # c: formats it as part of the expression
|
||||
// + a
|
||||
// )
|
||||
// ```
|
||||
let trailing = expression_comments.trailing;
|
||||
|
||||
let trailing_after_parentheses_start = trailing
|
||||
.iter()
|
||||
.position(|comment| {
|
||||
comment.is_unformatted()
|
||||
&& matches!(
|
||||
SimpleTokenizer::new(
|
||||
f.context().source(),
|
||||
TextRange::new(expression.end(), comment.start()),
|
||||
)
|
||||
.skip_trivia()
|
||||
.next(),
|
||||
Some(SimpleToken {
|
||||
kind: SimpleTokenKind::RParen,
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
.unwrap_or(trailing.len());
|
||||
|
||||
let trailing_after_parentheses = &trailing[trailing_after_parentheses_start..];
|
||||
|
||||
// Mark the comment as formatted to avoid that the formatting of the expression
|
||||
// formats the trailing comment inside of the parentheses.
|
||||
for comment in trailing_after_parentheses {
|
||||
comment.mark_formatted();
|
||||
}
|
||||
|
||||
if !leading_before_parentheses.is_empty() {
|
||||
leading_comments(leading_before_parentheses).fmt(f)?;
|
||||
}
|
||||
|
||||
expression
|
||||
.format()
|
||||
.with_options(Parentheses::Always)
|
||||
.fmt(f)?;
|
||||
|
||||
for comment in trailing_after_parentheses {
|
||||
comment.mark_unformatted();
|
||||
}
|
||||
|
||||
if !trailing_after_parentheses.is_empty() {
|
||||
trailing_comments(trailing_after_parentheses).fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
expression.format().with_options(Parentheses::Never).fmt(f)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Operator<'a> {
|
||||
symbol: OperatorSymbol,
|
||||
@@ -739,6 +1005,7 @@ impl Format<PyFormatContext<'_>> for Operator<'_> {
|
||||
enum OperatorSymbol {
|
||||
Binary(ruff_python_ast::Operator),
|
||||
Comparator(ruff_python_ast::CmpOp),
|
||||
Bool(ruff_python_ast::BoolOp),
|
||||
}
|
||||
|
||||
impl OperatorSymbol {
|
||||
@@ -750,6 +1017,7 @@ impl OperatorSymbol {
|
||||
match self {
|
||||
OperatorSymbol::Binary(operator) => OperatorPrecedence::from(operator),
|
||||
OperatorSymbol::Comparator(_) => OperatorPrecedence::Comparator,
|
||||
OperatorSymbol::Bool(_) => OperatorPrecedence::BooleanOperation,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -759,6 +1027,7 @@ impl Format<PyFormatContext<'_>> for OperatorSymbol {
|
||||
match self {
|
||||
OperatorSymbol::Binary(operator) => operator.format().fmt(f),
|
||||
OperatorSymbol::Comparator(operator) => operator.format().fmt(f),
|
||||
OperatorSymbol::Bool(bool) => bool.format().fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub struct FormatExprBinOp;
|
||||
impl FormatNodeRule<ExprBinOp> for FormatExprBinOp {
|
||||
#[inline]
|
||||
fn fmt_fields(&self, item: &ExprBinOp, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
BinaryLike::BinaryExpression(item).fmt(f)
|
||||
BinaryLike::Binary(item).fmt(f)
|
||||
}
|
||||
|
||||
fn fmt_dangling_comments(
|
||||
|
||||
@@ -1,80 +1,18 @@
|
||||
use ruff_formatter::{write, FormatOwnedWithRule, FormatRefWithRule, FormatRuleWithOptions};
|
||||
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule};
|
||||
use ruff_python_ast::node::AnyNodeRef;
|
||||
use ruff_python_ast::{BoolOp, Expr, ExprBoolOp};
|
||||
use ruff_python_ast::{BoolOp, ExprBoolOp};
|
||||
|
||||
use crate::comments::leading_comments;
|
||||
use crate::expression::parentheses::{
|
||||
in_parentheses_only_group, in_parentheses_only_soft_line_break_or_space, NeedsParentheses,
|
||||
OptionalParentheses,
|
||||
};
|
||||
use crate::expression::binary_like::BinaryLike;
|
||||
use crate::expression::parentheses::{NeedsParentheses, OptionalParentheses};
|
||||
use crate::prelude::*;
|
||||
|
||||
use super::parentheses::is_expression_parenthesized;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprBoolOp {
|
||||
layout: BoolOpLayout,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone)]
|
||||
pub enum BoolOpLayout {
|
||||
#[default]
|
||||
Default,
|
||||
Chained,
|
||||
}
|
||||
|
||||
impl FormatRuleWithOptions<ExprBoolOp, PyFormatContext<'_>> for FormatExprBoolOp {
|
||||
type Options = BoolOpLayout;
|
||||
fn with_options(mut self, options: Self::Options) -> Self {
|
||||
self.layout = options;
|
||||
self
|
||||
}
|
||||
}
|
||||
pub struct FormatExprBoolOp;
|
||||
|
||||
impl FormatNodeRule<ExprBoolOp> for FormatExprBoolOp {
|
||||
#[inline]
|
||||
fn fmt_fields(&self, item: &ExprBoolOp, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let ExprBoolOp {
|
||||
range: _,
|
||||
op,
|
||||
values,
|
||||
} = item;
|
||||
|
||||
let inner = format_with(|f: &mut PyFormatter| {
|
||||
let mut values = values.iter();
|
||||
let comments = f.context().comments().clone();
|
||||
|
||||
let Some(first) = values.next() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
FormatValue { value: first }.fmt(f)?;
|
||||
|
||||
for value in values {
|
||||
let leading_value_comments = comments.leading(value);
|
||||
// Format the expressions leading comments **before** the operator
|
||||
if leading_value_comments.is_empty() {
|
||||
write!(f, [in_parentheses_only_soft_line_break_or_space()])?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
[hard_line_break(), leading_comments(leading_value_comments)]
|
||||
)?;
|
||||
}
|
||||
|
||||
write!(f, [op.format(), space()])?;
|
||||
|
||||
FormatValue { value }.fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
if matches!(self.layout, BoolOpLayout::Chained) {
|
||||
// Chained boolean operations should not be given a new group
|
||||
inner.fmt(f)
|
||||
} else {
|
||||
in_parentheses_only_group(&inner).fmt(f)
|
||||
}
|
||||
BinaryLike::Bool(item).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,24 +26,6 @@ impl NeedsParentheses for ExprBoolOp {
|
||||
}
|
||||
}
|
||||
|
||||
struct FormatValue<'a> {
|
||||
value: &'a Expr,
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatValue<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
match self.value {
|
||||
Expr::BoolOp(bool_op)
|
||||
if !is_expression_parenthesized(bool_op.into(), f.context().source()) =>
|
||||
{
|
||||
// Mark chained boolean operations e.g. `x and y or z` and avoid creating a new group
|
||||
write!(f, [bool_op.format().with_options(BoolOpLayout::Chained)])
|
||||
}
|
||||
_ => write!(f, [in_parentheses_only_group(&self.value.format())]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct FormatBoolOp;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user