Compare commits
176 Commits
brent/cach
...
david/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0925fad4a | ||
|
|
808c94d509 | ||
|
|
b95d22c08e | ||
|
|
f3e66dd503 | ||
|
|
6516db7835 | ||
|
|
03c873765e | ||
|
|
c90707875e | ||
|
|
8e20e589f1 | ||
|
|
113e32b956 | ||
|
|
fdc18eefc3 | ||
|
|
5f40651ae7 | ||
|
|
ea031a3b39 | ||
|
|
93b64daa4a | ||
|
|
74376375e4 | ||
|
|
30f52d8cf5 | ||
|
|
77bc32b9b9 | ||
|
|
1a368b0bf9 | ||
|
|
134435415e | ||
|
|
bc6e105c18 | ||
|
|
6bd413df6c | ||
|
|
85bd961fd3 | ||
|
|
d37911685f | ||
|
|
580577e667 | ||
|
|
dce25da19a | ||
|
|
06cd249a9b | ||
|
|
48d5bd13fa | ||
|
|
e7e7b7bf21 | ||
|
|
57e2e8664f | ||
|
|
18aae21b9a | ||
|
|
d8151f0239 | ||
|
|
2ee56735e2 | ||
|
|
ade6a4262a | ||
|
|
d43e6fb9c6 | ||
|
|
b30d97e5e0 | ||
|
|
5c5d50d57a | ||
|
|
b3a26a50ad | ||
|
|
6a2d358d7a | ||
|
|
b07def07c9 | ||
|
|
2ab1502e51 | ||
|
|
a3f28baab4 | ||
|
|
a71513bae1 | ||
|
|
d2d4b115e3 | ||
|
|
27b03a9d7b | ||
|
|
32c454bb56 | ||
|
|
f6b7418def | ||
|
|
8f8c39c435 | ||
|
|
4739bc8d14 | ||
|
|
7b4103bcb6 | ||
|
|
38049aae12 | ||
|
|
ec3d5ebda2 | ||
|
|
d797592f70 | ||
|
|
eb02aa5676 | ||
|
|
e593761232 | ||
|
|
8979271ea8 | ||
|
|
d1a286226c | ||
|
|
1ba32684da | ||
|
|
70d4b271da | ||
|
|
feaedb1812 | ||
|
|
6237ecb4db | ||
|
|
2a5ace6e55 | ||
|
|
4ecf1d205a | ||
|
|
c5ac998892 | ||
|
|
04a8f64cd7 | ||
|
|
6e00adf308 | ||
|
|
864196b988 | ||
|
|
ae26fa020c | ||
|
|
88a679945c | ||
|
|
941be52358 | ||
|
|
13624ce17f | ||
|
|
edb2f8e997 | ||
|
|
5e6ad849ff | ||
|
|
865a9b3424 | ||
|
|
d449c541cb | ||
|
|
f7c6a6b2d0 | ||
|
|
656273bf3d | ||
|
|
81867ea7ce | ||
|
|
a54061e757 | ||
|
|
19569bf838 | ||
|
|
e0f4f25d28 | ||
|
|
c6a123290d | ||
|
|
d4f64cd474 | ||
|
|
e4f64480da | ||
|
|
4016aff057 | ||
|
|
24134837f3 | ||
|
|
130d4e1135 | ||
|
|
e63dfa3d18 | ||
|
|
6d0f3ef3a5 | ||
|
|
201b079084 | ||
|
|
2680f2ed81 | ||
|
|
afdfa042f3 | ||
|
|
8c0743df97 | ||
|
|
13634ff433 | ||
|
|
7a541f597f | ||
|
|
2deb50f4e3 | ||
|
|
85e22645aa | ||
|
|
d3f6de8b0e | ||
|
|
9eb8174209 | ||
|
|
9c68616d91 | ||
|
|
9280c7e945 | ||
|
|
e19145040f | ||
|
|
ef3a195f28 | ||
|
|
008bbfdf5a | ||
|
|
df5eba7583 | ||
|
|
469c50b0b7 | ||
|
|
738246627f | ||
|
|
e867830848 | ||
|
|
72fdb7d439 | ||
|
|
fbf1dfc782 | ||
|
|
a0d8ff51dd | ||
|
|
165091a31c | ||
|
|
4a4dc38b5b | ||
|
|
3e366fdf13 | ||
|
|
53e9e4421c | ||
|
|
859262bd49 | ||
|
|
c0768dfd96 | ||
|
|
d4eb4277ad | ||
|
|
b033fb6bfd | ||
|
|
f722bfa9e6 | ||
|
|
b124e182ca | ||
|
|
57373a7e4d | ||
|
|
ae9d450b5f | ||
|
|
c8c80e054e | ||
|
|
4bc34b82ef | ||
|
|
d9cab4d242 | ||
|
|
d77b7312b0 | ||
|
|
f9091ea8bb | ||
|
|
1d2181623c | ||
|
|
dc6be457b5 | ||
|
|
1079975b35 | ||
|
|
39eb0f6c6c | ||
|
|
d13228ab85 | ||
|
|
9461d3076f | ||
|
|
63d1d332b3 | ||
|
|
e0149cd9f3 | ||
|
|
2a00eca66b | ||
|
|
3d17897c02 | ||
|
|
fa1df4cedc | ||
|
|
89258f1938 | ||
|
|
1dcef1a011 | ||
|
|
ba629fe262 | ||
|
|
bb3a05f92b | ||
|
|
4daf59e5e7 | ||
|
|
88bd82938f | ||
|
|
5a55bab3f3 | ||
|
|
cc5885e564 | ||
|
|
4573a0f6a0 | ||
|
|
905b9d7f51 | ||
|
|
b605c3e232 | ||
|
|
c281891b5c | ||
|
|
53d795da67 | ||
|
|
385d6fa608 | ||
|
|
ba070bb6d5 | ||
|
|
dc10ab81bd | ||
|
|
7673d46b71 | ||
|
|
9d5ecacdc5 | ||
|
|
9af8597608 | ||
|
|
64e5780037 | ||
|
|
da8aa6a631 | ||
|
|
ee69d38000 | ||
|
|
fd335eb8b7 | ||
|
|
c82fa94e0a | ||
|
|
6d4687c9af | ||
|
|
9180cd094d | ||
|
|
9d98a66f65 | ||
|
|
cb60ecef6b | ||
|
|
215a1c55d4 | ||
|
|
5e29278aa2 | ||
|
|
af62d0368f | ||
|
|
30683e3a93 | ||
|
|
cbc8c08016 | ||
|
|
897889d1ce | ||
|
|
cb5a9ff8dc | ||
|
|
fcdffe4ac9 | ||
|
|
88de5727df | ||
|
|
b8dec79182 | ||
|
|
dc66019fbc |
8
.github/workflows/build-docker.yml
vendored
8
.github/workflows/build-docker.yml
vendored
@@ -63,7 +63,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Defining this makes sure the org.opencontainers.image.version OCI label becomes the actual release version and not the branch name
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
with:
|
||||
images: ${{ env.RUFF_BASE_IMG }}
|
||||
# Order is on purpose such that the label org.opencontainers.image.version has the first pattern with the full version
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
# ghcr.io prefers index level annotations
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
@@ -266,7 +266,7 @@ jobs:
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5.8.0
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: index
|
||||
with:
|
||||
|
||||
42
.github/workflows/ci.yaml
vendored
42
.github/workflows/ci.yaml
vendored
@@ -143,12 +143,12 @@ jobs:
|
||||
env:
|
||||
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
|
||||
run: |
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- ':**' \
|
||||
':!**/*.md' \
|
||||
':crates/ty_python_semantic/resources/mdtest/**/*.md' \
|
||||
# NOTE: Do not exclude all Markdown files here, but rather use
|
||||
# specific exclude patterns like 'docs/**'), because tests for
|
||||
# 'ty' are written in Markdown.
|
||||
if git diff --quiet "${MERGE_BASE}...HEAD" -- \
|
||||
':!docs/**' \
|
||||
':!assets/**' \
|
||||
':.github/workflows/ci.yaml' \
|
||||
; then
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
@@ -240,11 +240,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -298,11 +298,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@702b1908b5edf30d71a8d1666b724e0f0c6fa035 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Run tests"
|
||||
@@ -325,7 +325,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Run tests"
|
||||
@@ -429,7 +429,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
|
||||
uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
|
||||
with:
|
||||
tool: cargo-fuzz@0.11.2
|
||||
- name: "Install cargo-fuzz"
|
||||
@@ -451,7 +451,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
@@ -652,7 +652,7 @@ jobs:
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
workflow: "ci.yaml"
|
||||
check_artifacts: true
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- name: Fuzz
|
||||
env:
|
||||
FORCE_COLOR: 1
|
||||
@@ -682,7 +682,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
|
||||
- uses: cargo-bins/cargo-binstall@dd6a0ac24caa1243d18df0f770b941e990e8facc # v1.14.3
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -722,7 +722,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
@@ -765,7 +765,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
@@ -897,13 +897,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -911,7 +911,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
|
||||
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
@@ -930,13 +930,13 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@c99cc51b309eee71a866715cfa08c922f11cf898 # v2.56.19
|
||||
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -944,7 +944,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
|
||||
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
|
||||
with:
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
|
||||
6
.github/workflows/mypy_primer.yaml
vendored
6
.github/workflows/mypy_primer.yaml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
@@ -81,9 +81,9 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- 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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- 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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
|
||||
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
4
.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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
|
||||
4
.github/workflows/ty-ecosystem-report.yaml
vendored
4
.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@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@f0eec0e549684d8e1d7b8bc3e351202124b63bda"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@27dd66d9e397d986ef9c631119ee09556eab8af9"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
109
.github/workflows/typing_conformance.yaml
vendored
Normal file
109
.github/workflows/typing_conformance.yaml
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
name: Run typing conformance
|
||||
|
||||
permissions: {}
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "crates/ty*/**"
|
||||
- "crates/ruff_db"
|
||||
- "crates/ruff_python_ast"
|
||||
- "crates/ruff_python_parser"
|
||||
- ".github/workflows/typing_conformance.yaml"
|
||||
- ".github/workflows/typing_conformance_comment.yaml"
|
||||
- "Cargo.lock"
|
||||
- "!**.md"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
jobs:
|
||||
typing_conformance:
|
||||
name: Compute diagnostic diff
|
||||
runs-on: depot-ubuntu-22.04-32
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
path: ruff
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
repository: python/typing
|
||||
ref: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
|
||||
path: typing
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
|
||||
|
||||
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: rustup show
|
||||
|
||||
- name: Compute diagnostic diff
|
||||
shell: bash
|
||||
run: |
|
||||
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
|
||||
|
||||
# Build the executable for the old and new commit
|
||||
(
|
||||
cd ruff
|
||||
|
||||
echo "new commit"
|
||||
git checkout -b new_commit "${{ github.event.pull_request.head.sha }}"
|
||||
git rev-list --format=%s --max-count=1 new_commit
|
||||
cargo build --release --bin ty
|
||||
mv target/release/ty ty-new
|
||||
|
||||
echo "old commit (merge base)"
|
||||
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
|
||||
git checkout -b 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
|
||||
)
|
||||
|
||||
(
|
||||
cd typing/conformance/tests
|
||||
|
||||
echo "Running ty on old commit (merge base)"
|
||||
"$RUFF_DIR/ty-old" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/old-output.txt" 2>&1 || true
|
||||
|
||||
echo "Running ty on new commit"
|
||||
"$RUFF_DIR/ty-new" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/new-output.txt" 2>&1 || true
|
||||
)
|
||||
|
||||
if ! diff -u old-output.txt new-output.txt > typing_conformance_diagnostics.diff; then
|
||||
echo "Differences found between base and PR"
|
||||
else
|
||||
echo "No differences found"
|
||||
touch typing_conformance_diagnostics.diff
|
||||
fi
|
||||
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: typing_conformance_diagnostics_diff
|
||||
path: typing_conformance_diagnostics.diff
|
||||
|
||||
- name: Upload pr-number
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
97
.github/workflows/typing_conformance_comment.yaml
vendored
Normal file
97
.github/workflows/typing_conformance_comment.yaml
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
name: PR comment (typing_conformance)
|
||||
|
||||
on: # zizmor: ignore[dangerous-triggers]
|
||||
workflow_run:
|
||||
workflows: [Run typing conformance]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
workflow_run_id:
|
||||
description: The typing_conformance workflow that triggers the workflow run
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download PR number
|
||||
with:
|
||||
name: pr-number
|
||||
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Parse pull request number
|
||||
id: pr-number
|
||||
run: |
|
||||
if [[ -f pr-number ]]
|
||||
then
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: "Download typing_conformance results"
|
||||
id: download-typing_conformance_diff
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
with:
|
||||
name: typing_conformance_diagnostics_diff
|
||||
workflow: typing_conformance.yaml
|
||||
pr: ${{ steps.pr-number.outputs.pr-number }}
|
||||
path: pr/typing_conformance_diagnostics_diff
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Generate comment content
|
||||
id: generate-comment
|
||||
if: ${{ steps.download-typing_conformance_diff.outputs.found_artifact == 'true' }}
|
||||
run: |
|
||||
# Guard against malicious typing_conformance results that symlink to a secret
|
||||
# file on this runner
|
||||
if [[ -L pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff ]]
|
||||
then
|
||||
echo "Error: typing_conformance_diagnostics.diff cannot be a symlink"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Note this identifier is used to find the comment to update on
|
||||
# subsequent runs
|
||||
echo '<!-- generated-comment typing_conformance_diagnostics_diff -->' >> comment.txt
|
||||
|
||||
echo '## Diagnostic diff on typing conformance tests' >> comment.txt
|
||||
if [ -s "pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff" ]; then
|
||||
echo '<details>' >> comment.txt
|
||||
echo '<summary>Changes were detected when running ty on typing conformance tests</summary>' >> comment.txt
|
||||
echo '' >> comment.txt
|
||||
echo '```diff' >> comment.txt
|
||||
cat pr/typing_conformance_diagnostics_diff/typing_conformance_diagnostics.diff >> comment.txt
|
||||
echo '```' >> comment.txt
|
||||
echo '</details>' >> comment.txt
|
||||
else
|
||||
echo 'No changes detected when running ty on typing conformance tests ✅' >> comment.txt
|
||||
fi
|
||||
|
||||
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
|
||||
cat comment.txt >> "$GITHUB_OUTPUT"
|
||||
echo 'EOF' >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Find existing comment
|
||||
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
||||
if: steps.generate-comment.outcome == 'success'
|
||||
id: find-comment
|
||||
with:
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
comment-author: "github-actions[bot]"
|
||||
body-includes: "<!-- generated-comment typing_conformance_diagnostics_diff -->"
|
||||
|
||||
- name: Create or update comment
|
||||
if: steps.find-comment.outcome == 'success'
|
||||
uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4
|
||||
with:
|
||||
comment-id: ${{ steps.find-comment.outputs.comment-id }}
|
||||
issue-number: ${{ steps.pr-number.outputs.pr-number }}
|
||||
body-path: comment.txt
|
||||
edit-mode: replace
|
||||
@@ -81,10 +81,10 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.12.4
|
||||
rev: v0.12.7
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
- id: ruff-check
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
types_or: [python, pyi]
|
||||
require_serial: true
|
||||
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@@ -1,5 +1,53 @@
|
||||
# Changelog
|
||||
|
||||
## 0.12.7
|
||||
|
||||
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
|
||||
|
||||
## 0.12.6
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
|
||||
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
|
||||
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
|
||||
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
|
||||
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
|
||||
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
|
||||
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
|
||||
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
|
||||
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
|
||||
|
||||
### Performance
|
||||
|
||||
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
|
||||
|
||||
## 0.12.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
|
||||
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
|
||||
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
|
||||
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
|
||||
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
|
||||
|
||||
## 0.12.4
|
||||
|
||||
### Preview features
|
||||
|
||||
418
Cargo.lock
generated
418
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
14
Cargo.toml
14
Cargo.toml
@@ -57,6 +57,9 @@ assert_fs = { version = "1.1.0" }
|
||||
argfile = { version = "0.2.0" }
|
||||
bincode = { version = "2.0.0" }
|
||||
bitflags = { version = "2.5.0" }
|
||||
bitvec = { version = "1.0.1", default-features = false, features = [
|
||||
"alloc",
|
||||
] }
|
||||
bstr = { version = "1.9.1" }
|
||||
cachedir = { version = "0.3.1" }
|
||||
camino = { version = "1.1.7" }
|
||||
@@ -70,7 +73,7 @@ console_error_panic_hook = { version = "0.1.7" }
|
||||
console_log = { version = "1.0.0" }
|
||||
countme = { version = "3.0.1" }
|
||||
compact_str = "0.9.0"
|
||||
criterion = { version = "0.6.0", default-features = false }
|
||||
criterion = { version = "0.7.0", default-features = false }
|
||||
crossbeam = { version = "0.8.4" }
|
||||
dashmap = { version = "6.0.1" }
|
||||
dir-test = { version = "0.4.0" }
|
||||
@@ -80,7 +83,7 @@ etcetera = { version = "0.10.0" }
|
||||
fern = { version = "0.7.0" }
|
||||
filetime = { version = "0.2.23" }
|
||||
getrandom = { version = "0.3.1" }
|
||||
get-size2 = { version = "0.5.0", features = [
|
||||
get-size2 = { version = "0.6.0", features = [
|
||||
"derive",
|
||||
"smallvec",
|
||||
"hashbrown",
|
||||
@@ -138,7 +141,12 @@ 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", rev = "dba66f1a37acca014c2402f231ed5b361bd7d8fe" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "c2f4827b512b82842dbc84b1ccc4367500e301ed", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
"inventory",
|
||||
] }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
|
||||
@@ -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.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.4/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.12.7/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.12.7/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.4
|
||||
rev: v0.12.7
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.12.4"
|
||||
version = "0.12.7"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -169,6 +169,9 @@ pub struct AnalyzeGraphCommand {
|
||||
/// Attempt to detect imports from string literals.
|
||||
#[clap(long)]
|
||||
detect_string_imports: bool,
|
||||
/// The minimum number of dots in a string import to consider it a valid import.
|
||||
#[clap(long)]
|
||||
min_dots: Option<usize>,
|
||||
/// Enable preview mode. Use `--no-preview` to disable.
|
||||
#[arg(long, overrides_with("no_preview"))]
|
||||
preview: bool,
|
||||
@@ -808,6 +811,7 @@ impl AnalyzeGraphCommand {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
string_imports_min_dots: self.min_dots,
|
||||
preview: resolve_bool_arg(self.preview, self.no_preview).map(PreviewMode::from),
|
||||
target_version: self.target_version.map(ast::PythonVersion::from),
|
||||
..ExplicitConfigOverrides::default()
|
||||
@@ -1305,6 +1309,7 @@ struct ExplicitConfigOverrides {
|
||||
show_fixes: Option<bool>,
|
||||
extension: Option<Vec<ExtensionPair>>,
|
||||
detect_string_imports: Option<bool>,
|
||||
string_imports_min_dots: Option<usize>,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
@@ -1392,6 +1397,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
if let Some(detect_string_imports) = &self.detect_string_imports {
|
||||
config.analyze.detect_string_imports = Some(*detect_string_imports);
|
||||
}
|
||||
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
|
||||
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ impl LintCacheData {
|
||||
CacheMessage {
|
||||
rule,
|
||||
body: msg.body().to_string(),
|
||||
suggestion: msg.suggestion().map(ToString::to_string),
|
||||
suggestion: msg.first_help_text().map(ToString::to_string),
|
||||
range: msg.expect_range(),
|
||||
parent: msg.parent(),
|
||||
fix: msg.fix().cloned(),
|
||||
|
||||
@@ -102,7 +102,7 @@ pub(crate) fn analyze_graph(
|
||||
|
||||
// Resolve the per-file settings.
|
||||
let settings = resolver.resolve(path);
|
||||
let string_imports = settings.analyze.detect_string_imports;
|
||||
let string_imports = settings.analyze.string_imports;
|
||||
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
|
||||
|
||||
// Skip excluded files.
|
||||
|
||||
@@ -279,6 +279,7 @@ mod test {
|
||||
|
||||
TextEmitter::default()
|
||||
.with_show_fix_status(true)
|
||||
.with_color(false)
|
||||
.emit(
|
||||
&mut output,
|
||||
&diagnostics.inner,
|
||||
|
||||
@@ -264,6 +264,7 @@ impl Printer {
|
||||
.with_show_fix_diff(self.flags.intersects(Flags::SHOW_FIX_DIFF))
|
||||
.with_show_source(self.format == OutputFormat::Full)
|
||||
.with_unsafe_fixes(self.unsafe_fixes)
|
||||
.with_preview(preview)
|
||||
.emit(writer, &diagnostics.inner, &context)?;
|
||||
|
||||
if self.flags.intersects(Flags::SHOW_FIX_SUMMARY) {
|
||||
|
||||
@@ -57,33 +57,40 @@ fn dependencies() -> Result<()> {
|
||||
.write_str(indoc::indoc! {r#"
|
||||
def f(): pass
|
||||
"#})?;
|
||||
root.child("ruff")
|
||||
.child("e.pyi")
|
||||
.write_str(indoc::indoc! {r#"
|
||||
def f() -> None: ...
|
||||
"#})?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": [
|
||||
"ruff/d.py"
|
||||
],
|
||||
"ruff/d.py": [
|
||||
"ruff/e.py"
|
||||
],
|
||||
"ruff/e.py": []
|
||||
}
|
||||
assert_cmd_snapshot!(command().current_dir(&root), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": [
|
||||
"ruff/d.py"
|
||||
],
|
||||
"ruff/d.py": [
|
||||
"ruff/e.py",
|
||||
"ruff/e.pyi"
|
||||
],
|
||||
"ruff/e.py": [],
|
||||
"ruff/e.pyi": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
----- stderr -----
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
@@ -197,23 +204,43 @@ fn string_detection() -> Result<()> {
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
----- stderr -----
|
||||
"#);
|
||||
});
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().arg("--detect-string-imports").arg("--min-dots").arg("1").current_dir(&root), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2422,7 +2422,7 @@ requires-python = ">= 3.11"
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.11
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
@@ -2734,7 +2734,7 @@ requires-python = ">= 3.11"
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.10
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
@@ -3098,7 +3098,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.11
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
@@ -3478,7 +3478,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.11
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
@@ -3806,7 +3806,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.10
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
@@ -4134,7 +4134,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.9
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
@@ -4419,7 +4419,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.9
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
@@ -4757,7 +4757,7 @@ from typing import Union;foo: Union[int, str] = 1
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.10
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
|
||||
@@ -95,6 +95,6 @@ is stricter, which could affect the suggested fix. See [this FAQ section](https:
|
||||
## References
|
||||
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
|
||||
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
|
||||
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)
|
||||
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -22,11 +22,17 @@ exit_code: 1
|
||||
"description": "`os` imported but unused",
|
||||
"fingerprint": "4dbad37161e65c72",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
},
|
||||
"path": "input.py"
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 8,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 10,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -35,11 +41,17 @@ exit_code: 1
|
||||
"description": "Undefined name `y`",
|
||||
"fingerprint": "7af59862a085230",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 2,
|
||||
"end": 2
|
||||
},
|
||||
"path": "input.py"
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 5,
|
||||
"line": 2
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -48,11 +60,17 @@ exit_code: 1
|
||||
"description": "Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)",
|
||||
"fingerprint": "e558cec859bb66e8",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 3,
|
||||
"end": 3
|
||||
},
|
||||
"path": "input.py"
|
||||
"path": "input.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 1,
|
||||
"line": 3
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ exit_code: 1
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
|
||||
@@ -392,7 +392,7 @@ formatter.docstring_code_line_width = dynamic
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.7
|
||||
analyze.detect_string_imports = false
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
|
||||
|
||||
@@ -351,6 +351,41 @@ fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_tuple_implicit_instance_attributes(criterion: &mut Criterion) {
|
||||
setup_rayon();
|
||||
|
||||
criterion.bench_function("ty_micro[many_tuple_assignments]", |b| {
|
||||
b.iter_batched_ref(
|
||||
|| {
|
||||
// This is a regression benchmark for a case that used to hang:
|
||||
// https://github.com/astral-sh/ty/issues/765
|
||||
setup_micro_case(
|
||||
r#"
|
||||
from typing import Any
|
||||
|
||||
class A:
|
||||
foo: tuple[Any, ...]
|
||||
|
||||
class B(A):
|
||||
def __init__(self, parent: "C", x: tuple[Any]):
|
||||
self.foo = parent.foo + x
|
||||
|
||||
class C(A):
|
||||
def __init__(self, parent: B, x: tuple[Any]):
|
||||
self.foo = parent.foo + x
|
||||
"#,
|
||||
)
|
||||
},
|
||||
|case| {
|
||||
let Case { db, .. } = case;
|
||||
let result = db.check();
|
||||
assert_eq!(result.len(), 0);
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn benchmark_complex_constrained_attributes_1(criterion: &mut Criterion) {
|
||||
setup_rayon();
|
||||
|
||||
@@ -630,6 +665,7 @@ criterion_group!(
|
||||
micro,
|
||||
benchmark_many_string_assignments,
|
||||
benchmark_many_tuple_assignments,
|
||||
benchmark_tuple_implicit_instance_attributes,
|
||||
benchmark_complex_constrained_attributes_1,
|
||||
benchmark_complex_constrained_attributes_2,
|
||||
benchmark_many_enum_members,
|
||||
|
||||
@@ -25,7 +25,6 @@ ty_static = { workspace = true }
|
||||
anstyle = { workspace = true }
|
||||
arc-swap = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
dunce = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
@@ -59,6 +58,11 @@ tempfile = { workspace = true }
|
||||
cache = ["ruff_cache"]
|
||||
junit = ["dep:quick-junit"]
|
||||
os = ["ignore", "dep:etcetera"]
|
||||
serde = ["camino/serde1", "dep:serde", "dep:serde_json", "ruff_diagnostics/serde"]
|
||||
serde = [
|
||||
"camino/serde1",
|
||||
"dep:serde",
|
||||
"dep:serde_json",
|
||||
"ruff_diagnostics/serde",
|
||||
]
|
||||
# Exposes testing utilities.
|
||||
testing = ["tracing-subscriber"]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use std::{fmt::Formatter, path::Path, sync::Arc};
|
||||
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_diagnostics::{Applicability, Fix};
|
||||
use ruff_source_file::{LineColumn, SourceCode, SourceFile};
|
||||
|
||||
use ruff_annotate_snippets::Level as AnnotateLevel;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
|
||||
pub use self::render::{
|
||||
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
|
||||
};
|
||||
use crate::{Db, files::File};
|
||||
|
||||
mod render;
|
||||
@@ -19,7 +21,7 @@ mod stylesheet;
|
||||
/// characteristics in the inputs given to the tool. Typically, but not always,
|
||||
/// a characteristic is a deficiency. An example of a characteristic that is
|
||||
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct Diagnostic {
|
||||
/// The actual diagnostic.
|
||||
///
|
||||
@@ -122,7 +124,14 @@ impl Diagnostic {
|
||||
/// directly. If callers want or need to avoid cloning the diagnostic
|
||||
/// message, then they can also pass a `DiagnosticMessage` directly.
|
||||
pub fn info<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
|
||||
self.sub(SubDiagnostic::new(Severity::Info, message));
|
||||
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Info, message));
|
||||
}
|
||||
|
||||
/// Adds a "help" sub-diagnostic with the given message.
|
||||
///
|
||||
/// See the closely related [`Diagnostic::info`] method for more details.
|
||||
pub fn help<'a>(&mut self, message: impl IntoDiagnosticMessage + 'a) {
|
||||
self.sub(SubDiagnostic::new(SubDiagnosticSeverity::Help, message));
|
||||
}
|
||||
|
||||
/// Adds a "sub" diagnostic to this diagnostic.
|
||||
@@ -377,9 +386,15 @@ impl Diagnostic {
|
||||
self.primary_message()
|
||||
}
|
||||
|
||||
/// Returns the fix suggestion for the violation.
|
||||
pub fn suggestion(&self) -> Option<&str> {
|
||||
self.primary_annotation()?.get_message()
|
||||
/// Returns the message of the first sub-diagnostic with a `Help` severity.
|
||||
///
|
||||
/// Note that this is used as the fix title/suggestion for some of Ruff's output formats, but in
|
||||
/// general this is not the guaranteed meaning of such a message.
|
||||
pub fn first_help_text(&self) -> Option<&str> {
|
||||
self.sub_diagnostics()
|
||||
.iter()
|
||||
.find(|sub| matches!(sub.inner.severity, SubDiagnosticSeverity::Help))
|
||||
.map(|sub| sub.inner.message.as_str())
|
||||
}
|
||||
|
||||
/// Returns the URL for the rule documentation, if it exists.
|
||||
@@ -464,7 +479,7 @@ impl Diagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
struct DiagnosticInner {
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
@@ -540,7 +555,7 @@ impl Eq for RenderingSortKey<'_> {}
|
||||
/// Currently, the order in which sub-diagnostics are rendered relative to one
|
||||
/// another (for a single parent diagnostic) is the order in which they were
|
||||
/// attached to the diagnostic.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct SubDiagnostic {
|
||||
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
|
||||
/// pointer-sized.
|
||||
@@ -565,7 +580,10 @@ impl SubDiagnostic {
|
||||
/// Callers can pass anything that implements `std::fmt::Display`
|
||||
/// directly. If callers want or need to avoid cloning the diagnostic
|
||||
/// message, then they can also pass a `DiagnosticMessage` directly.
|
||||
pub fn new<'a>(severity: Severity, message: impl IntoDiagnosticMessage + 'a) -> SubDiagnostic {
|
||||
pub fn new<'a>(
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: impl IntoDiagnosticMessage + 'a,
|
||||
) -> SubDiagnostic {
|
||||
let inner = Box::new(SubDiagnosticInner {
|
||||
severity,
|
||||
message: message.into_diagnostic_message(),
|
||||
@@ -641,9 +659,9 @@ impl SubDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
struct SubDiagnosticInner {
|
||||
severity: Severity,
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: DiagnosticMessage,
|
||||
annotations: Vec<Annotation>,
|
||||
}
|
||||
@@ -669,7 +687,7 @@ struct SubDiagnosticInner {
|
||||
///
|
||||
/// Messages attached to annotations should also be as brief and specific as
|
||||
/// possible. Long messages could negative impact the quality of rendering.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct Annotation {
|
||||
/// The span of this annotation, corresponding to some subsequence of the
|
||||
/// user's input that we want to highlight.
|
||||
@@ -789,7 +807,7 @@ impl Annotation {
|
||||
///
|
||||
/// These tags are used to provide additional information about the annotation.
|
||||
/// and are passed through to the language server protocol.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub enum DiagnosticTag {
|
||||
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
|
||||
Unnecessary,
|
||||
@@ -998,7 +1016,7 @@ impl std::fmt::Display for DiagnosticId {
|
||||
///
|
||||
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
|
||||
/// emitting diagnostics from both ty and ruff.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub enum UnifiedFile {
|
||||
Ty(File),
|
||||
Ruff(SourceFile),
|
||||
@@ -1062,7 +1080,7 @@ impl DiagnosticSource {
|
||||
/// It consists of a `File` and an optional range into that file. When the
|
||||
/// range isn't present, it semantically implies that the diagnostic refers to
|
||||
/// the entire file. For example, when the file should be executable but isn't.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub struct Span {
|
||||
file: UnifiedFile,
|
||||
range: Option<TextRange>,
|
||||
@@ -1140,7 +1158,7 @@ impl From<crate::files::FileRange> for Span {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
@@ -1170,6 +1188,32 @@ impl Severity {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`Severity`] but exclusively for sub-diagnostics.
|
||||
///
|
||||
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
|
||||
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
|
||||
/// deleted and the two combined again.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
|
||||
pub enum SubDiagnosticSeverity {
|
||||
Help,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
impl SubDiagnosticSeverity {
|
||||
fn to_annotate(self) -> AnnotateLevel {
|
||||
match self {
|
||||
SubDiagnosticSeverity::Help => AnnotateLevel::Help,
|
||||
SubDiagnosticSeverity::Info => AnnotateLevel::Info,
|
||||
SubDiagnosticSeverity::Warning => AnnotateLevel::Warning,
|
||||
SubDiagnosticSeverity::Error => AnnotateLevel::Error,
|
||||
SubDiagnosticSeverity::Fatal => AnnotateLevel::Error,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for rendering diagnostics.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DisplayDiagnosticConfig {
|
||||
@@ -1196,6 +1240,15 @@ pub struct DisplayDiagnosticConfig {
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
)]
|
||||
preview: bool,
|
||||
/// Whether to hide the real `Severity` of diagnostics.
|
||||
///
|
||||
/// This is intended for temporary use by Ruff, which only has a single `error` severity at the
|
||||
/// moment. We should be able to remove this option when Ruff gets more severities.
|
||||
hide_severity: bool,
|
||||
/// Whether to show the availability of a fix in a diagnostic.
|
||||
show_fix_status: bool,
|
||||
/// The lowest applicability that should be shown when reporting diagnostics.
|
||||
fix_applicability: Applicability,
|
||||
}
|
||||
|
||||
impl DisplayDiagnosticConfig {
|
||||
@@ -1224,6 +1277,35 @@ impl DisplayDiagnosticConfig {
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to hide a diagnostic's severity or not.
|
||||
pub fn hide_severity(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
hide_severity: yes,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to show a fix's availability or not.
|
||||
pub fn show_fix_status(self, yes: bool) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
show_fix_status: 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
|
||||
/// availability for unsafe or display-only fixes.
|
||||
///
|
||||
/// Note that this option is currently ignored when `hide_severity` is false.
|
||||
pub fn fix_applicability(self, applicability: Applicability) -> DisplayDiagnosticConfig {
|
||||
DisplayDiagnosticConfig {
|
||||
fix_applicability: applicability,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DisplayDiagnosticConfig {
|
||||
@@ -1233,6 +1315,9 @@ impl Default for DisplayDiagnosticConfig {
|
||||
color: false,
|
||||
context: 2,
|
||||
preview: false,
|
||||
hide_severity: false,
|
||||
show_fix_status: false,
|
||||
fix_applicability: Applicability::Safe,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1343,7 +1428,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
||||
/// In most cases, callers shouldn't need to use this. Instead, there is
|
||||
/// a blanket trait implementation for `IntoDiagnosticMessage` for
|
||||
/// anything that implements `std::fmt::Display`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct DiagnosticMessage(Box<str>);
|
||||
|
||||
impl DiagnosticMessage {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
|
||||
@@ -7,9 +8,9 @@ use ruff_annotate_snippets::{
|
||||
};
|
||||
use ruff_notebook::{Notebook, NotebookIndex};
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::diagnostic::stylesheet::{DiagnosticStylesheet, fmt_styled};
|
||||
use crate::diagnostic::stylesheet::DiagnosticStylesheet;
|
||||
use crate::{
|
||||
Db,
|
||||
files::File,
|
||||
@@ -18,14 +19,17 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig, Severity,
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticSource, DisplayDiagnosticConfig,
|
||||
SubDiagnostic, UnifiedFile,
|
||||
};
|
||||
|
||||
use azure::AzureRenderer;
|
||||
use concise::ConciseRenderer;
|
||||
use pylint::PylintRenderer;
|
||||
|
||||
mod azure;
|
||||
mod concise;
|
||||
mod full;
|
||||
#[cfg(feature = "serde")]
|
||||
mod json;
|
||||
#[cfg(feature = "serde")]
|
||||
@@ -104,48 +108,7 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self.config.format {
|
||||
DiagnosticFormat::Concise => {
|
||||
let stylesheet = if self.config.color {
|
||||
DiagnosticStylesheet::styled()
|
||||
} else {
|
||||
DiagnosticStylesheet::plain()
|
||||
};
|
||||
|
||||
for diag in self.diagnostics {
|
||||
let (severity, severity_style) = match diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{severity}[{id}]",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(span) = diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
" {path}",
|
||||
path = fmt_styled(span.file().path(self.resolver), stylesheet.emphasis)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
write!(
|
||||
f,
|
||||
":{line}:{col}",
|
||||
line = fmt_styled(start.line, stylesheet.emphasis),
|
||||
col = fmt_styled(start.column, stylesheet.emphasis),
|
||||
)?;
|
||||
}
|
||||
write!(f, ":")?;
|
||||
}
|
||||
writeln!(f, " {message}", message = diag.concise_message())?;
|
||||
}
|
||||
ConciseRenderer::new(self.resolver, self.config).render(f, self.diagnostics)?;
|
||||
}
|
||||
DiagnosticFormat::Full => {
|
||||
let stylesheet = if self.config.color {
|
||||
@@ -256,7 +219,7 @@ impl<'a> Resolved<'a> {
|
||||
/// both.)
|
||||
#[derive(Debug)]
|
||||
struct ResolvedDiagnostic<'a> {
|
||||
severity: Severity,
|
||||
level: AnnotateLevel,
|
||||
id: Option<String>,
|
||||
message: String,
|
||||
annotations: Vec<ResolvedAnnotation<'a>>,
|
||||
@@ -281,7 +244,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
let id = Some(diag.inner.id.to_string());
|
||||
let message = diag.inner.message.as_str().to_string();
|
||||
ResolvedDiagnostic {
|
||||
severity: diag.inner.severity,
|
||||
level: diag.inner.severity.to_annotate(),
|
||||
id,
|
||||
message,
|
||||
annotations,
|
||||
@@ -304,7 +267,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
})
|
||||
.collect();
|
||||
ResolvedDiagnostic {
|
||||
severity: diag.inner.severity,
|
||||
level: diag.inner.severity.to_annotate(),
|
||||
id: None,
|
||||
message: diag.inner.message.as_str().to_string(),
|
||||
annotations,
|
||||
@@ -371,7 +334,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
snippets_by_input
|
||||
.sort_by(|snips1, snips2| snips1.has_primary.cmp(&snips2.has_primary).reverse());
|
||||
RenderableDiagnostic {
|
||||
severity: self.severity,
|
||||
level: self.level,
|
||||
id: self.id.as_deref(),
|
||||
message: &self.message,
|
||||
snippets_by_input,
|
||||
@@ -459,7 +422,7 @@ struct Renderable<'r> {
|
||||
#[derive(Debug)]
|
||||
struct RenderableDiagnostic<'r> {
|
||||
/// The severity of the diagnostic.
|
||||
severity: Severity,
|
||||
level: AnnotateLevel,
|
||||
/// The ID of the diagnostic. The ID can usually be used on the CLI or in a
|
||||
/// config file to change the severity of a lint.
|
||||
///
|
||||
@@ -478,7 +441,6 @@ struct RenderableDiagnostic<'r> {
|
||||
impl RenderableDiagnostic<'_> {
|
||||
/// Convert this to an "annotate" snippet.
|
||||
fn to_annotate(&self) -> AnnotateMessage<'_> {
|
||||
let level = self.severity.to_annotate();
|
||||
let snippets = self.snippets_by_input.iter().flat_map(|snippets| {
|
||||
let path = snippets.path;
|
||||
snippets
|
||||
@@ -486,7 +448,7 @@ impl RenderableDiagnostic<'_> {
|
||||
.iter()
|
||||
.map(|snippet| snippet.to_annotate(path))
|
||||
});
|
||||
let mut message = level.title(self.message);
|
||||
let mut message = self.level.title(self.message);
|
||||
if let Some(id) = self.id {
|
||||
message = message.id(id);
|
||||
}
|
||||
@@ -559,7 +521,7 @@ impl<'r> RenderableSnippets<'r> {
|
||||
#[derive(Debug)]
|
||||
struct RenderableSnippet<'r> {
|
||||
/// The actual snippet text.
|
||||
snippet: &'r str,
|
||||
snippet: Cow<'r, str>,
|
||||
/// The absolute line number corresponding to where this
|
||||
/// snippet begins.
|
||||
line_start: OneIndexed,
|
||||
@@ -619,6 +581,12 @@ impl<'r> RenderableSnippet<'r> {
|
||||
.iter()
|
||||
.map(|ann| RenderableAnnotation::new(snippet_start, ann))
|
||||
.collect();
|
||||
|
||||
let EscapedSourceCode {
|
||||
text: snippet,
|
||||
annotations,
|
||||
} = replace_unprintable(snippet, annotations).fix_up_empty_spans_after_line_terminator();
|
||||
|
||||
RenderableSnippet {
|
||||
snippet,
|
||||
line_start,
|
||||
@@ -629,7 +597,7 @@ impl<'r> RenderableSnippet<'r> {
|
||||
|
||||
/// Convert this to an "annotate" snippet.
|
||||
fn to_annotate<'a>(&'a self, path: &'a str) -> AnnotateSnippet<'a> {
|
||||
AnnotateSnippet::source(self.snippet)
|
||||
AnnotateSnippet::source(&self.snippet)
|
||||
.origin(path)
|
||||
.line_start(self.line_start.get())
|
||||
.annotations(
|
||||
@@ -859,12 +827,213 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
|
||||
path
|
||||
}
|
||||
|
||||
/// Given some source code and annotation ranges, this routine replaces
|
||||
/// unprintable characters with printable representations of them.
|
||||
///
|
||||
/// The source code and annotations returned are 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<'r>(
|
||||
source: &'r str,
|
||||
mut annotations: Vec<RenderableAnnotation<'r>>,
|
||||
) -> EscapedSourceCode<'r> {
|
||||
// Updates the annotation ranges 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_ranges = |index: usize, len: u32| {
|
||||
for ann in &mut annotations {
|
||||
if index < usize::from(ann.range.start()) {
|
||||
ann.range += TextSize::new(len - 1);
|
||||
} else if index < usize::from(ann.range.end()) {
|
||||
ann.range = ann.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,
|
||||
}
|
||||
};
|
||||
|
||||
let mut last_end = 0;
|
||||
let mut result = String::new();
|
||||
for (index, c) in source.char_indices() {
|
||||
if let Some(printable) = unprintable_replacement(c) {
|
||||
result.push_str(&source[last_end..index]);
|
||||
|
||||
let len = printable.text_len().to_u32();
|
||||
update_ranges(result.text_len().to_usize(), len);
|
||||
|
||||
result.push(printable);
|
||||
last_end = index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// No tabs or unprintable chars
|
||||
if result.is_empty() {
|
||||
EscapedSourceCode {
|
||||
annotations,
|
||||
text: Cow::Borrowed(source),
|
||||
}
|
||||
} else {
|
||||
result.push_str(&source[last_end..]);
|
||||
EscapedSourceCode {
|
||||
annotations,
|
||||
text: Cow::Owned(result),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EscapedSourceCode<'r> {
|
||||
text: Cow<'r, str>,
|
||||
annotations: Vec<RenderableAnnotation<'r>>,
|
||||
}
|
||||
|
||||
impl<'r> EscapedSourceCode<'r> {
|
||||
// This attempts to "fix up" the spans on each annotation 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> and
|
||||
// `ruff_linter::message::text::SourceCode::fix_up_empty_spans_after_line_terminator`,
|
||||
// from which this was adapted.
|
||||
fn fix_up_empty_spans_after_line_terminator(mut self) -> EscapedSourceCode<'r> {
|
||||
for ann in &mut self.annotations {
|
||||
let range = ann.range;
|
||||
if !range.is_empty()
|
||||
|| range.start() == TextSize::from(0)
|
||||
|| range.start() >= self.text.text_len()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !matches!(
|
||||
self.text.as_bytes()[range.start().to_usize() - 1],
|
||||
b'\n' | b'\r'
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
let start = range.start();
|
||||
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
|
||||
ann.range = TextRange::new(start, end);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the closest [`TextSize`] not less than the offset given for which
|
||||
/// `is_char_boundary` is `true`. Unless the offset given is greater than
|
||||
/// the length of the underlying contents, in which case, the length of the
|
||||
/// contents is returned.
|
||||
///
|
||||
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From `std`:
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::diagnostic::ceil_char_boundary;
|
||||
/// use ruff_text_size::{Ranged, TextLen, TextSize};
|
||||
///
|
||||
/// let source = "❤️🧡💛💚💙💜";
|
||||
/// assert_eq!(source.text_len(), TextSize::from(26));
|
||||
/// assert!(!source.is_char_boundary(13));
|
||||
///
|
||||
/// let closest = ceil_char_boundary(source, TextSize::from(13));
|
||||
/// assert_eq!(closest, TextSize::from(14));
|
||||
/// assert_eq!(&source[..closest.to_usize()], "❤️🧡💛");
|
||||
/// ```
|
||||
///
|
||||
/// Additional examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::diagnostic::ceil_char_boundary;
|
||||
/// use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
///
|
||||
/// let source = "Hello";
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ceil_char_boundary(source, TextSize::from(0)),
|
||||
/// TextSize::from(0)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ceil_char_boundary(source, TextSize::from(5)),
|
||||
/// TextSize::from(5)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ceil_char_boundary(source, TextSize::from(6)),
|
||||
/// TextSize::from(5)
|
||||
/// );
|
||||
///
|
||||
/// let source = "α";
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ceil_char_boundary(source, TextSize::from(0)),
|
||||
/// TextSize::from(0)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ceil_char_boundary(source, TextSize::from(1)),
|
||||
/// TextSize::from(2)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ceil_char_boundary(source, TextSize::from(2)),
|
||||
/// TextSize::from(2)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ceil_char_boundary(source, TextSize::from(3)),
|
||||
/// TextSize::from(2)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn ceil_char_boundary(text: &str, offset: TextSize) -> TextSize {
|
||||
let upper_bound = offset
|
||||
.to_u32()
|
||||
.saturating_add(4)
|
||||
.min(text.text_len().to_u32());
|
||||
(offset.to_u32()..upper_bound)
|
||||
.map(TextSize::from)
|
||||
.find(|offset| text.is_char_boundary(offset.to_usize()))
|
||||
.unwrap_or_else(|| TextSize::from(upper_bound))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use ruff_diagnostics::{Edit, Fix};
|
||||
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
|
||||
use crate::diagnostic::{Annotation, DiagnosticId, SecondaryCode, Severity, Span};
|
||||
use crate::diagnostic::{
|
||||
Annotation, DiagnosticId, IntoDiagnosticMessage, SecondaryCode, Severity, Span,
|
||||
SubDiagnosticSeverity,
|
||||
};
|
||||
use crate::files::system_path_to_file;
|
||||
use crate::system::{DbWithWritableSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
@@ -1548,7 +1717,7 @@ watermelon
|
||||
|
||||
let mut diag = env.err().primary("animals", "3", "3", "").build();
|
||||
diag.sub(
|
||||
env.sub_builder(Severity::Info, "this is a helpful note")
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
|
||||
.build(),
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
@@ -1577,15 +1746,15 @@ watermelon
|
||||
|
||||
let mut diag = env.err().primary("animals", "3", "3", "").build();
|
||||
diag.sub(
|
||||
env.sub_builder(Severity::Info, "this is a helpful note")
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "this is a helpful note")
|
||||
.build(),
|
||||
);
|
||||
diag.sub(
|
||||
env.sub_builder(Severity::Info, "another helpful note")
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "another helpful note")
|
||||
.build(),
|
||||
);
|
||||
diag.sub(
|
||||
env.sub_builder(Severity::Info, "and another helpful note")
|
||||
env.sub_builder(SubDiagnosticSeverity::Info, "and another helpful note")
|
||||
.build(),
|
||||
);
|
||||
insta::assert_snapshot!(
|
||||
@@ -2307,6 +2476,27 @@ watermelon
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Hide diagnostic severity when rendering.
|
||||
pub(super) fn hide_severity(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.hide_severity(yes);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Show fix availability when rendering.
|
||||
pub(super) fn show_fix_status(&mut self, yes: bool) {
|
||||
let mut config = std::mem::take(&mut self.config);
|
||||
config = config.show_fix_status(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);
|
||||
config = config.fix_applicability(applicability);
|
||||
self.config = config;
|
||||
}
|
||||
|
||||
/// Add a file with the given path and contents to this environment.
|
||||
pub(super) fn add(&mut self, path: &str, contents: &str) {
|
||||
let path = SystemPath::new(path);
|
||||
@@ -2370,11 +2560,11 @@ watermelon
|
||||
/// sub-diagnostic with "error" severity and canned values for
|
||||
/// its identifier and message.
|
||||
fn sub_warn(&mut self) -> SubDiagnosticBuilder<'_> {
|
||||
self.sub_builder(Severity::Warning, "sub-diagnostic message")
|
||||
self.sub_builder(SubDiagnosticSeverity::Warning, "sub-diagnostic message")
|
||||
}
|
||||
|
||||
/// Returns a builder for tersely constructing diagnostics.
|
||||
fn builder(
|
||||
pub(super) fn builder(
|
||||
&mut self,
|
||||
identifier: &'static str,
|
||||
severity: Severity,
|
||||
@@ -2391,7 +2581,11 @@ watermelon
|
||||
}
|
||||
|
||||
/// Returns a builder for tersely constructing sub-diagnostics.
|
||||
fn sub_builder(&mut self, severity: Severity, message: &str) -> SubDiagnosticBuilder<'_> {
|
||||
fn sub_builder(
|
||||
&mut self,
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: &str,
|
||||
) -> SubDiagnosticBuilder<'_> {
|
||||
let subdiag = SubDiagnostic::new(severity, message);
|
||||
SubDiagnosticBuilder { env: self, subdiag }
|
||||
}
|
||||
@@ -2437,7 +2631,7 @@ watermelon
|
||||
///
|
||||
/// See the docs on `TestEnvironment::span` for the meaning of
|
||||
/// `path`, `line_offset_start` and `line_offset_end`.
|
||||
fn primary(
|
||||
pub(super) fn primary(
|
||||
mut self,
|
||||
path: &str,
|
||||
line_offset_start: &str,
|
||||
@@ -2494,6 +2688,12 @@ watermelon
|
||||
self.diag.set_noqa_offset(noqa_offset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a "help" sub-diagnostic with the given message.
|
||||
fn help(mut self, message: impl IntoDiagnosticMessage) -> DiagnosticBuilder<'e> {
|
||||
self.diag.help(message);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper builder for tersely populating a `SubDiagnostic`.
|
||||
@@ -2600,7 +2800,8 @@ def fibonacci(n):
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("fib.py", "1:7", "1:9", "Remove unused import: `os`")
|
||||
.primary("fib.py", "1:7", "1:9", "")
|
||||
.help("Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(0),
|
||||
@@ -2613,12 +2814,8 @@ def fibonacci(n):
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary(
|
||||
"fib.py",
|
||||
"6:4",
|
||||
"6:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.primary("fib.py", "6:4", "6:5", "")
|
||||
.help("Remove assignment to unused variable `x`")
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::deletion(
|
||||
TextSize::from(94),
|
||||
@@ -2665,6 +2862,25 @@ if call(foo
|
||||
}
|
||||
|
||||
/// Create Ruff-style diagnostics for testing the various output formats for a notebook.
|
||||
///
|
||||
/// The concatenated cells look like this:
|
||||
///
|
||||
/// ```python
|
||||
/// # cell 1
|
||||
/// import os
|
||||
/// # cell 2
|
||||
/// import math
|
||||
///
|
||||
/// print('hello world')
|
||||
/// # cell 3
|
||||
/// def foo():
|
||||
/// print()
|
||||
/// x = 1
|
||||
/// ```
|
||||
///
|
||||
/// The first diagnostic is on the unused `os` import with location cell 1, row 2, column 8
|
||||
/// (`cell 1:2:8`). The second diagnostic is the unused `math` import at `cell 2:2:8`, and the
|
||||
/// third diagnostic is an unfixable unused variable at `cell 3:4:5`.
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "This is currently only used for JSON but will be needed soon for other formats"
|
||||
@@ -2720,7 +2936,8 @@ if call(foo
|
||||
|
||||
let diagnostics = vec![
|
||||
env.builder("unused-import", Severity::Error, "`os` imported but unused")
|
||||
.primary("notebook.ipynb", "2:7", "2:9", "Remove unused import: `os`")
|
||||
.primary("notebook.ipynb", "2:7", "2:9", "")
|
||||
.help("Remove unused import: `os`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(9),
|
||||
@@ -2733,12 +2950,8 @@ if call(foo
|
||||
Severity::Error,
|
||||
"`math` imported but unused",
|
||||
)
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"4:7",
|
||||
"4:11",
|
||||
"Remove unused import: `math`",
|
||||
)
|
||||
.primary("notebook.ipynb", "4:7", "4:11", "")
|
||||
.help("Remove unused import: `math`")
|
||||
.secondary_code("F401")
|
||||
.fix(Fix::safe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(28),
|
||||
@@ -2751,12 +2964,8 @@ if call(foo
|
||||
Severity::Error,
|
||||
"Local variable `x` is assigned to but never used",
|
||||
)
|
||||
.primary(
|
||||
"notebook.ipynb",
|
||||
"10:4",
|
||||
"10:5",
|
||||
"Remove assignment to unused variable `x`",
|
||||
)
|
||||
.primary("notebook.ipynb", "10:4", "10:5", "")
|
||||
.help("Remove assignment to unused variable `x`")
|
||||
.secondary_code("F841")
|
||||
.fix(Fix::unsafe_edit(Edit::range_deletion(TextRange::new(
|
||||
TextSize::from(94),
|
||||
|
||||
195
crates/ruff_db/src/diagnostic/render/concise.rs
Normal file
195
crates/ruff_db/src/diagnostic/render/concise.rs
Normal file
@@ -0,0 +1,195 @@
|
||||
use crate::diagnostic::{
|
||||
Diagnostic, DisplayDiagnosticConfig, Severity,
|
||||
stylesheet::{DiagnosticStylesheet, fmt_styled},
|
||||
};
|
||||
|
||||
use super::FileResolver;
|
||||
|
||||
pub(super) struct ConciseRenderer<'a> {
|
||||
resolver: &'a dyn FileResolver,
|
||||
config: &'a DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl<'a> ConciseRenderer<'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 sep = fmt_styled(":", stylesheet.separator);
|
||||
for diag in diagnostics {
|
||||
if let Some(span) = diag.primary_span() {
|
||||
write!(
|
||||
f,
|
||||
"{path}",
|
||||
path = fmt_styled(
|
||||
span.file().relative_path(self.resolver).to_string_lossy(),
|
||||
stylesheet.emphasis
|
||||
)
|
||||
)?;
|
||||
if let Some(range) = span.range() {
|
||||
let diagnostic_source = span.file().diagnostic_source(self.resolver);
|
||||
let start = diagnostic_source
|
||||
.as_source_code()
|
||||
.line_column(range.start());
|
||||
|
||||
if let Some(notebook_index) = self.resolver.notebook_index(span.file()) {
|
||||
write!(
|
||||
f,
|
||||
"{sep}cell {cell}{sep}{line}{sep}{col}",
|
||||
cell = notebook_index.cell(start.line).unwrap_or_default(),
|
||||
line = notebook_index.cell_row(start.line).unwrap_or_default(),
|
||||
col = start.column,
|
||||
)?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{sep}{line}{sep}{col}",
|
||||
line = start.line,
|
||||
col = start.column,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
write!(f, "{sep} ")?;
|
||||
}
|
||||
if self.config.hide_severity {
|
||||
if let Some(code) = diag.secondary_code() {
|
||||
write!(
|
||||
f,
|
||||
"{code} ",
|
||||
code = fmt_styled(code, stylesheet.secondary_code)
|
||||
)?;
|
||||
}
|
||||
if self.config.show_fix_status {
|
||||
if let Some(fix) = diag.fix() {
|
||||
// Do not display an indicator for inapplicable fixes
|
||||
if fix.applies(self.config.fix_applicability) {
|
||||
write!(f, "[{fix}] ", fix = fmt_styled("*", stylesheet.separator))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let (severity, severity_style) = match diag.severity() {
|
||||
Severity::Info => ("info", stylesheet.info),
|
||||
Severity::Warning => ("warning", stylesheet.warning),
|
||||
Severity::Error => ("error", stylesheet.error),
|
||||
Severity::Fatal => ("fatal", stylesheet.error),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{severity}[{id}] ",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(f, "{message}", message = diag.concise_message())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_diagnostics::Applicability;
|
||||
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat,
|
||||
render::tests::{
|
||||
TestEnvironment, create_diagnostics, create_notebook_diagnostics,
|
||||
create_syntax_error_diagnostics,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: error[unused-import] `os` imported but unused
|
||||
fib.py:6:5: error[unused-variable] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: error[undefined-name] Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes() {
|
||||
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: F401 [*] `os` imported but unused
|
||||
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes_preview() {
|
||||
let (mut env, diagnostics) = create_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
env.preview(true);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
fib.py:1:8: F401 [*] `os` imported but unused
|
||||
fib.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
||||
undef.py:1:4: F821 Undefined name `a`
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn show_fixes_syntax_errors() {
|
||||
let (mut env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
|
||||
env.hide_severity(true);
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
syntax_errors.py:1:15: SyntaxError: Expected one or more symbol names after import
|
||||
syntax_errors.py:3:12: SyntaxError: Expected ')', found newline
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
syntax_errors.py:1:15: error[invalid-syntax] SyntaxError: Expected one or more symbol names after import
|
||||
syntax_errors.py:3:12: error[invalid-syntax] SyntaxError: Expected ')', found newline
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_output() {
|
||||
let (env, diagnostics) = create_notebook_diagnostics(DiagnosticFormat::Concise);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
notebook.ipynb:cell 1:2:8: error[unused-import] `os` imported but unused
|
||||
notebook.ipynb:cell 2:2:8: error[unused-import] `math` imported but unused
|
||||
notebook.ipynb:cell 3:4:5: error[unused-variable] Local variable `x` is assigned to but never used
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_file() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.format(DiagnosticFormat::Concise);
|
||||
|
||||
let diag = env.err().build();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@"error[test-diagnostic] main diagnostic message",
|
||||
);
|
||||
}
|
||||
}
|
||||
201
crates/ruff_db/src/diagnostic/render/full.rs
Normal file
201
crates/ruff_db/src/diagnostic/render/full.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::diagnostic::{
|
||||
DiagnosticFormat, Severity,
|
||||
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn output() {
|
||||
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Full);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r#"
|
||||
error[unused-import]: `os` imported but unused
|
||||
--> fib.py:1:8
|
||||
|
|
||||
1 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
|
||||
error[unused-variable]: 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
|
||||
| ^
|
||||
7 | if n == 0:
|
||||
8 | return 0
|
||||
|
|
||||
help: Remove assignment to unused variable `x`
|
||||
|
||||
error[undefined-name]: Undefined name `a`
|
||||
--> undef.py:1:4
|
||||
|
|
||||
1 | if a == 1: pass
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syntax_errors() {
|
||||
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Full);
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
error[invalid-syntax]: SyntaxError: Expected one or more symbol names after import
|
||||
--> syntax_errors.py:1:15
|
||||
|
|
||||
1 | from os import
|
||||
| ^
|
||||
2 |
|
||||
3 | if call(foo
|
||||
|
|
||||
|
||||
error[invalid-syntax]: SyntaxError: Expected ')', found newline
|
||||
--> syntax_errors.py:3:12
|
||||
|
|
||||
1 | from os import
|
||||
2 |
|
||||
3 | if call(foo
|
||||
| ^
|
||||
4 | def bar():
|
||||
5 | pass
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit c9b99e4.
|
||||
///
|
||||
/// For example, without the fix, we get diagnostics like this:
|
||||
///
|
||||
/// ```
|
||||
/// error[no-indented-block]: Expected an indented block
|
||||
/// --> example.py:3:1
|
||||
/// |
|
||||
/// 2 | if False:
|
||||
/// | ^
|
||||
/// 3 | print()
|
||||
/// |
|
||||
/// ```
|
||||
///
|
||||
/// where the caret points to the end of the previous line instead of the start of the next.
|
||||
#[test]
|
||||
fn empty_span_after_line_terminator() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add(
|
||||
"example.py",
|
||||
r#"
|
||||
if False:
|
||||
print()
|
||||
"#,
|
||||
);
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let diagnostic = env
|
||||
.builder(
|
||||
"no-indented-block",
|
||||
Severity::Error,
|
||||
"Expected an indented block",
|
||||
)
|
||||
.primary("example.py", "3:0", "3:0", "")
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[no-indented-block]: Expected an indented block
|
||||
--> example.py:3:1
|
||||
|
|
||||
2 | if False:
|
||||
3 | print()
|
||||
| ^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit 2922490.
|
||||
///
|
||||
/// For example, without the fix, we get diagnostics like this:
|
||||
///
|
||||
/// ```
|
||||
/// error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
|
||||
/// --> example.py:1:25
|
||||
/// |
|
||||
/// 1 | nested_fstrings = f'␈{f'{f'␛'}'}'
|
||||
/// | ^
|
||||
/// |
|
||||
/// ```
|
||||
///
|
||||
/// where the caret points to the `f` in the f-string instead of the start of the invalid
|
||||
/// character (`^Z`).
|
||||
#[test]
|
||||
fn unprintable_characters() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add("example.py", "nested_fstrings = f'{f'{f''}'}'");
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let diagnostic = env
|
||||
.builder(
|
||||
"invalid-character-sub",
|
||||
Severity::Error,
|
||||
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
|
||||
--> example.py:1:25
|
||||
|
|
||||
1 | nested_fstrings = f'␈{f'{f'␛'}'}'
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_unprintable_characters() -> std::io::Result<()> {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add("example.py", "");
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let diagnostic = env
|
||||
.builder(
|
||||
"invalid-character-sub",
|
||||
Severity::Error,
|
||||
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
|
||||
--> example.py:1:2
|
||||
|
|
||||
1 | ␈␛
|
||||
| ^
|
||||
|
|
||||
"#);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that the header column matches the column in the user's input, even if we've replaced
|
||||
/// tabs with spaces for rendering purposes.
|
||||
#[test]
|
||||
fn tab_replacement() {
|
||||
let mut env = TestEnvironment::new();
|
||||
env.add("example.py", "def foo():\n\treturn 1");
|
||||
env.format(DiagnosticFormat::Full);
|
||||
|
||||
let diagnostic = env.err().primary("example.py", "2:1", "2:9", "").build();
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[test-diagnostic]: main diagnostic message
|
||||
--> example.py:2:2
|
||||
|
|
||||
1 | def foo():
|
||||
2 | return 1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@ pub(super) fn diagnostic_to_json<'a>(
|
||||
|
||||
let fix = diagnostic.fix().map(|fix| JsonFix {
|
||||
applicability: fix.applicability(),
|
||||
message: diagnostic.suggestion(),
|
||||
message: diagnostic.first_help_text(),
|
||||
edits: ExpandedEdits {
|
||||
edits: fix.edits(),
|
||||
notebook_index,
|
||||
|
||||
@@ -41,6 +41,8 @@ pub struct DiagnosticStylesheet {
|
||||
pub(crate) line_no: Style,
|
||||
pub(crate) emphasis: Style,
|
||||
pub(crate) none: Style,
|
||||
pub(crate) separator: Style,
|
||||
pub(crate) secondary_code: Style,
|
||||
}
|
||||
|
||||
impl Default for DiagnosticStylesheet {
|
||||
@@ -62,6 +64,8 @@ impl DiagnosticStylesheet {
|
||||
line_no: bright_blue.effects(Effects::BOLD),
|
||||
emphasis: Style::new().effects(Effects::BOLD),
|
||||
none: Style::new(),
|
||||
separator: AnsiColor::Cyan.on_default(),
|
||||
secondary_code: AnsiColor::Red.on_default().effects(Effects::BOLD),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +79,8 @@ impl DiagnosticStylesheet {
|
||||
line_no: Style::new(),
|
||||
emphasis: Style::new(),
|
||||
none: Style::new(),
|
||||
separator: Style::new(),
|
||||
secondary_code: Style::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::fmt;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
use dashmap::mapref::entry::Entry;
|
||||
pub use file_root::{FileRoot, FileRootKind};
|
||||
pub use path::FilePath;
|
||||
@@ -312,11 +311,6 @@ pub struct File {
|
||||
/// the file has been deleted is to change the status to `Deleted`.
|
||||
#[default]
|
||||
status: FileStatus,
|
||||
|
||||
/// Counter that counts the number of created file instances and active file instances.
|
||||
/// Only enabled in debug builds.
|
||||
#[default]
|
||||
count: Count<File>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
|
||||
@@ -21,7 +21,7 @@ use crate::source::source_text;
|
||||
/// reflected in the changed AST offsets.
|
||||
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
|
||||
/// for determining if a query result is unchanged.
|
||||
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::GetSize::get_heap_size)]
|
||||
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::heap_size)]
|
||||
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
||||
let _span = tracing::trace_span!("parsed_module", ?file).entered();
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_source_file::LineIndex;
|
||||
@@ -11,7 +9,7 @@ use crate::Db;
|
||||
use crate::files::{File, FilePath};
|
||||
|
||||
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
|
||||
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
|
||||
#[salsa::tracked(heap_size=get_size2::heap_size)]
|
||||
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
let path = file.path(db);
|
||||
let _span = tracing::trace_span!("source_text", file = %path).entered();
|
||||
@@ -38,11 +36,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
};
|
||||
|
||||
SourceText {
|
||||
inner: Arc::new(SourceTextInner {
|
||||
kind,
|
||||
read_error,
|
||||
count: Count::new(),
|
||||
}),
|
||||
inner: Arc::new(SourceTextInner { kind, read_error }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,21 +69,21 @@ impl SourceText {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match &self.inner.kind {
|
||||
SourceTextKind::Text(source) => source,
|
||||
SourceTextKind::Notebook(notebook) => notebook.source_code(),
|
||||
SourceTextKind::Notebook { notebook } => notebook.source_code(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying notebook if this is a notebook file.
|
||||
pub fn as_notebook(&self) -> Option<&Notebook> {
|
||||
match &self.inner.kind {
|
||||
SourceTextKind::Notebook(notebook) => Some(notebook),
|
||||
SourceTextKind::Notebook { notebook } => Some(notebook),
|
||||
SourceTextKind::Text(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a notebook source file.
|
||||
pub fn is_notebook(&self) -> bool {
|
||||
matches!(&self.inner.kind, SourceTextKind::Notebook(_))
|
||||
matches!(&self.inner.kind, SourceTextKind::Notebook { .. })
|
||||
}
|
||||
|
||||
/// Returns `true` if there was an error when reading the content of the file.
|
||||
@@ -114,7 +108,7 @@ impl std::fmt::Debug for SourceText {
|
||||
SourceTextKind::Text(text) => {
|
||||
dbg.field(text);
|
||||
}
|
||||
SourceTextKind::Notebook(notebook) => {
|
||||
SourceTextKind::Notebook { notebook } => {
|
||||
dbg.field(notebook);
|
||||
}
|
||||
}
|
||||
@@ -125,29 +119,19 @@ impl std::fmt::Debug for SourceText {
|
||||
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
struct SourceTextInner {
|
||||
#[get_size(ignore)]
|
||||
count: Count<SourceText>,
|
||||
kind: SourceTextKind,
|
||||
read_error: Option<SourceTextError>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
#[derive(Eq, PartialEq, get_size2::GetSize)]
|
||||
enum SourceTextKind {
|
||||
Text(String),
|
||||
Notebook(Box<Notebook>),
|
||||
}
|
||||
|
||||
impl get_size2::GetSize for SourceTextKind {
|
||||
fn get_heap_size(&self) -> usize {
|
||||
match self {
|
||||
SourceTextKind::Text(text) => text.get_heap_size(),
|
||||
// TODO: The `get-size` derive does not support ignoring enum variants.
|
||||
//
|
||||
// Jupyter notebooks are not very relevant for memory profiling, and contain
|
||||
// arbitrary JSON values that do not implement the `GetSize` trait.
|
||||
SourceTextKind::Notebook(_) => 0,
|
||||
}
|
||||
}
|
||||
Notebook {
|
||||
// Jupyter notebooks are not very relevant for memory profiling, and contain
|
||||
// arbitrary JSON values that do not implement the `GetSize` trait.
|
||||
#[get_size(ignore)]
|
||||
notebook: Box<Notebook>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<String> for SourceTextKind {
|
||||
@@ -158,7 +142,9 @@ impl From<String> for SourceTextKind {
|
||||
|
||||
impl From<Notebook> for SourceTextKind {
|
||||
fn from(notebook: Notebook) -> Self {
|
||||
SourceTextKind::Notebook(Box::new(notebook))
|
||||
SourceTextKind::Notebook {
|
||||
notebook: Box::new(notebook),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +157,7 @@ pub enum SourceTextError {
|
||||
}
|
||||
|
||||
/// Computes the [`LineIndex`] for `file`.
|
||||
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
|
||||
#[salsa::tracked(heap_size=get_size2::heap_size)]
|
||||
pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
|
||||
let _span = tracing::trace_span!("line_index", ?file).entered();
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ pub enum IsolationLevel {
|
||||
}
|
||||
|
||||
/// A collection of [`Edit`] elements to be applied to a source file.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Fix {
|
||||
/// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order.
|
||||
|
||||
@@ -20,6 +20,7 @@ ty_python_semantic = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
memchr = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::StringImports;
|
||||
use ruff_python_ast::visitor::source_order::{
|
||||
SourceOrderVisitor, walk_expr, walk_module, walk_stmt,
|
||||
};
|
||||
@@ -10,13 +11,13 @@ pub(crate) struct Collector<'a> {
|
||||
/// The path to the current module.
|
||||
module_path: Option<&'a [String]>,
|
||||
/// Whether to detect imports from string literals.
|
||||
string_imports: bool,
|
||||
string_imports: StringImports,
|
||||
/// The collected imports from the Python AST.
|
||||
imports: Vec<CollectedImport>,
|
||||
}
|
||||
|
||||
impl<'a> Collector<'a> {
|
||||
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: bool) -> Self {
|
||||
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: StringImports) -> Self {
|
||||
Self {
|
||||
module_path,
|
||||
string_imports,
|
||||
@@ -118,7 +119,7 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
|
||||
| Stmt::Continue(_)
|
||||
| Stmt::IpyEscapeCommand(_) => {
|
||||
// Only traverse simple statements when string imports is enabled.
|
||||
if self.string_imports {
|
||||
if self.string_imports.enabled {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
@@ -126,20 +127,26 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'ast Expr) {
|
||||
if self.string_imports {
|
||||
if self.string_imports.enabled {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
value,
|
||||
range: _,
|
||||
node_index: _,
|
||||
}) = expr
|
||||
{
|
||||
// Determine whether the string literal "looks like" an import statement: contains
|
||||
// a dot, and consists solely of valid Python identifiers.
|
||||
let value = value.to_str();
|
||||
if let Some(module_name) = ModuleName::new(value) {
|
||||
self.imports.push(CollectedImport::Import(module_name));
|
||||
// Determine whether the string literal "looks like" an import statement: contains
|
||||
// the requisite number of dots, and consists solely of valid Python identifiers.
|
||||
if self.string_imports.min_dots == 0
|
||||
|| memchr::memchr_iter(b'.', value.as_bytes()).count()
|
||||
>= self.string_imports.min_dots
|
||||
{
|
||||
if let Some(module_name) = ModuleName::new(value) {
|
||||
self.imports.push(CollectedImport::Import(module_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use ruff_python_parser::{Mode, ParseOptions, parse};
|
||||
use crate::collector::Collector;
|
||||
pub use crate::db::ModuleDb;
|
||||
use crate::resolver::Resolver;
|
||||
pub use crate::settings::{AnalyzeSettings, Direction};
|
||||
pub use crate::settings::{AnalyzeSettings, Direction, StringImports};
|
||||
|
||||
mod collector;
|
||||
mod db;
|
||||
@@ -26,7 +26,7 @@ impl ModuleImports {
|
||||
db: &ModuleDb,
|
||||
path: &SystemPath,
|
||||
package: Option<&SystemPath>,
|
||||
string_imports: bool,
|
||||
string_imports: StringImports,
|
||||
) -> Result<Self> {
|
||||
// Read and parse the source code.
|
||||
let source = std::fs::read_to_string(path)?;
|
||||
@@ -42,13 +42,11 @@ impl ModuleImports {
|
||||
// Resolve the imports.
|
||||
let mut resolved_imports = ModuleImports::default();
|
||||
for import in imports {
|
||||
let Some(resolved) = Resolver::new(db).resolve(import) else {
|
||||
continue;
|
||||
};
|
||||
let Some(path) = resolved.as_system_path() else {
|
||||
continue;
|
||||
};
|
||||
resolved_imports.insert(path.to_path_buf());
|
||||
for resolved in Resolver::new(db).resolve(import) {
|
||||
if let Some(path) = resolved.as_system_path() {
|
||||
resolved_imports.insert(path.to_path_buf());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(resolved_imports)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_db::files::FilePath;
|
||||
use ty_python_semantic::resolve_module;
|
||||
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
|
||||
|
||||
use crate::ModuleDb;
|
||||
use crate::collector::CollectedImport;
|
||||
@@ -16,24 +16,67 @@ impl<'a> Resolver<'a> {
|
||||
}
|
||||
|
||||
/// Resolve the [`CollectedImport`] into a [`FilePath`].
|
||||
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
|
||||
pub(crate) fn resolve(&self, import: CollectedImport) -> impl Iterator<Item = &'a FilePath> {
|
||||
match import {
|
||||
CollectedImport::Import(import) => {
|
||||
let module = resolve_module(self.db, &import)?;
|
||||
Some(module.file()?.path(self.db))
|
||||
// Attempt to resolve the module (e.g., given `import foo`, look for `foo`).
|
||||
let file = self.resolve_module(&import);
|
||||
|
||||
// If the file is a stub, look for the corresponding source file.
|
||||
let source_file = file
|
||||
.is_some_and(|file| file.extension() == Some("pyi"))
|
||||
.then(|| self.resolve_real_module(&import))
|
||||
.flatten();
|
||||
|
||||
std::iter::once(file)
|
||||
.chain(std::iter::once(source_file))
|
||||
.flatten()
|
||||
}
|
||||
CollectedImport::ImportFrom(import) => {
|
||||
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
|
||||
if let Some(file) = self.resolve_module(&import) {
|
||||
// If the file is a stub, look for the corresponding source file.
|
||||
let source_file = (file.extension() == Some("pyi"))
|
||||
.then(|| self.resolve_real_module(&import))
|
||||
.flatten();
|
||||
|
||||
return std::iter::once(Some(file))
|
||||
.chain(std::iter::once(source_file))
|
||||
.flatten();
|
||||
}
|
||||
|
||||
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
|
||||
let parent = import.parent();
|
||||
let file = parent
|
||||
.as_ref()
|
||||
.and_then(|parent| self.resolve_module(parent));
|
||||
|
||||
let module = resolve_module(self.db, &import).or_else(|| {
|
||||
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
|
||||
// If the file is a stub, look for the corresponding source file.
|
||||
let source_file = file
|
||||
.is_some_and(|file| file.extension() == Some("pyi"))
|
||||
.then(|| {
|
||||
parent
|
||||
.as_ref()
|
||||
.and_then(|parent| self.resolve_real_module(parent))
|
||||
})
|
||||
.flatten();
|
||||
|
||||
resolve_module(self.db, &parent?)
|
||||
})?;
|
||||
|
||||
Some(module.file()?.path(self.db))
|
||||
std::iter::once(file)
|
||||
.chain(std::iter::once(source_file))
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
pub(crate) fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
|
||||
let module = resolve_module(self.db, module_name)?;
|
||||
Some(module.file(self.db)?.path(self.db))
|
||||
}
|
||||
|
||||
/// Resolves a module name to a module (stubs not allowed).
|
||||
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
|
||||
let module = resolve_real_module(self.db, module_name)?;
|
||||
Some(module.file(self.db)?.path(self.db))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub struct AnalyzeSettings {
|
||||
pub exclude: FilePatternSet,
|
||||
pub preview: PreviewMode,
|
||||
pub target_version: PythonVersion,
|
||||
pub detect_string_imports: bool,
|
||||
pub string_imports: StringImports,
|
||||
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
|
||||
pub extension: ExtensionMapping,
|
||||
}
|
||||
@@ -26,7 +26,7 @@ impl fmt::Display for AnalyzeSettings {
|
||||
self.exclude,
|
||||
self.preview,
|
||||
self.target_version,
|
||||
self.detect_string_imports,
|
||||
self.string_imports,
|
||||
self.extension | debug,
|
||||
self.include_dependencies | debug,
|
||||
]
|
||||
@@ -35,6 +35,31 @@ impl fmt::Display for AnalyzeSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, CacheKey)]
|
||||
pub struct StringImports {
|
||||
pub enabled: bool,
|
||||
pub min_dots: usize,
|
||||
}
|
||||
|
||||
impl Default for StringImports {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
min_dots: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StringImports {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.enabled {
|
||||
write!(f, "enabled (min_dots: {})", self.min_dots)
|
||||
} else {
|
||||
write!(f, "disabled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, CacheKey)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.12.4"
|
||||
version = "0.12.7"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -89,3 +89,14 @@ print(1)
|
||||
# ///
|
||||
#
|
||||
# Foobar
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/19713
|
||||
|
||||
# mypy: ignore-errors
|
||||
# pyright: ignore-errors
|
||||
# pyrefly: ignore-errors
|
||||
# ty: ignore[unresolved-import]
|
||||
# pyrefly: ignore[unused-import]
|
||||
|
||||
print(1)
|
||||
|
||||
@@ -25,5 +25,5 @@ def my_func():
|
||||
|
||||
# t-strings - all ok
|
||||
t"0.0.0.0"
|
||||
"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
|
||||
"0.0.0.0" f"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
|
||||
t"0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
|
||||
t"0.0.0.0" t"0.0.0.0{expr}0.0.0.0" t"0.0.0.0{expr}0.0.0.0"
|
||||
|
||||
@@ -94,7 +94,7 @@ except Exception:
|
||||
logging.error("...", exc_info=True)
|
||||
|
||||
|
||||
from logging import error, exception
|
||||
from logging import critical, error, exception
|
||||
|
||||
try:
|
||||
pass
|
||||
@@ -114,6 +114,23 @@ except Exception:
|
||||
error("...", exc_info=None)
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
critical("...")
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
critical("...", exc_info=False)
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
critical("...", exc_info=None)
|
||||
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
@@ -125,6 +142,13 @@ try:
|
||||
except Exception:
|
||||
error("...", exc_info=True)
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
critical("...", exc_info=True)
|
||||
|
||||
|
||||
try:
|
||||
...
|
||||
except Exception as e:
|
||||
|
||||
@@ -650,3 +650,17 @@ f"""This is a test. {
|
||||
if True else
|
||||
"Don't add a trailing comma here ->"
|
||||
}"""
|
||||
|
||||
type X[
|
||||
T
|
||||
] = T
|
||||
def f[
|
||||
T
|
||||
](): pass
|
||||
class C[
|
||||
T
|
||||
]: pass
|
||||
|
||||
type X[T,] = T
|
||||
def f[T,](): pass
|
||||
class C[T,]: pass
|
||||
@@ -88,3 +88,25 @@ def f_multi_line_string2():
|
||||
example="example"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def raise_typing_cast_exception():
|
||||
import typing
|
||||
raise typing.cast("Exception", None)
|
||||
|
||||
|
||||
def f_typing_cast_excluded():
|
||||
from typing import cast
|
||||
raise cast(RuntimeError, "This should not trigger EM101")
|
||||
|
||||
|
||||
def f_typing_cast_excluded_import():
|
||||
import typing
|
||||
raise typing.cast(RuntimeError, "This should not trigger EM101")
|
||||
|
||||
|
||||
def f_typing_cast_excluded_aliased():
|
||||
from typing import cast as my_cast
|
||||
raise my_cast(RuntimeError, "This should not trigger EM101")
|
||||
|
||||
|
||||
|
||||
@@ -39,6 +39,11 @@ class NonEmptyWithInit:
|
||||
pass
|
||||
|
||||
|
||||
class NonEmptyChildWithInlineComment:
|
||||
value: int
|
||||
... # preserve me
|
||||
|
||||
|
||||
class EmptyClass:
|
||||
...
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ class NonEmptyWithInit:
|
||||
def __init__():
|
||||
pass
|
||||
|
||||
class NonEmptyChildWithInlineComment:
|
||||
value: int
|
||||
... # preserve me
|
||||
|
||||
# Not violations
|
||||
|
||||
class EmptyClass: ...
|
||||
|
||||
@@ -142,3 +142,7 @@ field47: typing.Optional[int] | typing.Optional[dict]
|
||||
# avoid reporting twice
|
||||
field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
field49: typing.Optional[complex | complex] | complex
|
||||
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
# Should throw duplicate union member but not fix
|
||||
isinstance(None, typing.Union[None, None])
|
||||
@@ -129,4 +129,35 @@ print(" x ".rsplit(maxsplit=0))
|
||||
print(" x ".rsplit(maxsplit=0))
|
||||
print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
print(" x ".rsplit(maxsplit=0))
|
||||
print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
print(" x ".rsplit(sep=None, maxsplit=0))
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/19581 - embedded quotes in raw strings
|
||||
r"""simple@example.com
|
||||
very.common@example.com
|
||||
FirstName.LastName@EasierReading.org
|
||||
x@example.com
|
||||
long.email-address-with-hyphens@and.subdomains.example.com
|
||||
user.name+tag+sorting@example.com
|
||||
name/surname@example.com
|
||||
xample@s.example
|
||||
" "@example.org
|
||||
"john..doe"@example.org
|
||||
mailhost!username@example.org
|
||||
"very.(),:;<>[]\".VERY.\"very@\\ \"very\".unusual"@strange.example.com
|
||||
user%example.com@example.org
|
||||
user-@example.org
|
||||
I❤️CHOCOLATE@example.com
|
||||
this\ still\"not\\allowed@example.com
|
||||
stellyamburrr985@example.com
|
||||
Abc.123@example.com
|
||||
user+mailbox/department=shipping@example.com
|
||||
!#$%&'*+-/=?^_`.{|}~@example.com
|
||||
"Abc@def"@example.com
|
||||
"Fred\ Bloggs"@example.com
|
||||
"Joe.\\Blow"@example.com""".split("\n")
|
||||
|
||||
|
||||
r"""first
|
||||
'no need' to escape
|
||||
"swap" quote style
|
||||
"use' ugly triple quotes""".split("\n")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from pathlib import Path, PurePath
|
||||
from pathlib import Path, PurePath, PosixPath, PurePosixPath, WindowsPath, PureWindowsPath
|
||||
from pathlib import Path as pth
|
||||
|
||||
|
||||
@@ -68,3 +68,11 @@ Path(".", "folder")
|
||||
PurePath(".", "folder")
|
||||
|
||||
Path()
|
||||
|
||||
from importlib.metadata import PackagePath
|
||||
|
||||
_ = PosixPath(".")
|
||||
_ = PurePosixPath(".")
|
||||
_ = WindowsPath(".")
|
||||
_ = PureWindowsPath(".")
|
||||
_ = PackagePath(".")
|
||||
|
||||
@@ -47,3 +47,19 @@ def _():
|
||||
from builtin import open
|
||||
|
||||
with open(p) as _: ... # No error
|
||||
|
||||
file = "file_1.py"
|
||||
|
||||
rename(file, "file_2.py")
|
||||
|
||||
rename(
|
||||
# commment 1
|
||||
file, # comment 2
|
||||
"file_2.py"
|
||||
,
|
||||
# comment 3
|
||||
)
|
||||
|
||||
rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
|
||||
|
||||
rename(file, "file_2.py", src_dir_fd=1)
|
||||
@@ -0,0 +1,3 @@
|
||||
"""Hello, world!"""\
|
||||
|
||||
x = 1; y = 2
|
||||
@@ -84,3 +84,25 @@ class MyRequestHandler(BaseHTTPRequestHandler):
|
||||
def dont_GET(self):
|
||||
pass
|
||||
|
||||
|
||||
from http.server import CGIHTTPRequestHandler
|
||||
|
||||
|
||||
class MyCGIRequestHandler(CGIHTTPRequestHandler):
|
||||
def do_OPTIONS(self):
|
||||
pass
|
||||
|
||||
def dont_OPTIONS(self):
|
||||
pass
|
||||
|
||||
|
||||
from http.server import SimpleHTTPRequestHandler
|
||||
|
||||
|
||||
class MySimpleRequestHandler(SimpleHTTPRequestHandler):
|
||||
def do_OPTIONS(self):
|
||||
pass
|
||||
|
||||
def dont_OPTIONS(self):
|
||||
pass
|
||||
|
||||
|
||||
@@ -278,3 +278,30 @@ def f():
|
||||
for i in src:
|
||||
if lambda: 0:
|
||||
dst.append(i)
|
||||
|
||||
def f():
|
||||
i = "xyz"
|
||||
result = []
|
||||
for i in range(3):
|
||||
result.append(x for x in [i])
|
||||
|
||||
def f():
|
||||
i = "xyz"
|
||||
result = []
|
||||
for i in range(3):
|
||||
result.append((x for x in [i]))
|
||||
|
||||
G_INDEX = None
|
||||
def f():
|
||||
global G_INDEX
|
||||
result = []
|
||||
for G_INDEX in range(3):
|
||||
result.append(G_INDEX)
|
||||
|
||||
def f():
|
||||
NL_INDEX = None
|
||||
def x():
|
||||
nonlocal NL_INDEX
|
||||
result = []
|
||||
for NL_INDEX in range(3):
|
||||
result.append(NL_INDEX)
|
||||
5
crates/ruff_linter/resources/test/fixtures/pylint/empty_comment_line_continuation.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/pylint/empty_comment_line_continuation.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
#
|
||||
x = 0 \
|
||||
#
|
||||
+1
|
||||
print(x)
|
||||
@@ -59,3 +59,7 @@ kwargs = {x: x for x in range(10)}
|
||||
"{1}_{0}".format(1, 2, *args)
|
||||
|
||||
"{1}_{0}".format(1, 2)
|
||||
|
||||
r"\d{{1,2}} {0}".format(42)
|
||||
|
||||
"{{{0}}}".format(123)
|
||||
|
||||
@@ -143,3 +143,23 @@ class NotAMethodButHardToDetect:
|
||||
# without risking false positives elsewhere or introducing complex heuristics
|
||||
# that users would find surprising and confusing
|
||||
FOO = sorted([x for x in BAR], key=lambda x: x.baz)
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/19305
|
||||
import pytest
|
||||
|
||||
@pytest.fixture
|
||||
def my_fixture_with_param(request):
|
||||
return request.param
|
||||
|
||||
@pytest.fixture()
|
||||
def my_fixture_with_param2(request):
|
||||
return request.param
|
||||
|
||||
|
||||
# Decorated function (should be ignored)
|
||||
def custom_decorator(func):
|
||||
return func
|
||||
|
||||
@custom_decorator
|
||||
def add(x, y):
|
||||
return x + y
|
||||
|
||||
@@ -55,3 +55,12 @@ _ = Decimal(0.1)
|
||||
_ = Decimal(-0.5)
|
||||
_ = Decimal(5.0)
|
||||
_ = decimal.Decimal(4.2)
|
||||
|
||||
# Cases with int and bool - should produce safe fixes
|
||||
_ = Decimal.from_float(1)
|
||||
_ = Decimal.from_float(True)
|
||||
|
||||
# Cases with non-finite floats - should produce safe fixes
|
||||
_ = Decimal.from_float(float("-nan"))
|
||||
_ = Decimal.from_float(float("\x2dnan"))
|
||||
_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
|
||||
|
||||
@@ -52,3 +52,7 @@ f"{repr(lambda: 1)}"
|
||||
f"{repr(x := 2)}"
|
||||
|
||||
f"{str(object=3)}"
|
||||
|
||||
f"{str(x for x in [])}"
|
||||
|
||||
f"{str((x for x in []))}"
|
||||
|
||||
@@ -65,3 +65,62 @@ class Foo:
|
||||
bar = "should've used attrs"
|
||||
|
||||
def __post_init__(self, bar: str = "ahhh", baz: str = "hmm") -> None: ...
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/18950
|
||||
@dataclass
|
||||
class Foo:
|
||||
def __post_init__(self, bar: int = (x := 1)) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Foo:
|
||||
def __post_init__(
|
||||
self,
|
||||
bar: int = (x := 1) # comment
|
||||
,
|
||||
baz: int = (y := 2), # comment
|
||||
foo = (a := 1) # comment
|
||||
,
|
||||
faz = (b := 2), # comment
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Foo:
|
||||
def __post_init__(
|
||||
self,
|
||||
bar: int = 1, # comment
|
||||
baz: int = 2, # comment
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Foo:
|
||||
def __post_init__(
|
||||
self,
|
||||
arg1: int = (1) # comment
|
||||
,
|
||||
arg2: int = ((1)) # comment
|
||||
,
|
||||
arg2: int = (i for i in range(10)) # comment
|
||||
,
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
|
||||
# makes little sense, but is valid syntax
|
||||
def fun_with_python_syntax():
|
||||
@dataclass
|
||||
class Foo:
|
||||
def __post_init__(
|
||||
self,
|
||||
bar: (int) = (yield from range(5)) # comment
|
||||
,
|
||||
) -> None:
|
||||
...
|
||||
|
||||
return Foo
|
||||
|
||||
@@ -53,3 +53,16 @@ regex.subn(br"""eak your machine with rm -""", rf"""/""")
|
||||
regex.splititer(both, non_literal)
|
||||
regex.subf(f, lambda _: r'means', '"format"')
|
||||
regex.subfn(fn, f'''a$1n't''', lambda: "'function'")
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/16713
|
||||
re.compile("\a\f\n\r\t\u27F2\U0001F0A1\v\x41") # with unsafe fix
|
||||
re.compile("\b") # without fix
|
||||
re.compile("\"") # without fix
|
||||
re.compile("\'") # without fix
|
||||
re.compile('\"') # without fix
|
||||
re.compile('\'') # without fix
|
||||
re.compile("\\") # without fix
|
||||
re.compile("\101") # without fix
|
||||
re.compile("a\
|
||||
b") # without fix
|
||||
|
||||
@@ -91,3 +91,20 @@ regex.subf(
|
||||
br''br""br''
|
||||
)
|
||||
regex.subfn(br'I\s\nee*d\s[O0o]me\x20\Qoffe\E, ' br'b')
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/16713
|
||||
re.compile(
|
||||
"["
|
||||
"\U0001F600-\U0001F64F" # emoticons
|
||||
"\U0001F300-\U0001F5FF" # symbols & pictographs
|
||||
"\U0001F680-\U0001F6FF" # transport & map symbols
|
||||
"\U0001F1E0-\U0001F1FF" # flags (iOS)
|
||||
"\U00002702-\U000027B0"
|
||||
"\U000024C2-\U0001F251"
|
||||
"\u200d" # zero width joiner
|
||||
"\u200c" # zero width non-joiner
|
||||
"\\u200c" # must not be escaped in a raw string
|
||||
"]+",
|
||||
flags=re.UNICODE,
|
||||
)
|
||||
|
||||
3
crates/ruff_linter/resources/test/fixtures/ruff/RUF039_py_version_sensitive.py
vendored
Normal file
3
crates/ruff_linter/resources/test/fixtures/ruff/RUF039_py_version_sensitive.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
import re
|
||||
|
||||
re.compile("\N{Partial Differential}") # with unsafe fix if python target is 3.8 or higher, else without fix
|
||||
@@ -1039,14 +1039,10 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
|
||||
}
|
||||
if checker.any_rule_enabled(&[
|
||||
Rule::OsChmod,
|
||||
Rule::OsMkdir,
|
||||
Rule::OsMakedirs,
|
||||
Rule::OsRename,
|
||||
Rule::OsReplace,
|
||||
Rule::OsStat,
|
||||
Rule::OsPathJoin,
|
||||
Rule::OsPathSamefile,
|
||||
Rule::OsPathSplitext,
|
||||
Rule::BuiltinOpen,
|
||||
Rule::PyPath,
|
||||
@@ -1112,6 +1108,18 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
|
||||
if checker.is_rule_enabled(Rule::OsGetcwd) {
|
||||
flake8_use_pathlib::rules::os_getcwd(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsChmod) {
|
||||
flake8_use_pathlib::rules::os_chmod(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsRename) {
|
||||
flake8_use_pathlib::rules::os_rename(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsReplace) {
|
||||
flake8_use_pathlib::rules::os_replace(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::OsPathSamefile) {
|
||||
flake8_use_pathlib::rules::os_path_samefile(checker, call, segments);
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
|
||||
flake8_use_pathlib::rules::path_constructor_current_directory(
|
||||
checker, call, segments,
|
||||
|
||||
@@ -590,6 +590,16 @@ impl<'a> Checker<'a> {
|
||||
member,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the [`LintContext`] for the current analysis.
|
||||
///
|
||||
/// Note that you should always prefer calling methods like `settings`, `report_diagnostic`, or
|
||||
/// `is_rule_enabled` directly on [`Checker`] when possible. This method exists only for the
|
||||
/// rare cases where rules or helper functions need to be accessed by both a `Checker` and a
|
||||
/// `LintContext` in different analysis phases.
|
||||
pub(crate) const fn context(&self) -> &'a LintContext<'a> {
|
||||
self.context
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TypingImporter<'a, 'b> {
|
||||
@@ -3216,6 +3226,11 @@ impl<'a> LintContext<'a> {
|
||||
pub(crate) fn iter(&mut self) -> impl Iterator<Item = &Diagnostic> {
|
||||
self.diagnostics.get_mut().iter()
|
||||
}
|
||||
|
||||
/// The [`LinterSettings`] for the current analysis, including the enabled rules.
|
||||
pub(crate) const fn settings(&self) -> &LinterSettings {
|
||||
self.settings
|
||||
}
|
||||
}
|
||||
|
||||
/// An abstraction for mutating a diagnostic.
|
||||
|
||||
@@ -16,7 +16,6 @@ use crate::rules::{
|
||||
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
|
||||
flake8_pyi, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
|
||||
};
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
use super::ast::LintContext;
|
||||
|
||||
@@ -27,7 +26,6 @@ pub(crate) fn check_tokens(
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
stylist: &Stylist,
|
||||
settings: &LinterSettings,
|
||||
source_type: PySourceType,
|
||||
cell_offsets: Option<&CellOffsets>,
|
||||
context: &mut LintContext,
|
||||
@@ -42,15 +40,8 @@ pub(crate) fn check_tokens(
|
||||
Rule::BlankLinesAfterFunctionOrClass,
|
||||
Rule::BlankLinesBeforeNestedDefinition,
|
||||
]) {
|
||||
BlankLinesChecker::new(
|
||||
locator,
|
||||
stylist,
|
||||
settings,
|
||||
source_type,
|
||||
cell_offsets,
|
||||
context,
|
||||
)
|
||||
.check_lines(tokens);
|
||||
BlankLinesChecker::new(locator, stylist, source_type, cell_offsets, context)
|
||||
.check_lines(tokens);
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::BlanketTypeIgnore) {
|
||||
@@ -58,17 +49,17 @@ pub(crate) fn check_tokens(
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::EmptyComment) {
|
||||
pylint::rules::empty_comments(context, comment_ranges, locator);
|
||||
pylint::rules::empty_comments(context, comment_ranges, locator, indexer);
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) {
|
||||
for range in comment_ranges {
|
||||
ruff::rules::ambiguous_unicode_character_comment(context, locator, range, settings);
|
||||
ruff::rules::ambiguous_unicode_character_comment(context, locator, range);
|
||||
}
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::CommentedOutCode) {
|
||||
eradicate::rules::commented_out_code(context, locator, comment_ranges, settings);
|
||||
eradicate::rules::commented_out_code(context, locator, comment_ranges);
|
||||
}
|
||||
|
||||
if context.is_rule_enabled(Rule::UTF8EncodingDeclaration) {
|
||||
@@ -110,7 +101,7 @@ pub(crate) fn check_tokens(
|
||||
Rule::SingleLineImplicitStringConcatenation,
|
||||
Rule::MultiLineImplicitStringConcatenation,
|
||||
]) {
|
||||
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer, settings);
|
||||
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer);
|
||||
}
|
||||
|
||||
if context.any_rule_enabled(&[
|
||||
|
||||
@@ -920,11 +920,11 @@ 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::violations::OsChmod),
|
||||
(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, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename),
|
||||
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace),
|
||||
(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),
|
||||
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
|
||||
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
|
||||
@@ -940,7 +940,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
|
||||
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
|
||||
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
|
||||
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile),
|
||||
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile),
|
||||
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
|
||||
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
|
||||
(Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath),
|
||||
|
||||
@@ -56,13 +56,19 @@ impl<'a> Insertion<'a> {
|
||||
stylist: &Stylist,
|
||||
) -> Insertion<'static> {
|
||||
// Skip over any docstrings.
|
||||
let mut location = if let Some(location) = match_docstring_end(body) {
|
||||
let mut location = if let Some(mut location) = match_docstring_end(body) {
|
||||
// If the first token after the docstring is a semicolon, insert after the semicolon as
|
||||
// an inline statement.
|
||||
if let Some(offset) = match_semicolon(locator.after(location)) {
|
||||
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() {
|
||||
location = locator.full_line_end(location);
|
||||
}
|
||||
|
||||
// Otherwise, advance to the next row.
|
||||
locator.full_line_end(location)
|
||||
} else {
|
||||
@@ -363,6 +369,16 @@ mod tests {
|
||||
Insertion::own_line("", TextSize::from(20), "\n")
|
||||
);
|
||||
|
||||
let contents = r#"
|
||||
"""Hello, world!"""\
|
||||
|
||||
"#
|
||||
.trim_start();
|
||||
assert_eq!(
|
||||
insert(contents)?,
|
||||
Insertion::own_line("", TextSize::from(22), "\n")
|
||||
);
|
||||
|
||||
let contents = r"
|
||||
x = 1
|
||||
"
|
||||
|
||||
@@ -188,7 +188,6 @@ pub fn check_path(
|
||||
locator,
|
||||
indexer,
|
||||
stylist,
|
||||
settings,
|
||||
source_type,
|
||||
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),
|
||||
&mut context,
|
||||
@@ -473,7 +472,7 @@ pub fn lint_only(
|
||||
&& !is_py314_support_enabled(settings)
|
||||
{
|
||||
warn_user_once!(
|
||||
"Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."
|
||||
"Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -584,7 +583,7 @@ pub fn lint_fix<'a>(
|
||||
&& !is_py314_support_enabled(settings)
|
||||
{
|
||||
warn_user_once!(
|
||||
"Support for Python 3.14 is under development and may be unstable. Enable `preview` to remove this warning."
|
||||
"Support for Python 3.14 is in preview and may undergo breaking changes. Enable `preview` to remove this warning."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -118,86 +118,6 @@ impl<'a> Locator<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the closest [`TextSize`] not less than the offset given for which
|
||||
/// `is_char_boundary` is `true`. Unless the offset given is greater than
|
||||
/// the length of the underlying contents, in which case, the length of the
|
||||
/// contents is returned.
|
||||
///
|
||||
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From `std`:
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_text_size::{Ranged, TextSize};
|
||||
/// use ruff_linter::Locator;
|
||||
///
|
||||
/// let locator = Locator::new("❤️🧡💛💚💙💜");
|
||||
/// assert_eq!(locator.text_len(), TextSize::from(26));
|
||||
/// assert!(!locator.contents().is_char_boundary(13));
|
||||
///
|
||||
/// let closest = locator.ceil_char_boundary(TextSize::from(13));
|
||||
/// assert_eq!(closest, TextSize::from(14));
|
||||
/// assert_eq!(&locator.contents()[..closest.to_usize()], "❤️🧡💛");
|
||||
/// ```
|
||||
///
|
||||
/// Additional examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
/// use ruff_linter::Locator;
|
||||
///
|
||||
/// let locator = Locator::new("Hello");
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// locator.ceil_char_boundary(TextSize::from(0)),
|
||||
/// TextSize::from(0)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// locator.ceil_char_boundary(TextSize::from(5)),
|
||||
/// TextSize::from(5)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// locator.ceil_char_boundary(TextSize::from(6)),
|
||||
/// TextSize::from(5)
|
||||
/// );
|
||||
///
|
||||
/// let locator = Locator::new("α");
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// locator.ceil_char_boundary(TextSize::from(0)),
|
||||
/// TextSize::from(0)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// locator.ceil_char_boundary(TextSize::from(1)),
|
||||
/// TextSize::from(2)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// locator.ceil_char_boundary(TextSize::from(2)),
|
||||
/// TextSize::from(2)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// locator.ceil_char_boundary(TextSize::from(3)),
|
||||
/// TextSize::from(2)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn ceil_char_boundary(&self, offset: TextSize) -> TextSize {
|
||||
let upper_bound = offset
|
||||
.to_u32()
|
||||
.saturating_add(4)
|
||||
.min(self.text_len().to_u32());
|
||||
(offset.to_u32()..upper_bound)
|
||||
.map(TextSize::from)
|
||||
.find(|offset| self.contents.is_char_boundary(offset.to_usize()))
|
||||
.unwrap_or_else(|| TextSize::from(upper_bound))
|
||||
}
|
||||
|
||||
/// Take the source code between the given [`TextRange`].
|
||||
#[inline]
|
||||
pub fn slice<T: Ranged>(&self, ranged: T) -> &'a str {
|
||||
|
||||
@@ -61,22 +61,17 @@ impl Serialize for SerializedMessages<'_> {
|
||||
let mut fingerprints = HashSet::<u64>::with_capacity(self.diagnostics.len());
|
||||
|
||||
for diagnostic in self.diagnostics {
|
||||
let start_location = diagnostic.expect_ruff_start_location();
|
||||
let end_location = diagnostic.expect_ruff_end_location();
|
||||
|
||||
let filename = diagnostic.expect_ruff_filename();
|
||||
let lines = if self.context.is_notebook(&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
|
||||
json!({
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
})
|
||||
Default::default()
|
||||
} else {
|
||||
json!({
|
||||
"begin": start_location.line,
|
||||
"end": end_location.line
|
||||
})
|
||||
(
|
||||
diagnostic.expect_ruff_start_location(),
|
||||
diagnostic.expect_ruff_end_location(),
|
||||
)
|
||||
};
|
||||
|
||||
let path = self.project_dir.as_ref().map_or_else(
|
||||
@@ -111,8 +106,11 @@ impl Serialize for SerializedMessages<'_> {
|
||||
"fingerprint": format!("{:x}", message_fingerprint),
|
||||
"location": {
|
||||
"path": path,
|
||||
"lines": lines
|
||||
}
|
||||
"positions": {
|
||||
"begin": start_location,
|
||||
"end": end_location,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
s.serialize_element(&value)?;
|
||||
|
||||
@@ -75,12 +75,13 @@ where
|
||||
);
|
||||
|
||||
let span = Span::from(file).with_range(range);
|
||||
let mut annotation = Annotation::primary(span);
|
||||
if let Some(suggestion) = suggestion {
|
||||
annotation = annotation.message(suggestion);
|
||||
}
|
||||
let annotation = Annotation::primary(span);
|
||||
diagnostic.annotate(annotation);
|
||||
|
||||
if let Some(suggestion) = suggestion {
|
||||
diagnostic.help(suggestion);
|
||||
}
|
||||
|
||||
if let Some(fix) = fix {
|
||||
diagnostic.set_fix(fix);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "`os` imported but unused",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
},
|
||||
"path": "fib.py"
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 8,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 10,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Local variable `x` is assigned to but never used",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 6,
|
||||
"end": 6
|
||||
},
|
||||
"path": "fib.py"
|
||||
"path": "fib.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 5,
|
||||
"line": 6
|
||||
},
|
||||
"end": {
|
||||
"column": 6,
|
||||
"line": 6
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -34,11 +46,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Undefined name `a`",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 1
|
||||
},
|
||||
"path": "undef.py"
|
||||
"path": "undef.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 4,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 5,
|
||||
"line": 1
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
|
||||
@@ -8,11 +8,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Expected one or more symbol names after import",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 1,
|
||||
"end": 2
|
||||
},
|
||||
"path": "syntax_errors.py"
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 15,
|
||||
"line": 1
|
||||
},
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
},
|
||||
@@ -21,11 +27,17 @@ expression: redact_fingerprint(&content)
|
||||
"description": "Expected ')', found newline",
|
||||
"fingerprint": "<redacted>",
|
||||
"location": {
|
||||
"lines": {
|
||||
"begin": 3,
|
||||
"end": 4
|
||||
},
|
||||
"path": "syntax_errors.py"
|
||||
"path": "syntax_errors.py",
|
||||
"positions": {
|
||||
"begin": {
|
||||
"column": 12,
|
||||
"line": 3
|
||||
},
|
||||
"end": {
|
||||
"column": 1,
|
||||
"line": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"severity": "major"
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ expression: value
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Preview\nWhen [preview](https://docs.astral.sh/ruff/preview/) is enabled,\nthe criterion for determining whether an import is first-party\nis stricter, which could affect the suggested fix. See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) for more details.\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
|
||||
@@ -6,14 +6,13 @@ use bitflags::bitflags;
|
||||
use colored::Colorize;
|
||||
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
||||
|
||||
use ruff_db::diagnostic::{Diagnostic, SecondaryCode};
|
||||
use ruff_db::diagnostic::{
|
||||
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode, ceil_char_boundary,
|
||||
};
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::{LineColumn, OneIndexed};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::fs::relativize_path;
|
||||
use crate::line_width::{IndentWidth, LineWidthBuilder};
|
||||
use crate::message::diff::Diff;
|
||||
use crate::message::{Emitter, EmitterContext};
|
||||
use crate::settings::types::UnsafeFixes;
|
||||
@@ -21,8 +20,6 @@ use crate::settings::types::UnsafeFixes;
|
||||
bitflags! {
|
||||
#[derive(Default)]
|
||||
struct EmitterFlags: u8 {
|
||||
/// Whether to show the fix status of a diagnostic.
|
||||
const SHOW_FIX_STATUS = 1 << 0;
|
||||
/// 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.
|
||||
@@ -30,17 +27,27 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct TextEmitter {
|
||||
flags: EmitterFlags,
|
||||
unsafe_fixes: UnsafeFixes,
|
||||
config: DisplayDiagnosticConfig,
|
||||
}
|
||||
|
||||
impl Default for TextEmitter {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
flags: EmitterFlags::default(),
|
||||
config: DisplayDiagnosticConfig::default()
|
||||
.format(DiagnosticFormat::Concise)
|
||||
.hide_severity(true)
|
||||
.color(!cfg!(test) && colored::control::SHOULD_COLORIZE.should_colorize()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextEmitter {
|
||||
#[must_use]
|
||||
pub fn with_show_fix_status(mut self, show_fix_status: bool) -> Self {
|
||||
self.flags
|
||||
.set(EmitterFlags::SHOW_FIX_STATUS, show_fix_status);
|
||||
self.config = self.config.show_fix_status(show_fix_status);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -58,7 +65,21 @@ impl TextEmitter {
|
||||
|
||||
#[must_use]
|
||||
pub fn with_unsafe_fixes(mut self, unsafe_fixes: UnsafeFixes) -> Self {
|
||||
self.unsafe_fixes = unsafe_fixes;
|
||||
self.config = self
|
||||
.config
|
||||
.fix_applicability(unsafe_fixes.required_applicability());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_preview(mut self, preview: bool) -> Self {
|
||||
self.config = self.config.preview(preview);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_color(mut self, color: bool) -> Self {
|
||||
self.config = self.config.color(color);
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -71,51 +92,10 @@ impl Emitter for TextEmitter {
|
||||
context: &EmitterContext,
|
||||
) -> anyhow::Result<()> {
|
||||
for message in diagnostics {
|
||||
write!(writer, "{}", message.display(context, &self.config))?;
|
||||
|
||||
let filename = message.expect_ruff_filename();
|
||||
write!(
|
||||
writer,
|
||||
"{path}{sep}",
|
||||
path = relativize_path(&filename).bold(),
|
||||
sep = ":".cyan(),
|
||||
)?;
|
||||
|
||||
let start_location = message.expect_ruff_start_location();
|
||||
let notebook_index = context.notebook_index(&filename);
|
||||
|
||||
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
|
||||
let diagnostic_location = if let Some(notebook_index) = notebook_index {
|
||||
write!(
|
||||
writer,
|
||||
"cell {cell}{sep}",
|
||||
cell = notebook_index
|
||||
.cell(start_location.line)
|
||||
.unwrap_or(OneIndexed::MIN),
|
||||
sep = ":".cyan(),
|
||||
)?;
|
||||
|
||||
LineColumn {
|
||||
line: notebook_index
|
||||
.cell_row(start_location.line)
|
||||
.unwrap_or(OneIndexed::MIN),
|
||||
column: start_location.column,
|
||||
}
|
||||
} else {
|
||||
start_location
|
||||
};
|
||||
|
||||
writeln!(
|
||||
writer,
|
||||
"{row}{sep}{col}{sep} {code_and_body}",
|
||||
row = diagnostic_location.line,
|
||||
col = diagnostic_location.column,
|
||||
sep = ":".cyan(),
|
||||
code_and_body = RuleCodeAndBody {
|
||||
message,
|
||||
show_fix_status: self.flags.intersects(EmitterFlags::SHOW_FIX_STATUS),
|
||||
unsafe_fixes: self.unsafe_fixes,
|
||||
}
|
||||
)?;
|
||||
|
||||
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
|
||||
// The `0..0` range is used to highlight file-level diagnostics.
|
||||
if message.expect_range() != TextRange::default() {
|
||||
@@ -186,7 +166,7 @@ pub(super) struct MessageCodeFrame<'a> {
|
||||
|
||||
impl Display for MessageCodeFrame<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let suggestion = self.message.suggestion();
|
||||
let suggestion = self.message.first_help_text();
|
||||
let footers = if let Some(suggestion) = suggestion {
|
||||
vec![Level::Help.title(suggestion)]
|
||||
} else {
|
||||
@@ -248,7 +228,7 @@ impl Display for MessageCodeFrame<'_> {
|
||||
let start_offset = source_code.line_start(start_index);
|
||||
let end_offset = source_code.line_end(end_index);
|
||||
|
||||
let source = replace_whitespace_and_unprintable(
|
||||
let source = replace_unprintable(
|
||||
source_code.slice(TextRange::new(start_offset, end_offset)),
|
||||
self.message.expect_range() - start_offset,
|
||||
)
|
||||
@@ -291,16 +271,20 @@ impl Display for MessageCodeFrame<'_> {
|
||||
}
|
||||
|
||||
/// Given some source code and an annotation range, this routine replaces
|
||||
/// tabs with ASCII whitespace, and unprintable characters with printable
|
||||
/// representations of them.
|
||||
/// 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).
|
||||
fn replace_whitespace_and_unprintable(source: &str, annotation_range: TextRange) -> SourceCode {
|
||||
///
|
||||
/// 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;
|
||||
let mut line_width = LineWidthBuilder::new(IndentWidth::default());
|
||||
|
||||
// Updates the range given by the caller whenever a single byte (at
|
||||
// `index` in `source`) is replaced with `len` bytes.
|
||||
@@ -329,19 +313,7 @@ fn replace_whitespace_and_unprintable(source: &str, annotation_range: TextRange)
|
||||
};
|
||||
|
||||
for (index, c) in source.char_indices() {
|
||||
let old_width = line_width.get();
|
||||
line_width = line_width.add_char(c);
|
||||
|
||||
if matches!(c, '\t') {
|
||||
let tab_width = u32::try_from(line_width.get() - old_width)
|
||||
.expect("small width because of tab size");
|
||||
result.push_str(&source[last_end..index]);
|
||||
for _ in 0..tab_width {
|
||||
result.push(' ');
|
||||
}
|
||||
last_end = index + 1;
|
||||
update_range(index, tab_width);
|
||||
} else if let Some(printable) = unprintable_replacement(c) {
|
||||
if let Some(printable) = unprintable_replacement(c) {
|
||||
result.push_str(&source[last_end..index]);
|
||||
result.push(printable);
|
||||
last_end = index + 1;
|
||||
@@ -396,9 +368,8 @@ impl<'a> SourceCode<'a> {
|
||||
if self.text.as_bytes()[self.annotation_range.start().to_usize() - 1] != b'\n' {
|
||||
return self;
|
||||
}
|
||||
let locator = Locator::new(&self.text);
|
||||
let start = self.annotation_range.start();
|
||||
let end = locator.ceil_char_boundary(start + TextSize::from(1));
|
||||
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
|
||||
SourceCode {
|
||||
annotation_range: TextRange::new(start, end),
|
||||
..self
|
||||
|
||||
@@ -134,6 +134,26 @@ pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) ->
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_chmod_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_rename_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_replace_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19404
|
||||
pub(crate) const fn is_fix_os_path_samefile_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19245
|
||||
pub(crate) const fn is_fix_os_getcwd_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
@@ -205,3 +225,8 @@ pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSet
|
||||
pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/19390
|
||||
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
(?:
|
||||
# Case-sensitive
|
||||
pyright
|
||||
| pyrefly
|
||||
| mypy:
|
||||
| type:\s*ignore
|
||||
| SPDX-License-Identifier:
|
||||
|
||||
@@ -5,7 +5,6 @@ use ruff_text_size::TextRange;
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
use crate::rules::eradicate::detection::comment_contains_code;
|
||||
@@ -51,7 +50,6 @@ pub(crate) fn commented_out_code(
|
||||
context: &LintContext,
|
||||
locator: &Locator,
|
||||
comment_ranges: &CommentRanges,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let mut comments = comment_ranges.into_iter().peekable();
|
||||
// Iterate over all comments in the document.
|
||||
@@ -65,7 +63,9 @@ pub(crate) fn commented_out_code(
|
||||
}
|
||||
|
||||
// Verify that the comment is on its own line, and that it contains code.
|
||||
if is_own_line_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
|
||||
if is_own_line_comment(line)
|
||||
&& comment_contains_code(line, &context.settings().task_tags[..])
|
||||
{
|
||||
if let Some(mut diagnostic) =
|
||||
context.report_diagnostic_if_enabled(CommentedOutCode, range)
|
||||
{
|
||||
|
||||
@@ -47,9 +47,10 @@ use crate::checkers::ast::Checker;
|
||||
/// raise
|
||||
/// ```
|
||||
///
|
||||
/// Exceptions that are logged via `logging.exception()` or `logging.error()`
|
||||
/// with `exc_info` enabled will _not_ be flagged, as this is a common pattern
|
||||
/// for propagating exception traces:
|
||||
/// Exceptions that are logged via `logging.exception()` or are logged via
|
||||
/// `logging.error()` or `logging.critical()` with `exc_info` enabled will
|
||||
/// _not_ be flagged, as this is a common pattern for propagating exception
|
||||
/// traces:
|
||||
/// ```python
|
||||
/// try:
|
||||
/// foo()
|
||||
@@ -201,7 +202,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> {
|
||||
) {
|
||||
if match attr.as_str() {
|
||||
"exception" => true,
|
||||
"error" => arguments
|
||||
"error" | "critical" => arguments
|
||||
.find_keyword("exc_info")
|
||||
.is_some_and(|keyword| is_const_true(&keyword.value)),
|
||||
_ => false,
|
||||
@@ -214,7 +215,7 @@ impl<'a> StatementVisitor<'a> for LogExceptionVisitor<'a> {
|
||||
if self.semantic.resolve_qualified_name(func).is_some_and(
|
||||
|qualified_name| match qualified_name.segments() {
|
||||
["logging", "exception"] => true,
|
||||
["logging", "error"] => arguments
|
||||
["logging", "error" | "critical"] => arguments
|
||||
.find_keyword("exc_info")
|
||||
.is_some_and(|keyword| is_const_true(&keyword.value)),
|
||||
_ => false,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_blind_except/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
BLE.py:25:8: BLE001 Do not catch blind exception: `BaseException`
|
||||
|
|
||||
@@ -121,3 +120,30 @@ BLE.py:113:8: BLE001 Do not catch blind exception: `Exception`
|
||||
| ^^^^^^^^^ BLE001
|
||||
114 | error("...", exc_info=None)
|
||||
|
|
||||
|
||||
BLE.py:119:8: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
117 | try:
|
||||
118 | pass
|
||||
119 | except Exception:
|
||||
| ^^^^^^^^^ BLE001
|
||||
120 | critical("...")
|
||||
|
|
||||
|
||||
BLE.py:125:8: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
123 | try:
|
||||
124 | pass
|
||||
125 | except Exception:
|
||||
| ^^^^^^^^^ BLE001
|
||||
126 | critical("...", exc_info=False)
|
||||
|
|
||||
|
||||
BLE.py:131:8: BLE001 Do not catch blind exception: `Exception`
|
||||
|
|
||||
129 | try:
|
||||
130 | pass
|
||||
131 | except Exception:
|
||||
| ^^^^^^^^^ BLE001
|
||||
132 | critical("...", exc_info=None)
|
||||
|
|
||||
|
||||
@@ -27,4 +27,23 @@ mod tests {
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("COM81.py"))]
|
||||
#[test_case(Path::new("COM81_syntax_error.py"))]
|
||||
fn preview_rules(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("preview__{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_commas").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: crate::settings::types::PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rules(vec![
|
||||
Rule::MissingTrailingComma,
|
||||
Rule::TrailingCommaOnBareTuple,
|
||||
Rule::ProhibitedTrailingComma,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
assert_diagnostics!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
use crate::preview::is_trailing_comma_type_params_enabled;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{AlwaysFixableViolation, Violation};
|
||||
use crate::{Edit, Fix};
|
||||
|
||||
@@ -24,6 +26,8 @@ enum TokenType {
|
||||
Def,
|
||||
For,
|
||||
Lambda,
|
||||
Class,
|
||||
Type,
|
||||
Irrelevant,
|
||||
}
|
||||
|
||||
@@ -69,6 +73,8 @@ impl From<(TokenKind, TextRange)> for SimpleToken {
|
||||
TokenKind::Lbrace => TokenType::OpeningCurlyBracket,
|
||||
TokenKind::Rbrace => TokenType::ClosingBracket,
|
||||
TokenKind::Def => TokenType::Def,
|
||||
TokenKind::Class => TokenType::Class,
|
||||
TokenKind::Type => TokenType::Type,
|
||||
TokenKind::For => TokenType::For,
|
||||
TokenKind::Lambda => TokenType::Lambda,
|
||||
// Import treated like a function.
|
||||
@@ -98,6 +104,8 @@ enum ContextType {
|
||||
Dict,
|
||||
/// Lambda parameter list, e.g. `lambda a, b`.
|
||||
LambdaParameters,
|
||||
/// Type parameter list, e.g. `def foo[T, U](): ...`
|
||||
TypeParameters,
|
||||
}
|
||||
|
||||
/// Comma context - described a comma-delimited "situation".
|
||||
@@ -290,7 +298,7 @@ pub(crate) fn trailing_commas(
|
||||
}
|
||||
|
||||
// Update the comma context stack.
|
||||
let context = update_context(token, prev, prev_prev, &mut stack);
|
||||
let context = update_context(token, prev, prev_prev, &mut stack, lint_context.settings());
|
||||
|
||||
check_token(token, prev, prev_prev, context, locator, lint_context);
|
||||
|
||||
@@ -326,6 +334,7 @@ fn check_token(
|
||||
ContextType::No => false,
|
||||
ContextType::FunctionParameters => true,
|
||||
ContextType::CallArguments => true,
|
||||
ContextType::TypeParameters => true,
|
||||
// `(1)` is not equivalent to `(1,)`.
|
||||
ContextType::Tuple => context.num_commas != 0,
|
||||
// `x[1]` is not equivalent to `x[1,]`.
|
||||
@@ -408,6 +417,7 @@ fn update_context(
|
||||
prev: SimpleToken,
|
||||
prev_prev: SimpleToken,
|
||||
stack: &mut Vec<Context>,
|
||||
settings: &LinterSettings,
|
||||
) -> Context {
|
||||
let new_context = match token.ty {
|
||||
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
|
||||
@@ -417,6 +427,17 @@ fn update_context(
|
||||
}
|
||||
_ => Context::new(ContextType::Tuple),
|
||||
},
|
||||
TokenType::OpeningSquareBracket if is_trailing_comma_type_params_enabled(settings) => {
|
||||
match (prev.ty, prev_prev.ty) {
|
||||
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
|
||||
Context::new(ContextType::TypeParameters)
|
||||
}
|
||||
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
|
||||
Context::new(ContextType::Subscript)
|
||||
}
|
||||
_ => Context::new(ContextType::List),
|
||||
}
|
||||
}
|
||||
TokenType::OpeningSquareBracket => match prev.ty {
|
||||
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
|
||||
Context::new(ContextType::Subscript)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
|
||||
---
|
||||
COM81_syntax_error.py:3:5: SyntaxError: Starred expression cannot be used here
|
||||
|
|
||||
1 | # Check for `flake8-commas` violation for a file containing syntax errors.
|
||||
2 | (
|
||||
3 | *args
|
||||
| ^^^^^
|
||||
4 | )
|
||||
|
|
||||
|
||||
COM81_syntax_error.py:6:9: SyntaxError: Type parameter list cannot be empty
|
||||
|
|
||||
4 | )
|
||||
5 |
|
||||
6 | def foo[(param1='test', param2='test',):
|
||||
| ^
|
||||
7 | pass
|
||||
|
|
||||
|
||||
COM81_syntax_error.py:6:38: COM819 Trailing comma prohibited
|
||||
|
|
||||
4 | )
|
||||
5 |
|
||||
6 | def foo[(param1='test', param2='test',):
|
||||
| ^ COM819
|
||||
7 | pass
|
||||
|
|
||||
= help: Remove trailing comma
|
||||
@@ -182,60 +182,27 @@ impl Violation for DotFormatInException {
|
||||
|
||||
/// EM101, EM102, EM103
|
||||
pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
|
||||
if let Expr::Call(ast::ExprCall {
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments: Arguments { args, .. },
|
||||
..
|
||||
}) = exc
|
||||
{
|
||||
if let Some(first) = args.first() {
|
||||
match first {
|
||||
// Check for string literals.
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) => {
|
||||
if checker.is_rule_enabled(Rule::RawStringInException) {
|
||||
if string.len() >= checker.settings().flake8_errmsg.max_string_length {
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(RawStringInException, first.range());
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.source(), stmt)
|
||||
{
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for byte string literals.
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => {
|
||||
if checker.settings().rules.enabled(Rule::RawStringInException) {
|
||||
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length
|
||||
&& is_raise_exception_byte_string_enabled(checker.settings())
|
||||
{
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(RawStringInException, first.range());
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.source(), stmt)
|
||||
{
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for f-strings.
|
||||
Expr::FString(_) => {
|
||||
if checker.is_rule_enabled(Rule::FStringInException) {
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if checker.semantic().match_typing_expr(func, "cast") {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(first) = args.first() {
|
||||
match first {
|
||||
// Check for string literals.
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value: string, .. }) => {
|
||||
if checker.is_rule_enabled(Rule::RawStringInException) {
|
||||
if string.len() >= checker.settings().flake8_errmsg.max_string_length {
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(FStringInException, first.range());
|
||||
checker.report_diagnostic(RawStringInException, first.range());
|
||||
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
@@ -247,32 +214,66 @@ pub(crate) fn string_in_exception(checker: &Checker, stmt: &Stmt, exc: &Expr) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for .format() calls.
|
||||
Expr::Call(ast::ExprCall { func, .. }) => {
|
||||
if checker.is_rule_enabled(Rule::DotFormatInException) {
|
||||
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) =
|
||||
func.as_ref()
|
||||
{
|
||||
if attr == "format" && value.is_literal_expr() {
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(DotFormatInException, first.range());
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.source(), stmt)
|
||||
{
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Check for byte string literals.
|
||||
Expr::BytesLiteral(ast::ExprBytesLiteral { value: bytes, .. }) => {
|
||||
if checker.settings().rules.enabled(Rule::RawStringInException) {
|
||||
if bytes.len() >= checker.settings().flake8_errmsg.max_string_length
|
||||
&& is_raise_exception_byte_string_enabled(checker.settings())
|
||||
{
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(RawStringInException, first.range());
|
||||
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for f-strings.
|
||||
Expr::FString(_) => {
|
||||
if checker.is_rule_enabled(Rule::FStringInException) {
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(FStringInException, first.range());
|
||||
if let Some(indentation) = whitespace::indentation(checker.source(), stmt) {
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check for .format() calls.
|
||||
Expr::Call(ast::ExprCall { func, .. }) => {
|
||||
if checker.is_rule_enabled(Rule::DotFormatInException) {
|
||||
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
|
||||
if attr == "format" && value.is_literal_expr() {
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(DotFormatInException, first.range());
|
||||
if let Some(indentation) =
|
||||
whitespace::indentation(checker.source(), stmt)
|
||||
{
|
||||
diagnostic.set_fix(generate_fix(
|
||||
stmt,
|
||||
first,
|
||||
indentation,
|
||||
checker.stylist(),
|
||||
checker.locator(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,3 +278,6 @@ EM.py:84:9: EM103 [*] Exception must not use a `.format()` string directly, assi
|
||||
91 |+ raise RuntimeError(
|
||||
92 |+ msg
|
||||
93 |+ )
|
||||
91 94 |
|
||||
92 95 |
|
||||
93 96 | def raise_typing_cast_exception():
|
||||
|
||||
@@ -343,3 +343,6 @@ EM.py:84:9: EM103 [*] Exception must not use a `.format()` string directly, assi
|
||||
91 |+ raise RuntimeError(
|
||||
92 |+ msg
|
||||
93 |+ )
|
||||
91 94 |
|
||||
92 95 |
|
||||
93 96 | def raise_typing_cast_exception():
|
||||
|
||||
@@ -11,7 +11,6 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::Locator;
|
||||
use crate::checkers::ast::LintContext;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
@@ -108,13 +107,15 @@ pub(crate) fn implicit(
|
||||
tokens: &Tokens,
|
||||
locator: &Locator,
|
||||
indexer: &Indexer,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
for (a_token, b_token) in tokens
|
||||
.iter()
|
||||
.filter(|token| {
|
||||
token.kind() != TokenKind::Comment
|
||||
&& (settings.flake8_implicit_str_concat.allow_multiline
|
||||
&& (context
|
||||
.settings()
|
||||
.flake8_implicit_str_concat
|
||||
.allow_multiline
|
||||
|| token.kind() != TokenKind::NonLogicalNewline)
|
||||
})
|
||||
.tuple_windows()
|
||||
|
||||
@@ -64,6 +64,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let mut union_type = UnionKind::TypingUnion;
|
||||
let mut optional_present = false;
|
||||
// Adds a member to `literal_exprs` if it is a `Literal` annotation
|
||||
let mut check_for_duplicate_members = |expr: &'a Expr, parent: &'a Expr| {
|
||||
if matches!(parent, Expr::BinOp(_)) {
|
||||
@@ -74,6 +75,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
&& is_optional_type(checker, expr)
|
||||
{
|
||||
// If the union member is an `Optional`, add a virtual `None` literal.
|
||||
optional_present = true;
|
||||
&VIRTUAL_NONE_LITERAL
|
||||
} else {
|
||||
expr
|
||||
@@ -87,7 +89,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
DuplicateUnionMember {
|
||||
duplicate_name: checker.generator().expr(virtual_expr),
|
||||
},
|
||||
// Use the real expression's range for diagnostics,
|
||||
// Use the real expression's range for diagnostics.
|
||||
expr.range(),
|
||||
));
|
||||
}
|
||||
@@ -104,6 +106,13 @@ pub(crate) fn duplicate_union_member<'a>(checker: &Checker, expr: &'a Expr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not reduce `Union[None, ... None]` to avoid introducing a `TypeError` unintentionally
|
||||
// e.g. `isinstance(None, Union[None, None])`, if reduced to `isinstance(None, None)`, causes
|
||||
// `TypeError: isinstance() arg 2 must be a type, a tuple of types, or a union` to throw.
|
||||
if unique_nodes.iter().all(|expr| expr.is_none_literal_expr()) && !optional_present {
|
||||
return;
|
||||
}
|
||||
|
||||
// Mark [`Fix`] as unsafe when comments are in range.
|
||||
let applicability = if checker.comment_ranges().intersects(expr.range()) {
|
||||
Applicability::Unsafe
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::whitespace::trailing_comment_start_offset;
|
||||
use ruff_python_ast::{Stmt, StmtExpr};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix;
|
||||
use crate::{Fix, FixAvailability, Violation};
|
||||
use crate::{Edit, Fix, FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Removes ellipses (`...`) in otherwise non-empty class bodies.
|
||||
@@ -50,15 +51,21 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt])
|
||||
}
|
||||
|
||||
for stmt in body {
|
||||
let Stmt::Expr(StmtExpr { value, .. }) = &stmt else {
|
||||
let Stmt::Expr(StmtExpr { value, .. }) = stmt else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if value.is_ellipsis_literal_expr() {
|
||||
let mut diagnostic =
|
||||
checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range());
|
||||
let edit =
|
||||
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
|
||||
|
||||
// Try to preserve trailing comment if it exists
|
||||
let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.source()) {
|
||||
Edit::range_deletion(stmt.range().add_end(index))
|
||||
} else {
|
||||
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer())
|
||||
};
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation(
|
||||
checker.semantic().current_statement_id(),
|
||||
)));
|
||||
|
||||
@@ -145,3 +145,22 @@ PYI013.py:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
37 36 |
|
||||
38 37 | def __init__():
|
||||
39 38 | pass
|
||||
|
||||
PYI013.py:44:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
42 | class NonEmptyChildWithInlineComment:
|
||||
43 | value: int
|
||||
44 | ... # preserve me
|
||||
| ^^^ PYI013
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Safe fix
|
||||
41 41 |
|
||||
42 42 | class NonEmptyChildWithInlineComment:
|
||||
43 43 | value: int
|
||||
44 |- ... # preserve me
|
||||
44 |+ # preserve me
|
||||
45 45 |
|
||||
46 46 |
|
||||
47 47 | class EmptyClass:
|
||||
|
||||
@@ -17,9 +17,10 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
3 3 | class OneAttributeClass:
|
||||
4 4 | value: int
|
||||
5 |- ... # Error
|
||||
6 5 |
|
||||
7 6 | class OneAttributeClass2:
|
||||
8 7 | ... # Error
|
||||
5 |+ # Error
|
||||
6 6 |
|
||||
7 7 | class OneAttributeClass2:
|
||||
8 8 | ... # Error
|
||||
|
||||
PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
@@ -35,9 +36,10 @@ PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
6 6 |
|
||||
7 7 | class OneAttributeClass2:
|
||||
8 |- ... # Error
|
||||
9 8 | value: int
|
||||
10 9 |
|
||||
11 10 | class MyClass:
|
||||
8 |+ # Error
|
||||
9 9 | value: int
|
||||
10 10 |
|
||||
11 11 | class MyClass:
|
||||
|
||||
PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
@@ -91,9 +93,10 @@ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
15 15 | class TwoEllipsesClass:
|
||||
16 16 | ...
|
||||
17 |- ... # Error
|
||||
18 17 |
|
||||
19 18 | class DocstringClass:
|
||||
20 19 | """
|
||||
17 |+ # Error
|
||||
18 18 |
|
||||
19 19 | class DocstringClass:
|
||||
20 20 | """
|
||||
|
||||
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
@@ -111,9 +114,10 @@ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
22 22 | """
|
||||
23 23 |
|
||||
24 |- ... # Error
|
||||
25 24 |
|
||||
26 25 | class NonEmptyChild(Exception):
|
||||
27 26 | value: int
|
||||
24 |+ # Error
|
||||
25 25 |
|
||||
26 26 | class NonEmptyChild(Exception):
|
||||
27 27 | value: int
|
||||
|
||||
PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
@@ -131,9 +135,10 @@ PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
26 26 | class NonEmptyChild(Exception):
|
||||
27 27 | value: int
|
||||
28 |- ... # Error
|
||||
29 28 |
|
||||
30 29 | class NonEmptyChild2(Exception):
|
||||
31 30 | ... # Error
|
||||
28 |+ # Error
|
||||
29 29 |
|
||||
30 30 | class NonEmptyChild2(Exception):
|
||||
31 31 | ... # Error
|
||||
|
||||
PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
@@ -149,9 +154,10 @@ PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
29 29 |
|
||||
30 30 | class NonEmptyChild2(Exception):
|
||||
31 |- ... # Error
|
||||
32 31 | value: int
|
||||
33 32 |
|
||||
34 33 | class NonEmptyWithInit:
|
||||
31 |+ # Error
|
||||
32 32 | value: int
|
||||
33 33 |
|
||||
34 34 | class NonEmptyWithInit:
|
||||
|
||||
PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
@@ -169,6 +175,28 @@ PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
34 34 | class NonEmptyWithInit:
|
||||
35 35 | value: int
|
||||
36 |- ... # Error
|
||||
37 36 |
|
||||
38 37 | def __init__():
|
||||
39 38 | pass
|
||||
36 |+ # Error
|
||||
37 37 |
|
||||
38 38 | def __init__():
|
||||
39 39 | pass
|
||||
|
||||
PYI013.pyi:43:5: PYI013 [*] Non-empty class body must not contain `...`
|
||||
|
|
||||
41 | class NonEmptyChildWithInlineComment:
|
||||
42 | value: int
|
||||
43 | ... # preserve me
|
||||
| ^^^ PYI013
|
||||
44 |
|
||||
45 | # Not violations
|
||||
|
|
||||
= help: Remove unnecessary `...`
|
||||
|
||||
ℹ Safe fix
|
||||
40 40 |
|
||||
41 41 | class NonEmptyChildWithInlineComment:
|
||||
42 42 | value: int
|
||||
43 |- ... # preserve me
|
||||
43 |+ # preserve me
|
||||
44 44 |
|
||||
45 45 | # Not violations
|
||||
46 46 |
|
||||
|
||||
@@ -974,6 +974,8 @@ PYI016.py:143:61: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[typing.Optional[complex], complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -981,6 +983,8 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -990,3 +994,15 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: typing.Optional[complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:148:37: PYI016 Duplicate union member `None`
|
||||
|
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 | # Should throw duplicate union member but not fix
|
||||
148 | isinstance(None, typing.Union[None, None])
|
||||
| ^^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `None`
|
||||
|
||||
@@ -1162,6 +1162,8 @@ PYI016.py:143:61: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[None, complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:143:72: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1179,6 +1181,8 @@ PYI016.py:143:72: PYI016 [*] Duplicate union member `complex`
|
||||
143 |-field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
143 |+field48: typing.Union[None, complex]
|
||||
144 144 | field49: typing.Optional[complex | complex] | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
||||
PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1186,6 +1190,8 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -1195,6 +1201,9 @@ PYI016.py:144:36: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: None | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
|
|
||||
@@ -1202,6 +1211,8 @@ PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 | field49: typing.Optional[complex | complex] | complex
|
||||
| ^^^^^^^ PYI016
|
||||
145 |
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
|
|
||||
= help: Remove duplicate union member `complex`
|
||||
|
||||
@@ -1211,3 +1222,15 @@ PYI016.py:144:47: PYI016 [*] Duplicate union member `complex`
|
||||
143 143 | field48: typing.Union[typing.Optional[typing.Union[complex, complex]], complex]
|
||||
144 |-field49: typing.Optional[complex | complex] | complex
|
||||
144 |+field49: None | complex
|
||||
145 145 |
|
||||
146 146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 147 | # Should throw duplicate union member but not fix
|
||||
|
||||
PYI016.py:148:37: PYI016 Duplicate union member `None`
|
||||
|
|
||||
146 | # Regression test for https://github.com/astral-sh/ruff/issues/19403
|
||||
147 | # Should throw duplicate union member but not fix
|
||||
148 | isinstance(None, typing.Union[None, None])
|
||||
| ^^^^ PYI016
|
||||
|
|
||||
= help: Remove duplicate union member `None`
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user