Compare commits
221 Commits
jack/seman
...
0.12.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2bc15bc15 | ||
|
|
e586f6dcc4 | ||
|
|
76a6b7e3e2 | ||
|
|
1ce65714c0 | ||
|
|
d9aaacd01f | ||
|
|
18eaa659c1 | ||
|
|
af259faed5 | ||
|
|
d75ef3823c | ||
|
|
89ca493fd9 | ||
|
|
4b80f5fa4f | ||
|
|
5663426d73 | ||
|
|
0b3548755c | ||
|
|
ce1dc21e7e | ||
|
|
7d0c8e045c | ||
|
|
d71518b369 | ||
|
|
9ab276b345 | ||
|
|
a60fb3f2c8 | ||
|
|
f558bf721c | ||
|
|
ea1c080881 | ||
|
|
73720c73be | ||
|
|
136abace92 | ||
|
|
bc7274d148 | ||
|
|
911d5cc973 | ||
|
|
8d6dc7d3a3 | ||
|
|
ef4897f9f3 | ||
|
|
ecf3c4ca11 | ||
|
|
33c5f6f4f8 | ||
|
|
ba47010150 | ||
|
|
db423ee978 | ||
|
|
a04823cfad | ||
|
|
d0bcf56bd9 | ||
|
|
f9bbee33f6 | ||
|
|
376e3ff395 | ||
|
|
b57cc5be33 | ||
|
|
3c1fe12259 | ||
|
|
e4289deb5a | ||
|
|
dbbcb7f452 | ||
|
|
c6dfdb1d39 | ||
|
|
c65029f9a5 | ||
|
|
a0bba718f6 | ||
|
|
1eab4dbd95 | ||
|
|
3eb3c3572b | ||
|
|
41bb24a87e | ||
|
|
87f0da139a | ||
|
|
f9bfc9ab5b | ||
|
|
59f7102606 | ||
|
|
862d2d0687 | ||
|
|
48edf46f3b | ||
|
|
ec86a4e960 | ||
|
|
e7237652a9 | ||
|
|
205eae14d2 | ||
|
|
f407f12f4c | ||
|
|
fb2d0af18c | ||
|
|
8ead02e0b1 | ||
|
|
330bb4efbf | ||
|
|
ad8c98117a | ||
|
|
06dbec8479 | ||
|
|
85931ab594 | ||
|
|
b3cc733f06 | ||
|
|
7abc41727b | ||
|
|
886c4e4773 | ||
|
|
bc6ea68733 | ||
|
|
796819e7a0 | ||
|
|
5508e8e528 | ||
|
|
0be3e1fbbf | ||
|
|
5d217b7f46 | ||
|
|
0b6ce1c788 | ||
|
|
0e9d77e43a | ||
|
|
8b827c3c6c | ||
|
|
c22395dbc6 | ||
|
|
11f521c768 | ||
|
|
c5e05df966 | ||
|
|
7a44ea680e | ||
|
|
f82025d919 | ||
|
|
365f521c37 | ||
|
|
fc5321e000 | ||
|
|
c68ff8d90b | ||
|
|
5931a5207d | ||
|
|
692be72f5a | ||
|
|
14fe1228e7 | ||
|
|
045cba382a | ||
|
|
a5cbca156c | ||
|
|
d43a3d34dd | ||
|
|
99111961c0 | ||
|
|
859475f017 | ||
|
|
7b75aee21d | ||
|
|
d04dcd991b | ||
|
|
39ee71c2a5 | ||
|
|
1a38831d53 | ||
|
|
ddd4bab67c | ||
|
|
468eb37d75 | ||
|
|
2e9c241d7e | ||
|
|
05478d5cc7 | ||
|
|
4db20f459c | ||
|
|
ec7c2efef9 | ||
|
|
79b2754215 | ||
|
|
a0ddf1f7c4 | ||
|
|
5b00ec981b | ||
|
|
306ef3bb02 | ||
|
|
a4cd13c6e2 | ||
|
|
e0c98874e2 | ||
|
|
f4be05a83b | ||
|
|
1d2128f918 | ||
|
|
276405b44e | ||
|
|
f019cfd15f | ||
|
|
33030b34cd | ||
|
|
656fc335f2 | ||
|
|
e0f4cec7a1 | ||
|
|
662d18bd05 | ||
|
|
c82e255ca8 | ||
|
|
58efd19f11 | ||
|
|
c6dcfe36d0 | ||
|
|
59b078b1bf | ||
|
|
5e943d3539 | ||
|
|
0967e7e088 | ||
|
|
600245478c | ||
|
|
e5c091b850 | ||
|
|
10301f6190 | ||
|
|
4242905b36 | ||
|
|
c20d906503 | ||
|
|
a04375173c | ||
|
|
e6dcdd29f2 | ||
|
|
24f6d2dc13 | ||
|
|
3314cf90ed | ||
|
|
0cb1abc1fc | ||
|
|
f6491cacd1 | ||
|
|
e4f1b587cc | ||
|
|
fbf24be8ae | ||
|
|
5e4fa9e442 | ||
|
|
67529edad6 | ||
|
|
4ac2b2c222 | ||
|
|
083bb85d9d | ||
|
|
c7af595fc1 | ||
|
|
7d8f7c20da | ||
|
|
76c933d10e | ||
|
|
d423191d94 | ||
|
|
c8d155b2b9 | ||
|
|
a5339a52c3 | ||
|
|
48772c04d7 | ||
|
|
510a07dee2 | ||
|
|
47d44e5f7b | ||
|
|
ec3163781c | ||
|
|
b892e4548e | ||
|
|
9ac39cee98 | ||
|
|
f4d8826428 | ||
|
|
527a690a73 | ||
|
|
f0e9c1d8f9 | ||
|
|
2e1d6623cd | ||
|
|
2dc2f68b0f | ||
|
|
26d6c3831f | ||
|
|
9ced219ffc | ||
|
|
f344dda82c | ||
|
|
6de84ed56e | ||
|
|
bd4506aac5 | ||
|
|
0e5577ab56 | ||
|
|
957320c0f1 | ||
|
|
f6093452ed | ||
|
|
82350a398e | ||
|
|
ce938fe205 | ||
|
|
7f8f1ab2c1 | ||
|
|
ef422460de | ||
|
|
dc2e8ab377 | ||
|
|
9aaa82d037 | ||
|
|
3288ac2dfb | ||
|
|
1167ed61cf | ||
|
|
2ee47d87b6 | ||
|
|
d324cedfc2 | ||
|
|
5a570c8e6d | ||
|
|
baadb5a78d | ||
|
|
df0648aae0 | ||
|
|
f0b03c3e86 | ||
|
|
9f6146a13d | ||
|
|
11d2cb6d56 | ||
|
|
d59282ebb5 | ||
|
|
e12747a903 | ||
|
|
5725c4b17f | ||
|
|
2f3c7ad1fc | ||
|
|
79c949f0f7 | ||
|
|
13bdba5d28 | ||
|
|
d76fd103ae | ||
|
|
ad28b80f96 | ||
|
|
3458f365da | ||
|
|
94cfdf4b40 | ||
|
|
498a04804d | ||
|
|
f34b65b7a0 | ||
|
|
6a05d46ef6 | ||
|
|
28820db1cd | ||
|
|
ea1aa9ebfe | ||
|
|
e72f10be2d | ||
|
|
d2fbf2af8f | ||
|
|
2abd683376 | ||
|
|
dc84645c36 | ||
|
|
f3f4db7104 | ||
|
|
5063a73d7f | ||
|
|
6bc52f2855 | ||
|
|
c433865801 | ||
|
|
5b6d0d17f1 | ||
|
|
5124cb393f | ||
|
|
11eb8d8f9f | ||
|
|
37617d1e37 | ||
|
|
14f6a3f133 | ||
|
|
ec65ca379d | ||
|
|
02c0db6781 | ||
|
|
18f2b27a55 | ||
|
|
618692cfd2 | ||
|
|
b8a9b1994b | ||
|
|
4d8ccb6125 | ||
|
|
8230b79829 | ||
|
|
5a116e48c3 | ||
|
|
3a542a80f6 | ||
|
|
4be6fc0979 | ||
|
|
7cc3f1ebe9 | ||
|
|
0ec4801b0d | ||
|
|
0095ff4c1a | ||
|
|
44755e6e86 | ||
|
|
8489816edc | ||
|
|
6b0eadfb4d | ||
|
|
8199154d54 | ||
|
|
50e1ecc086 | ||
|
|
fd35435281 | ||
|
|
fc72ff4a94 |
20
.github/workflows/build-binaries.yml
vendored
20
.github/workflows/build-binaries.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build sdist"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
command: sdist
|
||||
args: --out dist
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - x86_64"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
@@ -121,7 +121,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
@@ -177,7 +177,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --locked --out dist
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
@@ -292,6 +292,8 @@ jobs:
|
||||
maturin_docker_options: -e JEMALLOC_SYS_WITH_LG_PAGE=16
|
||||
- target: arm-unknown-linux-musleabihf
|
||||
arch: arm
|
||||
- target: riscv64gc-unknown-linux-gnu
|
||||
arch: riscv64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -304,7 +306,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
@@ -319,7 +321,7 @@ jobs:
|
||||
githubToken: ${{ github.token }}
|
||||
install: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends python3 python3-pip
|
||||
apt-get install -y --no-install-recommends python3 python3-pip libatomic1
|
||||
pip3 install -U pip
|
||||
run: |
|
||||
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
|
||||
@@ -370,7 +372,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
@@ -435,7 +437,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
|
||||
8
.github/workflows/build-docker.yml
vendored
8
.github/workflows/build-docker.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -169,7 +169,7 @@ jobs:
|
||||
steps:
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -276,7 +276,7 @@ jobs:
|
||||
type=pep440,pattern={{ version }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
type=pep440,pattern={{ major }}.{{ minor }},value=${{ fromJson(inputs.plan).announcement_tag }}
|
||||
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
48
.github/workflows/ci.yaml
vendored
48
.github/workflows/ci.yaml
vendored
@@ -38,7 +38,8 @@ jobs:
|
||||
fuzz: ${{ steps.check_fuzzer.outputs.changed }}
|
||||
# Flag that is set to "true" when code related to ty changes.
|
||||
ty: ${{ steps.check_ty.outputs.changed }}
|
||||
|
||||
# Flag that is set to "true" when code related to the py-fuzzer folder changes.
|
||||
py-fuzzer: ${{ steps.check_py_fuzzer.outputs.changed }}
|
||||
# Flag that is set to "true" when code related to the playground changes.
|
||||
playground: ${{ steps.check_playground.outputs.changed }}
|
||||
steps:
|
||||
@@ -68,7 +69,6 @@ jobs:
|
||||
':crates/ruff_text_size/**' \
|
||||
':crates/ruff_python_ast/**' \
|
||||
':crates/ruff_python_parser/**' \
|
||||
':python/py-fuzzer/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
@@ -138,6 +138,18 @@ jobs:
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if the py-fuzzer code changed
|
||||
id: check_py_fuzzer
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py_fuzzer/**' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Check if there was any code related change
|
||||
id: check_code
|
||||
env:
|
||||
@@ -238,7 +250,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
@@ -296,7 +308,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
@@ -381,7 +393,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
|
||||
- name: "Build"
|
||||
run: cargo build --release --locked
|
||||
|
||||
@@ -406,7 +418,7 @@ jobs:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
run: rustup default "${MSRV}"
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
|
||||
- name: "Build tests"
|
||||
shell: bash
|
||||
env:
|
||||
@@ -429,7 +441,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
|
||||
uses: cargo-bins/cargo-binstall@0dca8cf8dfb40cb77a29cece06933ce674674523 # v1.15.1
|
||||
with:
|
||||
tool: cargo-fuzz@0.11.2
|
||||
- name: "Install cargo-fuzz"
|
||||
@@ -443,7 +455,7 @@ jobs:
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && needs.determine_changes.outputs.parser == 'true' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.parser == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
@@ -451,7 +463,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -633,7 +645,7 @@ jobs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.ty == 'true' }}
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
@@ -652,7 +664,7 @@ jobs:
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
workflow: "ci.yaml"
|
||||
check_artifacts: true
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Fuzz
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
@@ -682,7 +694,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
|
||||
- uses: cargo-bins/cargo-binstall@0dca8cf8dfb40cb77a29cece06933ce674674523 # v1.15.1
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -703,7 +715,7 @@ jobs:
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@e10f6c464b90acceb5f640d31beda6d586ba7b4a # v1.49.3
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
args: --out dist
|
||||
- name: "Test wheel"
|
||||
@@ -722,13 +734,13 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: 22
|
||||
- name: "Cache pre-commit"
|
||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
@@ -765,7 +777,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -897,7 +909,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
@@ -930,7 +942,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
4
.github/workflows/daily_fuzz.yaml
vendored
4
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,11 +34,11 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
uses: rui314/setup-mold@7344740a9418dcdcb481c7df83d9fbd1d5072d7d # v1
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- name: Build ruff
|
||||
# A debug build means the script runs slower once it gets started,
|
||||
|
||||
5
.github/workflows/mypy_primer.yaml
vendored
5
.github/workflows/mypy_primer.yaml
vendored
@@ -11,6 +11,7 @@ on:
|
||||
- "crates/ruff_python_parser"
|
||||
- ".github/workflows/mypy_primer.yaml"
|
||||
- ".github/workflows/mypy_primer_comment.yaml"
|
||||
- "scripts/mypy_primer.sh"
|
||||
- "Cargo.lock"
|
||||
- "!**.md"
|
||||
|
||||
@@ -38,7 +39,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
@@ -81,7 +82,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/publish-pypi.yml
vendored
2
.github/workflows/publish-pypi.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -61,7 +61,7 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
@@ -124,19 +124,19 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -175,19 +175,19 @@ jobs:
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
@@ -251,13 +251,13 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- uses: actions/checkout@ff7abcd0c3c05ccf6adc123a8cd1fd4fb30fb493
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
|
||||
6
.github/workflows/sync_typeshed.yaml
vendored
6
.github/workflows/sync_typeshed.yaml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
git config --global user.email '<>'
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
uses: astral-sh/setup-uv@4959332f0f014c5280e7eac8b70c90cb574c9f9b # v6.6.0
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
|
||||
11
.github/workflows/typing_conformance.yaml
vendored
11
.github/workflows/typing_conformance.yaml
vendored
@@ -54,6 +54,9 @@ jobs:
|
||||
|
||||
- name: Compute diagnostic diff
|
||||
shell: bash
|
||||
env:
|
||||
# TODO: Remove this once we fixed the remaining panics in the conformance suite.
|
||||
TY_MAX_PARALLELISM: 1
|
||||
run: |
|
||||
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
|
||||
|
||||
@@ -63,15 +66,15 @@ jobs:
|
||||
|
||||
echo "new commit"
|
||||
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
|
||||
cargo build --release --bin ty
|
||||
mv target/release/ty ty-new
|
||||
cargo build --bin ty
|
||||
mv target/debug/ty ty-new
|
||||
|
||||
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
|
||||
git checkout -b old_commit "$MERGE_BASE"
|
||||
echo "old commit (merge base)"
|
||||
git rev-list --format=%s --max-count=1 old_commit
|
||||
cargo build --release --bin ty
|
||||
mv target/release/ty ty-old
|
||||
cargo build --bin ty
|
||||
mv target/debug/ty ty-old
|
||||
)
|
||||
|
||||
(
|
||||
|
||||
100
CHANGELOG.md
100
CHANGELOG.md
@@ -1,5 +1,105 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.11
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
|
||||
- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
|
||||
- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
|
||||
- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
|
||||
- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
|
||||
- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
|
||||
- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
|
||||
- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
|
||||
- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
|
||||
- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
|
||||
|
||||
## 0.12.10
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Implement fix for `maxsplit` without separator (`SIM905`) ([#19851](https://github.com/astral-sh/ruff/pull/19851))
|
||||
- \[`flake8-use-pathlib`\] Add fixes for `PTH102` and `PTH103` ([#19514](https://github.com/astral-sh/ruff/pull/19514))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`isort`\] Handle multiple continuation lines after module docstring (`I002`) ([#19818](https://github.com/astral-sh/ruff/pull/19818))
|
||||
- \[`pyupgrade`\] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) ([#19769](https://github.com/astral-sh/ruff/pull/19769))
|
||||
- \[`pyupgrade`\] Handle nested `Optional`s (`UP045`) ([#19770](https://github.com/astral-sh/ruff/pull/19770))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pycodestyle`\] Make `E731` fix unsafe instead of display-only for class assignments ([#19700](https://github.com/astral-sh/ruff/pull/19700))
|
||||
- \[`pyflakes`\] Add secondary annotation showing previous definition (`F811`) ([#19900](https://github.com/astral-sh/ruff/pull/19900))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix description of global config file discovery strategy ([#19188](https://github.com/astral-sh/ruff/pull/19188))
|
||||
- Update outdated links to <https://typing.python.org/en/latest/source/stubs.html> ([#19992](https://github.com/astral-sh/ruff/pull/19992))
|
||||
- \[`flake8-annotations`\] Remove unused import in example (`ANN401`) ([#20000](https://github.com/astral-sh/ruff/pull/20000))
|
||||
|
||||
## 0.12.9
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Add check for `airflow.secrets.cache.SecretCache` (`AIR301`) ([#17707](https://github.com/astral-sh/ruff/pull/17707))
|
||||
- \[`ruff`\] Offer a safe fix for multi-digit zeros (`RUF064`) ([#19847](https://github.com/astral-sh/ruff/pull/19847))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-blind-except`\] Fix `BLE001` false-positive on `raise ... from None` ([#19755](https://github.com/astral-sh/ruff/pull/19755))
|
||||
- \[`flake8-comprehensions`\] Fix false positive for `C420` with attribute, subscript, or slice assignment targets ([#19513](https://github.com/astral-sh/ruff/pull/19513))
|
||||
- \[`flake8-simplify`\] Fix handling of U+001C..U+001F whitespace (`SIM905`) ([#19849](https://github.com/astral-sh/ruff/pull/19849))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pylint`\] Use lowercase hex characters to match the formatter (`PLE2513`) ([#19808](https://github.com/astral-sh/ruff/pull/19808))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Fix `lint.future-annotations` link ([#19876](https://github.com/astral-sh/ruff/pull/19876))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
|
||||
|
||||
- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
|
||||
|
||||
- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
|
||||
|
||||
Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
|
||||
|
||||
```diff
|
||||
-unused.py:8:19: F401 [*] `pathlib` imported but unused
|
||||
+F401 [*] `pathlib` imported but unused
|
||||
+ --> unused.py:8:19
|
||||
|
|
||||
7 | # Unused, _not_ marked as required (due to the alias).
|
||||
8 | import pathlib as non_alias
|
||||
- | ^^^^^^^^^ F401
|
||||
+ | ^^^^^^^^^
|
||||
9 |
|
||||
10 | # Unused, marked as required.
|
||||
|
|
||||
- = help: Remove unused import: `pathlib`
|
||||
+help: Remove unused import: `pathlib`
|
||||
```
|
||||
|
||||
For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
|
||||
|
||||
## 0.12.8
|
||||
|
||||
### Preview features
|
||||
|
||||
227
Cargo.lock
generated
227
Cargo.lock
generated
@@ -128,9 +128,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
version = "1.0.99"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
@@ -257,9 +257,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
version = "2.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
@@ -295,7 +295,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata 0.4.10",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -322,9 +322,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.1.10"
|
||||
version = "1.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
|
||||
checksum = "5d07aa9a93b00c76f71bc35d598bed923f6d4f3a9ca5c24b7737ae1a292841c0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -408,9 +408,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.43"
|
||||
version = "4.5.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
|
||||
checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -418,9 +418,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.43"
|
||||
version = "4.5.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
|
||||
checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -461,9 +461,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.41"
|
||||
version = "4.5.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||
checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -485,7 +485,7 @@ checksum = "85a8ab73a1c02b0c15597b22e09c7dc36e63b2f601f9d1e83ac0c3decd38b1ae"
|
||||
dependencies = [
|
||||
"nix 0.29.0",
|
||||
"terminfo",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"which",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -955,7 +955,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1035,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1084,14 +1084,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
version = "0.2.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
|
||||
checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1118,9 +1118,9 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.1"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
|
||||
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
@@ -1178,7 +1178,7 @@ checksum = "5697765925a05c9d401dd04a93dfd662d336cc25fdcc3301220385a1ffcfdde5"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown 0.15.5",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -1218,9 +1218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
@@ -1231,7 +1231,7 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
@@ -1241,7 +1241,7 @@ version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"ignore",
|
||||
"walkdir",
|
||||
]
|
||||
@@ -1264,9 +1264,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
@@ -1279,7 +1279,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1430,9 +1430,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.3"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
|
||||
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||
dependencies = [
|
||||
"idna_adapter",
|
||||
"smallvec",
|
||||
@@ -1459,7 +1459,7 @@ dependencies = [
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata 0.4.10",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
@@ -1471,7 +1471,7 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1486,12 +1486,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown 0.15.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1521,7 +1521,7 @@ version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
@@ -1764,9 +1764,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
@@ -1780,7 +1780,7 @@ dependencies = [
|
||||
"paste",
|
||||
"peg",
|
||||
"regex",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1809,7 +1809,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
@@ -2014,7 +2014,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2026,7 +2026,7 @@ version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -2054,7 +2054,7 @@ version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
@@ -2127,9 +2127,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordermap"
|
||||
version = "0.5.8"
|
||||
version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d6bff06e4a5dc6416bead102d3e63c480dd852ffbb278bf8cfeb4966b329609"
|
||||
checksum = "2fd6fedcd996c8c97932075cc3811d83f53280f48d5620e4e3cab7f6a12678c4"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -2283,9 +2283,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.1"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
@@ -2294,7 +2294,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
@@ -2473,9 +2473,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2490,7 +2490,7 @@ dependencies = [
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"toml 0.8.23",
|
||||
]
|
||||
|
||||
@@ -2505,7 +2505,7 @@ dependencies = [
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2666,7 +2666,7 @@ version = "0.5.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2677,18 +2677,18 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"libredox",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.11.1"
|
||||
version = "1.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata 0.4.10",
|
||||
"regex-syntax 0.8.5",
|
||||
]
|
||||
|
||||
@@ -2703,9 +2703,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.9"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
|
||||
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2743,13 +2743,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.12.8"
|
||||
version = "0.12.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
"assert_fs",
|
||||
"bincode 2.0.1",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"cachedir",
|
||||
"clap",
|
||||
"clap_complete_command",
|
||||
@@ -2793,7 +2793,7 @@ dependencies = [
|
||||
"strum",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"tikv-jemallocator",
|
||||
"toml 0.9.5",
|
||||
"tracing",
|
||||
@@ -2870,6 +2870,7 @@ dependencies = [
|
||||
"insta",
|
||||
"matchit",
|
||||
"path-slash",
|
||||
"pathdiff",
|
||||
"quick-junit",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
@@ -2886,8 +2887,9 @@ dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ty_static",
|
||||
@@ -2934,6 +2936,7 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
"ty",
|
||||
"ty_project",
|
||||
"ty_python_semantic",
|
||||
"ty_static",
|
||||
"url",
|
||||
]
|
||||
@@ -2996,17 +2999,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.8"
|
||||
version = "0.12.11"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"clap",
|
||||
"colored 3.0.0",
|
||||
"fern",
|
||||
"glob",
|
||||
"globset",
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown 0.15.5",
|
||||
"imperative",
|
||||
"insta",
|
||||
"is-macro",
|
||||
@@ -3018,11 +3021,9 @@ dependencies = [
|
||||
"memchr",
|
||||
"natord",
|
||||
"path-absolutize",
|
||||
"pathdiff",
|
||||
"pep440_rs",
|
||||
"pyproject-toml",
|
||||
"regex",
|
||||
"ruff_annotate_snippets",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_diagnostics",
|
||||
@@ -3048,7 +3049,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"toml 0.9.5",
|
||||
"typed-arena",
|
||||
"unicode-normalization",
|
||||
@@ -3074,6 +3075,7 @@ name = "ruff_memory_usage"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"get-size2",
|
||||
"ordermap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3090,7 +3092,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"serde_with",
|
||||
"test-case",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -3106,7 +3108,7 @@ name = "ruff_python_ast"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
"is-macro",
|
||||
@@ -3121,7 +3123,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3175,7 +3177,7 @@ dependencies = [
|
||||
"similar",
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
@@ -3194,7 +3196,7 @@ dependencies = [
|
||||
name = "ruff_python_literal"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"itertools 0.14.0",
|
||||
"ruff_python_ast",
|
||||
"unic-ucd-category",
|
||||
@@ -3205,7 +3207,7 @@ name = "ruff_python_parser"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"bstr",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
@@ -3230,7 +3232,7 @@ dependencies = [
|
||||
name = "ruff_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"insta",
|
||||
"is-macro",
|
||||
"ruff_cache",
|
||||
@@ -3251,7 +3253,7 @@ dependencies = [
|
||||
name = "ruff_python_stdlib"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -3305,7 +3307,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"toml 0.9.5",
|
||||
"tracing",
|
||||
"tracing-log",
|
||||
@@ -3335,7 +3337,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.12.8"
|
||||
version = "0.12.11"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3428,11 +3430,11 @@ version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3450,13 +3452,13 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b121ee46c4483ba74c19e933a3522bd548eb7343#b121ee46c4483ba74c19e933a3522bd548eb7343"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
"crossbeam-queue",
|
||||
"crossbeam-utils",
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown 0.15.5",
|
||||
"hashlink",
|
||||
"indexmap",
|
||||
"intrusive-collections",
|
||||
@@ -3474,12 +3476,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b121ee46c4483ba74c19e933a3522bd548eb7343#b121ee46c4483ba74c19e933a3522bd548eb7343"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.23.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=b121ee46c4483ba74c19e933a3522bd548eb7343#b121ee46c4483ba74c19e933a3522bd548eb7343"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3576,9 +3578,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.142"
|
||||
version = "1.0.143"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7"
|
||||
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@@ -3789,9 +3791,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.104"
|
||||
version = "2.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3915,11 +3917,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.12"
|
||||
version = "2.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
|
||||
checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
|
||||
dependencies = [
|
||||
"thiserror-impl 2.0.12",
|
||||
"thiserror-impl 2.0.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3935,9 +3937,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.12"
|
||||
version = "2.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
|
||||
checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4136,9 +4138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-indicatif"
|
||||
version = "0.3.12"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1983afead46ff13a3c93581e0cec31d20b29efdd22cbdaa8b9f850eccf2c352"
|
||||
checksum = "04d4e11e0e27acef25a47f27e9435355fecdc488867fa2bc90e75b0700d2823d"
|
||||
dependencies = [
|
||||
"indicatif",
|
||||
"tracing",
|
||||
@@ -4238,17 +4240,22 @@ dependencies = [
|
||||
name = "ty_ide"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"get-size2",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
"rayon",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_memory_usage",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
"tracing",
|
||||
"ty_project",
|
||||
@@ -4271,7 +4278,7 @@ dependencies = [
|
||||
"pep440_rs",
|
||||
"rayon",
|
||||
"regex",
|
||||
"regex-automata 0.4.9",
|
||||
"regex-automata 0.4.10",
|
||||
"ruff_cache",
|
||||
"ruff_db",
|
||||
"ruff_macros",
|
||||
@@ -4284,7 +4291,7 @@ dependencies = [
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"toml 0.9.5",
|
||||
"tracing",
|
||||
"ty_combine",
|
||||
@@ -4297,7 +4304,7 @@ name = "ty_python_semantic"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"bitvec",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
@@ -4306,7 +4313,7 @@ dependencies = [
|
||||
"drop_bomb",
|
||||
"get-size2",
|
||||
"glob",
|
||||
"hashbrown 0.15.4",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
@@ -4337,7 +4344,7 @@ dependencies = [
|
||||
"strum_macros",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"tracing",
|
||||
"ty_python_semantic",
|
||||
"ty_static",
|
||||
@@ -4350,7 +4357,7 @@ name = "ty_server"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"crossbeam",
|
||||
"dunce",
|
||||
"insta",
|
||||
@@ -4371,7 +4378,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"ty_combine",
|
||||
@@ -4393,7 +4400,7 @@ name = "ty_test"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"insta",
|
||||
@@ -4412,7 +4419,7 @@ dependencies = [
|
||||
"serde",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"thiserror 2.0.16",
|
||||
"toml 0.9.5",
|
||||
"tracing",
|
||||
"ty_python_semantic",
|
||||
@@ -4589,9 +4596,9 @@ checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.4"
|
||||
version = "2.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
@@ -5143,7 +5150,7 @@ version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"bitflags 2.9.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -5,7 +5,7 @@ resolver = "2"
|
||||
[workspace.package]
|
||||
# Please update rustfmt.toml when bumping the Rust edition
|
||||
edition = "2024"
|
||||
rust-version = "1.86"
|
||||
rust-version = "1.87"
|
||||
homepage = "https://docs.astral.sh/ruff"
|
||||
documentation = "https://docs.astral.sh/ruff"
|
||||
repository = "https://github.com/astral-sh/ruff"
|
||||
@@ -143,7 +143,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "b121ee46c4483ba74c19e933a3522bd548eb7343", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
@@ -215,6 +215,8 @@ unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -2 }
|
||||
# Enabled at the crate level
|
||||
disallowed_methods = "allow"
|
||||
# Allowed pedantic lints
|
||||
char_lit_as_u8 = "allow"
|
||||
collapsible_else_if = "allow"
|
||||
@@ -253,6 +255,7 @@ unused_peekable = "warn"
|
||||
# Diagnostics are not actionable: Enable once https://github.com/rust-lang/rust-clippy/issues/13774 is resolved.
|
||||
large_stack_arrays = "allow"
|
||||
|
||||
|
||||
[profile.release]
|
||||
# Note that we set these explicitly, and these values
|
||||
# were chosen based on a trade-off between compile times
|
||||
|
||||
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.12.8/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.8/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.12.11/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.11/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.8
|
||||
rev: v0.12.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
17
clippy.toml
17
clippy.toml
@@ -24,3 +24,20 @@ ignore-interior-mutability = [
|
||||
# The expression is read-only.
|
||||
"ruff_python_ast::hashable::HashableExpr",
|
||||
]
|
||||
|
||||
disallowed-methods = [
|
||||
{ path = "std::env::var", reason = "Use System::env_var instead in ty crates" },
|
||||
{ path = "std::env::current_dir", reason = "Use System::current_directory instead in ty crates" },
|
||||
{ path = "std::fs::read_to_string", reason = "Use System::read_to_string instead in ty crates" },
|
||||
{ path = "std::fs::metadata", reason = "Use System::path_metadata instead in ty crates" },
|
||||
{ path = "std::fs::canonicalize", reason = "Use System::canonicalize_path instead in ty crates" },
|
||||
{ path = "dunce::canonicalize", reason = "Use System::canonicalize_path instead in ty crates" },
|
||||
{ path = "std::fs::read_dir", reason = "Use System::read_directory instead in ty crates" },
|
||||
{ path = "std::fs::write", reason = "Use WritableSystem::write_file instead in ty crates" },
|
||||
{ path = "std::fs::create_dir_all", reason = "Use WritableSystem::create_directory_all instead in ty crates" },
|
||||
{ path = "std::fs::File::create_new", reason = "Use WritableSystem::create_new_file instead in ty crates" },
|
||||
# Path methods that have System trait equivalents
|
||||
{ path = "std::path::Path::exists", reason = "Use System::path_exists instead in ty crates" },
|
||||
{ path = "std::path::Path::is_dir", reason = "Use System::is_directory instead in ty crates" },
|
||||
{ path = "std::path::Path::is_file", reason = "Use System::is_file instead in ty crates" },
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.8"
|
||||
version = "0.12.11"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -85,7 +85,7 @@ dist = true
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dependencies]
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), not(target_os = "aix"), not(target_os = "android"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dependencies]
|
||||
tikv-jemallocator = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -13,25 +13,16 @@ use itertools::Itertools;
|
||||
use log::{debug, error};
|
||||
use rayon::iter::ParallelIterator;
|
||||
use rayon::iter::{IntoParallelIterator, ParallelBridge};
|
||||
use ruff_linter::codes::Rule;
|
||||
use rustc_hash::FxHashMap;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use ruff_cache::{CacheKey, CacheKeyHasher};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_linter::message::create_lint_diagnostic;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::{VERSION, warn_user};
|
||||
use ruff_macros::CacheKey;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_workspace::Settings;
|
||||
use ruff_workspace::resolver::Resolver;
|
||||
|
||||
use crate::diagnostics::Diagnostics;
|
||||
|
||||
/// [`Path`] that is relative to the package root in [`PackageCache`].
|
||||
pub(crate) type RelativePath = Path;
|
||||
/// [`PathBuf`] that is relative to the package root in [`PackageCache`].
|
||||
@@ -298,13 +289,8 @@ impl Cache {
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn update_lint(
|
||||
&self,
|
||||
path: RelativePathBuf,
|
||||
key: &FileCacheKey,
|
||||
data: LintCacheData,
|
||||
) {
|
||||
self.update(path, key, ChangeData::Lint(data));
|
||||
pub(crate) fn set_linted(&self, path: RelativePathBuf, key: &FileCacheKey, yes: bool) {
|
||||
self.update(path, key, ChangeData::Linted(yes));
|
||||
}
|
||||
|
||||
pub(crate) fn set_formatted(&self, path: RelativePathBuf, key: &FileCacheKey) {
|
||||
@@ -339,42 +325,15 @@ pub(crate) struct FileCache {
|
||||
}
|
||||
|
||||
impl FileCache {
|
||||
/// Convert the file cache into `Diagnostics`, using `path` as file name.
|
||||
pub(crate) fn to_diagnostics(&self, path: &Path) -> Option<Diagnostics> {
|
||||
self.data.lint.as_ref().map(|lint| {
|
||||
let diagnostics = if lint.messages.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
|
||||
lint.messages
|
||||
.iter()
|
||||
.map(|msg| {
|
||||
create_lint_diagnostic(
|
||||
&msg.body,
|
||||
msg.suggestion.as_ref(),
|
||||
msg.range,
|
||||
msg.fix.clone(),
|
||||
msg.parent,
|
||||
file.clone(),
|
||||
msg.noqa_offset,
|
||||
msg.rule,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
|
||||
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
|
||||
} else {
|
||||
FxHashMap::default()
|
||||
};
|
||||
Diagnostics::new(diagnostics, notebook_indexes)
|
||||
})
|
||||
/// Return whether or not the file in the cache was linted and found to have no diagnostics.
|
||||
pub(crate) fn linted(&self) -> bool {
|
||||
self.data.linted
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, bincode::Decode, bincode::Encode)]
|
||||
struct FileCacheData {
|
||||
lint: Option<LintCacheData>,
|
||||
linted: bool,
|
||||
formatted: bool,
|
||||
}
|
||||
|
||||
@@ -410,88 +369,6 @@ pub(crate) fn init(path: &Path) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(bincode::Decode, Debug, bincode::Encode, PartialEq)]
|
||||
pub(crate) struct LintCacheData {
|
||||
/// Imports made.
|
||||
// pub(super) imports: ImportMap,
|
||||
/// Diagnostic messages.
|
||||
pub(super) messages: Vec<CacheMessage>,
|
||||
/// Source code of the file.
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// This will be empty if `messages` is empty.
|
||||
pub(super) source: String,
|
||||
/// Notebook index if this file is a Jupyter Notebook.
|
||||
#[bincode(with_serde)]
|
||||
pub(super) notebook_index: Option<NotebookIndex>,
|
||||
}
|
||||
|
||||
impl LintCacheData {
|
||||
pub(crate) fn from_diagnostics(
|
||||
diagnostics: &[Diagnostic],
|
||||
notebook_index: Option<NotebookIndex>,
|
||||
) -> Self {
|
||||
let source = if let Some(msg) = diagnostics.first() {
|
||||
msg.expect_ruff_source_file().source_text().to_owned()
|
||||
} else {
|
||||
String::new() // No messages, no need to keep the source!
|
||||
};
|
||||
|
||||
let messages = diagnostics
|
||||
.iter()
|
||||
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
|
||||
// this also serves to filter them out, but we shouldn't be caching files with syntax
|
||||
// errors anyway.
|
||||
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
|
||||
.map(|(rule, msg)| {
|
||||
// Make sure that all message use the same source file.
|
||||
assert_eq!(
|
||||
msg.expect_ruff_source_file(),
|
||||
diagnostics.first().unwrap().expect_ruff_source_file(),
|
||||
"message uses a different source file"
|
||||
);
|
||||
CacheMessage {
|
||||
rule,
|
||||
body: msg.body().to_string(),
|
||||
suggestion: msg.first_help_text().map(ToString::to_string),
|
||||
range: msg.expect_range(),
|
||||
parent: msg.parent(),
|
||||
fix: msg.fix().cloned(),
|
||||
noqa_offset: msg.noqa_offset(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
messages,
|
||||
source,
|
||||
notebook_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On disk representation of a diagnostic message.
|
||||
#[derive(bincode::Decode, Debug, bincode::Encode, PartialEq)]
|
||||
pub(super) struct CacheMessage {
|
||||
/// The rule for the cached diagnostic.
|
||||
#[bincode(with_serde)]
|
||||
rule: Rule,
|
||||
/// The message body to display to the user, to explain the diagnostic.
|
||||
body: String,
|
||||
/// The message to display to the user, to explain the suggested fix.
|
||||
suggestion: Option<String>,
|
||||
/// Range into the message's [`FileCache::source`].
|
||||
#[bincode(with_serde)]
|
||||
range: TextRange,
|
||||
#[bincode(with_serde)]
|
||||
parent: Option<TextSize>,
|
||||
#[bincode(with_serde)]
|
||||
fix: Option<Fix>,
|
||||
#[bincode(with_serde)]
|
||||
noqa_offset: Option<TextSize>,
|
||||
}
|
||||
|
||||
pub(crate) trait PackageCaches {
|
||||
fn get(&self, package_root: &Path) -> Option<&Cache>;
|
||||
|
||||
@@ -579,15 +456,15 @@ struct Change {
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ChangeData {
|
||||
Lint(LintCacheData),
|
||||
Linted(bool),
|
||||
Formatted,
|
||||
}
|
||||
|
||||
impl ChangeData {
|
||||
fn apply(self, data: &mut FileCacheData) {
|
||||
match self {
|
||||
ChangeData::Lint(new_lint) => {
|
||||
data.lint = Some(new_lint);
|
||||
ChangeData::Linted(yes) => {
|
||||
data.linted = yes;
|
||||
}
|
||||
ChangeData::Formatted => {
|
||||
data.formatted = true;
|
||||
@@ -612,7 +489,6 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use ruff_cache::CACHE_DIR_NAME;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_linter::package::PackageRoot;
|
||||
use ruff_linter::settings::LinterSettings;
|
||||
use ruff_linter::settings::flags;
|
||||
@@ -620,7 +496,7 @@ mod tests {
|
||||
use ruff_python_ast::{PySourceType, PythonVersion};
|
||||
use ruff_workspace::Settings;
|
||||
|
||||
use crate::cache::{self, FileCache, FileCacheData, FileCacheKey};
|
||||
use crate::cache::{self, ChangeData, FileCache, FileCacheData, FileCacheKey};
|
||||
use crate::cache::{Cache, RelativePathBuf};
|
||||
use crate::commands::format::{FormatCommandError, FormatMode, FormatResult, format_path};
|
||||
use crate::diagnostics::{Diagnostics, lint_path};
|
||||
@@ -647,7 +523,7 @@ mod tests {
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
let mut paths = Vec::new();
|
||||
let mut parse_errors = Vec::new();
|
||||
let mut paths_with_diagnostics = Vec::new();
|
||||
let mut expected_diagnostics = Diagnostics::default();
|
||||
for entry in fs::read_dir(&package_root).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
@@ -671,7 +547,7 @@ mod tests {
|
||||
continue;
|
||||
}
|
||||
|
||||
let diagnostics = lint_path(
|
||||
let mut diagnostics = lint_path(
|
||||
&path,
|
||||
Some(PackageRoot::root(&package_root)),
|
||||
&settings.linter,
|
||||
@@ -681,8 +557,15 @@ mod tests {
|
||||
UnsafeFixes::Enabled,
|
||||
)
|
||||
.unwrap();
|
||||
if diagnostics.inner.iter().any(Diagnostic::is_invalid_syntax) {
|
||||
parse_errors.push(path.clone());
|
||||
if diagnostics.inner.is_empty() {
|
||||
// We won't load a notebook index from the cache for files without diagnostics,
|
||||
// so remove them from `expected_diagnostics` too. This allows us to keep the
|
||||
// full equality assertion below.
|
||||
diagnostics
|
||||
.notebook_indexes
|
||||
.remove(&path.to_string_lossy().to_string());
|
||||
} else {
|
||||
paths_with_diagnostics.push(path.clone());
|
||||
}
|
||||
paths.push(path);
|
||||
expected_diagnostics += diagnostics;
|
||||
@@ -695,11 +578,11 @@ mod tests {
|
||||
let cache = Cache::open(package_root.clone(), &settings);
|
||||
assert_ne!(cache.package.files.len(), 0);
|
||||
|
||||
parse_errors.sort();
|
||||
paths_with_diagnostics.sort();
|
||||
|
||||
for path in &paths {
|
||||
if parse_errors.binary_search(path).is_ok() {
|
||||
continue; // We don't cache parsing errors.
|
||||
if paths_with_diagnostics.binary_search(path).is_ok() {
|
||||
continue; // We don't cache files with diagnostics.
|
||||
}
|
||||
|
||||
let relative_path = cache.relative_path(path).unwrap();
|
||||
@@ -733,7 +616,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cache_adds_file_on_lint() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("cache_adds_file_on_lint");
|
||||
let cache = test_cache.open();
|
||||
@@ -757,7 +640,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cache_adds_files_on_lint() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("cache_adds_files_on_lint");
|
||||
let cache = test_cache.open();
|
||||
@@ -782,6 +665,40 @@ mod tests {
|
||||
cache.persist().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_does_not_add_file_on_lint_with_diagnostic() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("cache_does_not_add_file_on_lint_with_diagnostic");
|
||||
let cache = test_cache.open();
|
||||
test_cache.write_source_file("source.py", source);
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
|
||||
cache.persist().unwrap();
|
||||
let cache = test_cache.open();
|
||||
|
||||
let results = test_cache
|
||||
.lint_file_with_cache("source.py", &cache)
|
||||
.expect("Failed to lint test file");
|
||||
assert_eq!(results.inner.len(), 1, "Expected one F822 diagnostic");
|
||||
assert_eq!(
|
||||
cache.changes.lock().unwrap().len(),
|
||||
1,
|
||||
"Files with diagnostics still trigger change events"
|
||||
);
|
||||
assert!(
|
||||
cache
|
||||
.changes
|
||||
.lock()
|
||||
.unwrap()
|
||||
.last()
|
||||
.is_some_and(|change| matches!(change.new_data, ChangeData::Linted(false))),
|
||||
"Files with diagnostics are marked as unlinted"
|
||||
);
|
||||
|
||||
cache.persist().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_adds_files_on_format() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
@@ -812,7 +729,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn cache_invalidated_on_file_modified_time() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("cache_invalidated_on_file_modified_time");
|
||||
let cache = test_cache.open();
|
||||
@@ -869,7 +786,7 @@ mod tests {
|
||||
file.set_permissions(perms)
|
||||
}
|
||||
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("cache_invalidated_on_permission_change");
|
||||
let cache = test_cache.open();
|
||||
@@ -922,7 +839,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Now actually lint a file.
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
|
||||
test_cache.write_source_file("new.py", source);
|
||||
let new_path_key = RelativePathBuf::from("new.py");
|
||||
assert_eq!(cache.changes.lock().unwrap().len(), 0);
|
||||
@@ -945,7 +862,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn format_updates_cache_entry() {
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\", \"b\"])\n";
|
||||
let source: &[u8] = b"a = 1\n\n__all__ = list([\"a\"])\n";
|
||||
|
||||
let test_cache = TestCache::new("format_updates_cache_entry");
|
||||
let cache = test_cache.open();
|
||||
@@ -979,7 +896,7 @@ mod tests {
|
||||
panic!("Cache entry for `source.py` is missing.");
|
||||
};
|
||||
|
||||
assert!(file_cache.data.lint.is_some());
|
||||
assert!(file_cache.data.linted);
|
||||
assert!(file_cache.data.formatted);
|
||||
}
|
||||
|
||||
@@ -1029,7 +946,7 @@ mod tests {
|
||||
panic!("Cache entry for `source.py` is missing.");
|
||||
};
|
||||
|
||||
assert_eq!(file_cache.data.lint, None);
|
||||
assert!(!file_cache.data.linted);
|
||||
assert!(file_cache.data.formatted);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,15 +20,21 @@ use ruff_linter::settings::types::UnsafeFixes;
|
||||
use ruff_linter::settings::{LinterSettings, flags};
|
||||
use ruff_linter::source_kind::{SourceError, SourceKind};
|
||||
use ruff_linter::{IOError, Violation, fs};
|
||||
use ruff_notebook::{Notebook, NotebookError, NotebookIndex};
|
||||
use ruff_notebook::{NotebookError, NotebookIndex};
|
||||
use ruff_python_ast::{PySourceType, SourceType, TomlSourceType};
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_workspace::Settings;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::cache::{Cache, FileCacheKey, LintCacheData};
|
||||
use crate::cache::{Cache, FileCache, FileCacheKey};
|
||||
|
||||
/// A collection of [`Diagnostic`]s and additional information needed to render them.
|
||||
///
|
||||
/// Note that `notebook_indexes` may be empty if there are no diagnostics because the
|
||||
/// `NotebookIndex` isn't cached in this case. This isn't a problem for any current uses as of
|
||||
/// 2025-08-12, which are all related to diagnostic rendering, but could be surprising if used
|
||||
/// differently in the future.
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub(crate) struct Diagnostics {
|
||||
pub(crate) inner: Vec<Diagnostic>,
|
||||
@@ -193,19 +199,9 @@ pub(crate) fn lint_path(
|
||||
let cache_key = FileCacheKey::from_path(path).context("Failed to create cache key")?;
|
||||
let cached_diagnostics = cache
|
||||
.get(relative_path, &cache_key)
|
||||
.and_then(|entry| entry.to_diagnostics(path));
|
||||
if let Some(diagnostics) = cached_diagnostics {
|
||||
// `FixMode::Generate` and `FixMode::Diff` rely on side-effects (writing to disk,
|
||||
// and writing the diff to stdout, respectively). If a file has diagnostics, we
|
||||
// need to avoid reading from and writing to the cache in these modes.
|
||||
if match fix_mode {
|
||||
flags::FixMode::Generate => true,
|
||||
flags::FixMode::Apply | flags::FixMode::Diff => {
|
||||
diagnostics.inner.is_empty() && diagnostics.fixed.is_empty()
|
||||
}
|
||||
} {
|
||||
return Ok(diagnostics);
|
||||
}
|
||||
.is_some_and(FileCache::linted);
|
||||
if cached_diagnostics {
|
||||
return Ok(Diagnostics::default());
|
||||
}
|
||||
|
||||
// Stash the file metadata for later so when we update the cache it reflects the prerun
|
||||
@@ -322,31 +318,21 @@ pub(crate) fn lint_path(
|
||||
(result, transformed, fixed)
|
||||
};
|
||||
|
||||
let has_error = result.has_syntax_errors();
|
||||
let diagnostics = result.diagnostics;
|
||||
|
||||
if let Some((cache, relative_path, key)) = caching {
|
||||
// We don't cache parsing errors.
|
||||
if !has_error {
|
||||
// `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk,
|
||||
// and writing the diff to stdout, respectively). If a file has diagnostics, we
|
||||
// need to avoid reading from and writing to the cache in these modes.
|
||||
if match fix_mode {
|
||||
flags::FixMode::Generate => true,
|
||||
flags::FixMode::Apply | flags::FixMode::Diff => {
|
||||
diagnostics.is_empty() && fixed.is_empty()
|
||||
}
|
||||
} {
|
||||
cache.update_lint(
|
||||
relative_path.to_owned(),
|
||||
&key,
|
||||
LintCacheData::from_diagnostics(
|
||||
&diagnostics,
|
||||
transformed.as_ipy_notebook().map(Notebook::index).cloned(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// `FixMode::Apply` and `FixMode::Diff` rely on side-effects (writing to disk,
|
||||
// and writing the diff to stdout, respectively). If a file has diagnostics
|
||||
// with fixes, we need to avoid reading from and writing to the cache in these
|
||||
// modes.
|
||||
let use_fixes = match fix_mode {
|
||||
flags::FixMode::Generate => true,
|
||||
flags::FixMode::Apply | flags::FixMode::Diff => fixed.is_empty(),
|
||||
};
|
||||
|
||||
// We don't cache files with diagnostics.
|
||||
let linted = diagnostics.is_empty() && use_fixes;
|
||||
cache.set_linted(relative_path.to_owned(), &key, linted);
|
||||
}
|
||||
|
||||
let notebook_indexes = if let SourceKind::IpyNotebook(notebook) = transformed {
|
||||
|
||||
@@ -19,7 +19,8 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "riscv64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
|
||||
@@ -15,8 +15,7 @@ use ruff_db::diagnostic::{
|
||||
use ruff_linter::fs::relativize_path;
|
||||
use ruff_linter::logging::LogLevel;
|
||||
use ruff_linter::message::{
|
||||
Emitter, EmitterContext, GithubEmitter, GitlabEmitter, GroupedEmitter, SarifEmitter,
|
||||
TextEmitter,
|
||||
Emitter, EmitterContext, GithubEmitter, GroupedEmitter, SarifEmitter, TextEmitter,
|
||||
};
|
||||
use ruff_linter::notify_user;
|
||||
use ruff_linter::settings::flags::{self};
|
||||
@@ -296,7 +295,11 @@ impl Printer {
|
||||
GithubEmitter.emit(writer, &diagnostics.inner, &context)?;
|
||||
}
|
||||
OutputFormat::Gitlab => {
|
||||
GitlabEmitter::default().emit(writer, &diagnostics.inner, &context)?;
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Gitlab)
|
||||
.preview(preview);
|
||||
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
|
||||
write!(writer, "{value}")?;
|
||||
}
|
||||
OutputFormat::Pylint => {
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
|
||||
@@ -115,12 +115,13 @@ fn stdin_error() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> -:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
@@ -139,12 +140,13 @@ fn stdin_filename() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
F401.py:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> F401.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
@@ -174,19 +176,21 @@ import bar # unused import
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
bar.py:2:8: F401 [*] `bar` imported but unused
|
||||
F401 [*] `bar` imported but unused
|
||||
--> bar.py:2:8
|
||||
|
|
||||
2 | import bar # unused import
|
||||
| ^^^ F401
|
||||
| ^^^
|
||||
|
|
||||
= help: Remove unused import: `bar`
|
||||
help: Remove unused import: `bar`
|
||||
|
||||
foo.py:2:8: F401 [*] `foo` imported but unused
|
||||
F401 [*] `foo` imported but unused
|
||||
--> foo.py:2:8
|
||||
|
|
||||
2 | import foo # unused import
|
||||
| ^^^ F401
|
||||
| ^^^
|
||||
|
|
||||
= help: Remove unused import: `foo`
|
||||
help: Remove unused import: `foo`
|
||||
|
||||
Found 2 errors.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
@@ -208,12 +212,13 @@ fn check_warn_stdin_filename_with_files() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
F401.py:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> F401.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
@@ -234,12 +239,13 @@ fn stdin_source_type_py() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
TCH.py:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> TCH.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
@@ -471,10 +477,11 @@ fn stdin_fix_jupyter() {
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
----- stderr -----
|
||||
Jupyter.ipynb:cell 3:1:7: F821 Undefined name `x`
|
||||
F821 Undefined name `x`
|
||||
--> Jupyter.ipynb:cell 3:1:7
|
||||
|
|
||||
1 | print(x)
|
||||
| ^ F821
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 3 errors (2 fixed, 1 remaining).
|
||||
@@ -569,19 +576,21 @@ fn stdin_override_parser_ipynb() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
Jupyter.py:cell 1:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> Jupyter.py:cell 1:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
Jupyter.py:cell 3:1:8: F401 [*] `sys` imported but unused
|
||||
F401 [*] `sys` imported but unused
|
||||
--> Jupyter.py:cell 3:1:8
|
||||
|
|
||||
1 | import sys
|
||||
| ^^^ F401
|
||||
| ^^^
|
||||
|
|
||||
= help: Remove unused import: `sys`
|
||||
help: Remove unused import: `sys`
|
||||
|
||||
Found 2 errors.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
@@ -605,12 +614,13 @@ fn stdin_override_parser_py() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
F401.ipynb:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> F401.ipynb:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
@@ -633,12 +643,13 @@ fn stdin_fix_when_not_fixable_should_still_print_contents() {
|
||||
print(sys.version)
|
||||
|
||||
----- stderr -----
|
||||
-:3:4: F634 If test is a tuple, which is always `True`
|
||||
F634 If test is a tuple, which is always `True`
|
||||
--> -:3:4
|
||||
|
|
||||
1 | import sys
|
||||
2 |
|
||||
3 | if (1, 2):
|
||||
| ^^^^^^ F634
|
||||
| ^^^^^^
|
||||
4 | print(sys.version)
|
||||
|
|
||||
|
||||
@@ -798,7 +809,8 @@ fn stdin_parse_error() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:16: invalid-syntax: Expected one or more symbol names after import
|
||||
invalid-syntax: Expected one or more symbol names after import
|
||||
--> -:1:16
|
||||
|
|
||||
1 | from foo import
|
||||
| ^
|
||||
@@ -818,14 +830,16 @@ fn stdin_multiple_parse_error() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:16: invalid-syntax: Expected one or more symbol names after import
|
||||
invalid-syntax: Expected one or more symbol names after import
|
||||
--> -:1:16
|
||||
|
|
||||
1 | from foo import
|
||||
| ^
|
||||
2 | bar =
|
||||
|
|
||||
|
||||
-:2:6: invalid-syntax: Expected an expression
|
||||
invalid-syntax: Expected an expression
|
||||
--> -:2:6
|
||||
|
|
||||
1 | from foo import
|
||||
2 | bar =
|
||||
@@ -847,7 +861,8 @@ fn parse_error_not_included() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:6: invalid-syntax: Expected an expression
|
||||
invalid-syntax: Expected an expression
|
||||
--> -:1:6
|
||||
|
|
||||
1 | foo =
|
||||
| ^
|
||||
@@ -867,10 +882,11 @@ fn full_output_preview() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
E741 Ambiguous variable name: `l`
|
||||
--> -:1:1
|
||||
|
|
||||
1 | l = 1
|
||||
| ^ E741
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
@@ -895,10 +911,11 @@ preview = true
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
E741 Ambiguous variable name: `l`
|
||||
--> -:1:1
|
||||
|
|
||||
1 | l = 1
|
||||
| ^ E741
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
@@ -916,10 +933,11 @@ fn full_output_format() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: E741 Ambiguous variable name: `l`
|
||||
E741 Ambiguous variable name: `l`
|
||||
--> -:1:1
|
||||
|
|
||||
1 | l = 1
|
||||
| ^ E741
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
@@ -1406,7 +1424,9 @@ fn redirect_direct() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
||||
RUF950 Hey this is a test rule that was redirected from another.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1438,7 +1458,9 @@ fn redirect_prefix() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
||||
RUF950 Hey this is a test rule that was redirected from another.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1455,7 +1477,9 @@ fn deprecated_direct() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
RUF920 Hey this is a deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1472,8 +1496,12 @@ fn deprecated_multiple_direct() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
-:1:1: RUF921 Hey this is another deprecated test rule.
|
||||
RUF920 Hey this is a deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF921 Hey this is another deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1491,8 +1519,12 @@ fn deprecated_indirect() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
-:1:1: RUF921 Hey this is another deprecated test rule.
|
||||
RUF920 Hey this is a deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF921 Hey this is another deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1638,22 +1670,23 @@ fn check_input_from_argfile() -> Result<()> {
|
||||
(file_a_path.display().to_string().as_str(), "/path/to/a.py"),
|
||||
]}, {
|
||||
assert_cmd_snapshot!(cmd
|
||||
.pass_stdin(""), @r###"
|
||||
.pass_stdin(""), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
/path/to/a.py:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> /path/to/a.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -1669,8 +1702,12 @@ fn check_hints_hidden_unsafe_fixes() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -1687,7 +1724,9 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -1705,8 +1744,12 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the --fix option.
|
||||
|
||||
@@ -1725,7 +1768,9 @@ fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1742,8 +1787,12 @@ fn check_shows_unsafe_fixes_with_opt_in() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
[*] 2 fixable with the --fix option.
|
||||
|
||||
@@ -1764,7 +1813,9 @@ fn fix_applies_safe_fixes_by_default() {
|
||||
# fix from stable-test-rule-safe-fix
|
||||
|
||||
----- stderr -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors (1 fixed, 1 remaining).
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
");
|
||||
@@ -1801,7 +1852,9 @@ fn fix_does_not_apply_display_only_fixes() {
|
||||
----- stdout -----
|
||||
def add_to_list(item, some_list=[]): ...
|
||||
----- stderr -----
|
||||
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
||||
RUF903 Hey this is a stable test rule with a display only fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
");
|
||||
}
|
||||
@@ -1819,7 +1872,9 @@ fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
|
||||
----- stdout -----
|
||||
def add_to_list(item, some_list=[]): ...
|
||||
----- stderr -----
|
||||
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
||||
RUF903 Hey this is a stable test rule with a display only fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
");
|
||||
}
|
||||
@@ -1836,7 +1891,9 @@ fn fix_only_unsafe_fixes_available() {
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 1 error.
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
");
|
||||
@@ -1972,8 +2029,12 @@ extend-unsafe-fixes = ["RUF901"]
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 Hey this is a stable test rule with a safe fix.
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF901 Hey this is a stable test rule with a safe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -2004,8 +2065,12 @@ extend-safe-fixes = ["RUF902"]
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
@@ -2038,8 +2103,12 @@ extend-safe-fixes = ["RUF902"]
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -2074,13 +2143,27 @@ extend-safe-fixes = ["RUF9"]
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF900 Hey this is a stable test rule.
|
||||
-:1:1: RUF901 Hey this is a stable test rule with a safe fix.
|
||||
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
-:1:1: RUF921 Hey this is another deprecated test rule.
|
||||
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
||||
RUF900 Hey this is a stable test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF901 Hey this is a stable test rule with a safe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF903 Hey this is a stable test rule with a display only fix.
|
||||
--> -:1:1
|
||||
|
||||
RUF920 Hey this is a deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF921 Hey this is another deprecated test rule.
|
||||
--> -:1:1
|
||||
|
||||
RUF950 Hey this is a test rule that was redirected from another.
|
||||
--> -:1:1
|
||||
|
||||
Found 7 errors.
|
||||
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -2141,10 +2224,11 @@ def log(x, base) -> float:
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:2:5: D417 Missing argument description in the docstring for `log`: `base`
|
||||
D417 Missing argument description in the docstring for `log`: `base`
|
||||
--> -:2:5
|
||||
|
|
||||
2 | def log(x, base) -> float:
|
||||
| ^^^ D417
|
||||
| ^^^
|
||||
3 | """Calculate natural log of a value
|
||||
|
|
||||
|
||||
@@ -2177,14 +2261,15 @@ select = ["RUF017"]
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:3:1: RUF017 Avoid quadratic list summation
|
||||
RUF017 Avoid quadratic list summation
|
||||
--> -:3:1
|
||||
|
|
||||
1 | x = [1, 2, 3]
|
||||
2 | y = [4, 5, 6]
|
||||
3 | sum([x, y], [])
|
||||
| ^^^^^^^^^^^^^^^ RUF017
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: Replace with `functools.reduce`
|
||||
help: Replace with `functools.reduce`
|
||||
|
||||
Found 1 error.
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
@@ -2217,14 +2302,15 @@ unfixable = ["RUF"]
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:3:1: RUF017 Avoid quadratic list summation
|
||||
RUF017 Avoid quadratic list summation
|
||||
--> -:3:1
|
||||
|
|
||||
1 | x = [1, 2, 3]
|
||||
2 | y = [4, 5, 6]
|
||||
3 | sum([x, y], [])
|
||||
| ^^^^^^^^^^^^^^^ RUF017
|
||||
| ^^^^^^^^^^^^^^^
|
||||
|
|
||||
= help: Replace with `functools.reduce`
|
||||
help: Replace with `functools.reduce`
|
||||
|
||||
Found 1 error.
|
||||
|
||||
@@ -2246,10 +2332,11 @@ fn pyproject_toml_stdin_syntax_error() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
pyproject.toml:1:9: RUF200 Failed to parse pyproject.toml: unclosed table, expected `]`
|
||||
RUF200 Failed to parse pyproject.toml: unclosed table, expected `]`
|
||||
--> pyproject.toml:1:9
|
||||
|
|
||||
1 | [project
|
||||
| ^ RUF200
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
@@ -2271,11 +2358,12 @@ fn pyproject_toml_stdin_schema_error() {
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
pyproject.toml:2:8: RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
|
||||
RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
|
||||
--> pyproject.toml:2:8
|
||||
|
|
||||
1 | [project]
|
||||
2 | name = 1
|
||||
| ^ RUF200
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
@@ -2363,11 +2451,12 @@ fn pyproject_toml_stdin_schema_error_fix() {
|
||||
[project]
|
||||
name = 1
|
||||
----- stderr -----
|
||||
pyproject.toml:2:8: RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
|
||||
RUF200 Failed to parse pyproject.toml: invalid type: integer `1`, expected a string
|
||||
--> pyproject.toml:2:8
|
||||
|
|
||||
1 | [project]
|
||||
2 | name = 1
|
||||
| ^ RUF200
|
||||
| ^
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
@@ -5588,15 +5588,15 @@ fn cookiecutter_globbing() -> Result<()> {
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--select=F811")
|
||||
.current_dir(tempdir.path()), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
{{cookiecutter.repo_name}}/tests/maintest.py:3:8: F811 [*] Redefinition of unused `foo` from line 1: `foo` redefined here
|
||||
Found 1 error.
|
||||
[*] 1 fixable with the `--fix` option.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -5801,3 +5801,32 @@ fn future_annotations_preview_warning() {
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn up045_nested_optional_flatten_all() {
|
||||
let contents = "\
|
||||
from typing import Optional
|
||||
nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
";
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--select", "UP045", "--diff", "--target-version", "py312"])
|
||||
.arg("-")
|
||||
.pass_stdin(contents),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -1,2 +1,2 @@
|
||||
from typing import Optional
|
||||
-nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
+nested_optional: str | None = None
|
||||
|
||||
|
||||
----- stderr -----
|
||||
Would fix 1 error.
|
||||
",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,25 +16,28 @@ info:
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
input.py:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> input.py:1:8
|
||||
|
|
||||
1 | import os # F401
|
||||
| ^^ F401
|
||||
| ^^
|
||||
2 | x = y # F821
|
||||
3 | match 42: # invalid-syntax
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
input.py:2:5: F821 Undefined name `y`
|
||||
F821 Undefined name `y`
|
||||
--> input.py:2:5
|
||||
|
|
||||
1 | import os # F401
|
||||
2 | x = y # F821
|
||||
| ^ F821
|
||||
| ^
|
||||
3 | match 42: # invalid-syntax
|
||||
4 | case _: ...
|
||||
|
|
||||
|
||||
input.py:3:1: invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
|
||||
--> input.py:3:1
|
||||
|
|
||||
1 | import os # F401
|
||||
2 | x = y # F821
|
||||
|
||||
@@ -19,60 +19,60 @@ exit_code: 1
|
||||
[
|
||||
{
|
||||
"check_name": "F401",
|
||||
"description": "`os` imported but unused",
|
||||
"description": "F401: `os` imported but unused",
|
||||
"severity": "major",
|
||||
"fingerprint": "4dbad37161e65c72",
|
||||
"location": {
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 8,
|
||||
"line": 1
|
||||
"line": 1,
|
||||
"column": 8
|
||||
},
|
||||
"end": {
|
||||
"column": 10,
|
||||
"line": 1
|
||||
"line": 1,
|
||||
"column": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
},
|
||||
{
|
||||
"check_name": "F821",
|
||||
"description": "Undefined name `y`",
|
||||
"description": "F821: Undefined name `y`",
|
||||
"severity": "major",
|
||||
"fingerprint": "7af59862a085230",
|
||||
"location": {
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 5,
|
||||
"line": 2
|
||||
"line": 2,
|
||||
"column": 5
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 2
|
||||
"line": 2,
|
||||
"column": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
},
|
||||
{
|
||||
"check_name": "syntax-error",
|
||||
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
|
||||
"check_name": "invalid-syntax",
|
||||
"description": "invalid-syntax: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
|
||||
"severity": "major",
|
||||
"fingerprint": "e558cec859bb66e8",
|
||||
"location": {
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 1,
|
||||
"line": 3
|
||||
"line": 3,
|
||||
"column": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 3
|
||||
"line": 3,
|
||||
"column": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
}
|
||||
]
|
||||
----- stderr -----
|
||||
|
||||
@@ -1201,11 +1201,16 @@ fn format_snippet<'m>(
|
||||
|
||||
let is_file_level = snippet.annotations.iter().any(|ann| ann.is_file_level);
|
||||
if is_file_level {
|
||||
assert!(
|
||||
snippet.source.is_empty(),
|
||||
"Non-empty file-level snippet that won't be rendered: {:?}",
|
||||
snippet.source
|
||||
);
|
||||
// TODO(brent) enable this assertion again once we set `is_file_level` for individual rules.
|
||||
// It's causing too many false positives currently when the default is to make any
|
||||
// annotation with a default range file-level. See
|
||||
// https://github.com/astral-sh/ruff/issues/19688.
|
||||
//
|
||||
// assert!(
|
||||
// snippet.source.is_empty(),
|
||||
// "Non-empty file-level snippet that won't be rendered: {:?}",
|
||||
// snippet.source
|
||||
// );
|
||||
let header = format_header(origin, main_range, &[], is_first, snippet.cell_index);
|
||||
return DisplaySet {
|
||||
display_lines: header.map_or_else(Vec::new, |header| vec![header]),
|
||||
@@ -1273,13 +1278,20 @@ fn format_header<'a>(
|
||||
..
|
||||
} = item
|
||||
{
|
||||
if main_range >= range.0 && main_range < range.1 + max(*end_line as usize, 1) {
|
||||
// At the very end of the `main_range`, report the location as the first character
|
||||
// in the next line instead of falling back to the default location of `1:1`. This
|
||||
// is another divergence from upstream.
|
||||
let end_of_range = range.1 + max(*end_line as usize, 1);
|
||||
if main_range >= range.0 && main_range < end_of_range {
|
||||
let char_column = text[0..(main_range - range.0).min(text.len())]
|
||||
.chars()
|
||||
.count();
|
||||
col = char_column + 1;
|
||||
line_offset = lineno.unwrap_or(1);
|
||||
break;
|
||||
} else if main_range == end_of_range {
|
||||
line_offset = lineno.map_or(1, |line| line + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,5 +86,5 @@ walltime = ["ruff_db/os", "ty_project", "divan"]
|
||||
[target.'cfg(target_os = "windows")'.dev-dependencies]
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies]
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64", target_arch = "riscv64")))'.dev-dependencies]
|
||||
tikv-jemallocator = { workspace = true }
|
||||
|
||||
@@ -21,7 +21,8 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "riscv64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
|
||||
@@ -18,7 +18,8 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "riscv64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
|
||||
@@ -26,7 +26,8 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "riscv64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
@@ -42,7 +43,8 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "riscv64"
|
||||
)
|
||||
))]
|
||||
#[unsafe(export_name = "_rjem_malloc_conf")]
|
||||
@@ -77,8 +79,11 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
|
||||
b.iter_batched(
|
||||
|| parsed.clone(),
|
||||
|parsed| {
|
||||
// Assert that file contains no parse errors
|
||||
assert!(parsed.has_valid_syntax());
|
||||
|
||||
let path = case.path();
|
||||
let result = lint_only(
|
||||
lint_only(
|
||||
&path,
|
||||
None,
|
||||
settings,
|
||||
@@ -86,10 +91,7 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
|
||||
&SourceKind::Python(case.code().to_string()),
|
||||
PySourceType::from(path.as_path()),
|
||||
ParseSource::Precomputed(parsed),
|
||||
);
|
||||
|
||||
// Assert that file contains no parse errors
|
||||
assert!(!result.has_syntax_errors());
|
||||
)
|
||||
},
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
|
||||
@@ -20,7 +20,8 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "riscv64"
|
||||
)
|
||||
))]
|
||||
#[global_allocator]
|
||||
|
||||
@@ -450,9 +450,6 @@ fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) {
|
||||
r#"
|
||||
class C:
|
||||
def f(self: "C"):
|
||||
self.a = ""
|
||||
self.b = ""
|
||||
|
||||
if isinstance(self.a, str):
|
||||
return
|
||||
|
||||
@@ -466,6 +463,56 @@ fn benchmark_complex_constrained_attributes_2(criterion: &mut Criterion) {
|
||||
return
|
||||
if isinstance(self.b, str):
|
||||
return
|
||||
if isinstance(self.b, str):
|
||||
return
|
||||
if isinstance(self.b, str):
|
||||
return
|
||||
|
||||
self.a = ""
|
||||
self.b = ""
|
||||
"#,
|
||||
)
|
||||
},
|
||||
|case| {
|
||||
let Case { db, .. } = case;
|
||||
let result = db.check();
|
||||
assert_eq!(result.len(), 0);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_complex_constrained_attributes_3(criterion: &mut Criterion) {
|
||||
setup_rayon();
|
||||
|
||||
criterion.bench_function("ty_micro[complex_constrained_attributes_3]", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| {
|
||||
// This is a regression test for https://github.com/astral-sh/ty/issues/758
|
||||
setup_micro_case(
|
||||
r#"
|
||||
class GridOut:
|
||||
def __init__(self: "GridOut") -> None:
|
||||
self._buffer = b""
|
||||
|
||||
def _read_size_or_line(self: "GridOut", size: int = -1):
|
||||
if size > self._position:
|
||||
size = self._position
|
||||
pass
|
||||
if size == 0:
|
||||
return bytes()
|
||||
|
||||
while size > 0:
|
||||
if self._buffer:
|
||||
buf = self._buffer
|
||||
self._buffer = b""
|
||||
else:
|
||||
buf = b""
|
||||
|
||||
if len(buf) > size:
|
||||
self._buffer = buf
|
||||
self._position -= len(self._buffer)
|
||||
"#,
|
||||
)
|
||||
},
|
||||
@@ -668,6 +715,7 @@ criterion_group!(
|
||||
benchmark_tuple_implicit_instance_attributes,
|
||||
benchmark_complex_constrained_attributes_1,
|
||||
benchmark_complex_constrained_attributes_2,
|
||||
benchmark_complex_constrained_attributes_3,
|
||||
benchmark_many_enum_members,
|
||||
);
|
||||
criterion_group!(project, anyio, attrs, hydra, datetype);
|
||||
|
||||
@@ -218,6 +218,24 @@ static TANJUN: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::ne
|
||||
)
|
||||
});
|
||||
|
||||
static STATIC_FRAME: std::sync::LazyLock<Benchmark<'static>> = std::sync::LazyLock::new(|| {
|
||||
Benchmark::new(
|
||||
RealWorldProject {
|
||||
name: "static-frame",
|
||||
repository: "https://github.com/static-frame/static-frame",
|
||||
commit: "34962b41baca5e7f98f5a758d530bff02748a421",
|
||||
paths: vec![SystemPath::new("static_frame")],
|
||||
// N.B. `arraykit` is installed as a dependency during mypy_primer runs,
|
||||
// but it takes much longer to be installed in a Codspeed run than it does in a mypy_primer run
|
||||
// (seems to be built from source on the Codspeed CI runners for some reason).
|
||||
dependencies: vec!["numpy"],
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
500,
|
||||
)
|
||||
});
|
||||
|
||||
#[track_caller]
|
||||
fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
bencher
|
||||
@@ -232,7 +250,7 @@ fn small(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
#[bench(args=[&*COLOUR_SCIENCE, &*PANDAS], sample_size=1, sample_count=3)]
|
||||
#[bench(args=[&*COLOUR_SCIENCE, &*PANDAS, &*STATIC_FRAME], sample_size=1, sample_count=3)]
|
||||
fn medium(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
@@ -34,12 +34,14 @@ glob = { workspace = true }
|
||||
ignore = { workspace = true, optional = true }
|
||||
matchit = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
pathdiff = { workspace = true }
|
||||
quick-junit = { workspace = true, optional = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
similar = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
@@ -52,7 +54,7 @@ web-time = { version = "1.1.0" }
|
||||
etcetera = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -254,6 +254,11 @@ impl Diagnostic {
|
||||
.find(|ann| ann.is_primary)
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of all annotations of this diagnostic.
|
||||
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
|
||||
Arc::make_mut(&mut self.inner).annotations.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the "primary" span of this diagnostic if one exists.
|
||||
///
|
||||
/// When there are multiple primary spans, then the first one that was
|
||||
@@ -310,11 +315,21 @@ impl Diagnostic {
|
||||
&self.inner.subs
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of the sub-diagnostics of this diagnostic.
|
||||
pub fn sub_diagnostics_mut(&mut self) -> impl Iterator<Item = &mut SubDiagnostic> {
|
||||
Arc::make_mut(&mut self.inner).subs.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns the fix for this diagnostic if it exists.
|
||||
pub fn fix(&self) -> Option<&Fix> {
|
||||
self.inner.fix.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn fix_mut(&mut self) -> Option<&mut Fix> {
|
||||
Arc::make_mut(&mut self.inner).fix.as_mut()
|
||||
}
|
||||
|
||||
/// Set the fix for this diagnostic.
|
||||
pub fn set_fix(&mut self, fix: Fix) {
|
||||
debug_assert!(
|
||||
@@ -621,6 +636,11 @@ impl SubDiagnostic {
|
||||
&self.inner.annotations
|
||||
}
|
||||
|
||||
/// Returns a mutable borrow of the annotations of this sub-diagnostic.
|
||||
pub fn annotations_mut(&mut self) -> impl Iterator<Item = &mut Annotation> {
|
||||
self.inner.annotations.iter_mut()
|
||||
}
|
||||
|
||||
/// Returns a shared borrow of the "primary" annotation of this diagnostic
|
||||
/// if one exists.
|
||||
///
|
||||
@@ -1279,6 +1299,10 @@ pub struct DisplayDiagnosticConfig {
|
||||
hide_severity: bool,
|
||||
/// Whether to show the availability of a fix in a diagnostic.
|
||||
show_fix_status: bool,
|
||||
/// Whether to show the diff for an available fix after the main diagnostic.
|
||||
///
|
||||
/// This currently only applies to `DiagnosticFormat::Full`.
|
||||
show_fix_diff: bool,
|
||||
/// The lowest applicability that should be shown when reporting diagnostics.
|
||||
fix_applicability: Applicability,
|
||||
}
|
||||
@@ -1326,6 +1350,14 @@ impl DisplayDiagnosticConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to show a diff for an available fix after the main diagnostic.
|
||||
pub fn show_fix_diff(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
show_fix_diff: yes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the lowest fix applicability that should be shown.
|
||||
///
|
||||
/// In other words, an applicability of `Safe` (the default) would suppress showing fixes or fix
|
||||
@@ -1349,6 +1381,7 @@ impl Default for DisplayDiagnosticConfig {
|
||||
preview: false,
|
||||
hide_severity: false,
|
||||
show_fix_status: false,
|
||||
show_fix_diff: false,
|
||||
fix_applicability: Applicability::Safe,
|
||||
}
|
||||
}
|
||||
@@ -1402,6 +1435,11 @@ pub enum DiagnosticFormat {
|
||||
/// Print diagnostics in the format expected by JUnit.
|
||||
#[cfg(feature = "junit")]
|
||||
Junit,
|
||||
/// Print diagnostics in the JSON format used by GitLab [Code Quality] reports.
|
||||
///
|
||||
/// [Code Quality]: https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
#[cfg(feature = "serde")]
|
||||
Gitlab,
|
||||
}
|
||||
|
||||
/// A representation of the kinds of messages inside a diagnostic.
|
||||
|
||||
@@ -2,15 +2,15 @@ use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
use full::FullRenderer;
|
||||
use ruff_annotate_snippets::{
|
||||
Annotation as AnnotateAnnotation, Level as AnnotateLevel, Message as AnnotateMessage,
|
||||
Renderer as AnnotateRenderer, Snippet as AnnotateSnippet,
|
||||
Snippet as AnnotateSnippet,
|
||||
};
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::diagnostic::stylesheet::DiagnosticStylesheet;
|
||||
use crate::{
|
||||
Db,
|
||||
files::File,
|
||||
@@ -31,6 +31,8 @@ mod azure;
|
||||
mod concise;
|
||||
mod full;
|
||||
#[cfg(feature = "serde")]
|
||||
mod gitlab;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json_lines;
|
||||
@@ -111,37 +113,7 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
ConciseRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
|
||||
}
|
||||
DiagnosticFormat::Full => {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
let mut renderer = if self.config.color {
|
||||
AnnotateRenderer::styled()
|
||||
} else {
|
||||
AnnotateRenderer::plain()
|
||||
}
|
||||
.cut_indicator("…");
|
||||
|
||||
renderer = renderer
|
||||
.error(stylesheet.error)
|
||||
.warning(stylesheet.warning)
|
||||
.info(stylesheet.info)
|
||||
.note(stylesheet.note)
|
||||
.help(stylesheet.help)
|
||||
.line_no(stylesheet.line_no)
|
||||
.emphasis(stylesheet.emphasis)
|
||||
.none(stylesheet.none);
|
||||
|
||||
for diag in self.diagnostics {
|
||||
let resolved = Resolved::new(self.resolver, diag, self.config);
|
||||
let renderable = resolved.to_renderable(self.config.context);
|
||||
for diag in renderable.diagnostics.iter() {
|
||||
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
FullRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
|
||||
}
|
||||
DiagnosticFormat::Azure => {
|
||||
AzureRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
@@ -166,6 +138,10 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
DiagnosticFormat::Junit => {
|
||||
junit::JunitRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
#[cfg(feature = "serde")]
|
||||
DiagnosticFormat::Gitlab => {
|
||||
gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -242,7 +218,12 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = ann.span.file.path(resolver);
|
||||
let path = ann
|
||||
.span
|
||||
.file
|
||||
.relative_path(resolver)
|
||||
.to_str()
|
||||
.unwrap_or_else(|| ann.span.file.path(resolver));
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
|
||||
})
|
||||
@@ -289,7 +270,12 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
.annotations
|
||||
.iter()
|
||||
.filter_map(|ann| {
|
||||
let path = ann.span.file.path(resolver);
|
||||
let path = ann
|
||||
.span
|
||||
.file
|
||||
.relative_path(resolver)
|
||||
.to_str()
|
||||
.unwrap_or_else(|| ann.span.file.path(resolver));
|
||||
let diagnostic_source = ann.span.file.diagnostic_source(resolver);
|
||||
ResolvedAnnotation::new(path, &diagnostic_source, ann, resolver)
|
||||
})
|
||||
@@ -655,6 +641,22 @@ impl<'r> RenderableSnippet<'r> {
|
||||
.as_source_code()
|
||||
.slice(TextRange::new(snippet_start, snippet_end));
|
||||
|
||||
// Strip the BOM from the beginning of the snippet, if present. Doing this here saves us the
|
||||
// trouble of updating the annotation ranges in `replace_unprintable`, and also allows us to
|
||||
// check that the BOM is at the very beginning of the file, not just the beginning of the
|
||||
// snippet.
|
||||
const BOM: char = '\u{feff}';
|
||||
let bom_len = BOM.text_len();
|
||||
let (snippet, snippet_start) =
|
||||
if snippet_start == TextSize::ZERO && snippet.starts_with(BOM) {
|
||||
(
|
||||
&snippet[bom_len.to_usize()..],
|
||||
snippet_start + TextSize::new(bom_len.to_u32()),
|
||||
)
|
||||
} else {
|
||||
(snippet, snippet_start)
|
||||
};
|
||||
|
||||
let annotations = anns
|
||||
.iter()
|
||||
.map(|ann| RenderableAnnotation::new(snippet_start, ann))
|
||||
@@ -719,7 +721,11 @@ impl<'r> RenderableAnnotation<'r> {
|
||||
/// lifetime parameter here refers to the lifetime of the resolver that
|
||||
/// created the given `ResolvedAnnotation`.
|
||||
fn new(snippet_start: TextSize, ann: &'_ ResolvedAnnotation<'r>) -> RenderableAnnotation<'r> {
|
||||
let range = ann.range - snippet_start;
|
||||
// This should only ever saturate if a BOM is present _and_ the annotation range points
|
||||
// before the BOM (i.e. at offset 0). In Ruff this typically results from the use of
|
||||
// `TextRange::default()` for a diagnostic range instead of a range relative to file
|
||||
// contents.
|
||||
let range = ann.range.checked_sub(snippet_start).unwrap_or(ann.range);
|
||||
RenderableAnnotation {
|
||||
range,
|
||||
message: ann.message,
|
||||
@@ -1000,7 +1006,12 @@ fn replace_unprintable<'r>(
|
||||
let mut last_end = 0;
|
||||
let mut result = String::new();
|
||||
for (index, c) in source.char_indices() {
|
||||
if let Some(printable) = unprintable_replacement(c) {
|
||||
// normalize `\r` line endings but don't double `\r\n`
|
||||
if c == '\r' && !source[index + 1..].starts_with("\n") {
|
||||
result.push_str(&source[last_end..index]);
|
||||
result.push('\n');
|
||||
last_end = index + 1;
|
||||
} else if let Some(printable) = unprintable_replacement(c) {
|
||||
result.push_str(&source[last_end..index]);
|
||||
|
||||
let len = printable.text_len().to_u32();
|
||||
@@ -2617,6 +2628,13 @@ watermelon
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Show a diff for the fix when rendering.
|
||||
pub(super) fn show_fix_diff(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.show_fix_diff(yes);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// The lowest fix applicability to show when rendering.
|
||||
pub(super) fn fix_applicability(&mut self, applicability: Applicability) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
|
||||
@@ -1,7 +1,304 @@
|
||||
use std::borrow::Cow;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use anstyle::Style;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
|
||||
use ruff_annotate_snippets::Renderer as AnnotateRenderer;
|
||||
use ruff_diagnostics::{Applicability, Fix};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::diagnostic::render::{FileResolver, Resolved};
|
||||
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
|
||||
use crate::diagnostic::{Diagnostic, DiagnosticSource, DisplayDiagnosticConfig};
|
||||
|
||||
pub(super) struct FullRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl<'a> FullRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver, config: &'a DisplayDiagnosticConfig) -> Self {
|
||||
Self { resolver, config }
|
||||
}
|
||||
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
let mut renderer = if self.config.color {
|
||||
AnnotateRenderer::styled()
|
||||
} else {
|
||||
AnnotateRenderer::plain()
|
||||
}
|
||||
.cut_indicator("…");
|
||||
|
||||
renderer = renderer
|
||||
.error(stylesheet.error)
|
||||
.warning(stylesheet.warning)
|
||||
.info(stylesheet.info)
|
||||
.note(stylesheet.note)
|
||||
.help(stylesheet.help)
|
||||
.line_no(stylesheet.line_no)
|
||||
.emphasis(stylesheet.emphasis)
|
||||
.none(stylesheet.none);
|
||||
|
||||
for diag in diagnostics {
|
||||
let resolved = Resolved::new(self.resolver, diag, self.config);
|
||||
let renderable = resolved.to_renderable(self.config.context);
|
||||
for diag in renderable.diagnostics.iter() {
|
||||
writeln!(f, "{}", renderer.render(diag.to_annotate()))?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
|
||||
if self.config.show_fix_diff {
|
||||
if let Some(diff) = Diff::from_diagnostic(diag, &stylesheet, self.resolver) {
|
||||
writeln!(f, "{diff}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a diff that shows the code fixes.
|
||||
///
|
||||
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try
|
||||
/// * Improve layout
|
||||
/// * Replace tabs with spaces for a consistent experience across terminals
|
||||
/// * Replace zero-width whitespaces
|
||||
/// * Print a simpler diff if only a single line has changed
|
||||
/// * Compute the diff from the `Edit` because diff calculation is expensive.
|
||||
struct Diff<'a> {
|
||||
fix: &'a Fix,
|
||||
diagnostic_source: DiagnosticSource,
|
||||
notebook_index: Option<NotebookIndex>,
|
||||
stylesheet: &'a DiagnosticStylesheet,
|
||||
}
|
||||
|
||||
impl<'a> Diff<'a> {
|
||||
fn from_diagnostic(
|
||||
diagnostic: &'a Diagnostic,
|
||||
stylesheet: &'a DiagnosticStylesheet,
|
||||
resolver: &'a dyn FileResolver,
|
||||
) -> Option<Diff<'a>> {
|
||||
let file = &diagnostic.primary_span_ref()?.file;
|
||||
Some(Diff {
|
||||
fix: diagnostic.fix()?,
|
||||
diagnostic_source: file.diagnostic_source(resolver),
|
||||
notebook_index: resolver.notebook_index(file),
|
||||
stylesheet,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Diff<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let source_code = self.diagnostic_source.as_source_code();
|
||||
let source_text = source_code.text();
|
||||
|
||||
// Partition the source code into end offsets for each cell. If `self.notebook_index` is
|
||||
// `None`, indicating a regular script file, all the lines will be in one "cell" under the
|
||||
// `None` key.
|
||||
let cells = if let Some(notebook_index) = &self.notebook_index {
|
||||
let mut last_cell = OneIndexed::MIN;
|
||||
let mut cells: Vec<(Option<OneIndexed>, TextSize)> = Vec::new();
|
||||
for (row, cell) in notebook_index.iter() {
|
||||
if cell != last_cell {
|
||||
let offset = source_code.line_start(row);
|
||||
cells.push((Some(last_cell), offset));
|
||||
last_cell = cell;
|
||||
}
|
||||
}
|
||||
cells.push((Some(last_cell), source_text.text_len()));
|
||||
cells
|
||||
} else {
|
||||
vec![(None, source_text.text_len())]
|
||||
};
|
||||
|
||||
let message = match self.fix.applicability() {
|
||||
// TODO(zanieb): Adjust this messaging once it's user-facing
|
||||
Applicability::Safe => "Safe fix",
|
||||
Applicability::Unsafe => "Unsafe fix",
|
||||
Applicability::DisplayOnly => "Display-only fix",
|
||||
};
|
||||
|
||||
// TODO(brent) `stylesheet.separator` is cyan rather than blue, as we had before. I think
|
||||
// we're getting rid of this soon anyway, so I didn't think it was worth adding another
|
||||
// style to the stylesheet temporarily. The color doesn't appear at all in the snapshot
|
||||
// tests, which is the only place these are currently used.
|
||||
writeln!(f, "ℹ {}", fmt_styled(message, self.stylesheet.separator))?;
|
||||
|
||||
let mut last_end = TextSize::ZERO;
|
||||
for (cell, offset) in cells {
|
||||
let range = TextRange::new(last_end, offset);
|
||||
last_end = offset;
|
||||
let input = source_code.slice(range);
|
||||
|
||||
let mut output = String::with_capacity(input.len());
|
||||
let mut last_end = range.start();
|
||||
|
||||
let mut applied = 0;
|
||||
for edit in self.fix.edits() {
|
||||
if range.contains_range(edit.range()) {
|
||||
output.push_str(source_code.slice(TextRange::new(last_end, edit.start())));
|
||||
output.push_str(edit.content().unwrap_or_default());
|
||||
last_end = edit.end();
|
||||
applied += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// No edits were applied, so there's no need to diff.
|
||||
if applied == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
output.push_str(&source_text[usize::from(last_end)..usize::from(range.end())]);
|
||||
|
||||
let diff = TextDiff::from_lines(input, &output);
|
||||
|
||||
let (largest_old, largest_new) = diff
|
||||
.ops()
|
||||
.last()
|
||||
.map(|op| (op.old_range().start, op.new_range().start))
|
||||
.unwrap_or_default();
|
||||
|
||||
let digit_with = OneIndexed::from_zero_indexed(largest_new.max(largest_old)).digits();
|
||||
|
||||
if let Some(cell) = cell {
|
||||
// Room for 2 digits, 2 x 1 space before each digit, 1 space, and 1 `|`. This
|
||||
// centers the three colons on the pipe.
|
||||
writeln!(f, "{:>1$} cell {cell}", ":::", 2 * digit_with.get() + 4)?;
|
||||
}
|
||||
|
||||
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
|
||||
if idx > 0 {
|
||||
writeln!(f, "{:-^1$}", "-", 80)?;
|
||||
}
|
||||
for op in group {
|
||||
for change in diff.iter_inline_changes(op) {
|
||||
let sign = match change.tag() {
|
||||
ChangeTag::Delete => "-",
|
||||
ChangeTag::Insert => "+",
|
||||
ChangeTag::Equal => " ",
|
||||
};
|
||||
|
||||
let line_style = LineStyle::from(change.tag(), self.stylesheet);
|
||||
|
||||
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
|
||||
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{} {} |{}",
|
||||
Line {
|
||||
index: old_index,
|
||||
width: digit_with,
|
||||
},
|
||||
Line {
|
||||
index: new_index,
|
||||
width: digit_with,
|
||||
},
|
||||
fmt_styled(line_style.apply_to(sign), self.stylesheet.emphasis),
|
||||
)?;
|
||||
|
||||
for (emphasized, value) in change.iter_strings_lossy() {
|
||||
let value = show_nonprinting(&value);
|
||||
if emphasized {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
fmt_styled(
|
||||
line_style.apply_to(&value),
|
||||
self.stylesheet.underline
|
||||
)
|
||||
)?;
|
||||
} else {
|
||||
write!(f, "{}", line_style.apply_to(&value))?;
|
||||
}
|
||||
}
|
||||
if change.missing_newline() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct LineStyle {
|
||||
style: Style,
|
||||
}
|
||||
|
||||
impl LineStyle {
|
||||
fn apply_to(&self, input: &str) -> impl std::fmt::Display {
|
||||
fmt_styled(input, self.style)
|
||||
}
|
||||
|
||||
fn from(value: ChangeTag, stylesheet: &DiagnosticStylesheet) -> LineStyle {
|
||||
match value {
|
||||
ChangeTag::Equal => LineStyle {
|
||||
style: stylesheet.none,
|
||||
},
|
||||
ChangeTag::Delete => LineStyle {
|
||||
style: stylesheet.deletion,
|
||||
},
|
||||
ChangeTag::Insert => LineStyle {
|
||||
style: stylesheet.insertion,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Line {
|
||||
index: Option<OneIndexed>,
|
||||
width: NonZeroUsize,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Line {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.index {
|
||||
None => {
|
||||
for _ in 0..self.width.get() {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_nonprinting(s: &str) -> Cow<'_, str> {
|
||||
if s.find(['\x07', '\x08', '\x1b', '\x7f']).is_some() {
|
||||
Cow::Owned(
|
||||
s.replace('\x07', "␇")
|
||||
.replace('\x08', "␈")
|
||||
.replace('\x1b', "␛")
|
||||
.replace('\x7f', "␡"),
|
||||
)
|
||||
} else {
|
||||
Cow::Borrowed(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_diagnostics::{Applicability, Fix};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::diagnostic::{
|
||||
Annotation, DiagnosticFormat, Severity,
|
||||
@@ -186,7 +483,7 @@ print()
|
||||
/// For example, without the fix, we get diagnostics like this:
|
||||
///
|
||||
/// ```
|
||||
/// error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
|
||||
/// error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1a" instead
|
||||
/// --> example.py:1:25
|
||||
/// |
|
||||
/// 1 | nested_fstrings = f'␈{f'{f'␛'}'}'
|
||||
@@ -206,13 +503,13 @@ print()
|
||||
.builder(
|
||||
"invalid-character-sub",
|
||||
Severity::Error,
|
||||
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
|
||||
r#"Invalid unescaped character SUB, use "\x1a" instead"#,
|
||||
)
|
||||
.primary("example.py", "1:24", "1:24", "")
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r#"
|
||||
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
|
||||
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1a" instead
|
||||
--> example.py:1:25
|
||||
|
|
||||
1 | nested_fstrings = f'␈{f'{f'␛'}'}'
|
||||
@@ -231,13 +528,13 @@ print()
|
||||
.builder(
|
||||
"invalid-character-sub",
|
||||
Severity::Error,
|
||||
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
|
||||
r#"Invalid unescaped character SUB, use "\x1a" instead"#,
|
||||
)
|
||||
.primary("example.py", "1:1", "1:1", "")
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r#"
|
||||
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
|
||||
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1a" instead
|
||||
--> example.py:1:2
|
||||
|
|
||||
1 | ␈␛
|
||||
@@ -400,4 +697,208 @@ print()
|
||||
help: Remove `print` statement
|
||||
");
|
||||
}
|
||||
|
||||
/// Test that we remap notebook cell line numbers in the diff as well as the main diagnostic.
|
||||
#[test]
|
||||
fn notebook_output_with_diff() {
|
||||
let (mut env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
|
||||
env.show_fix_diff(true);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
error[unused-import][*]: `os` imported but unused
|
||||
--> notebook.ipynb:cell 1:2:8
|
||||
|
|
||||
1 | # cell 1
|
||||
2 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
|
||||
ℹ Safe fix
|
||||
::: cell 1
|
||||
1 1 | # cell 1
|
||||
2 |-import os
|
||||
|
||||
error[unused-import][*]: `math` imported but unused
|
||||
--> notebook.ipynb:cell 2:2:8
|
||||
|
|
||||
1 | # cell 2
|
||||
2 | import math
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | print('hello world')
|
||||
|
|
||||
help: Remove unused import: `math`
|
||||
|
||||
ℹ Safe fix
|
||||
::: cell 2
|
||||
1 1 | # cell 2
|
||||
2 |-import math
|
||||
3 2 |
|
||||
4 3 | print('hello world')
|
||||
|
||||
error[unused-variable]: Local variable `x` is assigned to but never used
|
||||
--> notebook.ipynb:cell 3:4:5
|
||||
|
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
4 | x = 1
|
||||
| ^
|
||||
|
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
ℹ Unsafe fix
|
||||
::: cell 3
|
||||
1 1 | # cell 3
|
||||
2 2 | def foo():
|
||||
3 3 | print()
|
||||
4 |- x = 1
|
||||
5 4 |
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output_with_diff_spanning_cells() {
|
||||
let (mut env, mut diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Full);
|
||||
env.show_fix_diff(true);
|
||||
|
||||
// Move all of the edits from the later diagnostics to the first diagnostic to simulate a
|
||||
// single diagnostic with edits in different cells.
|
||||
let mut diagnostic = diagnostics.swap_remove(0);
|
||||
let fix = diagnostic.fix_mut().unwrap();
|
||||
let mut edits = fix.edits().to_vec();
|
||||
for diag in diagnostics {
|
||||
edits.extend_from_slice(diag.fix().unwrap().edits());
|
||||
}
|
||||
*fix = Fix::unsafe_edits(edits.remove(0), edits);
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[unused-import]: `os` imported but unused
|
||||
--> notebook.ipynb:cell 1:2:8
|
||||
|
|
||||
1 | # cell 1
|
||||
2 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
|
||||
ℹ Unsafe fix
|
||||
::: cell 1
|
||||
1 1 | # cell 1
|
||||
2 |-import os
|
||||
::: cell 2
|
||||
1 1 | # cell 2
|
||||
2 |-import math
|
||||
3 2 |
|
||||
4 3 | print('hello world')
|
||||
::: cell 3
|
||||
1 1 | # cell 3
|
||||
2 2 | def foo():
|
||||
3 3 | print()
|
||||
4 |- x = 1
|
||||
5 4 |
|
||||
");
|
||||
}
|
||||
|
||||
/// Carriage return (`\r`) is a valid line-ending in Python, so we should normalize this to a
|
||||
/// line feed (`\n`) for rendering. Otherwise we report a single long line for this case.
|
||||
#[test]
|
||||
fn normalize_carriage_return() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add(
|
||||
"example.py",
|
||||
"# Keep parenthesis around preserved CR\rint(-\r 1)\rint(+\r 1)",
|
||||
);
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let mut diagnostic = env.err().build();
|
||||
let span = env
|
||||
.path("example.py")
|
||||
.with_range(TextRange::at(TextSize::new(39), TextSize::new(0)));
|
||||
let annotation = Annotation::primary(span);
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> example.py:2:1
|
||||
|
|
||||
1 | # Keep parenthesis around preserved CR
|
||||
2 | int(-
|
||||
| ^
|
||||
3 | 1)
|
||||
4 | int(+
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// Without stripping the BOM, we report an error in column 2, unlike Ruff.
|
||||
#[test]
|
||||
fn strip_bom() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add("example.py", "\u{feff}import foo");
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let mut diagnostic = env.err().build();
|
||||
let span = env
|
||||
.path("example.py")
|
||||
.with_range(TextRange::at(TextSize::new(3), TextSize::new(0)));
|
||||
let annotation = Annotation::primary(span);
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> example.py:1:1
|
||||
|
|
||||
1 | import foo
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bom_with_default_range() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add("example.py", "\u{feff}import foo");
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let mut diagnostic = env.err().build();
|
||||
let span = env.path("example.py").with_range(TextRange::default());
|
||||
let annotation = Annotation::primary(span);
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> example.py:1:1
|
||||
|
|
||||
1 | import foo
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// We previously rendered this correctly, but the header was falling back to 1:1 for ranges
|
||||
/// pointing to the final newline in a file. Like Ruff, we now use the offset of the first
|
||||
/// character in the nonexistent final line in the header.
|
||||
#[test]
|
||||
fn end_of_file() {
|
||||
let mut env = TestEnvironment::new();
|
||||
let contents = "unexpected eof\n";
|
||||
env.add("example.py", contents);
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let mut diagnostic = env.err().build();
|
||||
let span = env
|
||||
.path("example.py")
|
||||
.with_range(TextRange::at(contents.text_len(), TextSize::new(0)));
|
||||
let annotation = Annotation::primary(span);
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> example.py:2:1
|
||||
|
|
||||
1 | unexpected eof
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
205
crates/ruff_db/src/diagnostic/render/gitlab.rs
Normal file
205
crates/ruff_db/src/diagnostic/render/gitlab.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
hash::{DefaultHasher, Hash, Hasher},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use ruff_source_file::LineColumn;
|
||||
use serde::{Serialize, Serializer, ser::SerializeSeq};
|
||||
|
||||
use crate::diagnostic::{Diagnostic, Severity};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct GitlabRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
}
|
||||
|
||||
impl<'a> GitlabRenderer<'a> {
|
||||
pub(super) fn new(resolver: &'a dyn FileResolver) -> Self {
|
||||
Self { resolver }
|
||||
}
|
||||
}
|
||||
|
||||
impl GitlabRenderer<'_> {
|
||||
pub(super) fn render(
|
||||
&self,
|
||||
f: &mut std::fmt::Formatter,
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&SerializedMessages {
|
||||
diagnostics,
|
||||
resolver: self.resolver,
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "We don't have access to a `System` here, \
|
||||
and this is only intended for use by GitLab CI, \
|
||||
which runs on a real `System`."
|
||||
)]
|
||||
project_dir: std::env::var("CI_PROJECT_DIR").ok().as_deref(),
|
||||
})
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializedMessages<'a> {
|
||||
diagnostics: &'a [Diagnostic],
|
||||
resolver: &'a dyn FileResolver,
|
||||
project_dir: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Serialize for SerializedMessages<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
|
||||
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
|
||||
|
||||
for diagnostic in self.diagnostics {
|
||||
let location = diagnostic
|
||||
.primary_span()
|
||||
.map(|span| {
|
||||
let file = span.file();
|
||||
let positions = if self.resolver.is_notebook(file) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
Default::default()
|
||||
} else {
|
||||
let diagnostic_source = file.diagnostic_source(self.resolver);
|
||||
let source_code = diagnostic_source.as_source_code();
|
||||
span.range()
|
||||
.map(|range| Positions {
|
||||
begin: source_code.line_column(range.start()),
|
||||
end: source_code.line_column(range.end()),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
|| file.relative_path(self.resolver).display().to_string(),
|
||||
|project_dir| relativize_path_to(file.path(self.resolver), project_dir),
|
||||
);
|
||||
|
||||
Location { path, positions }
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut message_fingerprint = fingerprint(diagnostic, &location.path, 0);
|
||||
|
||||
// Make sure that we do not get a fingerprint that is already in use
|
||||
// by adding in the previously generated one.
|
||||
while fingerprints.contains(&message_fingerprint) {
|
||||
message_fingerprint = fingerprint(diagnostic, &location.path, message_fingerprint);
|
||||
}
|
||||
fingerprints.insert(message_fingerprint);
|
||||
|
||||
let description = diagnostic.body();
|
||||
let check_name = diagnostic.secondary_code_or_id();
|
||||
let severity = match diagnostic.severity() {
|
||||
Severity::Info => "info",
|
||||
Severity::Warning => "minor",
|
||||
Severity::Error => "major",
|
||||
// Another option here is `blocker`
|
||||
Severity::Fatal => "critical",
|
||||
};
|
||||
|
||||
let value = Message {
|
||||
check_name,
|
||||
// GitLab doesn't display the separate `check_name` field in a Code Quality report,
|
||||
// so prepend it to the description too.
|
||||
description: format!("{check_name}: {description}"),
|
||||
severity,
|
||||
fingerprint: format!("{:x}", message_fingerprint),
|
||||
location,
|
||||
};
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Message<'a> {
|
||||
check_name: &'a str,
|
||||
description: String,
|
||||
severity: &'static str,
|
||||
fingerprint: String,
|
||||
location: Location,
|
||||
}
|
||||
|
||||
/// The place in the source code where the issue was discovered.
|
||||
///
|
||||
/// According to the CodeClimate report format [specification] linked from the GitLab [docs], this
|
||||
/// field is required, so we fall back on a default `path` and position if the diagnostic doesn't
|
||||
/// have a primary span.
|
||||
///
|
||||
/// [specification]: https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types
|
||||
/// [docs]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
|
||||
#[derive(Default, Serialize)]
|
||||
struct Location {
|
||||
path: String,
|
||||
positions: Positions,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize)]
|
||||
struct Positions {
|
||||
begin: LineColumn,
|
||||
end: LineColumn,
|
||||
}
|
||||
|
||||
/// Generate a unique fingerprint to identify a violation.
|
||||
fn fingerprint(diagnostic: &Diagnostic, project_path: &str, salt: u64) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
salt.hash(&mut hasher);
|
||||
diagnostic.name().hash(&mut hasher);
|
||||
project_path.hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
/// Convert an absolute path to be relative to the specified project root.
|
||||
fn relativize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root: R) -> String {
|
||||
format!(
|
||||
"{}",
|
||||
pathdiff::diff_paths(&path, project_root)
|
||||
.expect("Could not diff paths")
|
||||
.display()
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
|
||||
};
|
||||
|
||||
const FINGERPRINT_FILTERS: [(&str, &str); 1] = [(
|
||||
r#""fingerprint": "[a-z0-9]+","#,
|
||||
r#""fingerprint": "<redacted>","#,
|
||||
)];
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Gitlab);
|
||||
insta::with_settings!({filters => FINGERPRINT_FILTERS}, {
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Gitlab);
|
||||
insta::with_settings!({filters => FINGERPRINT_FILTERS}, {
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
source: crates/ruff_db/src/diagnostic/render/gitlab.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
[
|
||||
{
|
||||
"check_name": "F401",
|
||||
"description": "F401: `os` imported but unused",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 1,
|
||||
"column": 8
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check_name": "F841",
|
||||
"description": "F841: Local variable `x` is assigned to but never used",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 6,
|
||||
"column": 5
|
||||
},
|
||||
"end": {
|
||||
"line": 6,
|
||||
"column": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check_name": "F821",
|
||||
"description": "F821: Undefined name `a`",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "undef.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 1,
|
||||
"column": 4
|
||||
},
|
||||
"end": {
|
||||
"line": 1,
|
||||
"column": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
source: crates/ruff_db/src/diagnostic/render/gitlab.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
[
|
||||
{
|
||||
"check_name": "invalid-syntax",
|
||||
"description": "invalid-syntax: Expected one or more symbol names after import",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 1,
|
||||
"column": 15
|
||||
},
|
||||
"end": {
|
||||
"line": 2,
|
||||
"column": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"check_name": "invalid-syntax",
|
||||
"description": "invalid-syntax: Expected ')', found newline",
|
||||
"severity": "major",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"line": 3,
|
||||
"column": 12
|
||||
},
|
||||
"end": {
|
||||
"line": 4,
|
||||
"column": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -40,9 +40,12 @@ pub struct DiagnosticStylesheet {
|
||||
pub(crate) help: Style,
|
||||
pub(crate) line_no: Style,
|
||||
pub(crate) emphasis: Style,
|
||||
pub(crate) underline: Style,
|
||||
pub(crate) none: Style,
|
||||
pub(crate) separator: Style,
|
||||
pub(crate) secondary_code: Style,
|
||||
pub(crate) insertion: Style,
|
||||
pub(crate) deletion: Style,
|
||||
}
|
||||
|
||||
impl Default for DiagnosticStylesheet {
|
||||
@@ -63,9 +66,12 @@ impl DiagnosticStylesheet {
|
||||
help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD),
|
||||
line_no: bright_blue.effects(Effects::BOLD),
|
||||
emphasis: Style::new().effects(Effects::BOLD),
|
||||
underline: Style::new().effects(Effects::UNDERLINE),
|
||||
none: Style::new(),
|
||||
separator: AnsiColor::Cyan.on_default(),
|
||||
secondary_code: AnsiColor::Red.on_default().effects(Effects::BOLD),
|
||||
insertion: AnsiColor::Green.on_default(),
|
||||
deletion: AnsiColor::Red.on_default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +84,12 @@ impl DiagnosticStylesheet {
|
||||
help: Style::new(),
|
||||
line_no: Style::new(),
|
||||
emphasis: Style::new(),
|
||||
underline: Style::new(),
|
||||
none: Style::new(),
|
||||
separator: Style::new(),
|
||||
secondary_code: Style::new(),
|
||||
insertion: Style::new(),
|
||||
deletion: Style::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::system::file_time_now;
|
||||
/// * The last modification time of the file.
|
||||
/// * The hash of the file's content.
|
||||
/// * The revision as it comes from an external system, for example the LSP.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
|
||||
pub struct FileRevision(u128);
|
||||
|
||||
impl FileRevision {
|
||||
|
||||
@@ -87,11 +87,12 @@ impl Files {
|
||||
.system_by_path
|
||||
.entry(absolute.clone())
|
||||
.or_insert_with(|| {
|
||||
tracing::trace!("Adding file '{path}'");
|
||||
|
||||
let metadata = db.system().path_metadata(path);
|
||||
|
||||
tracing::trace!("Adding file '{absolute}'");
|
||||
|
||||
let durability = self
|
||||
.root(db, path)
|
||||
.root(db, &absolute)
|
||||
.map_or(Durability::default(), |root| root.durability(db));
|
||||
|
||||
let builder = File::builder(FilePath::System(absolute))
|
||||
@@ -289,7 +290,7 @@ impl std::panic::RefUnwindSafe for Files {}
|
||||
/// # Ordering
|
||||
/// Ordering is based on the file's salsa-assigned id and not on its values.
|
||||
/// The id may change between runs.
|
||||
#[salsa::input]
|
||||
#[salsa::input(heap_size=ruff_memory_usage::heap_size)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct File {
|
||||
/// The path of the file (immutable).
|
||||
@@ -521,7 +522,7 @@ impl VirtualFile {
|
||||
// The types in here need to be public because they're salsa ingredients but we
|
||||
// don't want them to be publicly accessible. That's why we put them into a private module.
|
||||
mod private {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
|
||||
pub enum FileStatus {
|
||||
/// The file exists.
|
||||
#[default]
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::system::{SystemPath, SystemPathBuf};
|
||||
/// The main usage of file roots is to determine a file's durability. But it can also be used
|
||||
/// to make a salsa query dependent on whether a file in a root has changed without writing any
|
||||
/// manual invalidation logic.
|
||||
#[salsa::input(debug)]
|
||||
#[salsa::input(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub struct FileRoot {
|
||||
/// The path of a root is guaranteed to never change.
|
||||
#[returns(deref)]
|
||||
@@ -37,7 +37,7 @@ impl FileRoot {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
|
||||
pub enum FileRootKind {
|
||||
/// The root of a project.
|
||||
Project,
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::fmt::{Display, Formatter};
|
||||
/// * a file stored on the [host system](crate::system::System).
|
||||
/// * a virtual file stored on the [host system](crate::system::System).
|
||||
/// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem).
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub enum FilePath {
|
||||
/// Path to a file on the [host system](crate::system::System).
|
||||
System(SystemPathBuf),
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
#![warn(
|
||||
clippy::disallowed_methods,
|
||||
reason = "Prefer System trait methods over std methods"
|
||||
)]
|
||||
|
||||
use crate::files::Files;
|
||||
use crate::system::System;
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
@@ -65,6 +70,10 @@ pub trait Db: salsa::Database {
|
||||
/// to process work in parallel. For example, to index a directory or checking the files of a project.
|
||||
/// ty can still spawn more threads for other tasks, e.g. to wait for a Ctrl+C signal or
|
||||
/// watching the files for changes.
|
||||
#[expect(
|
||||
clippy::disallowed_methods,
|
||||
reason = "We don't have access to System here, but this is also only used by the CLI and the server which always run on a real system."
|
||||
)]
|
||||
pub fn max_parallelism() -> NonZeroUsize {
|
||||
std::env::var(EnvVars::TY_MAX_PARALLELISM)
|
||||
.or_else(|_| std::env::var(EnvVars::RAYON_NUM_THREADS))
|
||||
|
||||
@@ -92,14 +92,14 @@ impl ParsedModule {
|
||||
self.inner.store(None);
|
||||
}
|
||||
|
||||
/// Returns a pointer for this [`ParsedModule`].
|
||||
/// Returns the pointer address of this [`ParsedModule`].
|
||||
///
|
||||
/// The pointer uniquely identifies the module within the current Salsa revision,
|
||||
/// regardless of whether particular [`ParsedModuleRef`] instances are garbage collected.
|
||||
pub fn as_ptr(&self) -> *const () {
|
||||
pub fn addr(&self) -> usize {
|
||||
// Note that the outer `Arc` in `inner` is stable across garbage collection, while the inner
|
||||
// `Arc` within the `ArcSwap` may change.
|
||||
Arc::as_ptr(&self.inner).cast()
|
||||
Arc::as_ptr(&self.inner).addr()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,9 +202,13 @@ mod indexed {
|
||||
|
||||
/// Returns the node at the given index.
|
||||
pub fn get_by_index<'ast>(&'ast self, index: NodeIndex) -> AnyRootNodeRef<'ast> {
|
||||
let index = index
|
||||
.as_u32()
|
||||
.expect("attempted to access uninitialized `NodeIndex`");
|
||||
|
||||
// Note that this method restores the correct lifetime: the nodes are valid for as
|
||||
// long as the reference to `IndexedModule` is alive.
|
||||
self.index[index.as_usize()]
|
||||
self.index[index as usize]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +224,7 @@ mod indexed {
|
||||
T: HasNodeIndex + std::fmt::Debug,
|
||||
AnyRootNodeRef<'a>: From<&'a T>,
|
||||
{
|
||||
node.node_index().set(self.index);
|
||||
node.node_index().set(NodeIndex::from(self.index));
|
||||
self.nodes.push(AnyRootNodeRef::from(node));
|
||||
self.index += 1;
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ pub type Result<T> = std::io::Result<T>;
|
||||
/// * File watching isn't supported.
|
||||
///
|
||||
/// Abstracting the system also enables tests to use a more efficient in-memory file system.
|
||||
pub trait System: Debug {
|
||||
pub trait System: Debug + Sync + Send {
|
||||
/// Reads the metadata of the file or directory at `path`.
|
||||
///
|
||||
/// This function will traverse symbolic links to query information about the destination file.
|
||||
@@ -197,6 +197,8 @@ pub trait System: Debug {
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn System>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![allow(clippy::disallowed_methods)]
|
||||
|
||||
use super::walk_directory::{
|
||||
self, DirectoryWalker, WalkDirectoryBuilder, WalkDirectoryConfiguration,
|
||||
WalkDirectoryVisitorBuilder, WalkState,
|
||||
@@ -255,6 +257,10 @@ impl System for OsSystem {
|
||||
fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
|
||||
std::env::var(name)
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn System> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl OsSystem {
|
||||
|
||||
@@ -762,7 +762,7 @@ impl SystemVirtualPath {
|
||||
}
|
||||
|
||||
/// An owned, virtual path on [`System`](`super::System`) (akin to [`String`]).
|
||||
#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord)]
|
||||
#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord, get_size2::GetSize)]
|
||||
pub struct SystemVirtualPathBuf(String);
|
||||
|
||||
impl SystemVirtualPathBuf {
|
||||
|
||||
@@ -146,6 +146,10 @@ impl System for TestSystem {
|
||||
fn case_sensitivity(&self) -> CaseSensitivity {
|
||||
self.system().case_sensitivity()
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn System> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestSystem {
|
||||
@@ -394,6 +398,13 @@ impl System for InMemorySystem {
|
||||
fn case_sensitivity(&self) -> CaseSensitivity {
|
||||
CaseSensitivity::CaseSensitive
|
||||
}
|
||||
|
||||
fn dyn_clone(&self) -> Box<dyn System> {
|
||||
Box::new(Self {
|
||||
user_config_directory: Mutex::new(self.user_config_directory.lock().unwrap().clone()),
|
||||
memory_fs: self.memory_fs.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WritableSystem for InMemorySystem {
|
||||
|
||||
@@ -13,6 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ty = { workspace = true }
|
||||
ty_project = { workspace = true, features = ["schemars"] }
|
||||
ty_python_semantic = { workspace = true }
|
||||
ty_static = { workspace = true }
|
||||
ruff = { workspace = true }
|
||||
ruff_formatter = { workspace = true }
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
}
|
||||
|
||||
fn generate_markdown() -> String {
|
||||
let registry = &*ty_project::DEFAULT_LINT_REGISTRY;
|
||||
let registry = ty_python_semantic::default_lint_registry();
|
||||
|
||||
let mut output = String::new();
|
||||
|
||||
|
||||
@@ -14,8 +14,11 @@ license = { workspace = true }
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_text_size = { workspace = true, features = ["get-size"] }
|
||||
|
||||
get-size2 = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
serde = { workspace = true, optional = true, features = [] }
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde", "ruff_text_size/serde"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.8"
|
||||
version = "0.12.11"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -13,7 +13,6 @@ license = { workspace = true }
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
ruff_annotate_snippets = { workspace = true }
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["junit", "serde"] }
|
||||
ruff_diagnostics = { workspace = true, features = ["serde"] }
|
||||
@@ -52,7 +51,6 @@ path-absolutize = { workspace = true, features = [
|
||||
"once_cell_cache",
|
||||
"use_unix_paths_on_wasm",
|
||||
] }
|
||||
pathdiff = { workspace = true }
|
||||
pep440_rs = { workspace = true }
|
||||
pyproject-toml = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
||||
@@ -13,6 +13,7 @@ from airflow.api_connexion.security import requires_access
|
||||
from airflow.contrib.aws_athena_hook import AWSAthenaHook
|
||||
from airflow.datasets import DatasetAliasEvent
|
||||
from airflow.operators.subdag import SubDagOperator
|
||||
from airflow.secrets.cache import SecretCache
|
||||
from airflow.secrets.local_filesystem import LocalFilesystemBackend
|
||||
from airflow.triggers.external_task import TaskStateTrigger
|
||||
from airflow.utils import dates
|
||||
@@ -56,6 +57,9 @@ SubDagOperator()
|
||||
# get_connection
|
||||
LocalFilesystemBackend()
|
||||
|
||||
# airflow.secrets.cache
|
||||
SecretCache()
|
||||
|
||||
|
||||
# airflow.triggers.external_task
|
||||
TaskStateTrigger()
|
||||
|
||||
@@ -34,7 +34,7 @@ task_group()
|
||||
setup()
|
||||
from airflow.decorators import teardown
|
||||
from airflow.io.path import ObjectStoragePath
|
||||
from airflow.io.storage import attach
|
||||
from airflow.io.store import attach
|
||||
from airflow.models import DAG as DAGFromModel
|
||||
from airflow.models import (
|
||||
Connection,
|
||||
@@ -74,3 +74,36 @@ DatasetOrTimeSchedule()
|
||||
|
||||
# airflow.utils.dag_parsing_context
|
||||
get_parsing_context()
|
||||
|
||||
from airflow.decorators.base import (
|
||||
DecoratedMappedOperator,
|
||||
DecoratedOperator,
|
||||
TaskDecorator,
|
||||
get_unique_task_id,
|
||||
task_decorator_factory,
|
||||
)
|
||||
|
||||
# airflow.decorators.base
|
||||
DecoratedMappedOperator()
|
||||
DecoratedOperator()
|
||||
TaskDecorator()
|
||||
get_unique_task_id()
|
||||
task_decorator_factory()
|
||||
|
||||
|
||||
from airflow.models import Param
|
||||
|
||||
# airflow.models
|
||||
Param()
|
||||
|
||||
|
||||
from airflow.sensors.base import (
|
||||
BaseSensorOperator,
|
||||
PokeReturnValue,
|
||||
poke_mode_only,
|
||||
)
|
||||
|
||||
# airflow.sensors.base
|
||||
BaseSensorOperator()
|
||||
PokeReturnValue()
|
||||
poke_mode_only()
|
||||
|
||||
@@ -9,7 +9,6 @@ from airflow.operators.empty import EmptyOperator
|
||||
from airflow.operators.latest_only import LatestOnlyOperator
|
||||
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
|
||||
from airflow.operators.weekday import BranchDayOfWeekOperator
|
||||
from airflow.sensors.date_time import DateTimeSensor
|
||||
|
||||
FSHook()
|
||||
PackageIndexHook()
|
||||
@@ -22,7 +21,6 @@ EmptyOperator()
|
||||
|
||||
LatestOnlyOperator()
|
||||
BranchDayOfWeekOperator()
|
||||
DateTimeSensor()
|
||||
|
||||
from airflow.operators.python import (
|
||||
BranchPythonOperator,
|
||||
@@ -30,16 +28,23 @@ from airflow.operators.python import (
|
||||
PythonVirtualenvOperator,
|
||||
ShortCircuitOperator,
|
||||
)
|
||||
from airflow.sensors.bash import BashSensor
|
||||
from airflow.sensors.date_time import DateTimeSensor
|
||||
|
||||
BranchPythonOperator()
|
||||
PythonOperator()
|
||||
PythonVirtualenvOperator()
|
||||
ShortCircuitOperator()
|
||||
|
||||
BashSensor()
|
||||
DateTimeSensor()
|
||||
from airflow.sensors.date_time import DateTimeSensorAsync
|
||||
from airflow.sensors.external_task import (
|
||||
ExternalTaskMarker,
|
||||
ExternalTaskSensor,
|
||||
)
|
||||
from airflow.sensors.time_sensor import (
|
||||
TimeSensor,
|
||||
TimeSensorAsync,
|
||||
)
|
||||
from airflow.sensors.filesystem import FileSensor
|
||||
from airflow.sensors.python import PythonSensor
|
||||
|
||||
BranchPythonOperator()
|
||||
PythonOperator()
|
||||
@@ -49,6 +54,13 @@ DateTimeSensorAsync()
|
||||
ExternalTaskMarker()
|
||||
ExternalTaskSensor()
|
||||
FileSensor()
|
||||
PythonSensor()
|
||||
|
||||
from airflow.sensors.time_sensor import (
|
||||
TimeSensor,
|
||||
TimeSensorAsync,
|
||||
)
|
||||
|
||||
TimeSensor()
|
||||
TimeSensorAsync()
|
||||
|
||||
|
||||
75
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC212.py
vendored
Normal file
75
crates/ruff_linter/resources/test/fixtures/flake8_async/ASYNC212.py
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
def foo():
|
||||
client = httpx.Client()
|
||||
client.close() # Ok
|
||||
client.delete() # Ok
|
||||
client.get() # Ok
|
||||
client.head() # Ok
|
||||
client.options() # Ok
|
||||
client.patch() # Ok
|
||||
client.post() # Ok
|
||||
client.put() # Ok
|
||||
client.request() # Ok
|
||||
client.send() # Ok
|
||||
client.stream() # Ok
|
||||
|
||||
client.anything() # Ok
|
||||
client.build_request() # Ok
|
||||
client.is_closed # Ok
|
||||
|
||||
|
||||
async def foo():
|
||||
client = httpx.Client()
|
||||
client.close() # ASYNC212
|
||||
client.delete() # ASYNC212
|
||||
client.get() # ASYNC212
|
||||
client.head() # ASYNC212
|
||||
client.options() # ASYNC212
|
||||
client.patch() # ASYNC212
|
||||
client.post() # ASYNC212
|
||||
client.put() # ASYNC212
|
||||
client.request() # ASYNC212
|
||||
client.send() # ASYNC212
|
||||
client.stream() # ASYNC212
|
||||
|
||||
client.anything() # Ok
|
||||
client.build_request() # Ok
|
||||
client.is_closed # Ok
|
||||
|
||||
|
||||
async def foo(client: httpx.Client):
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
async def foo(client: httpx.Client | None):
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
async def foo(client: Optional[httpx.Client]):
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
async def foo():
|
||||
client: httpx.Client = ...
|
||||
client.request() # ASYNC212
|
||||
client.anything() # Ok
|
||||
|
||||
|
||||
global_client = httpx.Client()
|
||||
|
||||
|
||||
async def foo():
|
||||
global_client.request() # ASYNC212
|
||||
global_client.anything() # Ok
|
||||
|
||||
|
||||
async def foo():
|
||||
async with httpx.AsyncClient() as client:
|
||||
await client.get() # Ok
|
||||
@@ -154,6 +154,11 @@ try:
|
||||
except Exception as e:
|
||||
raise ValueError from e
|
||||
|
||||
try:
|
||||
...
|
||||
except Exception as e:
|
||||
raise e from ValueError("hello")
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
@@ -245,3 +250,9 @@ try:
|
||||
pass
|
||||
except (Exception, ValueError) as e:
|
||||
raise e
|
||||
|
||||
# `from None` cause
|
||||
try:
|
||||
pass
|
||||
except BaseException as e:
|
||||
raise e from None
|
||||
|
||||
43
crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420_3.py
vendored
Normal file
43
crates/ruff_linter/resources/test/fixtures/flake8_comprehensions/C420_3.py
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
class C: a = None
|
||||
{C.a: None for C.a in "abc"}
|
||||
print(C.a)
|
||||
|
||||
x = [None]
|
||||
{x[0]: None for x[0] in "abc"}
|
||||
print(x)
|
||||
|
||||
class C(list):
|
||||
def __getitem__(self, index, /):
|
||||
item = super().__getitem__(index)
|
||||
if isinstance(index, slice): item = tuple(item)
|
||||
return item
|
||||
x = C()
|
||||
{x[:0]: None for x[:0] in "abc"}
|
||||
print(x)
|
||||
|
||||
|
||||
class C:
|
||||
a = None
|
||||
|
||||
def func():
|
||||
{(C.a,): None for (C.a,) in "abc"} # OK
|
||||
|
||||
|
||||
def func():
|
||||
obj = type('obj', (), {'attr': 1})()
|
||||
{(obj.attr,): None for (obj.attr,) in "abc"} # OK
|
||||
|
||||
|
||||
def func():
|
||||
lst = [1, 2, 3]
|
||||
{(lst[0],): None for (lst[0],) in "abc"} # OK
|
||||
|
||||
|
||||
def func():
|
||||
lst = [1, 2, 3, 4, 5]
|
||||
{(lst[1:3],): None for (lst[1:3],) in "abc"} # OK
|
||||
|
||||
|
||||
# C420: side-effecting assignment targets
|
||||
# These should NOT trigger C420 because they have side-effecting assignment targets
|
||||
# See https://github.com/astral-sh/ruff/issues/19511
|
||||
@@ -17,3 +17,50 @@ info(f"{__name__}")
|
||||
# Don't trigger for t-strings
|
||||
info(t"{name}")
|
||||
info(t"{__name__}")
|
||||
|
||||
count = 5
|
||||
total = 9
|
||||
directory_path = "/home/hamir/ruff/crates/ruff_linter/resources/test/"
|
||||
logging.info(f"{count} out of {total} files in {directory_path} checked")
|
||||
|
||||
|
||||
|
||||
x = 99
|
||||
fmt = "08d"
|
||||
logger.info(f"{x:{'08d'}}")
|
||||
logger.info(f"{x:>10} {x:{fmt}}")
|
||||
|
||||
logging.info(f"")
|
||||
logging.info(f"This message doesn't have any variables.")
|
||||
|
||||
obj = {"key": "value"}
|
||||
logging.info(f"Object: {obj!r}")
|
||||
|
||||
items_count = 3
|
||||
logging.warning(f"Items: {items_count:d}")
|
||||
|
||||
data = {"status": "active"}
|
||||
logging.info(f"Processing {len(data)} items")
|
||||
logging.info(f"Status: {data.get('status', 'unknown').upper()}")
|
||||
|
||||
|
||||
result = 123
|
||||
logging.info(f"Calculated result: {result + 100}")
|
||||
|
||||
temperature = 123
|
||||
logging.info(f"Temperature: {temperature:.1f}°C")
|
||||
|
||||
class FilePath:
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
logging.info(f"No changes made to {file_path.name}.")
|
||||
|
||||
user = "tron"
|
||||
balance = 123.45
|
||||
logging.error(f"Error {404}: User {user} has insufficient balance ${balance:.2f}")
|
||||
|
||||
import logging
|
||||
|
||||
x = 1
|
||||
logging.error(f"{x} -> %s", x)
|
||||
|
||||
10
crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004_arg_order.py
vendored
Normal file
10
crates/ruff_linter/resources/test/fixtures/flake8_logging_format/G004_arg_order.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Test f-string argument order."""
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
X = 1
|
||||
Y = 2
|
||||
logger.error(f"{X} -> %s", Y)
|
||||
logger.error(f"{Y} -> %s", X)
|
||||
@@ -161,3 +161,12 @@ r"""first
|
||||
'no need' to escape
|
||||
"swap" quote style
|
||||
"use' ugly triple quotes""".split("\n")
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/19845
|
||||
print("S\x1cP\x1dL\x1eI\x1fT".split())
|
||||
print("\x1c\x1d\x1e\x1f>".split(maxsplit=0))
|
||||
print("<\x1c\x1d\x1e\x1f".rsplit(maxsplit=0))
|
||||
|
||||
# leading/trailing whitespace should not count towards maxsplit
|
||||
" a b c d ".split(maxsplit=2) # ["a", "b", "c d "]
|
||||
" a b c d ".rsplit(maxsplit=2) # [" a b", "c", "d"]
|
||||
|
||||
@@ -13,3 +13,11 @@ Path("tmp/python").symlink_to("usr/bin/python", target_is_directory=True) # Ok
|
||||
fd = os.open(".", os.O_RDONLY)
|
||||
os.symlink("source.txt", "link.txt", dir_fd=fd) # Ok: dir_fd is not supported by pathlib
|
||||
os.close(fd)
|
||||
|
||||
os.symlink(src="usr/bin/python", dst="tmp/python", unknown=True)
|
||||
os.symlink("usr/bin/python", dst="tmp/python", target_is_directory=False)
|
||||
|
||||
os.symlink(src="usr/bin/python", dst="tmp/python", dir_fd=None)
|
||||
|
||||
os.symlink("usr/bin/python", dst="tmp/python", target_is_directory= True )
|
||||
os.symlink("usr/bin/python", dst="tmp/python", target_is_directory="nonboolean")
|
||||
|
||||
@@ -106,4 +106,22 @@ os.replace("src", "dst", src_dir_fd=1)
|
||||
os.replace("src", "dst", dst_dir_fd=2)
|
||||
|
||||
os.getcwd()
|
||||
os.getcwdb()
|
||||
os.getcwdb()
|
||||
|
||||
os.mkdir(path="directory")
|
||||
|
||||
os.mkdir(
|
||||
# comment 1
|
||||
"directory",
|
||||
mode=0o777
|
||||
)
|
||||
|
||||
os.mkdir("directory", mode=0o777, dir_fd=1)
|
||||
|
||||
os.makedirs("name", 0o777, exist_ok=False)
|
||||
|
||||
os.makedirs("name", 0o777, False)
|
||||
|
||||
os.makedirs(name="name", mode=0o777, exist_ok=False)
|
||||
|
||||
os.makedirs("name", unknown_kwarg=True)
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
"""Hello, world!"""\
|
||||
\
|
||||
|
||||
x = 1; y = 2
|
||||
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_35.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/pyflakes/F401_35.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
Test: allowed-unused-imports-top-level-module
|
||||
"""
|
||||
|
||||
# No errors
|
||||
|
||||
def f():
|
||||
import hvplot
|
||||
def f():
|
||||
import hvplot.pandas
|
||||
def f():
|
||||
import hvplot.pandas.plots
|
||||
def f():
|
||||
from hvplot.pandas import scatter_matrix
|
||||
def f():
|
||||
from hvplot.pandas.plots import scatter_matrix
|
||||
@@ -151,3 +151,39 @@ def f():
|
||||
pass
|
||||
except Exception as _:
|
||||
pass
|
||||
|
||||
|
||||
# OK, `__class__` in this case is not the special `__class__` cell, so we don't
|
||||
# emit a diagnostic. (It has its own special semantics -- see
|
||||
# https://github.com/astral-sh/ruff/pull/20048#discussion_r2298338048 -- but
|
||||
# those aren't relevant here.)
|
||||
class A:
|
||||
__class__ = 1
|
||||
|
||||
|
||||
# The following three cases are flagged because they declare local `__class__`
|
||||
# variables that don't refer to the special `__class__` cell.
|
||||
class A:
|
||||
def set_class(self, cls):
|
||||
__class__ = cls # F841
|
||||
|
||||
|
||||
class A:
|
||||
class B:
|
||||
def set_class(self, cls):
|
||||
__class__ = cls # F841
|
||||
|
||||
|
||||
class A:
|
||||
def foo():
|
||||
class B:
|
||||
print(__class__)
|
||||
def set_class(self, cls):
|
||||
__class__ = cls # F841
|
||||
|
||||
|
||||
# OK, the `__class__` cell is nonlocal and declared as such.
|
||||
class NonlocalDunderClass:
|
||||
def foo():
|
||||
nonlocal __class__
|
||||
__class__ = 1
|
||||
|
||||
@@ -44,3 +44,8 @@ def f():
|
||||
def g():
|
||||
nonlocal x
|
||||
x = 2
|
||||
|
||||
# OK
|
||||
class A:
|
||||
def method(self):
|
||||
nonlocal __class__
|
||||
|
||||
18
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP010_1.py
vendored
Normal file
18
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP010_1.py
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
from __future__ import nested_scopes, generators
|
||||
from __future__ import with_statement, unicode_literals
|
||||
|
||||
from __future__ import absolute_import, division
|
||||
from __future__ import generator_stop
|
||||
from __future__ import print_function, nested_scopes, generator_stop
|
||||
|
||||
print(with_statement)
|
||||
generators = 1
|
||||
|
||||
|
||||
class Foo():
|
||||
|
||||
def boo(self):
|
||||
print(division)
|
||||
|
||||
|
||||
__all__ = ["print_function", "generator_stop"]
|
||||
@@ -69,3 +69,10 @@ a7: OptionalTE[typing.NamedTuple] = None
|
||||
a8: typing_extensions.Optional[typing.NamedTuple] = None
|
||||
a9: "Optional[NamedTuple]" = None
|
||||
a10: Optional[NamedTupleTE] = None
|
||||
|
||||
|
||||
# Test for: https://github.com/astral-sh/ruff/issues/19746
|
||||
# Nested Optional types should be flattened
|
||||
nested_optional: Optional[Optional[str]] = None
|
||||
nested_optional_typing: typing.Optional[Optional[int]] = None
|
||||
triple_nested_optional: Optional[Optional[Optional[str]]] = None
|
||||
|
||||
@@ -118,3 +118,10 @@ def func():
|
||||
return lambda: value
|
||||
|
||||
defaultdict(constant_factory("<missing>"))
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=t"") # OK
|
||||
|
||||
|
||||
def func():
|
||||
defaultdict(default_factory=t"hello") # OK
|
||||
|
||||
@@ -124,3 +124,19 @@ def fun_with_python_syntax():
|
||||
...
|
||||
|
||||
return Foo
|
||||
|
||||
|
||||
@dataclass
|
||||
class C:
|
||||
def __post_init__(self, x: tuple[int, ...] = (
|
||||
1,
|
||||
2,
|
||||
)) -> None:
|
||||
self.x = x
|
||||
|
||||
|
||||
@dataclass
|
||||
class D:
|
||||
def __post_init__(self, x: int = """
|
||||
""") -> None:
|
||||
self.x = x
|
||||
|
||||
@@ -102,3 +102,8 @@ deque("abc") # OK
|
||||
deque(b"abc") # OK
|
||||
deque(f"" "a") # OK
|
||||
deque(f"{x}" "") # OK
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/19951
|
||||
deque(t"")
|
||||
deque(t"" t"")
|
||||
deque(t"{""}") # OK
|
||||
|
||||
@@ -51,3 +51,11 @@ dbm.ndbm.open("db", "r", 0o600) # OK
|
||||
|
||||
os.fchmod(0, 256) # 0o400
|
||||
os.fchmod(0, 493) # 0o755
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/19010
|
||||
os.chmod("foo", 000) # Error
|
||||
os.chmod("foo", 0000) # Error
|
||||
|
||||
os.chmod("foo", 0b0) # Error
|
||||
os.chmod("foo", 0x0) # Error
|
||||
os.chmod("foo", 0) # Ok
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ruff_python_semantic::{Binding, ScopeKind};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{
|
||||
flake8_builtins, flake8_pyi, flake8_type_checking, flake8_unused_arguments, pep8_naming,
|
||||
pyflakes, pylint, ruff,
|
||||
pyflakes, pylint, pyupgrade, ruff,
|
||||
};
|
||||
|
||||
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
|
||||
@@ -45,6 +46,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
|
||||
Rule::UnusedStaticMethodArgument,
|
||||
Rule::UnusedUnpackedVariable,
|
||||
Rule::UnusedVariable,
|
||||
Rule::UnnecessaryFutureImport,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
@@ -224,6 +226,11 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::UnusedImport) {
|
||||
pyflakes::rules::unused_import(checker, scope);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryFutureImport) {
|
||||
if checker.target_version() >= PythonVersion::PY37 {
|
||||
pyupgrade::rules::unnecessary_future_import(checker, scope);
|
||||
}
|
||||
}
|
||||
|
||||
if checker.is_rule_enabled(Rule::ImportPrivateName) {
|
||||
pylint::rules::import_private_name(checker, scope);
|
||||
|
||||
@@ -660,6 +660,9 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::BlockingHttpCallInAsyncFunction) {
|
||||
flake8_async::rules::blocking_http_call(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::BlockingHttpCallHttpxInAsyncFunction) {
|
||||
flake8_async::rules::blocking_http_call_httpx(checker, call);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::BlockingOpenCallInAsyncFunction) {
|
||||
flake8_async::rules::blocking_open_call(checker, call);
|
||||
}
|
||||
@@ -1039,8 +1042,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
|
||||
}
|
||||
if checker.any_rule_enabled(&[
|
||||
Rule::OsMkdir,
|
||||
Rule::OsMakedirs,
|
||||
Rule::OsStat,
|
||||
Rule::OsPathJoin,
|
||||
Rule::OsPathSplitext,
|
||||
@@ -1048,7 +1049,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
Rule::PyPath,
|
||||
Rule::Glob,
|
||||
Rule::OsListdir,
|
||||
Rule::OsSymlink,
|
||||
]) {
|
||||
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
|
||||
}
|
||||
@@ -1120,6 +1120,15 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::OsPathSamefile) {
|
||||
flake8_use_pathlib::rules::os_path_samefile(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsMkdir) {
|
||||
flake8_use_pathlib::rules::os_mkdir(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsMakedirs) {
|
||||
flake8_use_pathlib::rules::os_makedirs(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsSymlink) {
|
||||
flake8_use_pathlib::rules::os_symlink(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
|
||||
flake8_use_pathlib::rules::path_constructor_current_directory(
|
||||
checker, call, segments,
|
||||
|
||||
@@ -728,13 +728,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
pylint::rules::non_ascii_module_import(checker, alias);
|
||||
}
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryFutureImport) {
|
||||
if checker.target_version() >= PythonVersion::PY37 {
|
||||
if let Some("__future__") = module {
|
||||
pyupgrade::rules::unnecessary_future_import(checker, stmt, names);
|
||||
}
|
||||
}
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::DeprecatedMockImport) {
|
||||
pyupgrade::rules::deprecated_mock_import(checker, stmt);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ use itertools::Itertools;
|
||||
use log::debug;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, IntoDiagnosticMessage, Span};
|
||||
use ruff_diagnostics::{Applicability, Fix, IsolationLevel};
|
||||
use ruff_notebook::{CellOffsets, NotebookIndex};
|
||||
use ruff_python_ast::helpers::{collect_import_from_member, is_docstring_stmt, to_module_path};
|
||||
@@ -684,8 +684,7 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
| SemanticSyntaxErrorKind::LoadBeforeNonlocalDeclaration { .. }
|
||||
| SemanticSyntaxErrorKind::NonlocalAndGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedGlobal(_)
|
||||
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_)
|
||||
| SemanticSyntaxErrorKind::NoBindingForNonlocal(_) => {
|
||||
| SemanticSyntaxErrorKind::AnnotatedNonlocal(_) => {
|
||||
self.semantic_errors.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
@@ -704,7 +703,10 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
match scope.kind {
|
||||
ScopeKind::Class(_) | ScopeKind::Lambda(_) => return false,
|
||||
ScopeKind::Function(ast::StmtFunctionDef { is_async, .. }) => return *is_async,
|
||||
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
|
||||
ScopeKind::Generator { .. }
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::Type
|
||||
| ScopeKind::DunderClassCell => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -715,7 +717,10 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
match scope.kind {
|
||||
ScopeKind::Class(_) => return false,
|
||||
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
|
||||
ScopeKind::Generator { .. } | ScopeKind::Module | ScopeKind::Type => {}
|
||||
ScopeKind::Generator { .. }
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::Type
|
||||
| ScopeKind::DunderClassCell => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -726,7 +731,7 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
match scope.kind {
|
||||
ScopeKind::Class(_) | ScopeKind::Generator { .. } => return false,
|
||||
ScopeKind::Function(_) | ScopeKind::Lambda(_) => return true,
|
||||
ScopeKind::Module | ScopeKind::Type => {}
|
||||
ScopeKind::Module | ScopeKind::Type | ScopeKind::DunderClassCell => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
@@ -1093,6 +1098,24 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// Here we add the implicit scope surrounding a method which allows code in the
|
||||
// method to access `__class__` at runtime. See the `ScopeKind::DunderClassCell`
|
||||
// docs for more information.
|
||||
let added_dunder_class_scope = if self.semantic.current_scope().kind.is_class() {
|
||||
self.semantic.push_scope(ScopeKind::DunderClassCell);
|
||||
let binding_id = self.semantic.push_binding(
|
||||
TextRange::default(),
|
||||
BindingKind::DunderClassCell,
|
||||
BindingFlags::empty(),
|
||||
);
|
||||
self.semantic
|
||||
.current_scope_mut()
|
||||
.add("__class__", binding_id);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.semantic.push_scope(ScopeKind::Type);
|
||||
|
||||
if let Some(type_params) = type_params {
|
||||
@@ -1156,6 +1179,9 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
self.semantic.pop_scope(); // Function scope
|
||||
self.semantic.pop_definition();
|
||||
self.semantic.pop_scope(); // Type parameter scope
|
||||
if added_dunder_class_scope {
|
||||
self.semantic.pop_scope(); // `__class__` cell closure scope
|
||||
}
|
||||
self.add_binding(
|
||||
name,
|
||||
stmt.identifier(),
|
||||
@@ -3306,6 +3332,17 @@ impl DiagnosticGuard<'_, '_> {
|
||||
Err(err) => log::debug!("Failed to create fix for {}: {}", self.name(), err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a secondary annotation with the given message and range.
|
||||
pub(crate) fn secondary_annotation<'a>(
|
||||
&mut self,
|
||||
message: impl IntoDiagnosticMessage + 'a,
|
||||
range: impl Ranged,
|
||||
) {
|
||||
let span = Span::from(self.context.source_file.clone()).with_range(range.range());
|
||||
let ann = Annotation::secondary(span).message(message);
|
||||
self.diagnostic.as_mut().unwrap().annotate(ann);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for DiagnosticGuard<'_, '_> {
|
||||
|
||||
@@ -336,6 +336,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
|
||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
|
||||
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
||||
(Flake8Async, "212") => (RuleGroup::Preview, rules::flake8_async::rules::BlockingHttpCallHttpxInAsyncFunction),
|
||||
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
|
||||
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
|
||||
(Flake8Async, "222") => (RuleGroup::Stable, rules::flake8_async::rules::WaitForProcessInAsyncFunction),
|
||||
@@ -921,8 +922,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
// flake8-use-pathlib
|
||||
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
|
||||
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsChmod),
|
||||
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
|
||||
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
|
||||
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsMkdir),
|
||||
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsMakedirs),
|
||||
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRename),
|
||||
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReplace),
|
||||
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
|
||||
@@ -954,7 +955,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "207") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::Glob),
|
||||
(Flake8UsePathlib, "208") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsListdir),
|
||||
(Flake8UsePathlib, "210") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::InvalidPathlibWithSuffix),
|
||||
(Flake8UsePathlib, "211") => (RuleGroup::Preview, rules::flake8_use_pathlib::violations::OsSymlink),
|
||||
(Flake8UsePathlib, "211") => (RuleGroup::Preview, rules::flake8_use_pathlib::rules::OsSymlink),
|
||||
|
||||
// flake8-logging-format
|
||||
(Flake8LoggingFormat, "001") => (RuleGroup::Stable, rules::flake8_logging_format::violations::LoggingStringFormat),
|
||||
|
||||
@@ -58,13 +58,3 @@ pub fn relativize_path<P: AsRef<Path>>(path: P) -> String {
|
||||
}
|
||||
format!("{}", path.display())
|
||||
}
|
||||
|
||||
/// Convert an absolute path to be relative to the specified project root.
|
||||
pub fn relativize_path_to<P: AsRef<Path>, R: AsRef<Path>>(path: P, project_root: R) -> String {
|
||||
format!(
|
||||
"{}",
|
||||
pathdiff::diff_paths(&path, project_root)
|
||||
.expect("Could not diff paths")
|
||||
.display()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -63,9 +63,9 @@ impl<'a> Insertion<'a> {
|
||||
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
|
||||
}
|
||||
|
||||
// If the first token after the docstring is a continuation character (i.e. "\"), advance
|
||||
// an additional row to prevent inserting in the same logical line.
|
||||
if match_continuation(locator.after(location)).is_some() {
|
||||
// While the first token after the docstring is a continuation character (i.e. "\"), advance
|
||||
// additional rows to prevent inserting in the same logical line.
|
||||
while match_continuation(locator.after(location)).is_some() {
|
||||
location = locator.full_line_end(location);
|
||||
}
|
||||
|
||||
@@ -379,6 +379,17 @@ mod tests {
|
||||
Insertion::own_line("", TextSize::from(22), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
"""Hello, world!"""\
|
||||
\
|
||||
|
||||
"#
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::own_line("", TextSize::from(24), "\n")
|
||||
);
|
||||
|
||||
let contents = r"
|
||||
x = 1
|
||||
"
|
||||
|
||||
@@ -44,44 +44,15 @@ pub struct LinterResult {
|
||||
/// Flag indicating that the parsed source code does not contain any
|
||||
/// [`ParseError`]s
|
||||
has_valid_syntax: bool,
|
||||
/// Flag indicating that the parsed source code does not contain any [`ParseError`]s,
|
||||
/// [`UnsupportedSyntaxError`]s, or [`SemanticSyntaxError`]s.
|
||||
has_no_syntax_errors: bool,
|
||||
}
|
||||
|
||||
impl LinterResult {
|
||||
/// Returns `true` if the parsed source code contains any [`ParseError`]s *or*
|
||||
/// [`UnsupportedSyntaxError`]s.
|
||||
///
|
||||
/// See [`LinterResult::has_invalid_syntax`] for a version specific to [`ParseError`]s.
|
||||
pub fn has_syntax_errors(&self) -> bool {
|
||||
!self.has_no_syntax_errors()
|
||||
}
|
||||
|
||||
/// Returns `true` if the parsed source code does not contain any [`ParseError`]s *or*
|
||||
/// [`UnsupportedSyntaxError`]s.
|
||||
///
|
||||
/// See [`LinterResult::has_valid_syntax`] for a version specific to [`ParseError`]s.
|
||||
pub fn has_no_syntax_errors(&self) -> bool {
|
||||
self.has_valid_syntax() && self.has_no_syntax_errors
|
||||
}
|
||||
|
||||
/// Returns `true` if the parsed source code is valid i.e., it has no [`ParseError`]s.
|
||||
///
|
||||
/// Note that this does not include version-related [`UnsupportedSyntaxError`]s.
|
||||
///
|
||||
/// See [`LinterResult::has_no_syntax_errors`] for a version that takes these into account.
|
||||
pub fn has_valid_syntax(&self) -> bool {
|
||||
self.has_valid_syntax
|
||||
}
|
||||
|
||||
/// Returns `true` if the parsed source code is invalid i.e., it has [`ParseError`]s.
|
||||
///
|
||||
/// Note that this does not include version-related [`UnsupportedSyntaxError`]s.
|
||||
///
|
||||
/// See [`LinterResult::has_no_syntax_errors`] for a version that takes these into account.
|
||||
/// Note that this does not include version-related [`UnsupportedSyntaxError`]s or
|
||||
/// [`SemanticSyntaxError`]s.
|
||||
pub fn has_invalid_syntax(&self) -> bool {
|
||||
!self.has_valid_syntax()
|
||||
!self.has_valid_syntax
|
||||
}
|
||||
}
|
||||
|
||||
@@ -513,7 +484,6 @@ pub fn lint_only(
|
||||
|
||||
LinterResult {
|
||||
has_valid_syntax: parsed.has_valid_syntax(),
|
||||
has_no_syntax_errors: !diagnostics.iter().any(Diagnostic::is_invalid_syntax),
|
||||
diagnostics,
|
||||
}
|
||||
}
|
||||
@@ -670,7 +640,6 @@ pub fn lint_fix<'a>(
|
||||
result: LinterResult {
|
||||
diagnostics,
|
||||
has_valid_syntax,
|
||||
has_no_syntax_errors,
|
||||
},
|
||||
transformed,
|
||||
fixed,
|
||||
|
||||
@@ -1,202 +0,0 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
use colored::{Color, ColoredString, Colorize, Styles};
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_source_file::{OneIndexed, SourceFile};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::text_helpers::ShowNonprinting;
|
||||
use crate::{Applicability, Fix};
|
||||
|
||||
/// Renders a diff that shows the code fixes.
|
||||
///
|
||||
/// The implementation isn't fully fledged out and only used by tests. Before using in production, try
|
||||
/// * Improve layout
|
||||
/// * Replace tabs with spaces for a consistent experience across terminals
|
||||
/// * Replace zero-width whitespaces
|
||||
/// * Print a simpler diff if only a single line has changed
|
||||
/// * Compute the diff from the [`Edit`] because diff calculation is expensive.
|
||||
pub(super) struct Diff<'a> {
|
||||
fix: &'a Fix,
|
||||
source_code: &'a SourceFile,
|
||||
}
|
||||
|
||||
impl<'a> Diff<'a> {
|
||||
pub(crate) fn from_message(message: &'a Diagnostic) -> Option<Diff<'a>> {
|
||||
message.fix().map(|fix| Diff {
|
||||
source_code: message.expect_ruff_source_file(),
|
||||
fix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Diff<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// TODO(dhruvmanila): Add support for Notebook cells once it's user-facing
|
||||
let mut output = String::with_capacity(self.source_code.source_text().len());
|
||||
let mut last_end = TextSize::default();
|
||||
|
||||
for edit in self.fix.edits() {
|
||||
output.push_str(
|
||||
self.source_code
|
||||
.slice(TextRange::new(last_end, edit.start())),
|
||||
);
|
||||
output.push_str(edit.content().unwrap_or_default());
|
||||
last_end = edit.end();
|
||||
}
|
||||
|
||||
output.push_str(&self.source_code.source_text()[usize::from(last_end)..]);
|
||||
|
||||
let diff = TextDiff::from_lines(self.source_code.source_text(), &output);
|
||||
|
||||
let message = match self.fix.applicability() {
|
||||
// TODO(zanieb): Adjust this messaging once it's user-facing
|
||||
Applicability::Safe => "Safe fix",
|
||||
Applicability::Unsafe => "Unsafe fix",
|
||||
Applicability::DisplayOnly => "Display-only fix",
|
||||
};
|
||||
writeln!(f, "ℹ {}", message.blue())?;
|
||||
|
||||
let (largest_old, largest_new) = diff
|
||||
.ops()
|
||||
.last()
|
||||
.map(|op| (op.old_range().start, op.new_range().start))
|
||||
.unwrap_or_default();
|
||||
|
||||
let digit_with =
|
||||
calculate_print_width(OneIndexed::from_zero_indexed(largest_new.max(largest_old)));
|
||||
|
||||
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
|
||||
if idx > 0 {
|
||||
writeln!(f, "{:-^1$}", "-", 80)?;
|
||||
}
|
||||
for op in group {
|
||||
for change in diff.iter_inline_changes(op) {
|
||||
let sign = match change.tag() {
|
||||
ChangeTag::Delete => "-",
|
||||
ChangeTag::Insert => "+",
|
||||
ChangeTag::Equal => " ",
|
||||
};
|
||||
|
||||
let line_style = LineStyle::from(change.tag());
|
||||
|
||||
let old_index = change.old_index().map(OneIndexed::from_zero_indexed);
|
||||
let new_index = change.new_index().map(OneIndexed::from_zero_indexed);
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{} {} |{}",
|
||||
Line {
|
||||
index: old_index,
|
||||
width: digit_with
|
||||
},
|
||||
Line {
|
||||
index: new_index,
|
||||
width: digit_with
|
||||
},
|
||||
line_style.apply_to(sign).bold()
|
||||
)?;
|
||||
|
||||
for (emphasized, value) in change.iter_strings_lossy() {
|
||||
let value = value.show_nonprinting();
|
||||
if emphasized {
|
||||
write!(f, "{}", line_style.apply_to(&value).underline().on_black())?;
|
||||
} else {
|
||||
write!(f, "{}", line_style.apply_to(&value))?;
|
||||
}
|
||||
}
|
||||
if change.missing_newline() {
|
||||
writeln!(f)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct LineStyle {
|
||||
fgcolor: Option<Color>,
|
||||
style: Option<Styles>,
|
||||
}
|
||||
|
||||
impl LineStyle {
|
||||
fn apply_to(&self, input: &str) -> ColoredString {
|
||||
let mut colored = ColoredString::from(input);
|
||||
if let Some(color) = self.fgcolor {
|
||||
colored = colored.color(color);
|
||||
}
|
||||
|
||||
if let Some(style) = self.style {
|
||||
match style {
|
||||
Styles::Clear => colored.clear(),
|
||||
Styles::Bold => colored.bold(),
|
||||
Styles::Dimmed => colored.dimmed(),
|
||||
Styles::Underline => colored.underline(),
|
||||
Styles::Reversed => colored.reversed(),
|
||||
Styles::Italic => colored.italic(),
|
||||
Styles::Blink => colored.blink(),
|
||||
Styles::Hidden => colored.hidden(),
|
||||
Styles::Strikethrough => colored.strikethrough(),
|
||||
}
|
||||
} else {
|
||||
colored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChangeTag> for LineStyle {
|
||||
fn from(value: ChangeTag) -> Self {
|
||||
match value {
|
||||
ChangeTag::Equal => LineStyle {
|
||||
fgcolor: None,
|
||||
style: Some(Styles::Dimmed),
|
||||
},
|
||||
ChangeTag::Delete => LineStyle {
|
||||
fgcolor: Some(Color::Red),
|
||||
style: None,
|
||||
},
|
||||
ChangeTag::Insert => LineStyle {
|
||||
fgcolor: Some(Color::Green),
|
||||
style: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Line {
|
||||
index: Option<OneIndexed>,
|
||||
width: NonZeroUsize,
|
||||
}
|
||||
|
||||
impl Display for Line {
|
||||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
|
||||
match self.index {
|
||||
None => {
|
||||
for _ in 0..self.width.get() {
|
||||
f.write_str(" ")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Some(idx) => write!(f, "{:<width$}", idx, width = self.width.get()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate the length of the string representation of `value`
|
||||
pub(super) fn calculate_print_width(mut value: OneIndexed) -> NonZeroUsize {
|
||||
const TEN: OneIndexed = OneIndexed::from_zero_indexed(9);
|
||||
|
||||
let mut width = OneIndexed::ONE;
|
||||
|
||||
while value >= TEN {
|
||||
value = OneIndexed::new(value.get() / 10).unwrap_or(OneIndexed::MIN);
|
||||
width = width.checked_add(1).unwrap();
|
||||
}
|
||||
|
||||
width
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
use std::collections::HashSet;
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
|
||||
use serde::ser::SerializeSeq;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
|
||||
use crate::fs::{relativize_path, relativize_path_to};
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
|
||||
/// Generate JSON with violations in GitLab CI format
|
||||
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implement-a-custom-tool
|
||||
pub struct GitlabEmitter {
|
||||
project_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for GitlabEmitter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
project_dir: std::env::var("CI_PROJECT_DIR").ok(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Emitter for GitlabEmitter {
|
||||
fn emit(
|
||||
&mut self,
|
||||
writer: &mut dyn Write,
|
||||
diagnostics: &[Diagnostic],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
serde_json::to_writer_pretty(
|
||||
writer,
|
||||
&SerializedMessages {
|
||||
diagnostics,
|
||||
context,
|
||||
project_dir: self.project_dir.as_deref(),
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializedMessages<'a> {
|
||||
diagnostics: &'a [Diagnostic],
|
||||
context: &'a EmitterContext<'a>,
|
||||
project_dir: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl Serialize for SerializedMessages<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut s = serializer.serialize_seq(Some(self.diagnostics.len()))?;
|
||||
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
|
||||
|
||||
for diagnostic in self.diagnostics {
|
||||
let filename = diagnostic.expect_ruff_filename();
|
||||
|
||||
let (start_location, end_location) = if self.context.is_notebook(&filename) {
|
||||
// We can't give a reasonable location for the structured formats,
|
||||
// so we show one that's clearly a fallback
|
||||
Default::default()
|
||||
} else {
|
||||
(
|
||||
diagnostic.expect_ruff_start_location(),
|
||||
diagnostic.expect_ruff_end_location(),
|
||||
)
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
|| relativize_path(&filename),
|
||||
|project_dir| relativize_path_to(&filename, project_dir),
|
||||
);
|
||||
|
||||
let mut message_fingerprint = fingerprint(diagnostic, &path, 0);
|
||||
|
||||
// Make sure that we do not get a fingerprint that is already in use
|
||||
// by adding in the previously generated one.
|
||||
while fingerprints.contains(&message_fingerprint) {
|
||||
message_fingerprint = fingerprint(diagnostic, &path, message_fingerprint);
|
||||
}
|
||||
fingerprints.insert(message_fingerprint);
|
||||
|
||||
let (description, check_name) = if let Some(code) = diagnostic.secondary_code() {
|
||||
(diagnostic.body().to_string(), code.as_str())
|
||||
} else {
|
||||
let description = diagnostic.body();
|
||||
let description_without_prefix = description
|
||||
.strip_prefix("SyntaxError: ")
|
||||
.unwrap_or(description);
|
||||
|
||||
(description_without_prefix.to_string(), "syntax-error")
|
||||
};
|
||||
|
||||
let value = json!({
|
||||
"check_name": check_name,
|
||||
"description": description,
|
||||
"severity": "major",
|
||||
"fingerprint": format!("{:x}", message_fingerprint),
|
||||
"location": {
|
||||
"path": path,
|
||||
"positions": {
|
||||
"begin": start_location,
|
||||
"end": end_location,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
}
|
||||
|
||||
s.end()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a unique fingerprint to identify a violation.
|
||||
fn fingerprint(message: &Diagnostic, project_path: &str, salt: u64) -> u64 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
salt.hash(&mut hasher);
|
||||
message.name().hash(&mut hasher);
|
||||
project_path.hash(&mut hasher);
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use crate::message::GitlabEmitter;
|
||||
use crate::message::tests::{
|
||||
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let mut emitter = GitlabEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(redact_fingerprint(&content));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let mut emitter = GitlabEmitter::default();
|
||||
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
|
||||
|
||||
assert_snapshot!(redact_fingerprint(&content));
|
||||
}
|
||||
|
||||
// Redact the fingerprint because the default hasher isn't stable across platforms.
|
||||
fn redact_fingerprint(content: &str) -> String {
|
||||
static FINGERPRINT_HAY_KEY: &str = r#""fingerprint": ""#;
|
||||
|
||||
let mut output = String::with_capacity(content.len());
|
||||
let mut last = 0;
|
||||
|
||||
for (start, _) in content.match_indices(FINGERPRINT_HAY_KEY) {
|
||||
let fingerprint_hash_start = start + FINGERPRINT_HAY_KEY.len();
|
||||
output.push_str(&content[last..fingerprint_hash_start]);
|
||||
output.push_str("<redacted>");
|
||||
last = fingerprint_hash_start
|
||||
+ content[fingerprint_hash_start..]
|
||||
.find('"')
|
||||
.expect("Expected terminating quote");
|
||||
}
|
||||
|
||||
output.push_str(&content[last..]);
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
use std::num::NonZeroUsize;
|
||||
@@ -6,18 +7,15 @@ use colored::Colorize;
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_source_file::{LineColumn, OneIndexed};
|
||||
|
||||
use crate::fs::relativize_path;
|
||||
use crate::message::diff::calculate_print_width;
|
||||
use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
|
||||
use crate::message::{Emitter, EmitterContext, MessageWithLocation, group_diagnostics_by_filename};
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
use crate::settings::types::UnsafeFixes;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GroupedEmitter {
|
||||
show_fix_status: bool,
|
||||
show_source: bool,
|
||||
unsafe_fixes: UnsafeFixes,
|
||||
}
|
||||
|
||||
@@ -28,12 +26,6 @@ impl GroupedEmitter {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_show_source(mut self, show_source: bool) -> Self {
|
||||
self.show_source = show_source;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
|
||||
self.unsafe_fixes = unsafe_fixes;
|
||||
@@ -60,8 +52,8 @@ impl Emitter for GroupedEmitter {
|
||||
max_column_length = max_column_length.max(message.start_location.column);
|
||||
}
|
||||
|
||||
let row_length = calculate_print_width(max_row_length);
|
||||
let column_length = calculate_print_width(max_column_length);
|
||||
let row_length = max_row_length.digits();
|
||||
let column_length = max_column_length.digits();
|
||||
|
||||
// Print the filename.
|
||||
writeln!(writer, "{}:", relativize_path(&*filename).underline())?;
|
||||
@@ -76,29 +68,53 @@ impl Emitter for GroupedEmitter {
|
||||
message,
|
||||
show_fix_status: self.show_fix_status,
|
||||
unsafe_fixes: self.unsafe_fixes,
|
||||
show_source: self.show_source,
|
||||
row_length,
|
||||
column_length,
|
||||
}
|
||||
)?;
|
||||
}
|
||||
|
||||
// Print a blank line between files, unless we're showing the source, in which case
|
||||
// we'll have already printed a blank line between messages.
|
||||
if !self.show_source {
|
||||
writeln!(writer)?;
|
||||
}
|
||||
// Print a blank line between files.
|
||||
writeln!(writer)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageWithLocation<'a> {
|
||||
message: &'a Diagnostic,
|
||||
start_location: LineColumn,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for MessageWithLocation<'_> {
|
||||
type Target = Diagnostic;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
fn group_diagnostics_by_filename(
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> BTreeMap<String, Vec<MessageWithLocation<'_>>> {
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for diagnostic in diagnostics {
|
||||
grouped_messages
|
||||
.entry(diagnostic.expect_ruff_filename())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(MessageWithLocation {
|
||||
message: diagnostic,
|
||||
start_location: diagnostic.expect_ruff_start_location(),
|
||||
});
|
||||
}
|
||||
grouped_messages
|
||||
}
|
||||
|
||||
struct DisplayGroupedMessage<'a> {
|
||||
message: MessageWithLocation<'a>,
|
||||
show_fix_status: bool,
|
||||
unsafe_fixes: UnsafeFixes,
|
||||
show_source: bool,
|
||||
row_length: NonZeroUsize,
|
||||
column_length: NonZeroUsize,
|
||||
notebook_index: Option<&'a NotebookIndex>,
|
||||
@@ -114,8 +130,7 @@ impl Display for DisplayGroupedMessage<'_> {
|
||||
write!(
|
||||
f,
|
||||
" {row_padding}",
|
||||
row_padding = " "
|
||||
.repeat(self.row_length.get() - calculate_print_width(start_location.line).get())
|
||||
row_padding = " ".repeat(self.row_length.get() - start_location.line.digits().get())
|
||||
)?;
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
@@ -142,9 +157,8 @@ impl Display for DisplayGroupedMessage<'_> {
|
||||
f,
|
||||
"{row}{sep}{col}{col_padding} {code_and_body}",
|
||||
sep = ":".cyan(),
|
||||
col_padding = " ".repeat(
|
||||
self.column_length.get() - calculate_print_width(start_location.column).get()
|
||||
),
|
||||
col_padding =
|
||||
" ".repeat(self.column_length.get() - start_location.column.digits().get()),
|
||||
code_and_body = RuleCodeAndBody {
|
||||
message,
|
||||
show_fix_status: self.show_fix_status,
|
||||
@@ -152,51 +166,50 @@ impl Display for DisplayGroupedMessage<'_> {
|
||||
},
|
||||
)?;
|
||||
|
||||
if self.show_source {
|
||||
use std::fmt::Write;
|
||||
let mut padded = PadAdapter::new(f);
|
||||
writeln!(
|
||||
padded,
|
||||
"{}",
|
||||
MessageCodeFrame {
|
||||
message,
|
||||
notebook_index: self.notebook_index
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct RuleCodeAndBody<'a> {
|
||||
pub(crate) message: &'a Diagnostic,
|
||||
pub(crate) show_fix_status: bool,
|
||||
pub(crate) unsafe_fixes: UnsafeFixes,
|
||||
}
|
||||
|
||||
impl Display for RuleCodeAndBody<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if self.show_fix_status {
|
||||
if let Some(fix) = self.message.fix() {
|
||||
// Do not display an indicator for inapplicable fixes
|
||||
if fix.applies(self.unsafe_fixes.required_applicability()) {
|
||||
if let Some(code) = self.message.secondary_code() {
|
||||
write!(f, "{} ", code.red().bold())?;
|
||||
}
|
||||
return write!(
|
||||
f,
|
||||
"{fix}{body}",
|
||||
fix = format_args!("[{}] ", "*".cyan()),
|
||||
body = self.message.body(),
|
||||
);
|
||||
}
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adapter that adds a ' ' at the start of every line without the need to copy the string.
|
||||
/// Inspired by Rust's `debug_struct()` internal implementation that also uses a `PadAdapter`.
|
||||
struct PadAdapter<'buf> {
|
||||
buf: &'buf mut (dyn std::fmt::Write + 'buf),
|
||||
on_newline: bool,
|
||||
}
|
||||
|
||||
impl<'buf> PadAdapter<'buf> {
|
||||
fn new(buf: &'buf mut (dyn std::fmt::Write + 'buf)) -> Self {
|
||||
Self {
|
||||
buf,
|
||||
on_newline: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Write for PadAdapter<'_> {
|
||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||
for s in s.split_inclusive('\n') {
|
||||
if self.on_newline {
|
||||
self.buf.write_str(" ")?;
|
||||
}
|
||||
|
||||
self.on_newline = s.ends_with('\n');
|
||||
self.buf.write_str(s)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
if let Some(code) = self.message.secondary_code() {
|
||||
write!(
|
||||
f,
|
||||
"{code} {body}",
|
||||
code = code.red().bold(),
|
||||
body = self.message.body(),
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{code}: {body}",
|
||||
code = self.message.id().as_str().red().bold(),
|
||||
body = self.message.body(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,19 +239,9 @@ mod tests {
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_source() {
|
||||
let mut emitter = GroupedEmitter::default().with_show_source(true);
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fix_status() {
|
||||
let mut emitter = GroupedEmitter::default()
|
||||
.with_show_fix_status(true)
|
||||
.with_show_source(true);
|
||||
let mut emitter = GroupedEmitter::default().with_show_fix_status(true);
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
assert_snapshot!(content);
|
||||
@@ -248,7 +251,6 @@ mod tests {
|
||||
fn fix_status_unsafe() {
|
||||
let mut emitter = GroupedEmitter::default()
|
||||
.with_show_fix_status(true)
|
||||
.with_show_source(true)
|
||||
.with_unsafe_fixes(UnsafeFixes::Enabled);
|
||||
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use std::io::Write;
|
||||
use std::ops::Deref;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -12,10 +10,9 @@ use ruff_db::diagnostic::{
|
||||
use ruff_db::files::File;
|
||||
|
||||
pub use github::GithubEmitter;
|
||||
pub use gitlab::GitlabEmitter;
|
||||
pub use grouped::GroupedEmitter;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{LineColumn, SourceFile};
|
||||
use ruff_source_file::SourceFile;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
pub use sarif::SarifEmitter;
|
||||
pub use text::TextEmitter;
|
||||
@@ -23,9 +20,7 @@ pub use text::TextEmitter;
|
||||
use crate::Fix;
|
||||
use crate::registry::Rule;
|
||||
|
||||
mod diff;
|
||||
mod github;
|
||||
mod gitlab;
|
||||
mod grouped;
|
||||
mod sarif;
|
||||
mod text;
|
||||
@@ -134,35 +129,6 @@ impl FileResolver for EmitterContext<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
struct MessageWithLocation<'a> {
|
||||
message: &'a Diagnostic,
|
||||
start_location: LineColumn,
|
||||
}
|
||||
|
||||
impl Deref for MessageWithLocation<'_> {
|
||||
type Target = Diagnostic;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
fn group_diagnostics_by_filename(
|
||||
diagnostics: &[Diagnostic],
|
||||
) -> BTreeMap<String, Vec<MessageWithLocation<'_>>> {
|
||||
let mut grouped_messages = BTreeMap::default();
|
||||
for diagnostic in diagnostics {
|
||||
grouped_messages
|
||||
.entry(diagnostic.expect_ruff_filename())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(MessageWithLocation {
|
||||
message: diagnostic,
|
||||
start_location: diagnostic.expect_ruff_start_location(),
|
||||
});
|
||||
}
|
||||
grouped_messages
|
||||
}
|
||||
|
||||
/// Display format for [`Diagnostic`]s.
|
||||
///
|
||||
/// The emitter serializes a slice of [`Diagnostic`]s and writes them to a [`Write`].
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/gitlab.rs
|
||||
expression: redact_fingerprint(&content)
|
||||
---
|
||||
[
|
||||
{
|
||||
"check_name": "F401",
|
||||
"description": "`os` imported but unused",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 8,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 10,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "F841",
|
||||
"description": "Local variable `x` is assigned to but never used",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 5,
|
||||
"line": 6
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "F821",
|
||||
"description": "Undefined name `a`",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "undef.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 4,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 5,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/gitlab.rs
|
||||
expression: redact_fingerprint(&content)
|
||||
---
|
||||
[
|
||||
{
|
||||
"check_name": "syntax-error",
|
||||
"description": "Expected one or more symbol names after import",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 15,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
{
|
||||
"check_name": "syntax-error",
|
||||
"description": "Expected ')', found newline",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 12,
|
||||
"line": 3
|
||||
},
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
]
|
||||
@@ -1,30 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/grouped.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
---
|
||||
fib.py:
|
||||
1:8 F401 `os` imported but unused
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
|
||||
6:5 F841 Local variable `x` is assigned to but never used
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^ F841
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
|
||||
|
||||
undef.py:
|
||||
1:4 F821 Undefined name `a`
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
|
|
||||
|
||||
@@ -1,30 +1,10 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/grouped.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
---
|
||||
fib.py:
|
||||
1:8 F401 [*] `os` imported but unused
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
|
||||
6:5 F841 [*] Local variable `x` is assigned to but never used
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^ F841
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
|
||||
|
||||
undef.py:
|
||||
1:4 F821 Undefined name `a`
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
|
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/grouped.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
---
|
||||
fib.py:
|
||||
1:8 F401 `os` imported but unused
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
|
||||
6:5 F841 Local variable `x` is assigned to but never used
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^ F841
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
|
||||
undef.py:
|
||||
1:4 F821 Undefined name `a`
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
|
|
||||
@@ -1,28 +1,30 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/text.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
---
|
||||
fib.py:1:8: F401 `os` imported but unused
|
||||
F401 `os` imported but unused
|
||||
--> fib.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
fib.py:6:5: F841 Local variable `x` is assigned to but never used
|
||||
F841 Local variable `x` is assigned to but never used
|
||||
--> fib.py:6:5
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^ F841
|
||||
| ^
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
F821 Undefined name `a`
|
||||
--> undef.py:1:4
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
| ^
|
||||
|
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/text.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
---
|
||||
fib.py:1:8: F401 `os` imported but unused
|
||||
F401 `os` imported but unused
|
||||
--> fib.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
fib.py:6:5: F841 Local variable `x` is assigned to but never used
|
||||
F841 Local variable `x` is assigned to but never used
|
||||
--> fib.py:6:5
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^ F841
|
||||
| ^
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
F821 Undefined name `a`
|
||||
--> undef.py:1:4
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
| ^
|
||||
|
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/text.rs
|
||||
expression: content
|
||||
snapshot_kind: text
|
||||
---
|
||||
fib.py:1:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> fib.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
F841 [*] Local variable `x` is assigned to but never used
|
||||
--> fib.py:6:5
|
||||
|
|
||||
4 | def fibonacci(n):
|
||||
5 | """Compute the nth number in the Fibonacci sequence."""
|
||||
6 | x = 1
|
||||
| ^ F841
|
||||
| ^
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
F821 Undefined name `a`
|
||||
--> undef.py:1:4
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^ F821
|
||||
| ^
|
||||
|
|
||||
|
||||
@@ -2,29 +2,32 @@
|
||||
source: crates/ruff_linter/src/message/text.rs
|
||||
expression: content
|
||||
---
|
||||
notebook.ipynb:cell 1:2:8: F401 [*] `os` imported but unused
|
||||
F401 [*] `os` imported but unused
|
||||
--> notebook.ipynb:cell 1:2:8
|
||||
|
|
||||
1 | # cell 1
|
||||
2 | import os
|
||||
| ^^ F401
|
||||
| ^^
|
||||
|
|
||||
= help: Remove unused import: `os`
|
||||
help: Remove unused import: `os`
|
||||
|
||||
notebook.ipynb:cell 2:2:8: F401 [*] `math` imported but unused
|
||||
F401 [*] `math` imported but unused
|
||||
--> notebook.ipynb:cell 2:2:8
|
||||
|
|
||||
1 | # cell 2
|
||||
2 | import math
|
||||
| ^^^^ F401
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | print('hello world')
|
||||
|
|
||||
= help: Remove unused import: `math`
|
||||
help: Remove unused import: `math`
|
||||
|
||||
notebook.ipynb:cell 3:4:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
F841 [*] Local variable `x` is assigned to but never used
|
||||
--> notebook.ipynb:cell 3:4:5
|
||||
|
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
4 | x = 1
|
||||
| ^ F841
|
||||
| ^
|
||||
|
|
||||
= help: Remove assignment to unused variable `x`
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
source: crates/ruff_linter/src/message/text.rs
|
||||
expression: content
|
||||
---
|
||||
syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
|
||||
invalid-syntax: Expected one or more symbol names after import
|
||||
--> syntax_errors.py:1:15
|
||||
|
|
||||
1 | from os import
|
||||
| ^
|
||||
2 |
|
||||
3 | if call(foo
|
||||
4 | def bar():
|
||||
|
|
||||
|
||||
syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline
|
||||
invalid-syntax: Expected ')', found newline
|
||||
--> syntax_errors.py:3:12
|
||||
|
|
||||
1 | from os import
|
||||
2 |
|
||||
|
||||
@@ -1,41 +1,19 @@
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io::Write;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
||||
|
||||
use ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode, ceil_char_boundary,
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
|
||||
};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
use crate::settings::types::UnsafeFixes;
|
||||
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
struct EmitterFlags: u8 {
|
||||
/// Whether to show the diff of a fix, for diagnostics that have a fix.
|
||||
const SHOW_FIX_DIFF = 1 << 1;
|
||||
/// Whether to show the source code of a diagnostic.
|
||||
const SHOW_SOURCE = 1 << 2;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextEmitter {
|
||||
flags: EmitterFlags,
|
||||
config: DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl Default for TextEmitter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
flags: EmitterFlags::default(),
|
||||
config: DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Concise)
|
||||
.hide_severity(true)
|
||||
@@ -53,13 +31,17 @@ impl TextEmitter {
|
||||
|
||||
#[must_use]
|
||||
pub fn with_show_fix_diff(mut self, show_fix_diff: bool) -> Self {
|
||||
self.flags.set(EmitterFlags::SHOW_FIX_DIFF, show_fix_diff);
|
||||
self.config = self.config.show_fix_diff(show_fix_diff);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_show_source(mut self, show_source: bool) -> Self {
|
||||
self.flags.set(EmitterFlags::SHOW_SOURCE, show_source);
|
||||
self.config = self.config.format(if show_source {
|
||||
DiagnosticFormat::Full
|
||||
} else {
|
||||
DiagnosticFormat::Concise
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
@@ -91,297 +73,16 @@ impl Emitter for TextEmitter {
|
||||
diagnostics: &[Diagnostic],
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in diagnostics {
|
||||
write!(writer, "{}", message.display(context, &self.config))?;
|
||||
|
||||
let filename = message.expect_ruff_filename();
|
||||
let notebook_index = context.notebook_index(&filename);
|
||||
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
|
||||
// The `0..0` range is used to highlight file-level diagnostics.
|
||||
if message.expect_range() != TextRange::default() {
|
||||
writeln!(
|
||||
writer,
|
||||
"{}",
|
||||
MessageCodeFrame {
|
||||
message,
|
||||
notebook_index
|
||||
}
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.flags.intersects(EmitterFlags::SHOW_FIX_DIFF) {
|
||||
if let Some(diff) = Diff::from_message(message) {
|
||||
writeln!(writer, "{diff}")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
DisplayDiagnostics::new(context, &self.config, diagnostics)
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct RuleCodeAndBody<'a> {
|
||||
pub(crate) message: &'a Diagnostic,
|
||||
pub(crate) show_fix_status: bool,
|
||||
pub(crate) unsafe_fixes: UnsafeFixes,
|
||||
}
|
||||
|
||||
impl Display for RuleCodeAndBody<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if self.show_fix_status {
|
||||
if let Some(fix) = self.message.fix() {
|
||||
// Do not display an indicator for inapplicable fixes
|
||||
if fix.applies(self.unsafe_fixes.required_applicability()) {
|
||||
if let Some(code) = self.message.secondary_code() {
|
||||
write!(f, "{} ", code.red().bold())?;
|
||||
}
|
||||
return write!(
|
||||
f,
|
||||
"{fix}{body}",
|
||||
fix = format_args!("[{}] ", "*".cyan()),
|
||||
body = self.message.body(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(code) = self.message.secondary_code() {
|
||||
write!(
|
||||
f,
|
||||
"{code} {body}",
|
||||
code = code.red().bold(),
|
||||
body = self.message.body(),
|
||||
)
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{code}: {body}",
|
||||
code = self.message.id().as_str().red().bold(),
|
||||
body = self.message.body(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct MessageCodeFrame<'a> {
|
||||
pub(crate) message: &'a Diagnostic,
|
||||
pub(crate) notebook_index: Option<&'a NotebookIndex>,
|
||||
}
|
||||
|
||||
impl Display for MessageCodeFrame<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let suggestion = self.message.first_help_text();
|
||||
let footers = if let Some(suggestion) = suggestion {
|
||||
vec![Level::Help.title(suggestion)]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let source_file = self.message.expect_ruff_source_file();
|
||||
let source_code = source_file.to_source_code();
|
||||
|
||||
let content_start_index = source_code.line_index(self.message.expect_range().start());
|
||||
let mut start_index = content_start_index.saturating_sub(2);
|
||||
|
||||
// If we're working with a Jupyter Notebook, skip the lines which are
|
||||
// outside of the cell containing the diagnostic.
|
||||
if let Some(index) = self.notebook_index {
|
||||
let content_start_cell = index.cell(content_start_index).unwrap_or(OneIndexed::MIN);
|
||||
while start_index < content_start_index {
|
||||
if index.cell(start_index).unwrap_or(OneIndexed::MIN) == content_start_cell {
|
||||
break;
|
||||
}
|
||||
start_index = start_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Trim leading empty lines.
|
||||
while start_index < content_start_index {
|
||||
if !source_code.line_text(start_index).trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
start_index = start_index.saturating_add(1);
|
||||
}
|
||||
|
||||
let content_end_index = source_code.line_index(self.message.expect_range().end());
|
||||
let mut end_index = content_end_index
|
||||
.saturating_add(2)
|
||||
.min(OneIndexed::from_zero_indexed(source_code.line_count()));
|
||||
|
||||
// If we're working with a Jupyter Notebook, skip the lines which are
|
||||
// outside of the cell containing the diagnostic.
|
||||
if let Some(index) = self.notebook_index {
|
||||
let content_end_cell = index.cell(content_end_index).unwrap_or(OneIndexed::MIN);
|
||||
while end_index > content_end_index {
|
||||
if index.cell(end_index).unwrap_or(OneIndexed::MIN) == content_end_cell {
|
||||
break;
|
||||
}
|
||||
end_index = end_index.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Trim trailing empty lines.
|
||||
while end_index > content_end_index {
|
||||
if !source_code.line_text(end_index).trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
end_index = end_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
let start_offset = source_code.line_start(start_index);
|
||||
let end_offset = source_code.line_end(end_index);
|
||||
|
||||
let source = replace_unprintable(
|
||||
source_code.slice(TextRange::new(start_offset, end_offset)),
|
||||
self.message.expect_range() - start_offset,
|
||||
)
|
||||
.fix_up_empty_spans_after_line_terminator();
|
||||
|
||||
let label = self
|
||||
.message
|
||||
.secondary_code()
|
||||
.map(SecondaryCode::as_str)
|
||||
.unwrap_or_default();
|
||||
|
||||
let line_start = self.notebook_index.map_or_else(
|
||||
|| start_index.get(),
|
||||
|notebook_index| {
|
||||
notebook_index
|
||||
.cell_row(start_index)
|
||||
.unwrap_or(OneIndexed::MIN)
|
||||
.get()
|
||||
},
|
||||
);
|
||||
|
||||
let span = usize::from(source.annotation_range.start())
|
||||
..usize::from(source.annotation_range.end());
|
||||
let annotation = Level::Error.span(span).label(label);
|
||||
let snippet = Snippet::source(&source.text)
|
||||
.line_start(line_start)
|
||||
.annotation(annotation)
|
||||
.fold(false);
|
||||
let message = Level::None.title("").snippet(snippet).footers(footers);
|
||||
|
||||
let renderer = if !cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize() {
|
||||
Renderer::styled()
|
||||
} else {
|
||||
Renderer::plain()
|
||||
}
|
||||
.cut_indicator("…");
|
||||
let rendered = renderer.render(message);
|
||||
writeln!(f, "{rendered}")
|
||||
}
|
||||
}
|
||||
|
||||
/// Given some source code and an annotation range, this routine replaces
|
||||
/// unprintable characters with printable representations of them.
|
||||
///
|
||||
/// The source code returned has an annotation that is updated to reflect
|
||||
/// changes made to the source code (if any).
|
||||
///
|
||||
/// We don't need to normalize whitespace, such as converting tabs to spaces,
|
||||
/// because `annotate-snippets` handles that internally. Similarly, it's safe to
|
||||
/// modify the annotation ranges by inserting 3-byte Unicode replacements
|
||||
/// because `annotate-snippets` will account for their actual width when
|
||||
/// rendering and displaying the column to the user.
|
||||
fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode<'_> {
|
||||
let mut result = String::new();
|
||||
let mut last_end = 0;
|
||||
let mut range = annotation_range;
|
||||
|
||||
// Updates the range given by the caller whenever a single byte (at
|
||||
// `index` in `source`) is replaced with `len` bytes.
|
||||
//
|
||||
// When the index occurs before the start of the range, the range is
|
||||
// offset by `len`. When the range occurs after or at the start but before
|
||||
// the end, then the end of the range only is offset by `len`.
|
||||
let mut update_range = |index, len| {
|
||||
if index < usize::from(annotation_range.start()) {
|
||||
range += TextSize::new(len - 1);
|
||||
} else if index < usize::from(annotation_range.end()) {
|
||||
range = range.add_end(TextSize::new(len - 1));
|
||||
}
|
||||
};
|
||||
|
||||
// If `c` is an unprintable character, then this returns a printable
|
||||
// representation of it (using a fancier Unicode codepoint).
|
||||
let unprintable_replacement = |c: char| -> Option<char> {
|
||||
match c {
|
||||
'\x07' => Some('␇'),
|
||||
'\x08' => Some('␈'),
|
||||
'\x1b' => Some('␛'),
|
||||
'\x7f' => Some('␡'),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
|
||||
for (index, c) in source.char_indices() {
|
||||
if let Some(printable) = unprintable_replacement(c) {
|
||||
result.push_str(&source[last_end..index]);
|
||||
result.push(printable);
|
||||
last_end = index + 1;
|
||||
|
||||
let len = printable.text_len().to_u32();
|
||||
update_range(index, len);
|
||||
}
|
||||
}
|
||||
|
||||
// No tabs or unprintable chars
|
||||
if result.is_empty() {
|
||||
SourceCode {
|
||||
annotation_range,
|
||||
text: Cow::Borrowed(source),
|
||||
}
|
||||
} else {
|
||||
result.push_str(&source[last_end..]);
|
||||
SourceCode {
|
||||
annotation_range: range,
|
||||
text: Cow::Owned(result),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceCode<'a> {
|
||||
text: Cow<'a, str>,
|
||||
annotation_range: TextRange,
|
||||
}
|
||||
|
||||
impl<'a> SourceCode<'a> {
|
||||
/// This attempts to "fix up" the span on `SourceCode` in the case where
|
||||
/// it's an empty span immediately following a line terminator.
|
||||
///
|
||||
/// At present, `annotate-snippets` (both upstream and our vendored copy)
|
||||
/// will render annotations of such spans to point to the space immediately
|
||||
/// following the previous line. But ideally, this should point to the space
|
||||
/// immediately preceding the next line.
|
||||
///
|
||||
/// After attempting to fix `annotate-snippets` and giving up after a couple
|
||||
/// hours, this routine takes a different tact: it adjusts the span to be
|
||||
/// non-empty and it will cover the first codepoint of the following line.
|
||||
/// This forces `annotate-snippets` to point to the right place.
|
||||
///
|
||||
/// See also: <https://github.com/astral-sh/ruff/issues/15509>
|
||||
fn fix_up_empty_spans_after_line_terminator(self) -> SourceCode<'a> {
|
||||
if !self.annotation_range.is_empty()
|
||||
|| self.annotation_range.start() == TextSize::from(0)
|
||||
|| self.annotation_range.start() >= self.text.text_len()
|
||||
{
|
||||
return self;
|
||||
}
|
||||
if self.text.as_bytes()[self.annotation_range.start().to_usize() - 1] != b'\n' {
|
||||
return self;
|
||||
}
|
||||
let start = self.annotation_range.start();
|
||||
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
|
||||
SourceCode {
|
||||
annotation_range: TextRange::new(start, end),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user