Compare commits
160 Commits
0.14.4
...
dcreager/f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b136e56b0 | ||
|
|
dc802d31f2 | ||
|
|
83466ed774 | ||
|
|
8ea1c15410 | ||
|
|
83134fb380 | ||
|
|
0d47334f3b | ||
|
|
a8f7ccf2ca | ||
|
|
ce06094ada | ||
|
|
97935518e9 | ||
|
|
68ebd5132c | ||
|
|
ac9c83e581 | ||
|
|
5dd56264fb | ||
|
|
18a14bfaf1 | ||
|
|
ffce0de3c4 | ||
|
|
e29f3ed0fe | ||
|
|
663f78e644 | ||
|
|
c796a70ec9 | ||
|
|
192c37d540 | ||
|
|
0645418f00 | ||
|
|
62343a101a | ||
|
|
8dad289062 | ||
|
|
f67236b932 | ||
|
|
cbc6863b8c | ||
|
|
7043d51df0 | ||
|
|
5ca9c15fc8 | ||
|
|
7a739d6b76 | ||
|
|
d5a95ec824 | ||
|
|
b1e354bd99 | ||
|
|
e4a32ba644 | ||
|
|
ac2d07e83c | ||
|
|
8156b45173 | ||
|
|
d063c71177 | ||
|
|
c16ef709f6 | ||
|
|
04a3ec3689 | ||
|
|
1a86e13472 | ||
|
|
901e9cdf49 | ||
|
|
58fa1d71b6 | ||
|
|
d9fc0f08b4 | ||
|
|
09deeabda5 | ||
|
|
1436e688cc | ||
|
|
d6c34b98a5 | ||
|
|
1b50e032a4 | ||
|
|
687ed292f6 | ||
|
|
0554b1ca8a | ||
|
|
bbe42bc775 | ||
|
|
0d2cd84df4 | ||
|
|
665f68036c | ||
|
|
f5fb5c388a | ||
|
|
dbd72480a9 | ||
|
|
75c1a0ae55 | ||
|
|
7a546809c4 | ||
|
|
3065f8dbbc | ||
|
|
fb5b8c3653 | ||
|
|
efa2b5167f | ||
|
|
29acc1e860 | ||
|
|
698231a47a | ||
|
|
d63b4b0383 | ||
|
|
c5d654bce8 | ||
|
|
3e7e91724c | ||
|
|
2a2b719f00 | ||
|
|
ffb7bdd595 | ||
|
|
0a55327d64 | ||
|
|
008e9d06e1 | ||
|
|
8529d79a70 | ||
|
|
8599c7e5b3 | ||
|
|
5f501374c4 | ||
|
|
e9a5337136 | ||
|
|
05cf53aae8 | ||
|
|
6a26f86778 | ||
|
|
d0314131fb | ||
|
|
696d7a5d68 | ||
|
|
66e9d57797 | ||
|
|
87dafb8787 | ||
|
|
9e80e5a3a6 | ||
|
|
f9cc26aa12 | ||
|
|
d49c326309 | ||
|
|
e70fccbf25 | ||
|
|
90b32f3b3b | ||
|
|
99694b6e4a | ||
|
|
67e54fffe1 | ||
|
|
a01b0d7780 | ||
|
|
04ab9170d6 | ||
|
|
12e74ae894 | ||
|
|
d64b2f747c | ||
|
|
cd183c5e1f | ||
|
|
eb1957cd17 | ||
|
|
7e3dd0764a | ||
|
|
a6abd65c2c | ||
|
|
3d4b0559f1 | ||
|
|
2f6f3e1042 | ||
|
|
9dd666d677 | ||
|
|
a1d9cb5830 | ||
|
|
8a85a2961e | ||
|
|
43427abb61 | ||
|
|
84c3cecad6 | ||
|
|
e8e8180888 | ||
|
|
f5cf672ed4 | ||
|
|
6322f37015 | ||
|
|
d272a623d3 | ||
|
|
19c7994e90 | ||
|
|
725ae69773 | ||
|
|
d2c3996f4e | ||
|
|
988c38c013 | ||
|
|
164c2a6cc6 | ||
|
|
1bbe4f0d5e | ||
|
|
cd7354a5c6 | ||
|
|
ec48a47a88 | ||
|
|
43297d3455 | ||
|
|
4373974dd9 | ||
|
|
e4374f14ed | ||
|
|
03bd0619e9 | ||
|
|
bd8812127d | ||
|
|
44b0c9ebac | ||
|
|
7b237d316f | ||
|
|
36cce347fd | ||
|
|
33b942c7ad | ||
|
|
9ce3230add | ||
|
|
2bc6c78e26 | ||
|
|
1fd852fb3f | ||
|
|
5f3e086ee4 | ||
|
|
039a69fa8c | ||
|
|
3656b44877 | ||
|
|
98869f0307 | ||
|
|
d258302b08 | ||
|
|
deeda56906 | ||
|
|
f63a9f2334 | ||
|
|
e4dc406a3d | ||
|
|
1d188476b6 | ||
|
|
4821c050ef | ||
|
|
835e31b3ff | ||
|
|
8d1efe964a | ||
|
|
04e7cecab3 | ||
|
|
84a810736d | ||
|
|
f44598dc11 | ||
|
|
ab46c8de0f | ||
|
|
a6f2dee33b | ||
|
|
238f151371 | ||
|
|
3fa609929f | ||
|
|
73b1fce74a | ||
|
|
52bd22003b | ||
|
|
0a6d6b6194 | ||
|
|
ca51feb319 | ||
|
|
e0a3cbb048 | ||
|
|
f4f259395c | ||
|
|
2a1d412f72 | ||
|
|
dd751e8d07 | ||
|
|
020ff1723b | ||
|
|
09e6af16c8 | ||
|
|
76efc8061d | ||
|
|
16de4aa3cc | ||
|
|
e06e108095 | ||
|
|
b6add3ee6d | ||
|
|
39c21d7c6c | ||
|
|
1617292e9f | ||
|
|
faae72b836 | ||
|
|
276f1d0d88 | ||
|
|
ed18112cfa | ||
|
|
8ba1cfebed | ||
|
|
6185a2af9e | ||
|
|
6cc3393ccd |
@@ -7,6 +7,10 @@ serial = { max-threads = 1 }
|
||||
filter = 'binary(file_watching)'
|
||||
test-group = 'serial'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = 'binary(e2e)'
|
||||
test-group = 'serial'
|
||||
|
||||
[profile.ci]
|
||||
# Print out output for failing tests as soon as they fail, and also at the end
|
||||
# of the run (for easy scrollability).
|
||||
|
||||
243
.github/workflows/ci.yaml
vendored
243
.github/workflows/ci.yaml
vendored
@@ -231,6 +231,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: |
|
||||
rustup component add clippy
|
||||
@@ -251,20 +253,23 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: ty mdtests (GitHub annotations)
|
||||
@@ -291,14 +296,6 @@ jobs:
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ty
|
||||
path: target/debug/ty
|
||||
|
||||
cargo-test-linux-release:
|
||||
name: "cargo test (linux, release)"
|
||||
@@ -315,24 +312,24 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
|
||||
run: cargo nextest run --cargo-profile profiling --all-features
|
||||
- name: "Run doctests"
|
||||
run: cargo test --doc --profile profiling --all-features
|
||||
|
||||
cargo-test-other:
|
||||
strategy:
|
||||
@@ -350,14 +347,16 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: "true"
|
||||
- name: "Run tests"
|
||||
@@ -376,6 +375,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
@@ -411,6 +412,8 @@ jobs:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
env:
|
||||
MSRV: ${{ steps.msrv.outputs.value }}
|
||||
@@ -435,12 +438,13 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@b3f755e95653da9a2d25b99154edfdbd5b356d0a # v1.15.10
|
||||
uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
|
||||
- name: "Install cargo-fuzz"
|
||||
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
|
||||
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
|
||||
@@ -449,9 +453,7 @@ jobs:
|
||||
fuzz-parser:
|
||||
name: "fuzz parser"
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
needs: determine_changes
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.parser == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
@@ -460,27 +462,24 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
name: Download Ruff binary to test
|
||||
id: download-cached-binary
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
name: ruff
|
||||
path: ruff-to-test
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Build Ruff binary
|
||||
run: cargo build --bin ruff
|
||||
- name: Fuzz
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.download-cached-binary.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
(
|
||||
uv run \
|
||||
--python="${PYTHON_VERSION}" \
|
||||
--project=./python/py-fuzzer \
|
||||
--locked \
|
||||
fuzz \
|
||||
--test-executable="${DOWNLOAD_PATH}/ruff" \
|
||||
--test-executable=target/debug/ruff \
|
||||
--bin=ruff \
|
||||
0-500
|
||||
)
|
||||
@@ -496,7 +495,9 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup component add rustfmt
|
||||
# Run all code generation scripts, and verify that the current output is
|
||||
@@ -520,9 +521,7 @@ jobs:
|
||||
ecosystem:
|
||||
name: "ecosystem"
|
||||
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
needs: determine_changes
|
||||
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
|
||||
# Ecosystem check needs linter and/or formatter changes.
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
|
||||
@@ -530,26 +529,37 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.ref }}
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
activate-environment: true
|
||||
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
name: Download comparison Ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download baseline Ruff binary
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
name: ruff
|
||||
branch: ${{ github.event.pull_request.base.ref }}
|
||||
workflow: "ci.yaml"
|
||||
check_artifacts: true
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: false
|
||||
|
||||
- name: Build baseline version
|
||||
run: |
|
||||
cargo build --bin ruff
|
||||
mv target/debug/ruff target/debug/ruff-baseline
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
clean: false
|
||||
|
||||
- name: Build comparison version
|
||||
run: cargo build --bin ruff
|
||||
|
||||
- name: Install ruff-ecosystem
|
||||
run: |
|
||||
@@ -557,16 +567,11 @@ jobs:
|
||||
|
||||
- name: Run `ruff check` stable ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
|
||||
ruff-ecosystem check ./target/debug/ruff-baseline ./target/debug/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-check-stable
|
||||
|
||||
cat ecosystem-result-check-stable > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Linter (stable)" > ecosystem-result
|
||||
@@ -575,16 +580,11 @@ jobs:
|
||||
|
||||
- name: Run `ruff check` preview ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem check ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
|
||||
ruff-ecosystem check ./target/debug/ruff-baseline ./target/debug/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-check-preview
|
||||
|
||||
cat ecosystem-result-check-preview > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Linter (preview)" >> ecosystem-result
|
||||
@@ -593,16 +593,11 @@ jobs:
|
||||
|
||||
- name: Run `ruff format` stable ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
|
||||
ruff-ecosystem format ./target/debug/ruff-baseline ./target/debug/ruff --cache ./checkouts --output-format markdown | tee ecosystem-result-format-stable
|
||||
|
||||
cat ecosystem-result-format-stable > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Formatter (stable)" >> ecosystem-result
|
||||
@@ -611,32 +606,19 @@ jobs:
|
||||
|
||||
- name: Run `ruff format` preview ecosystem check
|
||||
if: ${{ needs.determine_changes.outputs.formatter == 'true' }}
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ./ruff "${DOWNLOAD_PATH}/ruff"
|
||||
|
||||
# Set pipefail to avoid hiding errors with tee
|
||||
set -eo pipefail
|
||||
|
||||
ruff-ecosystem format ./ruff "${DOWNLOAD_PATH}/ruff" --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
|
||||
ruff-ecosystem format ./target/debug/ruff-baseline ./target/debug/ruff --cache ./checkouts --output-format markdown --force-preview | tee ecosystem-result-format-preview
|
||||
|
||||
cat ecosystem-result-format-preview > "$GITHUB_STEP_SUMMARY"
|
||||
echo "### Formatter (preview)" >> ecosystem-result
|
||||
cat ecosystem-result-format-preview >> ecosystem-result
|
||||
echo "" >> ecosystem-result
|
||||
|
||||
- name: Export pull request number
|
||||
run: |
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
name: Upload PR Number
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
# NOTE: astral-sh-bot uses this artifact to post comments on PRs.
|
||||
# Make sure to update the bot if you rename the artifact.
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
name: Upload Results
|
||||
with:
|
||||
@@ -656,8 +638,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
@@ -700,7 +684,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@b3f755e95653da9a2d25b99154edfdbd5b356d0a # v1.15.10
|
||||
- uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -713,14 +697,16 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Run ty completion evaluation"
|
||||
run: cargo run --release --package ty_completion_eval -- all --threshold 0.4 --tasks /tmp/completion-evaluation-tasks.csv
|
||||
run: cargo run --profile profiling --package ty_completion_eval -- all --threshold 0.4 --tasks /tmp/completion-evaluation-tasks.csv
|
||||
- name: "Ensure there are no changes"
|
||||
run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv
|
||||
|
||||
@@ -738,6 +724,8 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels"
|
||||
@@ -760,8 +748,10 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -792,6 +782,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1
|
||||
@@ -800,7 +792,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
python-version: 3.13
|
||||
activate-environment: true
|
||||
@@ -834,6 +826,8 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Run checks"
|
||||
@@ -847,9 +841,7 @@ jobs:
|
||||
name: "test ruff-lsp"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
needs:
|
||||
- cargo-test-linux
|
||||
- determine_changes
|
||||
needs: determine_changes
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3.0.0
|
||||
@@ -857,37 +849,46 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
name: "Download ruff-lsp source"
|
||||
name: "Checkout ruff source"
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: false
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: Build Ruff binary
|
||||
run: cargo build -p ruff --bin ruff
|
||||
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
name: "Checkout ruff-lsp source"
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
path: ruff-lsp
|
||||
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
# installation fails on 3.13 and newer
|
||||
python-version: "3.12"
|
||||
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
name: Download development ruff binary
|
||||
id: ruff-target
|
||||
with:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- name: Install ruff-lsp dependencies
|
||||
run: |
|
||||
cd ruff-lsp
|
||||
just install
|
||||
|
||||
- name: Run ruff-lsp tests
|
||||
env:
|
||||
DOWNLOAD_PATH: ${{ steps.ruff-target.outputs.download-path }}
|
||||
run: |
|
||||
# Setup development binary
|
||||
pip uninstall --yes ruff
|
||||
chmod +x "${DOWNLOAD_PATH}/ruff"
|
||||
export PATH="${DOWNLOAD_PATH}:${PATH}"
|
||||
export PATH="${PWD}/target/debug:${PATH}"
|
||||
ruff version
|
||||
|
||||
cd ruff-lsp
|
||||
just test
|
||||
|
||||
check-playground:
|
||||
@@ -904,6 +905,8 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
node-version: 22
|
||||
@@ -942,13 +945,15 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -956,7 +961,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: cargo codspeed run
|
||||
@@ -980,13 +985,15 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -994,7 +1001,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
with:
|
||||
mode: instrumentation
|
||||
run: cargo codspeed run
|
||||
@@ -1018,13 +1025,15 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1032,7 +1041,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
env:
|
||||
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
|
||||
# appear to provide much useful insight for our walltime benchmarks right now
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -34,7 +34,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
|
||||
14
.github/workflows/mypy_primer.yaml
vendored
14
.github/workflows/mypy_primer.yaml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
@@ -55,24 +55,20 @@ jobs:
|
||||
- name: Run mypy_primer
|
||||
env:
|
||||
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
|
||||
CLICOLOR_FORCE: "1"
|
||||
DIFF_FILE: mypy_primer.diff
|
||||
run: |
|
||||
cd ruff
|
||||
scripts/mypy_primer.sh
|
||||
echo ${{ github.event.number }} > ../pr-number
|
||||
|
||||
# NOTE: astral-sh-bot uses this artifact to post comments on PRs.
|
||||
# Make sure to update the bot if you rename the artifact.
|
||||
- name: Upload diff
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: mypy_primer_diff
|
||||
path: mypy_primer.diff
|
||||
|
||||
- name: Upload pr-number
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
memory_usage:
|
||||
name: Run memory statistics
|
||||
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
|
||||
@@ -85,7 +81,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
with:
|
||||
|
||||
122
.github/workflows/mypy_primer_comment.yaml
vendored
122
.github/workflows/mypy_primer_comment.yaml
vendored
@@ -1,122 +0,0 @@
|
||||
name: PR comment (mypy_primer)
|
||||
|
||||
on: # zizmor: ignore[dangerous-triggers]
|
||||
workflow_run:
|
||||
workflows: [Run mypy_primer]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
workflow_run_id:
|
||||
description: The mypy_primer 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 mypy_primer results"
|
||||
id: download-mypy_primer_diff
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
with:
|
||||
name: mypy_primer_diff
|
||||
workflow: mypy_primer.yaml
|
||||
pr: ${{ steps.pr-number.outputs.pr-number }}
|
||||
path: pr/mypy_primer_diff
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: "Download mypy_primer memory results"
|
||||
id: download-mypy_primer_memory_diff
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
with:
|
||||
name: mypy_primer_memory_diff
|
||||
workflow: mypy_primer.yaml
|
||||
pr: ${{ steps.pr-number.outputs.pr-number }}
|
||||
path: pr/mypy_primer_memory_diff
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Generate comment content
|
||||
id: generate-comment
|
||||
if: ${{ steps.download-mypy_primer_diff.outputs.found_artifact == 'true' && steps.download-mypy_primer_memory_diff.outputs.found_artifact == 'true' }}
|
||||
run: |
|
||||
# Guard against malicious mypy_primer results that symlink to a secret
|
||||
# file on this runner
|
||||
if [[ -L pr/mypy_primer_diff/mypy_primer.diff ]] || [[ -L pr/mypy_primer_memory_diff/mypy_primer_memory.diff ]]
|
||||
then
|
||||
echo "Error: mypy_primer.diff and mypy_primer_memory.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 mypy_primer -->' >> comment.txt
|
||||
|
||||
echo '## `mypy_primer` results' >> comment.txt
|
||||
if [ -s "pr/mypy_primer_diff/mypy_primer.diff" ]; then
|
||||
echo '<details>' >> comment.txt
|
||||
echo '<summary>Changes were detected when running on open source projects</summary>' >> comment.txt
|
||||
echo '' >> comment.txt
|
||||
echo '```diff' >> comment.txt
|
||||
cat pr/mypy_primer_diff/mypy_primer.diff >> comment.txt
|
||||
echo '```' >> comment.txt
|
||||
echo '</details>' >> comment.txt
|
||||
else
|
||||
echo 'No ecosystem changes detected ✅' >> comment.txt
|
||||
fi
|
||||
|
||||
if [ -s "pr/mypy_primer_memory_diff/mypy_primer_memory.diff" ]; then
|
||||
echo '<details>' >> comment.txt
|
||||
echo '<summary>Memory usage changes were detected when running on open source projects</summary>' >> comment.txt
|
||||
echo '' >> comment.txt
|
||||
echo '```diff' >> comment.txt
|
||||
cat pr/mypy_primer_memory_diff/mypy_primer_memory.diff >> comment.txt
|
||||
echo '```' >> comment.txt
|
||||
echo '</details>' >> comment.txt
|
||||
else
|
||||
echo 'No memory usage changes detected ✅' >> 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 mypy_primer -->"
|
||||
|
||||
- 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
|
||||
88
.github/workflows/pr-comment.yaml
vendored
88
.github/workflows/pr-comment.yaml
vendored
@@ -1,88 +0,0 @@
|
||||
name: Ecosystem check comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [CI]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
workflow_run_id:
|
||||
description: The ecosystem workflow that triggers the workflow run
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
|
||||
name: Download pull request 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 ecosystem results"
|
||||
id: download-ecosystem-result
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
with:
|
||||
name: ecosystem-result
|
||||
workflow: ci.yaml
|
||||
pr: ${{ steps.pr-number.outputs.pr-number }}
|
||||
path: pr/ecosystem
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Generate comment content
|
||||
id: generate-comment
|
||||
if: steps.download-ecosystem-result.outputs.found_artifact == 'true'
|
||||
run: |
|
||||
# Guard against malicious ecosystem results that symlink to a secret
|
||||
# file on this runner
|
||||
if [[ -L pr/ecosystem/ecosystem-result ]]
|
||||
then
|
||||
echo "Error: ecosystem-result cannot be a symlink"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Note this identifier is used to find the comment to update on
|
||||
# subsequent runs
|
||||
echo '<!-- generated-comment ecosystem -->' >> comment.txt
|
||||
|
||||
echo '## `ruff-ecosystem` results' >> comment.txt
|
||||
cat pr/ecosystem/ecosystem-result >> comment.txt
|
||||
echo "" >> comment.txt
|
||||
|
||||
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 ecosystem -->"
|
||||
|
||||
- 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
|
||||
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
pattern: wheels-*
|
||||
|
||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -68,7 +68,7 @@ jobs:
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.2/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
|
||||
with:
|
||||
@@ -166,8 +166,8 @@ jobs:
|
||||
- custom-build-binaries
|
||||
- custom-build-docker
|
||||
- build-global-artifacts
|
||||
# Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
|
||||
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
|
||||
15
.github/workflows/sync_typeshed.yaml
vendored
15
.github/workflows/sync_typeshed.yaml
vendored
@@ -77,7 +77,7 @@ jobs:
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
git config --global user.email '<>'
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: Sync typeshed stubs
|
||||
run: |
|
||||
rm -rf "ruff/${VENDORED_TYPESHED}"
|
||||
@@ -131,7 +131,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: true
|
||||
ref: ${{ env.UPSTREAM_BRANCH}}
|
||||
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
- uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name typeshedbot
|
||||
@@ -207,17 +207,22 @@ jobs:
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
|
||||
uses: taiki-e/install-action@537c30d2b45cc3aa3fb35e2bbcfb61ef93fd6f02 # v2.62.52
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Update snapshots
|
||||
if: ${{ success() }}
|
||||
run: |
|
||||
cargo r \
|
||||
--profile=profiling \
|
||||
-p ty_completion_eval \
|
||||
-- all --tasks ./crates/ty_completion_eval/completion-evaluation-tasks.csv
|
||||
|
||||
# The `cargo insta` docs indicate that `--unreferenced=delete` might be a good option,
|
||||
# but from local testing it appears to just revert all changes made by `cargo insta test --accept`.
|
||||
#
|
||||
|
||||
14
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
14
.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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e5c5f5b2d762af91b28490537fe0077334165693"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--repository ruff \
|
||||
@@ -112,8 +112,6 @@ jobs:
|
||||
|
||||
cat diff-statistics.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
id: deploy
|
||||
@@ -131,18 +129,14 @@ jobs:
|
||||
echo >> comment.md
|
||||
echo "**[Full report with detailed diff]($DEPLOYMENT_URL/diff)** ([timing results]($DEPLOYMENT_URL/timing))" >> comment.md
|
||||
|
||||
# NOTE: astral-sh-bot uses this artifact to post comments on PRs.
|
||||
# Make sure to update the bot if you rename the artifact.
|
||||
- name: Upload comment
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: comment.md
|
||||
path: comment.md
|
||||
|
||||
- name: Upload pr-number
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: pr-number
|
||||
path: pr-number
|
||||
|
||||
- name: Upload diagnostics diff
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
name: PR comment (ty ecosystem-analyzer)
|
||||
|
||||
on: # zizmor: ignore[dangerous-triggers]
|
||||
workflow_run:
|
||||
workflows: [ty ecosystem-analyzer]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
workflow_run_id:
|
||||
description: The ty ecosystem-analyzer 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 comment.md"
|
||||
id: download-comment
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
with:
|
||||
name: comment.md
|
||||
workflow: ty-ecosystem-analyzer.yaml
|
||||
pr: ${{ steps.pr-number.outputs.pr-number }}
|
||||
path: pr/comment
|
||||
workflow_conclusion: completed
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- name: Generate comment content
|
||||
id: generate-comment
|
||||
if: ${{ steps.download-comment.outputs.found_artifact == 'true' }}
|
||||
run: |
|
||||
# Guard against malicious ty ecosystem-analyzer results that symlink to a secret
|
||||
# file on this runner
|
||||
if [[ -L pr/comment/comment.md ]]
|
||||
then
|
||||
echo "Error: comment.md cannot be a symlink"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Note: this identifier is used to find the comment to update on subsequent runs
|
||||
echo '<!-- generated-comment ty ecosystem-analyzer -->' > comment.md
|
||||
echo >> comment.md
|
||||
cat pr/comment/comment.md >> comment.md
|
||||
|
||||
echo 'comment<<EOF' >> "$GITHUB_OUTPUT"
|
||||
cat comment.md >> "$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 ty ecosystem-analyzer -->"
|
||||
|
||||
- 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.md
|
||||
edit-mode: replace
|
||||
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@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
cd ..
|
||||
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
|
||||
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@e5c5f5b2d762af91b28490537fe0077334165693"
|
||||
|
||||
ecosystem-analyzer \
|
||||
--verbose \
|
||||
|
||||
11
.github/workflows/typing_conformance.yaml
vendored
11
.github/workflows/typing_conformance.yaml
vendored
@@ -94,21 +94,18 @@ jobs:
|
||||
touch typing_conformance_diagnostics.diff
|
||||
fi
|
||||
|
||||
echo ${{ github.event.number }} > pr-number
|
||||
echo "${CONFORMANCE_SUITE_COMMIT}" > conformance-suite-commit
|
||||
|
||||
# NOTE: astral-sh-bot uses this artifact to post comments on PRs.
|
||||
# Make sure to update the bot if you rename the artifact.
|
||||
- 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
|
||||
|
||||
# NOTE: astral-sh-bot uses this artifact to post comments on PRs.
|
||||
# Make sure to update the bot if you rename the artifact.
|
||||
- name: Upload conformance suite commit
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
|
||||
112
.github/workflows/typing_conformance_comment.yaml
vendored
112
.github/workflows/typing_conformance_comment.yaml
vendored
@@ -1,112 +0,0 @@
|
||||
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 suite commit
|
||||
with:
|
||||
name: conformance-suite-commit
|
||||
run_id: ${{ github.event.workflow_run.id || github.event.inputs.workflow_run_id }}
|
||||
if_no_artifact_found: ignore
|
||||
allow_forks: true
|
||||
|
||||
- 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
|
||||
|
||||
if [[ -f conformance-suite-commit ]]
|
||||
then
|
||||
echo "## Diagnostic diff on [typing conformance tests](https://github.com/python/typing/tree/$(<conformance-suite-commit)/conformance)" >> comment.txt
|
||||
else
|
||||
echo "conformance-suite-commit file not found"
|
||||
echo "## Diagnostic diff on typing conformance tests" >> comment.txt
|
||||
fi
|
||||
|
||||
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
|
||||
3
.github/zizmor.yml
vendored
3
.github/zizmor.yml
vendored
@@ -3,9 +3,6 @@
|
||||
#
|
||||
# TODO: can we remove the ignores here so that our workflows are more secure?
|
||||
rules:
|
||||
dangerous-triggers:
|
||||
ignore:
|
||||
- pr-comment.yaml
|
||||
cache-poisoning:
|
||||
ignore:
|
||||
- build-docker.yml
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.5
|
||||
|
||||
Released on 2025-11-13.
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Apply `SIM113` when index variable is of type `int` ([#21395](https://github.com/astral-sh/ruff/pull/21395))
|
||||
- \[`pydoclint`\] Fix false positive when Sphinx directives follow a "Raises" section (`DOC502`) ([#20535](https://github.com/astral-sh/ruff/pull/20535))
|
||||
- \[`pydoclint`\] Support NumPy-style comma-separated parameters (`DOC102`) ([#20972](https://github.com/astral-sh/ruff/pull/20972))
|
||||
- \[`refurb`\] Auto-fix annotated assignments (`FURB101`) ([#21278](https://github.com/astral-sh/ruff/pull/21278))
|
||||
- \[`ruff`\] Ignore `str()` when not used for simple conversion (`RUF065`) ([#21330](https://github.com/astral-sh/ruff/pull/21330))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix syntax error false positive on alternative `match` patterns ([#21362](https://github.com/astral-sh/ruff/pull/21362))
|
||||
- \[`flake8-simplify`\] Fix false positive for iterable initializers with generator arguments (`SIM222`) ([#21187](https://github.com/astral-sh/ruff/pull/21187))
|
||||
- \[`pyupgrade`\] Fix false positive on relative imports from local `.builtins` module (`UP029`) ([#21309](https://github.com/astral-sh/ruff/pull/21309))
|
||||
- \[`pyupgrade`\] Consistently set the deprecated tag (`UP035`) ([#21396](https://github.com/astral-sh/ruff/pull/21396))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`refurb`\] Detect empty f-strings (`FURB105`) ([#21348](https://github.com/astral-sh/ruff/pull/21348))
|
||||
|
||||
### CLI
|
||||
|
||||
- Add option to provide a reason to `--add-noqa` ([#21294](https://github.com/astral-sh/ruff/pull/21294))
|
||||
- Add upstream linter URL to `ruff linter --output-format=json` ([#21316](https://github.com/astral-sh/ruff/pull/21316))
|
||||
- Add color to `--help` ([#21337](https://github.com/astral-sh/ruff/pull/21337))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add a new "Opening a PR" section to the contribution guide ([#21298](https://github.com/astral-sh/ruff/pull/21298))
|
||||
- Added the PyScripter IDE to the list of "Who is using Ruff?" ([#21402](https://github.com/astral-sh/ruff/pull/21402))
|
||||
- Update PyCharm setup instructions ([#21409](https://github.com/astral-sh/ruff/pull/21409))
|
||||
- \[`flake8-annotations`\] Add link to `allow-star-arg-any` option (`ANN401`) ([#21326](https://github.com/astral-sh/ruff/pull/21326))
|
||||
|
||||
### Other changes
|
||||
|
||||
- \[`configuration`\] Improve error message when `line-length` exceeds `u16::MAX` ([#21329](https://github.com/astral-sh/ruff/pull/21329))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@njhearp](https://github.com/njhearp)
|
||||
- [@11happy](https://github.com/11happy)
|
||||
- [@hugovk](https://github.com/hugovk)
|
||||
- [@Gankra](https://github.com/Gankra)
|
||||
- [@ntBre](https://github.com/ntBre)
|
||||
- [@pyscripter](https://github.com/pyscripter)
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@MichaReiser](https://github.com/MichaReiser)
|
||||
- [@henryiii](https://github.com/henryiii)
|
||||
- [@charliecloudberry](https://github.com/charliecloudberry)
|
||||
|
||||
## 0.14.4
|
||||
|
||||
Released on 2025-11-06.
|
||||
|
||||
@@ -280,6 +280,55 @@ Note that plugin-specific configuration options are defined in their own modules
|
||||
|
||||
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
|
||||
|
||||
### Opening a PR
|
||||
|
||||
After you finish your changes, the next step is to open a PR. By default, two
|
||||
sections will be filled into the PR body: the summary and the test plan.
|
||||
|
||||
#### The summary
|
||||
|
||||
The summary is intended to give us as maintainers information about your PR.
|
||||
This should typically include a link to the relevant issue(s) you're addressing
|
||||
in your PR, as well as a summary of the issue and your approach to fixing it. If
|
||||
you have any questions about your approach or design, or if you considered
|
||||
alternative approaches, that can also be helpful to include.
|
||||
|
||||
AI can be helpful in generating both the code and summary of your PR, but a
|
||||
successful contribution should still be carefully reviewed by you and the
|
||||
summary editorialized before submitting a PR. A great summary is thorough but
|
||||
also succinct and gives us the context we need to review your PR.
|
||||
|
||||
You can find examples of excellent issues and PRs by searching for the
|
||||
[`great writeup`](https://github.com/astral-sh/ruff/issues?q=label%3A%22great%20writeup%22)
|
||||
label.
|
||||
|
||||
#### The test plan
|
||||
|
||||
The test plan is likely to be shorter than the summary and can be as simple as
|
||||
"Added new snapshot tests for `RUF123`," at least for rule bugs. For LSP or some
|
||||
types of CLI changes, in particular, it can also be helpful to include
|
||||
screenshots or recordings of your change in action.
|
||||
|
||||
#### Ecosystem report
|
||||
|
||||
After opening the PR, an ecosystem report will be run as part of CI. This shows
|
||||
a diff of linter and formatter behavior before and after the changes in your PR.
|
||||
Going through these changes and reporting your findings in the PR summary or an
|
||||
additional comment help us to review your PR more efficiently. It's also a great
|
||||
way to find new test cases to incorporate into your PR if you identify any
|
||||
issues.
|
||||
|
||||
#### PR status
|
||||
|
||||
To help us know when your PR is ready for review again, please either move your
|
||||
PR back to a draft while working on it (marking it ready for review afterwards
|
||||
will ping the previous reviewers) or explicitly re-request a review. This helps
|
||||
us to avoid re-reviewing a PR while you're still working on it and also to
|
||||
prioritize PRs that are definitely ready for review.
|
||||
|
||||
You can also thumbs-up or mark as resolved any comments we leave to let us know
|
||||
you addressed them.
|
||||
|
||||
## MkDocs
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
81
Cargo.lock
generated
81
Cargo.lock
generated
@@ -1016,7 +1016,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1238,9 +1238,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size-derive2"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46b134aa084df7c3a513a1035c52f623e4b3065dfaf3d905a4f28a2e79b5bb3f"
|
||||
checksum = "ff47daa61505c85af126e9dd64af6a342a33dc0cccfe1be74ceadc7d352e6efd"
|
||||
dependencies = [
|
||||
"attribute-derive",
|
||||
"quote",
|
||||
@@ -1249,13 +1249,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "get-size2"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0d51c9f2e956a517619ad9e7eaebc7a573f9c49b38152e12eade750f89156f9"
|
||||
checksum = "ac7bb8710e1f09672102be7ddf39f764d8440ae74a9f4e30aaa4820dcdffa4af"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"get-size-derive2",
|
||||
"hashbrown 0.16.0",
|
||||
"indexmap",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
@@ -1575,9 +1576,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indicatif"
|
||||
version = "0.18.2"
|
||||
version = "0.18.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade6dfcba0dfb62ad59e59e7241ec8912af34fd29e0e743e3db992bd278e8b65"
|
||||
checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88"
|
||||
dependencies = [
|
||||
"console 0.16.1",
|
||||
"portable-atomic",
|
||||
@@ -1752,24 +1753,24 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
|
||||
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde",
|
||||
"windows-sys 0.59.0",
|
||||
"serde_core",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.15"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
|
||||
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1851,9 +1852,9 @@ checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "1.8.5"
|
||||
version = "1.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d56bcd52d9b5e5f43e7fba20eb1f423ccb18c84cdf1cb506b8c1b95776b0b49"
|
||||
checksum = "6aea7143e4a0ed59b87a1ee71e198500889f8b005311136be15e84c97a6fcd8d"
|
||||
dependencies = [
|
||||
"annotate-snippets",
|
||||
"libcst_derive",
|
||||
@@ -1866,9 +1867,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libcst_derive"
|
||||
version = "1.8.5"
|
||||
version = "1.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fcf5a725c4db703660124fe0edb98285f1605d0b87b7ee8684b699764a4f01a"
|
||||
checksum = "0903173ea316c34a44d0497161e04d9210af44f5f5e89bf2f55d9a254c9a0e8d"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -2606,9 +2607,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-junit"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ed1a693391a16317257103ad06a88c6529ac640846021da7c435a06fffdacd7"
|
||||
checksum = "6ee9342d671fae8d66b3ae9fd7a9714dfd089c04d2a8b1ec0436ef77aee15e5f"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"indexmap",
|
||||
@@ -2621,9 +2622,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.37.5"
|
||||
version = "0.38.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
|
||||
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2650,9 +2651,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -2858,7 +2859,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -3004,6 +3005,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar",
|
||||
"supports-hyperlinks",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
@@ -3115,7 +3117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3470,7 +3472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3568,7 +3570,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3586,7 +3588,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3610,12 +3612,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=05a9af7f554b64b8aadc2eeb6f2caf73d0408d09#05a9af7f554b64b8aadc2eeb6f2caf73d0408d09"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=a885bb4c4c192741b8a17418fef81a71e33d111e#a885bb4c4c192741b8a17418fef81a71e33d111e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3926,10 +3928,16 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.108"
|
||||
name = "supports-hyperlinks"
|
||||
version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
checksum = "804f44ed3c63152de6a9f90acbea1a110441de43006ea51bcce8f436196a288b"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.110"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3963,7 +3971,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4521,6 +4529,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"smallvec",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
@@ -5011,7 +5020,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "05a9af7f554b64b8aadc2eeb6f2caf73d0408d09", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a885bb4c4c192741b8a17418fef81a71e33d111e", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
@@ -173,6 +173,7 @@ snapbox = { version = "0.6.0", features = [
|
||||
static_assertions = "1.1.0"
|
||||
strum = { version = "0.27.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.27.0" }
|
||||
supports-hyperlinks = { version = "3.1.0" }
|
||||
syn = { version = "2.0.55" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
test-case = { version = "3.3.1" }
|
||||
|
||||
@@ -147,8 +147,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.14.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.4/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.14.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.5/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -181,7 +181,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.14.4
|
||||
rev: v0.14.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
@@ -491,6 +491,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- [PyTorch](https://github.com/pytorch/pytorch)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
- [Pylint](https://github.com/PyCQA/pylint)
|
||||
- [PyScripter](https://github.com/pyscripter/pyscripter)
|
||||
- [PyVista](https://github.com/pyvista/pyvista)
|
||||
- [Reflex](https://github.com/reflex-dev/reflex)
|
||||
- [River](https://github.com/online-ml/river)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,8 @@ use std::sync::Arc;
|
||||
|
||||
use crate::commands::completions::config::{OptionString, OptionStringParser};
|
||||
use anyhow::bail;
|
||||
use clap::builder::Styles;
|
||||
use clap::builder::styling::{AnsiColor, Effects};
|
||||
use clap::builder::{TypedValueParser, ValueParserFactory};
|
||||
use clap::{Parser, Subcommand, command};
|
||||
use colored::Colorize;
|
||||
@@ -78,6 +80,13 @@ impl GlobalConfigArgs {
|
||||
}
|
||||
}
|
||||
|
||||
// Configures Clap v3-style help menu colors
|
||||
const STYLES: Styles = Styles::styled()
|
||||
.header(AnsiColor::Green.on_default().effects(Effects::BOLD))
|
||||
.usage(AnsiColor::Green.on_default().effects(Effects::BOLD))
|
||||
.literal(AnsiColor::Cyan.on_default().effects(Effects::BOLD))
|
||||
.placeholder(AnsiColor::Cyan.on_default());
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
author,
|
||||
@@ -86,6 +95,7 @@ impl GlobalConfigArgs {
|
||||
after_help = "For help with a specific command, see: `ruff help <command>`."
|
||||
)]
|
||||
#[command(version)]
|
||||
#[command(styles = STYLES)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub(crate) command: Command,
|
||||
@@ -157,6 +167,7 @@ pub enum AnalyzeCommand {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, clap::Parser)]
|
||||
#[expect(clippy::struct_excessive_bools)]
|
||||
pub struct AnalyzeGraphCommand {
|
||||
/// List of files or directories to include.
|
||||
#[clap(help = "List of files or directories to include [default: .]")]
|
||||
@@ -183,6 +194,12 @@ pub struct AnalyzeGraphCommand {
|
||||
/// Path to a virtual environment to use for resolving additional dependencies
|
||||
#[arg(long)]
|
||||
python: Option<PathBuf>,
|
||||
/// Include imports that are only used for type checking (i.e., imports within `if TYPE_CHECKING:` blocks).
|
||||
/// Use `--no-type-checking-imports` to exclude imports that are only used for type checking.
|
||||
#[arg(long, overrides_with("no_type_checking_imports"))]
|
||||
type_checking_imports: bool,
|
||||
#[arg(long, overrides_with("type_checking_imports"), hide = true)]
|
||||
no_type_checking_imports: bool,
|
||||
}
|
||||
|
||||
// The `Parser` derive is for ruff_dev, for ruff `Args` would be sufficient
|
||||
@@ -405,8 +422,13 @@ pub struct CheckCommand {
|
||||
)]
|
||||
pub statistics: bool,
|
||||
/// Enable automatic additions of `noqa` directives to failing lines.
|
||||
/// Optionally provide a reason to append after the codes.
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "REASON",
|
||||
default_missing_value = "",
|
||||
num_args = 0..=1,
|
||||
require_equals = true,
|
||||
// conflicts_with = "add_noqa",
|
||||
conflicts_with = "show_files",
|
||||
conflicts_with = "show_settings",
|
||||
@@ -418,7 +440,7 @@ pub struct CheckCommand {
|
||||
conflicts_with = "fix",
|
||||
conflicts_with = "diff",
|
||||
)]
|
||||
pub add_noqa: bool,
|
||||
pub add_noqa: Option<String>,
|
||||
/// See the files Ruff will be run against with the current settings.
|
||||
#[arg(
|
||||
long,
|
||||
@@ -824,6 +846,10 @@ impl AnalyzeGraphCommand {
|
||||
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),
|
||||
type_checking_imports: resolve_bool_arg(
|
||||
self.type_checking_imports,
|
||||
self.no_type_checking_imports,
|
||||
),
|
||||
..ExplicitConfigOverrides::default()
|
||||
};
|
||||
|
||||
@@ -1047,7 +1073,7 @@ Possible choices:
|
||||
/// etc.).
|
||||
#[expect(clippy::struct_excessive_bools)]
|
||||
pub struct CheckArguments {
|
||||
pub add_noqa: bool,
|
||||
pub add_noqa: Option<String>,
|
||||
pub diff: bool,
|
||||
pub exit_non_zero_on_fix: bool,
|
||||
pub exit_zero: bool,
|
||||
@@ -1320,6 +1346,7 @@ struct ExplicitConfigOverrides {
|
||||
extension: Option<Vec<ExtensionPair>>,
|
||||
detect_string_imports: Option<bool>,
|
||||
string_imports_min_dots: Option<usize>,
|
||||
type_checking_imports: Option<bool>,
|
||||
}
|
||||
|
||||
impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
@@ -1410,6 +1437,9 @@ impl ConfigurationTransformer for ExplicitConfigOverrides {
|
||||
if let Some(string_imports_min_dots) = &self.string_imports_min_dots {
|
||||
config.analyze.string_imports_min_dots = Some(*string_imports_min_dots);
|
||||
}
|
||||
if let Some(type_checking_imports) = &self.type_checking_imports {
|
||||
config.analyze.type_checking_imports = Some(*type_checking_imports);
|
||||
}
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ pub(crate) fn add_noqa(
|
||||
files: &[PathBuf],
|
||||
pyproject_config: &PyprojectConfig,
|
||||
config_arguments: &ConfigArguments,
|
||||
reason: Option<&str>,
|
||||
) -> Result<usize> {
|
||||
// Collect all the files to check.
|
||||
let start = Instant::now();
|
||||
@@ -76,7 +77,14 @@ pub(crate) fn add_noqa(
|
||||
return None;
|
||||
}
|
||||
};
|
||||
match add_noqa_to_path(path, package, &source_kind, source_type, &settings.linter) {
|
||||
match add_noqa_to_path(
|
||||
path,
|
||||
package,
|
||||
&source_kind,
|
||||
source_type,
|
||||
&settings.linter,
|
||||
reason,
|
||||
) {
|
||||
Ok(count) => Some(count),
|
||||
Err(e) => {
|
||||
error!("Failed to add noqa to {}: {e}", path.display());
|
||||
|
||||
@@ -105,6 +105,7 @@ pub(crate) fn analyze_graph(
|
||||
let settings = resolver.resolve(path);
|
||||
let string_imports = settings.analyze.string_imports;
|
||||
let include_dependencies = settings.analyze.include_dependencies.get(path).cloned();
|
||||
let type_checking_imports = settings.analyze.type_checking_imports;
|
||||
|
||||
// Skip excluded files.
|
||||
if (settings.file_resolver.force_exclude || !resolved_file.is_root())
|
||||
@@ -167,6 +168,7 @@ pub(crate) fn analyze_graph(
|
||||
&path,
|
||||
package.as_deref(),
|
||||
string_imports,
|
||||
type_checking_imports,
|
||||
)
|
||||
.unwrap_or_else(|err| {
|
||||
warn!("Failed to generate import map for {path}: {err}");
|
||||
|
||||
@@ -16,6 +16,8 @@ struct LinterInfo {
|
||||
prefix: &'static str,
|
||||
name: &'static str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
url: Option<&'static str>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
categories: Option<Vec<LinterCategoryInfo>>,
|
||||
}
|
||||
|
||||
@@ -50,6 +52,7 @@ pub(crate) fn linter(format: HelpFormat) -> Result<()> {
|
||||
.map(|linter_info| LinterInfo {
|
||||
prefix: linter_info.common_prefix(),
|
||||
name: linter_info.name(),
|
||||
url: linter_info.url(),
|
||||
categories: linter_info.upstream_categories().map(|cats| {
|
||||
cats.iter()
|
||||
.map(|c| LinterCategoryInfo {
|
||||
|
||||
@@ -319,12 +319,20 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
|
||||
warn_user!("Detected debug build without --no-cache.");
|
||||
}
|
||||
|
||||
if cli.add_noqa {
|
||||
if let Some(reason) = &cli.add_noqa {
|
||||
if !fix_mode.is_generate() {
|
||||
warn_user!("--fix is incompatible with --add-noqa.");
|
||||
}
|
||||
if reason.contains(['\n', '\r']) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"--add-noqa <reason> cannot contain newline characters"
|
||||
));
|
||||
}
|
||||
|
||||
let reason_opt = (!reason.is_empty()).then_some(reason.as_str());
|
||||
|
||||
let modifications =
|
||||
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments)?;
|
||||
commands::add_noqa::add_noqa(&files, &pyproject_config, &config_arguments, reason_opt)?;
|
||||
if modifications > 0 && config_arguments.log_level >= LogLevel::Default {
|
||||
let s = if modifications == 1 { "" } else { "s" };
|
||||
#[expect(clippy::print_stderr)]
|
||||
|
||||
193
crates/ruff/tests/cli/analyze_graph.rs
Normal file
193
crates/ruff/tests/cli/analyze_graph.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use std::process::Command;
|
||||
|
||||
use insta_cmd::assert_cmd_snapshot;
|
||||
|
||||
use crate::CliTest;
|
||||
|
||||
#[test]
|
||||
fn type_checking_imports() -> anyhow::Result<()> {
|
||||
let test = AnalyzeTest::with_files([
|
||||
("ruff/__init__.py", ""),
|
||||
(
|
||||
"ruff/a.py",
|
||||
r#"
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ruff.b
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ruff.c
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"ruff/b.py",
|
||||
r#"
|
||||
if TYPE_CHECKING:
|
||||
from ruff import c
|
||||
"#,
|
||||
),
|
||||
("ruff/c.py", ""),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(test.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py",
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
test.command()
|
||||
.arg("--no-type-checking-imports"),
|
||||
@r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_checking_imports_from_config() -> anyhow::Result<()> {
|
||||
let test = AnalyzeTest::with_files([
|
||||
("ruff/__init__.py", ""),
|
||||
(
|
||||
"ruff/a.py",
|
||||
r#"
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import ruff.b
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import ruff.c
|
||||
"#,
|
||||
),
|
||||
(
|
||||
"ruff/b.py",
|
||||
r#"
|
||||
if TYPE_CHECKING:
|
||||
from ruff import c
|
||||
"#,
|
||||
),
|
||||
("ruff/c.py", ""),
|
||||
(
|
||||
"ruff.toml",
|
||||
r#"
|
||||
[analyze]
|
||||
type-checking-imports = false
|
||||
"#,
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(test.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py"
|
||||
],
|
||||
"ruff/b.py": [],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
test.write_file(
|
||||
"ruff.toml",
|
||||
r#"
|
||||
[analyze]
|
||||
type-checking-imports = true
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(test.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"ruff/__init__.py": [],
|
||||
"ruff/a.py": [
|
||||
"ruff/b.py",
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/b.py": [
|
||||
"ruff/c.py"
|
||||
],
|
||||
"ruff/c.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct AnalyzeTest {
|
||||
cli_test: CliTest,
|
||||
}
|
||||
|
||||
impl AnalyzeTest {
|
||||
pub(crate) fn new() -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
cli_test: CliTest::with_settings(|_, mut settings| {
|
||||
settings.add_filter(r#"\\\\"#, "/");
|
||||
settings
|
||||
})?,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_files<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
case.write_files(files)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
#[expect(unused)]
|
||||
fn with_file(path: impl AsRef<std::path::Path>, content: &str) -> anyhow::Result<Self> {
|
||||
let fixture = Self::new()?;
|
||||
fixture.write_file(path, content)?;
|
||||
Ok(fixture)
|
||||
}
|
||||
|
||||
fn command(&self) -> Command {
|
||||
let mut command = self.cli_test.command();
|
||||
command.arg("analyze").arg("graph").arg("--preview");
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AnalyzeTest {
|
||||
type Target = CliTest;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.cli_test
|
||||
}
|
||||
}
|
||||
@@ -1760,6 +1760,64 @@ from foo import ( # noqa: F401
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_noqa_with_reason() -> Result<()> {
|
||||
let fixture = CliTest::new()?;
|
||||
fixture.write_file(
|
||||
"test.py",
|
||||
r#"import os
|
||||
|
||||
def foo():
|
||||
x = 1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(fixture
|
||||
.check_command()
|
||||
.arg("--add-noqa=TODO: fix")
|
||||
.arg("--select=F401,F841")
|
||||
.arg("test.py"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
Added 2 noqa directives.
|
||||
");
|
||||
|
||||
let content = fs::read_to_string(fixture.root().join("test.py"))?;
|
||||
insta::assert_snapshot!(content, @r"
|
||||
import os # noqa: F401 TODO: fix
|
||||
|
||||
def foo():
|
||||
x = 1 # noqa: F841 TODO: fix
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_noqa_with_newline_in_reason() -> Result<()> {
|
||||
let fixture = CliTest::new()?;
|
||||
fixture.write_file("test.py", "import os\n")?;
|
||||
|
||||
assert_cmd_snapshot!(fixture
|
||||
.check_command()
|
||||
.arg("--add-noqa=line1\nline2")
|
||||
.arg("--select=F401")
|
||||
.arg("test.py"), @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: --add-noqa <reason> cannot contain newline characters
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Infer `3.11` from `requires-python` in `pyproject.toml`.
|
||||
#[test]
|
||||
fn requires_python() -> Result<()> {
|
||||
|
||||
@@ -15,6 +15,7 @@ use std::{
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
mod analyze_graph;
|
||||
mod format;
|
||||
mod lint;
|
||||
|
||||
@@ -62,9 +63,7 @@ impl CliTest {
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> anyhow::Result<Self> {
|
||||
let case = Self::new()?;
|
||||
for file in files {
|
||||
case.write_file(file.0, file.1)?;
|
||||
}
|
||||
case.write_files(files)?;
|
||||
Ok(case)
|
||||
}
|
||||
|
||||
@@ -153,6 +152,16 @@ impl CliTest {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_files<'a>(
|
||||
&self,
|
||||
files: impl IntoIterator<Item = (&'a str, &'a str)>,
|
||||
) -> Result<()> {
|
||||
for file in files {
|
||||
self.write_file(file.0, file.1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the path to the test directory root.
|
||||
pub(crate) fn root(&self) -> &Path {
|
||||
&self.project_dir
|
||||
|
||||
@@ -9,7 +9,6 @@ info:
|
||||
- concise
|
||||
- "--show-settings"
|
||||
- test.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -284,5 +283,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -12,7 +12,6 @@ info:
|
||||
- UP007
|
||||
- test.py
|
||||
- "-"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -286,5 +285,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -13,7 +13,6 @@ info:
|
||||
- UP007
|
||||
- test.py
|
||||
- "-"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -288,5 +287,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -14,7 +14,6 @@ info:
|
||||
- py310
|
||||
- test.py
|
||||
- "-"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -288,5 +287,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -11,7 +11,6 @@ info:
|
||||
- "--select"
|
||||
- UP007
|
||||
- foo/test.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -285,5 +284,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -11,7 +11,6 @@ info:
|
||||
- "--select"
|
||||
- UP007
|
||||
- foo/test.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -285,5 +284,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -283,5 +283,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -283,5 +283,6 @@ analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -9,7 +9,6 @@ info:
|
||||
- concise
|
||||
- test.py
|
||||
- "--show-settings"
|
||||
snapshot_kind: text
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
@@ -284,5 +283,6 @@ analyze.target_version = 3.11
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -396,5 +396,6 @@ analyze.target_version = 3.7
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
//! styling.
|
||||
//!
|
||||
//! The above snippet has been built out of the following structure:
|
||||
use crate::snippet;
|
||||
use crate::{Id, snippet};
|
||||
use std::cmp::{Reverse, max, min};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
@@ -189,6 +189,7 @@ impl DisplaySet<'_> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn format_annotation(
|
||||
&self,
|
||||
line_offset: usize,
|
||||
@@ -199,11 +200,13 @@ impl DisplaySet<'_> {
|
||||
) -> fmt::Result {
|
||||
let hide_severity = annotation.annotation_type.is_none();
|
||||
let color = get_annotation_style(&annotation.annotation_type, stylesheet);
|
||||
|
||||
let formatted_len = if let Some(id) = &annotation.id {
|
||||
let id_len = id.id.len();
|
||||
if hide_severity {
|
||||
id.len()
|
||||
id_len
|
||||
} else {
|
||||
2 + id.len() + annotation_type_len(&annotation.annotation_type)
|
||||
2 + id_len + annotation_type_len(&annotation.annotation_type)
|
||||
}
|
||||
} else {
|
||||
annotation_type_len(&annotation.annotation_type)
|
||||
@@ -256,9 +259,20 @@ impl DisplaySet<'_> {
|
||||
let annotation_type = annotation_type_str(&annotation.annotation_type);
|
||||
if let Some(id) = annotation.id {
|
||||
if hide_severity {
|
||||
buffer.append(line_offset, &format!("{id} "), *stylesheet.error());
|
||||
buffer.append(
|
||||
line_offset,
|
||||
&format!("{id} ", id = fmt_with_hyperlink(id.id, id.url, stylesheet)),
|
||||
*stylesheet.error(),
|
||||
);
|
||||
} else {
|
||||
buffer.append(line_offset, &format!("{annotation_type}[{id}]"), *color);
|
||||
buffer.append(
|
||||
line_offset,
|
||||
&format!(
|
||||
"{annotation_type}[{id}]",
|
||||
id = fmt_with_hyperlink(id.id, id.url, stylesheet)
|
||||
),
|
||||
*color,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
buffer.append(line_offset, annotation_type, *color);
|
||||
@@ -707,7 +721,7 @@ impl DisplaySet<'_> {
|
||||
let style =
|
||||
get_annotation_style(&annotation.annotation_type, stylesheet);
|
||||
let mut formatted_len = if let Some(id) = &annotation.annotation.id {
|
||||
2 + id.len()
|
||||
2 + id.id.len()
|
||||
+ annotation_type_len(&annotation.annotation.annotation_type)
|
||||
} else {
|
||||
annotation_type_len(&annotation.annotation.annotation_type)
|
||||
@@ -724,7 +738,10 @@ impl DisplaySet<'_> {
|
||||
} else if formatted_len != 0 {
|
||||
formatted_len += 2;
|
||||
let id = match &annotation.annotation.id {
|
||||
Some(id) => format!("[{id}]"),
|
||||
Some(id) => format!(
|
||||
"[{id}]",
|
||||
id = fmt_with_hyperlink(&id.id, id.url, stylesheet)
|
||||
),
|
||||
None => String::new(),
|
||||
};
|
||||
buffer.puts(
|
||||
@@ -827,7 +844,7 @@ impl DisplaySet<'_> {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Annotation<'a> {
|
||||
pub(crate) annotation_type: DisplayAnnotationType,
|
||||
pub(crate) id: Option<&'a str>,
|
||||
pub(crate) id: Option<Id<'a>>,
|
||||
pub(crate) label: Vec<DisplayTextFragment<'a>>,
|
||||
pub(crate) is_fixable: bool,
|
||||
}
|
||||
@@ -1140,7 +1157,7 @@ fn format_message<'m>(
|
||||
|
||||
fn format_title<'a>(
|
||||
level: crate::Level,
|
||||
id: Option<&'a str>,
|
||||
id: Option<Id<'a>>,
|
||||
label: &'a str,
|
||||
is_fixable: bool,
|
||||
) -> DisplayLine<'a> {
|
||||
@@ -1158,7 +1175,7 @@ fn format_title<'a>(
|
||||
|
||||
fn format_footer<'a>(
|
||||
level: crate::Level,
|
||||
id: Option<&'a str>,
|
||||
id: Option<Id<'a>>,
|
||||
label: &'a str,
|
||||
) -> Vec<DisplayLine<'a>> {
|
||||
let mut result = vec![];
|
||||
@@ -1706,6 +1723,7 @@ fn format_body<'m>(
|
||||
annotation: Annotation {
|
||||
annotation_type,
|
||||
id: None,
|
||||
|
||||
label: format_label(annotation.label, None),
|
||||
is_fixable: false,
|
||||
},
|
||||
@@ -1887,3 +1905,40 @@ fn char_width(c: char) -> Option<usize> {
|
||||
unicode_width::UnicodeWidthChar::width(c)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn fmt_with_hyperlink<'a, T>(
|
||||
content: T,
|
||||
url: Option<&'a str>,
|
||||
stylesheet: &Stylesheet,
|
||||
) -> impl std::fmt::Display + 'a
|
||||
where
|
||||
T: std::fmt::Display + 'a,
|
||||
{
|
||||
struct FmtHyperlink<'a, T> {
|
||||
content: T,
|
||||
url: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(url) = self.url {
|
||||
write!(f, "\x1B]8;;{url}\x1B\\")?;
|
||||
}
|
||||
|
||||
self.content.fmt(f)?;
|
||||
|
||||
if self.url.is_some() {
|
||||
f.write_str("\x1B]8;;\x1B\\")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let url = if stylesheet.hyperlink { url } else { None };
|
||||
|
||||
FmtHyperlink { content, url }
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ impl Renderer {
|
||||
}
|
||||
.effects(Effects::BOLD),
|
||||
none: Style::new(),
|
||||
hyperlink: true,
|
||||
},
|
||||
..Self::plain()
|
||||
}
|
||||
@@ -154,6 +155,11 @@ impl Renderer {
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn hyperlink(mut self, hyperlink: bool) -> Self {
|
||||
self.stylesheet.hyperlink = hyperlink;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the string used for when a long line is cut.
|
||||
///
|
||||
/// The default is `...` (three `U+002E` characters).
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(crate) struct Stylesheet {
|
||||
pub(crate) line_no: Style,
|
||||
pub(crate) emphasis: Style,
|
||||
pub(crate) none: Style,
|
||||
pub(crate) hyperlink: bool,
|
||||
}
|
||||
|
||||
impl Default for Stylesheet {
|
||||
@@ -29,6 +30,7 @@ impl Stylesheet {
|
||||
line_no: Style::new(),
|
||||
emphasis: Style::new(),
|
||||
none: Style::new(),
|
||||
hyperlink: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,13 +12,19 @@
|
||||
|
||||
use std::ops::Range;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
||||
pub(crate) struct Id<'a> {
|
||||
pub(crate) id: &'a str,
|
||||
pub(crate) url: Option<&'a str>,
|
||||
}
|
||||
|
||||
/// Primary structure provided for formatting
|
||||
///
|
||||
/// See [`Level::title`] to create a [`Message`]
|
||||
#[derive(Debug)]
|
||||
pub struct Message<'a> {
|
||||
pub(crate) level: Level,
|
||||
pub(crate) id: Option<&'a str>,
|
||||
pub(crate) id: Option<Id<'a>>,
|
||||
pub(crate) title: &'a str,
|
||||
pub(crate) snippets: Vec<Snippet<'a>>,
|
||||
pub(crate) footer: Vec<Message<'a>>,
|
||||
@@ -28,7 +34,12 @@ pub struct Message<'a> {
|
||||
|
||||
impl<'a> Message<'a> {
|
||||
pub fn id(mut self, id: &'a str) -> Self {
|
||||
self.id = Some(id);
|
||||
self.id = Some(Id { id, url: None });
|
||||
self
|
||||
}
|
||||
|
||||
pub fn id_with_url(mut self, id: &'a str, url: Option<&'a str>) -> Self {
|
||||
self.id = Some(Id { id, url });
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
@@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY313,
|
||||
},
|
||||
110,
|
||||
120,
|
||||
);
|
||||
|
||||
bench_project(&benchmark, criterion);
|
||||
|
||||
@@ -71,16 +71,13 @@ impl Display for Benchmark<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_project(db: &ProjectDatabase, max_diagnostics: usize) {
|
||||
fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usize) {
|
||||
let result = db.check();
|
||||
let diagnostics = result.len();
|
||||
|
||||
assert!(
|
||||
diagnostics > 1 && diagnostics <= max_diagnostics,
|
||||
"Expected between {} and {} diagnostics but got {}",
|
||||
1,
|
||||
max_diagnostics,
|
||||
diagnostics
|
||||
"Expected between 1 and {max_diagnostics} diagnostics on project '{project_name}' but got {diagnostics}",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -184,7 +181,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY39,
|
||||
},
|
||||
1000,
|
||||
5000,
|
||||
);
|
||||
|
||||
static SYMPY: Benchmark = Benchmark::new(
|
||||
@@ -226,7 +223,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
800,
|
||||
900,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
@@ -234,11 +231,11 @@ fn run_single_threaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
bencher
|
||||
.with_inputs(|| benchmark.setup_iteration())
|
||||
.bench_local_refs(|db| {
|
||||
check_project(db, benchmark.max_diagnostics);
|
||||
check_project(db, benchmark.project.name, benchmark.max_diagnostics);
|
||||
});
|
||||
}
|
||||
|
||||
#[bench(args=[&ALTAIR, &FREQTRADE, &PYDANTIC, &TANJUN], sample_size=2, sample_count=3)]
|
||||
#[bench(args=[&ALTAIR, &FREQTRADE, &TANJUN], sample_size=2, sample_count=3)]
|
||||
fn small(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
@@ -248,12 +245,12 @@ fn medium(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
#[bench(args=[&SYMPY], sample_size=1, sample_count=2)]
|
||||
#[bench(args=[&SYMPY, &PYDANTIC], sample_size=1, sample_count=2)]
|
||||
fn large(bencher: Bencher, benchmark: &Benchmark) {
|
||||
run_single_threaded(bencher, benchmark);
|
||||
}
|
||||
|
||||
#[bench(args=[&PYDANTIC], sample_size=3, sample_count=8)]
|
||||
#[bench(args=[&ALTAIR], sample_size=3, sample_count=8)]
|
||||
fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
|
||||
|
||||
@@ -261,7 +258,7 @@ fn multithreaded(bencher: Bencher, benchmark: &Benchmark) {
|
||||
.with_inputs(|| benchmark.setup_iteration())
|
||||
.bench_local_values(|db| {
|
||||
thread_pool.install(|| {
|
||||
check_project(&db, benchmark.max_diagnostics);
|
||||
check_project(&db, benchmark.project.name, benchmark.max_diagnostics);
|
||||
db
|
||||
})
|
||||
});
|
||||
@@ -285,7 +282,7 @@ fn main() {
|
||||
// branch when looking up the ingredient index.
|
||||
{
|
||||
let db = TANJUN.setup_iteration();
|
||||
check_project(&db, TANJUN.max_diagnostics);
|
||||
check_project(&db, TANJUN.project.name, TANJUN.max_diagnostics);
|
||||
}
|
||||
|
||||
divan::main();
|
||||
|
||||
@@ -42,6 +42,7 @@ schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
similar = { workspace = true }
|
||||
supports-hyperlinks = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, optional = true }
|
||||
|
||||
@@ -64,6 +64,8 @@ impl Diagnostic {
|
||||
id,
|
||||
severity,
|
||||
message: message.into_diagnostic_message(),
|
||||
custom_concise_message: None,
|
||||
documentation_url: None,
|
||||
annotations: vec![],
|
||||
subs: vec![],
|
||||
fix: None,
|
||||
@@ -213,6 +215,10 @@ impl Diagnostic {
|
||||
/// cases, just converting it to a string (or printing it) will do what
|
||||
/// you want.
|
||||
pub fn concise_message(&self) -> ConciseMessage<'_> {
|
||||
if let Some(custom_message) = &self.inner.custom_concise_message {
|
||||
return ConciseMessage::Custom(custom_message.as_str());
|
||||
}
|
||||
|
||||
let main = self.inner.message.as_str();
|
||||
let annotation = self
|
||||
.primary_annotation()
|
||||
@@ -226,6 +232,15 @@ impl Diagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a custom message for the concise formatting of this diagnostic.
|
||||
///
|
||||
/// This overrides the default behavior of generating a concise message
|
||||
/// from the main diagnostic message and the primary annotation.
|
||||
pub fn set_concise_message(&mut self, message: impl IntoDiagnosticMessage) {
|
||||
Arc::make_mut(&mut self.inner).custom_concise_message =
|
||||
Some(message.into_diagnostic_message());
|
||||
}
|
||||
|
||||
/// Returns the severity of this diagnostic.
|
||||
///
|
||||
/// Note that this may be different than the severity of sub-diagnostics.
|
||||
@@ -356,6 +371,14 @@ impl Diagnostic {
|
||||
.is_some_and(|fix| fix.applies(config.fix_applicability))
|
||||
}
|
||||
|
||||
pub fn documentation_url(&self) -> Option<&str> {
|
||||
self.inner.documentation_url.as_deref()
|
||||
}
|
||||
|
||||
pub fn set_documentation_url(&mut self, url: Option<String>) {
|
||||
Arc::make_mut(&mut self.inner).documentation_url = url;
|
||||
}
|
||||
|
||||
/// Returns the offset of the parent statement for this diagnostic if it exists.
|
||||
///
|
||||
/// This is primarily used for checking noqa/secondary code suppressions.
|
||||
@@ -429,28 +452,6 @@ impl Diagnostic {
|
||||
.map(|sub| sub.inner.message.as_str())
|
||||
}
|
||||
|
||||
/// Returns the URL for the rule documentation, if it exists.
|
||||
pub fn to_ruff_url(&self) -> Option<String> {
|
||||
match self.id() {
|
||||
DiagnosticId::Panic
|
||||
| DiagnosticId::Io
|
||||
| DiagnosticId::InvalidSyntax
|
||||
| DiagnosticId::RevealedType
|
||||
| DiagnosticId::UnknownRule
|
||||
| DiagnosticId::InvalidGlob
|
||||
| DiagnosticId::EmptyInclude
|
||||
| DiagnosticId::UnnecessaryOverridesSection
|
||||
| DiagnosticId::UselessOverridesSection
|
||||
| DiagnosticId::DeprecatedSetting
|
||||
| DiagnosticId::Unformatted
|
||||
| DiagnosticId::InvalidCliOption
|
||||
| DiagnosticId::InternalError => None,
|
||||
DiagnosticId::Lint(lint_name) => {
|
||||
Some(format!("{}/rules/{lint_name}", env!("CARGO_PKG_HOMEPAGE")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the filename for the message.
|
||||
///
|
||||
/// Panics if the diagnostic has no primary span, or if its file is not a `SourceFile`.
|
||||
@@ -530,8 +531,10 @@ impl Diagnostic {
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
struct DiagnosticInner {
|
||||
id: DiagnosticId,
|
||||
documentation_url: Option<String>,
|
||||
severity: Severity,
|
||||
message: DiagnosticMessage,
|
||||
custom_concise_message: Option<DiagnosticMessage>,
|
||||
annotations: Vec<Annotation>,
|
||||
subs: Vec<SubDiagnostic>,
|
||||
fix: Option<Fix>,
|
||||
@@ -1520,6 +1523,8 @@ pub enum ConciseMessage<'a> {
|
||||
/// This indicates that the diagnostic is probably using the old
|
||||
/// model.
|
||||
Empty,
|
||||
/// A custom concise message has been provided.
|
||||
Custom(&'a str),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ConciseMessage<'_> {
|
||||
@@ -1535,6 +1540,9 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
||||
write!(f, "{main}: {annotation}")
|
||||
}
|
||||
ConciseMessage::Empty => Ok(()),
|
||||
ConciseMessage::Custom(message) => {
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ impl<'a> Resolved<'a> {
|
||||
struct ResolvedDiagnostic<'a> {
|
||||
level: AnnotateLevel,
|
||||
id: Option<String>,
|
||||
documentation_url: Option<String>,
|
||||
message: String,
|
||||
annotations: Vec<ResolvedAnnotation<'a>>,
|
||||
is_fixable: bool,
|
||||
@@ -240,12 +241,12 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
// `DisplaySet::format_annotation` for both cases, but this is a small hack to improve
|
||||
// the formatting of syntax errors for now. This should also be kept consistent with the
|
||||
// concise formatting.
|
||||
Some(diag.secondary_code().map_or_else(
|
||||
diag.secondary_code().map_or_else(
|
||||
|| format!("{id}:", id = diag.inner.id),
|
||||
|code| code.to_string(),
|
||||
))
|
||||
)
|
||||
} else {
|
||||
Some(diag.inner.id.to_string())
|
||||
diag.inner.id.to_string()
|
||||
};
|
||||
|
||||
let level = if config.hide_severity {
|
||||
@@ -256,7 +257,8 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
|
||||
ResolvedDiagnostic {
|
||||
level,
|
||||
id,
|
||||
id: Some(id),
|
||||
documentation_url: diag.documentation_url().map(ToString::to_string),
|
||||
message: diag.inner.message.as_str().to_string(),
|
||||
annotations,
|
||||
is_fixable: config.show_fix_status && diag.has_applicable_fix(config),
|
||||
@@ -287,6 +289,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
ResolvedDiagnostic {
|
||||
level: diag.inner.severity.to_annotate(),
|
||||
id: None,
|
||||
documentation_url: None,
|
||||
message: diag.inner.message.as_str().to_string(),
|
||||
annotations,
|
||||
is_fixable: false,
|
||||
@@ -385,6 +388,7 @@ impl<'a> ResolvedDiagnostic<'a> {
|
||||
RenderableDiagnostic {
|
||||
level: self.level,
|
||||
id: self.id.as_deref(),
|
||||
documentation_url: self.documentation_url.as_deref(),
|
||||
message: &self.message,
|
||||
snippets_by_input,
|
||||
is_fixable: self.is_fixable,
|
||||
@@ -485,6 +489,7 @@ struct RenderableDiagnostic<'r> {
|
||||
/// An ID is always present for top-level diagnostics and always absent for
|
||||
/// sub-diagnostics.
|
||||
id: Option<&'r str>,
|
||||
documentation_url: Option<&'r str>,
|
||||
/// The message emitted with the diagnostic, before any snippets are
|
||||
/// rendered.
|
||||
message: &'r str,
|
||||
@@ -519,7 +524,7 @@ impl RenderableDiagnostic<'_> {
|
||||
.is_fixable(self.is_fixable)
|
||||
.lineno_offset(self.header_offset);
|
||||
if let Some(id) = self.id {
|
||||
message = message.id(id);
|
||||
message = message.id_with_url(id, self.documentation_url);
|
||||
}
|
||||
message.snippets(snippets)
|
||||
}
|
||||
@@ -2876,6 +2881,12 @@ watermelon
|
||||
self.diag.help(message);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the documentation URL for the diagnostic.
|
||||
pub(super) fn documentation_url(mut self, url: impl Into<String>) -> DiagnosticBuilder<'e> {
|
||||
self.diag.set_documentation_url(Some(url.into()));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper builder for tersely populating a `SubDiagnostic`.
|
||||
@@ -2990,6 +3001,7 @@ def fibonacci(n):
|
||||
TextSize::from(10),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(7))
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
|
||||
.build(),
|
||||
env.builder(
|
||||
"unused-variable",
|
||||
@@ -3004,11 +3016,13 @@ def fibonacci(n):
|
||||
TextSize::from(99),
|
||||
)))
|
||||
.noqa_offset(TextSize::from(94))
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
|
||||
.build(),
|
||||
env.builder("undefined-name", Severity::Error, "Undefined name `a`")
|
||||
.primary("undef.py", "1:3", "1:4", "")
|
||||
.secondary_code("F821")
|
||||
.noqa_offset(TextSize::from(3))
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/undefined-name")
|
||||
.build(),
|
||||
];
|
||||
|
||||
@@ -3123,6 +3137,7 @@ if call(foo
|
||||
TextSize::from(19),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(16))
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
|
||||
.build(),
|
||||
env.builder(
|
||||
"unused-import",
|
||||
@@ -3137,6 +3152,7 @@ if call(foo
|
||||
TextSize::from(40),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(35))
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/unused-import")
|
||||
.build(),
|
||||
env.builder(
|
||||
"unused-variable",
|
||||
@@ -3151,6 +3167,7 @@ if call(foo
|
||||
TextSize::from(104),
|
||||
))))
|
||||
.noqa_offset(TextSize::from(98))
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/unused-variable")
|
||||
.build(),
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::diagnostic::{
|
||||
Diagnostic, DisplayDiagnosticConfig, Severity,
|
||||
stylesheet::{DiagnosticStylesheet, fmt_styled},
|
||||
stylesheet::{DiagnosticStylesheet, fmt_styled, fmt_with_hyperlink},
|
||||
};
|
||||
|
||||
use super::FileResolver;
|
||||
@@ -62,18 +62,29 @@ impl<'a> ConciseRenderer<'a> {
|
||||
}
|
||||
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)
|
||||
code = fmt_styled(
|
||||
fmt_with_hyperlink(&code, diag.documentation_url(), &stylesheet),
|
||||
stylesheet.secondary_code
|
||||
)
|
||||
)?;
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"{id}: ",
|
||||
id = fmt_styled(diag.inner.id.as_str(), stylesheet.secondary_code)
|
||||
id = fmt_styled(
|
||||
fmt_with_hyperlink(
|
||||
&diag.inner.id,
|
||||
diag.documentation_url(),
|
||||
&stylesheet
|
||||
),
|
||||
stylesheet.secondary_code
|
||||
)
|
||||
)?;
|
||||
}
|
||||
if self.config.show_fix_status {
|
||||
@@ -93,7 +104,10 @@ impl<'a> ConciseRenderer<'a> {
|
||||
f,
|
||||
"{severity}[{id}] ",
|
||||
severity = fmt_styled(severity, severity_style),
|
||||
id = fmt_styled(diag.id(), stylesheet.emphasis)
|
||||
id = fmt_styled(
|
||||
fmt_with_hyperlink(&diag.id(), diag.documentation_url(), &stylesheet),
|
||||
stylesheet.emphasis
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,8 @@ impl<'a> FullRenderer<'a> {
|
||||
.help(stylesheet.help)
|
||||
.line_no(stylesheet.line_no)
|
||||
.emphasis(stylesheet.emphasis)
|
||||
.none(stylesheet.none);
|
||||
.none(stylesheet.none)
|
||||
.hyperlink(stylesheet.hyperlink);
|
||||
|
||||
for diag in diagnostics {
|
||||
let resolved = Resolved::new(self.resolver, diag, self.config);
|
||||
@@ -112,16 +113,16 @@ impl std::fmt::Display for Diff<'_> {
|
||||
// `None`, indicating a regular script file, all the lines will be in one "cell" under the
|
||||
// `None` key.
|
||||
let cells = if let Some(notebook_index) = &self.notebook_index {
|
||||
let mut last_cell = OneIndexed::MIN;
|
||||
let mut last_cell_index = OneIndexed::MIN;
|
||||
let mut cells: Vec<(Option<OneIndexed>, TextSize)> = Vec::new();
|
||||
for (row, cell) in notebook_index.iter() {
|
||||
if cell != last_cell {
|
||||
let offset = source_code.line_start(row);
|
||||
cells.push((Some(last_cell), offset));
|
||||
last_cell = cell;
|
||||
for cell in notebook_index.iter() {
|
||||
if cell.cell_index() != last_cell_index {
|
||||
let offset = source_code.line_start(cell.start_row());
|
||||
cells.push((Some(last_cell_index), offset));
|
||||
last_cell_index = cell.cell_index();
|
||||
}
|
||||
}
|
||||
cells.push((Some(last_cell), source_text.text_len()));
|
||||
cells.push((Some(last_cell_index), source_text.text_len()));
|
||||
cells
|
||||
} else {
|
||||
vec![(None, source_text.text_len())]
|
||||
@@ -703,52 +704,7 @@ print()
|
||||
env.show_fix_status(true);
|
||||
env.fix_applicability(Applicability::DisplayOnly);
|
||||
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics), @r"
|
||||
error[unused-import][*]: `os` imported but unused
|
||||
--> notebook.ipynb:cell 1:2:8
|
||||
|
|
||||
1 | # cell 1
|
||||
2 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
::: cell 1
|
||||
1 | # cell 1
|
||||
- import os
|
||||
|
||||
error[unused-import][*]: `math` imported but unused
|
||||
--> notebook.ipynb:cell 2:2:8
|
||||
|
|
||||
1 | # cell 2
|
||||
2 | import math
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | print('hello world')
|
||||
|
|
||||
help: Remove unused import: `math`
|
||||
::: cell 2
|
||||
1 | # cell 2
|
||||
- import math
|
||||
2 |
|
||||
3 | print('hello world')
|
||||
|
||||
error[unused-variable][*]: Local variable `x` is assigned to but never used
|
||||
--> notebook.ipynb:cell 3:4:5
|
||||
|
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
4 | x = 1
|
||||
| ^
|
||||
|
|
||||
help: Remove assignment to unused variable `x`
|
||||
::: cell 3
|
||||
1 | # cell 3
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
- x = 1
|
||||
4 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
");
|
||||
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -768,31 +724,7 @@ print()
|
||||
}
|
||||
*fix = Fix::unsafe_edits(edits.remove(0), edits);
|
||||
|
||||
insta::assert_snapshot!(env.render(&diagnostic), @r"
|
||||
error[unused-import][*]: `os` imported but unused
|
||||
--> notebook.ipynb:cell 1:2:8
|
||||
|
|
||||
1 | # cell 1
|
||||
2 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
::: cell 1
|
||||
1 | # cell 1
|
||||
- import os
|
||||
::: cell 2
|
||||
1 | # cell 2
|
||||
- import math
|
||||
2 |
|
||||
3 | print('hello world')
|
||||
::: cell 3
|
||||
1 | # cell 3
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
- x = 1
|
||||
4 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
");
|
||||
insta::assert_snapshot!(env.render(&diagnostic));
|
||||
}
|
||||
|
||||
/// Carriage return (`\r`) is a valid line-ending in Python, so we should normalize this to a
|
||||
|
||||
@@ -100,7 +100,7 @@ pub(super) fn diagnostic_to_json<'a>(
|
||||
if config.preview {
|
||||
JsonDiagnostic {
|
||||
code: diagnostic.secondary_code_or_id(),
|
||||
url: diagnostic.to_ruff_url(),
|
||||
url: diagnostic.documentation_url(),
|
||||
message: diagnostic.body(),
|
||||
fix,
|
||||
cell: notebook_cell_index,
|
||||
@@ -112,7 +112,7 @@ pub(super) fn diagnostic_to_json<'a>(
|
||||
} else {
|
||||
JsonDiagnostic {
|
||||
code: diagnostic.secondary_code_or_id(),
|
||||
url: diagnostic.to_ruff_url(),
|
||||
url: diagnostic.documentation_url(),
|
||||
message: diagnostic.body(),
|
||||
fix,
|
||||
cell: notebook_cell_index,
|
||||
@@ -228,7 +228,7 @@ pub(crate) struct JsonDiagnostic<'a> {
|
||||
location: Option<JsonLocation>,
|
||||
message: &'a str,
|
||||
noqa_row: Option<OneIndexed>,
|
||||
url: Option<String>,
|
||||
url: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -294,7 +294,10 @@ mod tests {
|
||||
env.format(DiagnosticFormat::Json);
|
||||
env.preview(false);
|
||||
|
||||
let diag = env.err().build();
|
||||
let diag = env
|
||||
.err()
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
@@ -328,7 +331,10 @@ mod tests {
|
||||
env.format(DiagnosticFormat::Json);
|
||||
env.preview(true);
|
||||
|
||||
let diag = env.err().build();
|
||||
let diag = env
|
||||
.err()
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(
|
||||
env.render(&diag),
|
||||
|
||||
@@ -82,7 +82,7 @@ fn diagnostic_to_rdjson<'a>(
|
||||
value: diagnostic
|
||||
.secondary_code()
|
||||
.map_or_else(|| diagnostic.name(), |code| code.as_str()),
|
||||
url: diagnostic.to_ruff_url(),
|
||||
url: diagnostic.documentation_url(),
|
||||
},
|
||||
suggestions: rdjson_suggestions(
|
||||
edits,
|
||||
@@ -182,7 +182,7 @@ impl RdjsonRange {
|
||||
#[derive(Serialize)]
|
||||
struct RdjsonCode<'a> {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
url: Option<String>,
|
||||
url: Option<&'a str>,
|
||||
value: &'a str,
|
||||
}
|
||||
|
||||
@@ -217,7 +217,10 @@ mod tests {
|
||||
env.format(DiagnosticFormat::Rdjson);
|
||||
env.preview(false);
|
||||
|
||||
let diag = env.err().build();
|
||||
let diag = env
|
||||
.err()
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(env.render(&diag));
|
||||
}
|
||||
@@ -228,7 +231,10 @@ mod tests {
|
||||
env.format(DiagnosticFormat::Rdjson);
|
||||
env.preview(true);
|
||||
|
||||
let diag = env.err().build();
|
||||
let diag = env
|
||||
.err()
|
||||
.documentation_url("https://docs.astral.sh/ruff/rules/test-diagnostic")
|
||||
.build();
|
||||
|
||||
insta::assert_snapshot!(env.render(&diag));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
source: crates/ruff_db/src/diagnostic/render/full.rs
|
||||
expression: env.render_diagnostics(&diagnostics)
|
||||
---
|
||||
error[unused-import][*]: `os` imported but unused
|
||||
--> notebook.ipynb:cell 1:2:8
|
||||
|
|
||||
1 | # cell 1
|
||||
2 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
::: cell 1
|
||||
1 | # cell 1
|
||||
- import os
|
||||
|
||||
error[unused-import][*]: `math` imported but unused
|
||||
--> notebook.ipynb:cell 2:2:8
|
||||
|
|
||||
1 | # cell 2
|
||||
2 | import math
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | print('hello world')
|
||||
|
|
||||
help: Remove unused import: `math`
|
||||
::: cell 2
|
||||
1 | # cell 2
|
||||
- import math
|
||||
2 |
|
||||
3 | print('hello world')
|
||||
|
||||
error[unused-variable][*]: Local variable `x` is assigned to but never used
|
||||
--> notebook.ipynb:cell 3:4:5
|
||||
|
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
4 | x = 1
|
||||
| ^
|
||||
|
|
||||
help: Remove assignment to unused variable `x`
|
||||
::: cell 3
|
||||
1 | # cell 3
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
- x = 1
|
||||
4 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: crates/ruff_db/src/diagnostic/render/full.rs
|
||||
expression: env.render(&diagnostic)
|
||||
---
|
||||
error[unused-import][*]: `os` imported but unused
|
||||
--> notebook.ipynb:cell 1:2:8
|
||||
|
|
||||
1 | # cell 1
|
||||
2 | import os
|
||||
| ^^
|
||||
|
|
||||
help: Remove unused import: `os`
|
||||
::: cell 1
|
||||
1 | # cell 1
|
||||
- import os
|
||||
::: cell 2
|
||||
1 | # cell 2
|
||||
- import math
|
||||
2 |
|
||||
3 | print('hello world')
|
||||
::: cell 3
|
||||
1 | # cell 3
|
||||
2 | def foo():
|
||||
3 | print()
|
||||
- x = 1
|
||||
4 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
@@ -31,6 +31,43 @@ where
|
||||
FmtStyled { content, style }
|
||||
}
|
||||
|
||||
pub(super) fn fmt_with_hyperlink<'a, T>(
|
||||
content: T,
|
||||
url: Option<&'a str>,
|
||||
stylesheet: &DiagnosticStylesheet,
|
||||
) -> impl std::fmt::Display + 'a
|
||||
where
|
||||
T: std::fmt::Display + 'a,
|
||||
{
|
||||
struct FmtHyperlink<'a, T> {
|
||||
content: T,
|
||||
url: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Display for FmtHyperlink<'_, T>
|
||||
where
|
||||
T: std::fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(url) = self.url {
|
||||
write!(f, "\x1B]8;;{url}\x1B\\")?;
|
||||
}
|
||||
|
||||
self.content.fmt(f)?;
|
||||
|
||||
if self.url.is_some() {
|
||||
f.write_str("\x1B]8;;\x1B\\")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let url = if stylesheet.hyperlink { url } else { None };
|
||||
|
||||
FmtHyperlink { content, url }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DiagnosticStylesheet {
|
||||
pub(crate) error: Style,
|
||||
@@ -47,6 +84,7 @@ pub struct DiagnosticStylesheet {
|
||||
pub(crate) deletion: Style,
|
||||
pub(crate) insertion_line_no: Style,
|
||||
pub(crate) deletion_line_no: Style,
|
||||
pub(crate) hyperlink: bool,
|
||||
}
|
||||
|
||||
impl Default for DiagnosticStylesheet {
|
||||
@@ -59,6 +97,8 @@ impl DiagnosticStylesheet {
|
||||
/// Default terminal styling
|
||||
pub fn styled() -> Self {
|
||||
let bright_blue = AnsiColor::BrightBlue.on_default();
|
||||
|
||||
let hyperlink = supports_hyperlinks::supports_hyperlinks();
|
||||
Self {
|
||||
error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD),
|
||||
warning: AnsiColor::Yellow.on_default().effects(Effects::BOLD),
|
||||
@@ -74,6 +114,7 @@ impl DiagnosticStylesheet {
|
||||
deletion: AnsiColor::Red.on_default(),
|
||||
insertion_line_no: AnsiColor::Green.on_default().effects(Effects::BOLD),
|
||||
deletion_line_no: AnsiColor::Red.on_default().effects(Effects::BOLD),
|
||||
hyperlink,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +134,7 @@ impl DiagnosticStylesheet {
|
||||
deletion: Style::new(),
|
||||
insertion_line_no: Style::new(),
|
||||
deletion_line_no: Style::new(),
|
||||
hyperlink: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +475,12 @@ impl File {
|
||||
self.path(db).as_str().ends_with("__init__.pyi")
|
||||
}
|
||||
|
||||
/// Returns `true` if the file is an `__init__.pyi`
|
||||
pub fn is_package(self, db: &dyn Db) -> bool {
|
||||
let path = self.path(db).as_str();
|
||||
path.ends_with("__init__.pyi") || path.ends_with("__init__.py")
|
||||
}
|
||||
|
||||
pub fn source_type(self, db: &dyn Db) -> PySourceType {
|
||||
match self.path(db) {
|
||||
FilePath::System(path) => path
|
||||
|
||||
@@ -7,6 +7,7 @@ use ruff_source_file::LineIndex;
|
||||
|
||||
use crate::Db;
|
||||
use crate::files::{File, FilePath};
|
||||
use crate::system::System;
|
||||
|
||||
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
|
||||
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
|
||||
@@ -15,7 +16,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
let _span = tracing::trace_span!("source_text", file = %path).entered();
|
||||
let mut read_error = None;
|
||||
|
||||
let kind = if is_notebook(file.path(db)) {
|
||||
let kind = if is_notebook(db.system(), path) {
|
||||
file.read_to_notebook(db)
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::debug!("Failed to read notebook '{path}': {error}");
|
||||
@@ -40,18 +41,17 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_notebook(path: &FilePath) -> bool {
|
||||
match path {
|
||||
FilePath::System(system) => system.extension().is_some_and(|extension| {
|
||||
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
|
||||
}),
|
||||
FilePath::SystemVirtual(system_virtual) => {
|
||||
system_virtual.extension().is_some_and(|extension| {
|
||||
PySourceType::try_from_extension(extension) == Some(PySourceType::Ipynb)
|
||||
})
|
||||
}
|
||||
FilePath::Vendored(_) => false,
|
||||
}
|
||||
fn is_notebook(system: &dyn System, path: &FilePath) -> bool {
|
||||
let source_type = match path {
|
||||
FilePath::System(path) => system.source_type(path),
|
||||
FilePath::SystemVirtual(system_virtual) => system.virtual_path_source_type(system_virtual),
|
||||
FilePath::Vendored(_) => return false,
|
||||
};
|
||||
|
||||
let with_extension_fallback =
|
||||
source_type.or_else(|| PySourceType::try_from_extension(path.extension()?));
|
||||
|
||||
with_extension_fallback == Some(PySourceType::Ipynb)
|
||||
}
|
||||
|
||||
/// The source text of a file containing python code.
|
||||
|
||||
@@ -9,6 +9,7 @@ pub use os::OsSystem;
|
||||
|
||||
use filetime::FileTime;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -16,12 +17,11 @@ use std::{fmt, io};
|
||||
pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
|
||||
use walk_directory::WalkDirectoryBuilder;
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
|
||||
pub use self::path::{
|
||||
DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, SystemVirtualPath,
|
||||
SystemVirtualPathBuf, deduplicate_nested_paths,
|
||||
};
|
||||
use crate::file_revision::FileRevision;
|
||||
|
||||
mod memory_fs;
|
||||
#[cfg(feature = "os")]
|
||||
@@ -66,6 +66,35 @@ pub trait System: Debug + Sync + Send {
|
||||
/// See [dunce::canonicalize] for more information.
|
||||
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
|
||||
|
||||
/// Returns the source type for `path` if known or `None`.
|
||||
///
|
||||
/// The default is to always return `None`, assuming the system
|
||||
/// has no additional information and that the caller should
|
||||
/// rely on the file extension instead.
|
||||
///
|
||||
/// This is primarily used for the LSP integration to respect
|
||||
/// the chosen language (or the fact that it is a notebook) in
|
||||
/// the editor.
|
||||
fn source_type(&self, path: &SystemPath) -> Option<PySourceType> {
|
||||
let _ = path;
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns the source type for `path` if known or `None`.
|
||||
///
|
||||
/// The default is to always return `None`, assuming the system
|
||||
/// has no additional information and that the caller should
|
||||
/// rely on the file extension instead.
|
||||
///
|
||||
/// This is primarily used for the LSP integration to respect
|
||||
/// the chosen language (or the fact that it is a notebook) in
|
||||
/// the editor.
|
||||
fn virtual_path_source_type(&self, path: &SystemVirtualPath) -> Option<PySourceType> {
|
||||
let _ = path;
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Reads the content of the file at `path` into a [`String`].
|
||||
fn read_to_string(&self, path: &SystemPath) -> Result<String>;
|
||||
|
||||
|
||||
@@ -14,14 +14,21 @@ pub(crate) struct Collector<'a> {
|
||||
string_imports: StringImports,
|
||||
/// The collected imports from the Python AST.
|
||||
imports: Vec<CollectedImport>,
|
||||
/// Whether to detect type checking imports
|
||||
type_checking_imports: bool,
|
||||
}
|
||||
|
||||
impl<'a> Collector<'a> {
|
||||
pub(crate) fn new(module_path: Option<&'a [String]>, string_imports: StringImports) -> Self {
|
||||
pub(crate) fn new(
|
||||
module_path: Option<&'a [String]>,
|
||||
string_imports: StringImports,
|
||||
type_checking_imports: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
module_path,
|
||||
string_imports,
|
||||
imports: Vec::new(),
|
||||
type_checking_imports,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +98,25 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::If(ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
node_index: _,
|
||||
}) => {
|
||||
// Skip TYPE_CHECKING blocks if not requested
|
||||
if self.type_checking_imports || !is_type_checking_condition(test) {
|
||||
self.visit_body(body);
|
||||
}
|
||||
|
||||
for clause in elif_else_clauses {
|
||||
self.visit_elif_else_clause(clause);
|
||||
}
|
||||
}
|
||||
Stmt::FunctionDef(_)
|
||||
| Stmt::ClassDef(_)
|
||||
| Stmt::While(_)
|
||||
| Stmt::If(_)
|
||||
| Stmt::With(_)
|
||||
| Stmt::Match(_)
|
||||
| Stmt::Try(_)
|
||||
@@ -152,6 +174,30 @@ impl<'ast> SourceOrderVisitor<'ast> for Collector<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an expression is a `TYPE_CHECKING` condition.
|
||||
///
|
||||
/// Returns `true` for:
|
||||
/// - `TYPE_CHECKING`
|
||||
/// - `typing.TYPE_CHECKING`
|
||||
///
|
||||
/// NOTE: Aliased `TYPE_CHECKING`, i.e. `import typing.TYPE_CHECKING as TC; if TC: ...`
|
||||
/// will not be detected!
|
||||
fn is_type_checking_condition(expr: &Expr) -> bool {
|
||||
match expr {
|
||||
// `if TYPE_CHECKING:`
|
||||
Expr::Name(ast::ExprName { id, .. }) => id.as_str() == "TYPE_CHECKING",
|
||||
// `if typing.TYPE_CHECKING:`
|
||||
Expr::Attribute(ast::ExprAttribute { value, attr, .. }) => {
|
||||
attr.as_str() == "TYPE_CHECKING"
|
||||
&& matches!(
|
||||
value.as_ref(),
|
||||
Expr::Name(ast::ExprName { id, .. }) if id.as_str() == "typing"
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CollectedImport {
|
||||
/// The import was part of an `import` statement.
|
||||
|
||||
@@ -30,6 +30,7 @@ impl ModuleImports {
|
||||
path: &SystemPath,
|
||||
package: Option<&SystemPath>,
|
||||
string_imports: StringImports,
|
||||
type_checking_imports: bool,
|
||||
) -> Result<Self> {
|
||||
// Parse the source code.
|
||||
let parsed = parse(source, ParseOptions::from(source_type))?;
|
||||
@@ -38,8 +39,12 @@ impl ModuleImports {
|
||||
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));
|
||||
|
||||
// Collect the imports.
|
||||
let imports =
|
||||
Collector::new(module_path.as_deref(), string_imports).collect(parsed.syntax());
|
||||
let imports = Collector::new(
|
||||
module_path.as_deref(),
|
||||
string_imports,
|
||||
type_checking_imports,
|
||||
)
|
||||
.collect(parsed.syntax());
|
||||
|
||||
// Resolve the imports.
|
||||
let mut resolved_imports = ModuleImports::default();
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Default, Clone, CacheKey)]
|
||||
#[derive(Debug, Clone, CacheKey)]
|
||||
pub struct AnalyzeSettings {
|
||||
pub exclude: FilePatternSet,
|
||||
pub preview: PreviewMode,
|
||||
@@ -14,6 +14,21 @@ pub struct AnalyzeSettings {
|
||||
pub string_imports: StringImports,
|
||||
pub include_dependencies: BTreeMap<PathBuf, (PathBuf, Vec<String>)>,
|
||||
pub extension: ExtensionMapping,
|
||||
pub type_checking_imports: bool,
|
||||
}
|
||||
|
||||
impl Default for AnalyzeSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
exclude: FilePatternSet::default(),
|
||||
preview: PreviewMode::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
string_imports: StringImports::default(),
|
||||
include_dependencies: BTreeMap::default(),
|
||||
extension: ExtensionMapping::default(),
|
||||
type_checking_imports: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AnalyzeSettings {
|
||||
@@ -29,6 +44,7 @@ impl fmt::Display for AnalyzeSettings {
|
||||
self.string_imports,
|
||||
self.extension | debug,
|
||||
self.include_dependencies | debug,
|
||||
self.type_checking_imports,
|
||||
]
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.4"
|
||||
version = "0.14.5"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -4,3 +4,31 @@ CommunityData("public", mpModel=0) # S508
|
||||
CommunityData("public", mpModel=1) # S508
|
||||
|
||||
CommunityData("public", mpModel=2) # OK
|
||||
|
||||
# New API paths
|
||||
import pysnmp.hlapi.asyncio
|
||||
import pysnmp.hlapi.v1arch
|
||||
import pysnmp.hlapi.v1arch.asyncio
|
||||
import pysnmp.hlapi.v1arch.asyncio.auth
|
||||
import pysnmp.hlapi.v3arch
|
||||
import pysnmp.hlapi.v3arch.asyncio
|
||||
import pysnmp.hlapi.v3arch.asyncio.auth
|
||||
import pysnmp.hlapi.auth
|
||||
|
||||
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
||||
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
||||
pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
||||
|
||||
pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
|
||||
pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
|
||||
pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=2) # OK
|
||||
pysnmp.hlapi.v1arch.CommunityData("public", mpModel=2) # OK
|
||||
pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=2) # OK
|
||||
pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=2) # OK
|
||||
pysnmp.hlapi.v3arch.CommunityData("public", mpModel=2) # OK
|
||||
pysnmp.hlapi.auth.CommunityData("public", mpModel=2) # OK
|
||||
|
||||
@@ -5,3 +5,19 @@ insecure = UsmUserData("securityName") # S509
|
||||
auth_no_priv = UsmUserData("securityName", "authName") # S509
|
||||
|
||||
less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
||||
|
||||
# New API paths
|
||||
import pysnmp.hlapi.asyncio
|
||||
import pysnmp.hlapi.v3arch.asyncio
|
||||
import pysnmp.hlapi.v3arch.asyncio.auth
|
||||
import pysnmp.hlapi.auth
|
||||
|
||||
pysnmp.hlapi.asyncio.UsmUserData("user") # S509
|
||||
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
|
||||
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
||||
pysnmp.hlapi.auth.UsmUserData("user") # S509
|
||||
|
||||
pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
|
||||
pysnmp.hlapi.v3arch.asyncio.UsmUserData("user", "authkey", "privkey") # OK
|
||||
pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user", "authkey", "privkey") # OK
|
||||
pysnmp.hlapi.auth.UsmUserData("user", "authkey", "privkey") # OK
|
||||
|
||||
@@ -46,7 +46,8 @@ def func():
|
||||
|
||||
|
||||
def func():
|
||||
# OK (index doesn't start at 0
|
||||
# SIM113
|
||||
# https://github.com/astral-sh/ruff/pull/21395
|
||||
idx = 10
|
||||
for x in range(5):
|
||||
g(x, idx)
|
||||
|
||||
@@ -204,3 +204,15 @@ x = 1
|
||||
print(f"{x=}" or "bar") # SIM222
|
||||
(lambda: 1) or True # SIM222
|
||||
(i for i in range(1)) or "bar" # SIM222
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/21136
|
||||
def get_items():
|
||||
return tuple(item for item in Item.objects.all()) or None # OK
|
||||
|
||||
|
||||
def get_items_list():
|
||||
return tuple([item for item in items]) or None # OK
|
||||
|
||||
|
||||
def get_items_set():
|
||||
return tuple({item for item in items}) or None # OK
|
||||
|
||||
@@ -371,6 +371,61 @@ class Foo:
|
||||
"""
|
||||
return
|
||||
|
||||
# DOC102 - Test case from issue #20959: comma-separated parameters
|
||||
def leq(x: object, y: object) -> bool:
|
||||
"""Compare two objects for loose equality.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x1, x2 : object
|
||||
Objects.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
Whether the objects are identical or equal.
|
||||
"""
|
||||
return x is y or x == y
|
||||
|
||||
|
||||
# OK - comma-separated parameters that match function signature
|
||||
def compare_values(x1: int, x2: int) -> bool:
|
||||
"""Compare two integer values.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
x1, x2 : int
|
||||
Values to compare.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if values are equal.
|
||||
"""
|
||||
return x1 == x2
|
||||
|
||||
|
||||
# DOC102 - mixed comma-separated and regular parameters
|
||||
def process_data(data, x1: str, x2: str) -> str:
|
||||
"""Process data with multiple string parameters.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : list
|
||||
Input data to process.
|
||||
x1, x2 : str
|
||||
String parameters for processing.
|
||||
extra_param : str
|
||||
Extra parameter not in signature.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
Processed result.
|
||||
"""
|
||||
return f"{x1}{x2}{len(data)}"
|
||||
|
||||
|
||||
# OK
|
||||
def baz(x: int) -> int:
|
||||
"""
|
||||
@@ -389,3 +444,21 @@ def baz(x: int) -> int:
|
||||
int
|
||||
"""
|
||||
return x
|
||||
|
||||
|
||||
# OK - comma-separated parameters without type annotations
|
||||
def add_numbers(a, b):
|
||||
"""
|
||||
Adds two numbers and returns the result.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
a, b
|
||||
The numbers to add.
|
||||
|
||||
Returns
|
||||
-------
|
||||
int
|
||||
The sum of the two numbers.
|
||||
"""
|
||||
return a + b
|
||||
|
||||
@@ -83,6 +83,37 @@ def calculate_speed(distance: float, time: float) -> float:
|
||||
raise
|
||||
|
||||
|
||||
# DOC502 regression for Sphinx directive after Raises (issue #18959)
|
||||
def foo():
|
||||
"""First line.
|
||||
|
||||
Raises:
|
||||
ValueError:
|
||||
some text
|
||||
|
||||
.. versionadded:: 0.7.0
|
||||
The ``init_kwargs`` argument.
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
|
||||
# DOC502 regression for following section with colons
|
||||
def example_with_following_section():
|
||||
"""Summary.
|
||||
|
||||
Returns:
|
||||
str: The resulting expression.
|
||||
|
||||
Raises:
|
||||
ValueError: If the unit is not valid.
|
||||
|
||||
Relation to `time_range_lookup`:
|
||||
- Handles the "start of" modifier.
|
||||
- Example: "start of month" → `DATETRUNC()`.
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
|
||||
# This should NOT trigger DOC502 because OSError is explicitly re-raised
|
||||
def f():
|
||||
"""Do nothing.
|
||||
|
||||
@@ -117,3 +117,33 @@ def calculate_speed(distance: float, time: float) -> float:
|
||||
except TypeError:
|
||||
print("Not a number? Shame on you!")
|
||||
raise
|
||||
|
||||
|
||||
# DOC502 regression for Sphinx directive after Raises (issue #18959)
|
||||
def foo():
|
||||
"""First line.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
some text
|
||||
|
||||
.. versionadded:: 0.7.0
|
||||
The ``init_kwargs`` argument.
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
# Make sure we don't bail out on a Sphinx directive in the description of one
|
||||
# of the exceptions
|
||||
def foo():
|
||||
"""First line.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
some text
|
||||
.. math:: e^{xception}
|
||||
ZeroDivisionError
|
||||
Will not be raised, DOC502
|
||||
"""
|
||||
raise ValueError
|
||||
|
||||
5
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP029_2.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP029_2.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
from .builtins import next
|
||||
from ..builtins import str
|
||||
from ...builtins import int
|
||||
from .builtins import next as _next
|
||||
|
||||
@@ -125,3 +125,18 @@ with open(*filename, mode="r") as f:
|
||||
# `buffering`.
|
||||
with open(*filename, file="file.txt", mode="r") as f:
|
||||
x = f.read()
|
||||
|
||||
# FURB101
|
||||
with open("file.txt", encoding="utf-8") as f:
|
||||
contents: str = f.read()
|
||||
|
||||
# FURB101 but no fix because it would remove the assignment to `x`
|
||||
with open("file.txt", encoding="utf-8") as f:
|
||||
contents, x = f.read(), 2
|
||||
|
||||
# FURB101 but no fix because it would remove the `process_contents` call
|
||||
with open("file.txt", encoding="utf-8") as f:
|
||||
contents = process_contents(f.read())
|
||||
|
||||
with open("file.txt", encoding="utf-8") as f:
|
||||
contents: str = process_contents(f.read())
|
||||
|
||||
@@ -152,4 +152,13 @@ import json
|
||||
data = {"price": 100}
|
||||
|
||||
with open("test.json", "wb") as f:
|
||||
f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
f.write(json.dumps(data, indent=4).encode("utf-8"))
|
||||
|
||||
# See: https://github.com/astral-sh/ruff/issues/21381
|
||||
with open("tmp_path/pyproject.toml", "w") as f:
|
||||
f.write(dedent(
|
||||
"""
|
||||
[project]
|
||||
other = 1.234
|
||||
""",
|
||||
))
|
||||
|
||||
@@ -19,6 +19,9 @@ print("", *args, sep="")
|
||||
print("", **kwargs)
|
||||
print(sep="\t")
|
||||
print(sep=print(1))
|
||||
print(f"")
|
||||
print(f"", sep=",")
|
||||
print(f"", end="bar")
|
||||
|
||||
# OK.
|
||||
|
||||
@@ -33,3 +36,4 @@ print("foo", "", sep=",")
|
||||
print("foo", "", "bar", "", sep=",")
|
||||
print("", "", **kwargs)
|
||||
print(*args, sep=",")
|
||||
print(f"foo")
|
||||
|
||||
@@ -132,3 +132,9 @@ class AWithQuotes:
|
||||
final_variable: 'Final[list[int]]' = []
|
||||
class_variable_without_subscript: 'ClassVar' = []
|
||||
final_variable_without_subscript: 'Final' = []
|
||||
|
||||
|
||||
# Reassignment of a ClassVar should not trigger RUF012
|
||||
class P:
|
||||
class_variable: ClassVar[list] = [10, 20, 30, 40, 50]
|
||||
class_variable = [*class_variable[0::1], *class_variable[2::3]]
|
||||
|
||||
34
crates/ruff_linter/resources/test/fixtures/ruff/RUF065_1.py
vendored
Normal file
34
crates/ruff_linter/resources/test/fixtures/ruff/RUF065_1.py
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
import logging
|
||||
|
||||
# Test cases for str() that should NOT be flagged (issue #21315)
|
||||
# str() with no arguments - should not be flagged
|
||||
logging.warning("%s", str())
|
||||
|
||||
# str() with multiple arguments - should not be flagged
|
||||
logging.warning("%s", str(b"\xe2\x9a\xa0", "utf-8"))
|
||||
|
||||
# str() with starred arguments - should not be flagged
|
||||
logging.warning("%s", str(*(b"\xf0\x9f\x9a\xa7", "utf-8")))
|
||||
|
||||
# str() with keyword unpacking - should not be flagged
|
||||
logging.warning("%s", str(**{"object": b"\xf0\x9f\x9a\xa8", "encoding": "utf-8"}))
|
||||
|
||||
# str() with single keyword argument - should be flagged (equivalent to str("!"))
|
||||
logging.warning("%s", str(object="!"))
|
||||
|
||||
|
||||
# Complex conversion specifiers that make oct() and hex() necessary
|
||||
# These should NOT be flagged because the behavior differs between %s and %#o/%#x
|
||||
# https://github.com/astral-sh/ruff/issues/21458
|
||||
|
||||
# %06s with oct() - zero-pad flag with width (should NOT be flagged)
|
||||
logging.warning("%06s", oct(123))
|
||||
|
||||
# % s with oct() - blank sign flag (should NOT be flagged)
|
||||
logging.warning("% s", oct(123))
|
||||
|
||||
# %+s with oct() - sign char flag (should NOT be flagged)
|
||||
logging.warning("%+s", oct(123))
|
||||
|
||||
# %.3s with hex() - precision (should NOT be flagged)
|
||||
logging.warning("%.3s", hex(123))
|
||||
@@ -717,7 +717,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
if checker.is_rule_enabled(Rule::UnnecessaryBuiltinImport) {
|
||||
if let Some(module) = module {
|
||||
pyupgrade::rules::unnecessary_builtin_import(checker, stmt, module, names);
|
||||
pyupgrade::rules::unnecessary_builtin_import(
|
||||
checker, stmt, module, names, level,
|
||||
);
|
||||
}
|
||||
}
|
||||
if checker.any_rule_enabled(&[
|
||||
|
||||
@@ -860,23 +860,17 @@ impl SemanticSyntaxContext for Checker<'_> {
|
||||
}
|
||||
|
||||
fn is_bound_parameter(&self, name: &str) -> bool {
|
||||
for scope in self.semantic.current_scopes() {
|
||||
match scope.kind {
|
||||
ScopeKind::Class(_) => return false,
|
||||
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. })
|
||||
| ScopeKind::Lambda(ast::ExprLambda {
|
||||
parameters: Some(parameters),
|
||||
..
|
||||
}) => return parameters.includes(name),
|
||||
ScopeKind::Lambda(_)
|
||||
| ScopeKind::Generator { .. }
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::Type
|
||||
| ScopeKind::DunderClassCell => {}
|
||||
match self.semantic.current_scope().kind {
|
||||
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. }) => {
|
||||
parameters.includes(name)
|
||||
}
|
||||
ScopeKind::Class(_)
|
||||
| ScopeKind::Lambda(_)
|
||||
| ScopeKind::Generator { .. }
|
||||
| ScopeKind::Module
|
||||
| ScopeKind::Type
|
||||
| ScopeKind::DunderClassCell => false,
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ impl<'a> Importer<'a> {
|
||||
.into_edit(&required_import)
|
||||
} else {
|
||||
// Insert at the start of the file.
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
|
||||
.into_edit(&required_import)
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ impl<'a> Importer<'a> {
|
||||
Insertion::end_of_statement(stmt, self.source, self.stylist)
|
||||
} else {
|
||||
// Insert at the start of the file.
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
|
||||
};
|
||||
let add_import_edit = insertion.into_edit(&content);
|
||||
|
||||
@@ -498,7 +498,7 @@ impl<'a> Importer<'a> {
|
||||
Insertion::end_of_statement(stmt, self.source, self.stylist)
|
||||
} else {
|
||||
// Insert at the start of the file.
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
|
||||
Insertion::start_of_file(self.python_ast, self.source, self.stylist, None)
|
||||
};
|
||||
if insertion.is_inline() {
|
||||
Err(anyhow::anyhow!(
|
||||
|
||||
@@ -51,13 +51,17 @@ impl<'de> serde::Deserialize<'de> for LineLength {
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let value = u16::deserialize(deserializer)?;
|
||||
Self::try_from(value).map_err(|_| {
|
||||
serde::de::Error::custom(format!(
|
||||
"line-length must be between 1 and {} (got {value})",
|
||||
Self::MAX,
|
||||
))
|
||||
})
|
||||
let value = i64::deserialize(deserializer)?;
|
||||
|
||||
u16::try_from(value)
|
||||
.ok()
|
||||
.and_then(|u16_value| Self::try_from(u16_value).ok())
|
||||
.ok_or_else(|| {
|
||||
serde::de::Error::custom(format!(
|
||||
"line-length must be between 1 and {} (got {value})",
|
||||
Self::MAX,
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -377,6 +377,7 @@ pub fn add_noqa_to_path(
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
settings: &LinterSettings,
|
||||
reason: Option<&str>,
|
||||
) -> Result<usize> {
|
||||
// Parse once.
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
@@ -425,6 +426,7 @@ pub fn add_noqa_to_path(
|
||||
&settings.external,
|
||||
&directives.noqa_line_for,
|
||||
stylist.line_ending(),
|
||||
reason,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ where
|
||||
}
|
||||
|
||||
diagnostic.set_secondary_code(SecondaryCode::new(rule.noqa_code().to_string()));
|
||||
diagnostic.set_documentation_url(rule.url());
|
||||
|
||||
diagnostic
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ pub fn generate_noqa_edits(
|
||||
let exemption = FileExemption::from(&file_directives);
|
||||
let directives = NoqaDirectives::from_commented_ranges(comment_ranges, external, path, locator);
|
||||
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
|
||||
build_noqa_edits_by_diagnostic(comments, locator, line_ending)
|
||||
build_noqa_edits_by_diagnostic(comments, locator, line_ending, None)
|
||||
}
|
||||
|
||||
/// A directive to ignore a set of rules either for a given line of Python source code or an entire file (e.g.,
|
||||
@@ -715,6 +715,7 @@ impl Display for LexicalError {
|
||||
impl Error for LexicalError {}
|
||||
|
||||
/// Adds noqa comments to suppress all messages of a file.
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) fn add_noqa(
|
||||
path: &Path,
|
||||
diagnostics: &[Diagnostic],
|
||||
@@ -723,6 +724,7 @@ pub(crate) fn add_noqa(
|
||||
external: &[String],
|
||||
noqa_line_for: &NoqaMapping,
|
||||
line_ending: LineEnding,
|
||||
reason: Option<&str>,
|
||||
) -> Result<usize> {
|
||||
let (count, output) = add_noqa_inner(
|
||||
path,
|
||||
@@ -732,12 +734,14 @@ pub(crate) fn add_noqa(
|
||||
external,
|
||||
noqa_line_for,
|
||||
line_ending,
|
||||
reason,
|
||||
);
|
||||
|
||||
fs::write(path, output)?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
fn add_noqa_inner(
|
||||
path: &Path,
|
||||
diagnostics: &[Diagnostic],
|
||||
@@ -746,6 +750,7 @@ fn add_noqa_inner(
|
||||
external: &[String],
|
||||
noqa_line_for: &NoqaMapping,
|
||||
line_ending: LineEnding,
|
||||
reason: Option<&str>,
|
||||
) -> (usize, String) {
|
||||
let mut count = 0;
|
||||
|
||||
@@ -757,7 +762,7 @@ fn add_noqa_inner(
|
||||
|
||||
let comments = find_noqa_comments(diagnostics, locator, &exemption, &directives, noqa_line_for);
|
||||
|
||||
let edits = build_noqa_edits_by_line(comments, locator, line_ending);
|
||||
let edits = build_noqa_edits_by_line(comments, locator, line_ending, reason);
|
||||
|
||||
let contents = locator.contents();
|
||||
|
||||
@@ -783,6 +788,7 @@ fn build_noqa_edits_by_diagnostic(
|
||||
comments: Vec<Option<NoqaComment>>,
|
||||
locator: &Locator,
|
||||
line_ending: LineEnding,
|
||||
reason: Option<&str>,
|
||||
) -> Vec<Option<Edit>> {
|
||||
let mut edits = Vec::default();
|
||||
for comment in comments {
|
||||
@@ -794,6 +800,7 @@ fn build_noqa_edits_by_diagnostic(
|
||||
FxHashSet::from_iter([comment.code]),
|
||||
locator,
|
||||
line_ending,
|
||||
reason,
|
||||
) {
|
||||
edits.push(Some(noqa_edit.into_edit()));
|
||||
}
|
||||
@@ -808,6 +815,7 @@ fn build_noqa_edits_by_line<'a>(
|
||||
comments: Vec<Option<NoqaComment<'a>>>,
|
||||
locator: &Locator,
|
||||
line_ending: LineEnding,
|
||||
reason: Option<&'a str>,
|
||||
) -> BTreeMap<TextSize, NoqaEdit<'a>> {
|
||||
let mut comments_by_line = BTreeMap::default();
|
||||
for comment in comments.into_iter().flatten() {
|
||||
@@ -831,6 +839,7 @@ fn build_noqa_edits_by_line<'a>(
|
||||
.collect(),
|
||||
locator,
|
||||
line_ending,
|
||||
reason,
|
||||
) {
|
||||
edits.insert(offset, edit);
|
||||
}
|
||||
@@ -927,6 +936,7 @@ struct NoqaEdit<'a> {
|
||||
noqa_codes: FxHashSet<&'a SecondaryCode>,
|
||||
codes: Option<&'a Codes<'a>>,
|
||||
line_ending: LineEnding,
|
||||
reason: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl NoqaEdit<'_> {
|
||||
@@ -954,6 +964,9 @@ impl NoqaEdit<'_> {
|
||||
push_codes(writer, self.noqa_codes.iter().sorted_unstable());
|
||||
}
|
||||
}
|
||||
if let Some(reason) = self.reason {
|
||||
write!(writer, " {reason}").unwrap();
|
||||
}
|
||||
write!(writer, "{}", self.line_ending.as_str()).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -970,6 +983,7 @@ fn generate_noqa_edit<'a>(
|
||||
noqa_codes: FxHashSet<&'a SecondaryCode>,
|
||||
locator: &Locator,
|
||||
line_ending: LineEnding,
|
||||
reason: Option<&'a str>,
|
||||
) -> Option<NoqaEdit<'a>> {
|
||||
let line_range = locator.full_line_range(offset);
|
||||
|
||||
@@ -999,6 +1013,7 @@ fn generate_noqa_edit<'a>(
|
||||
noqa_codes,
|
||||
codes,
|
||||
line_ending,
|
||||
reason,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2832,6 +2847,7 @@ mod tests {
|
||||
&[],
|
||||
&noqa_line_for,
|
||||
LineEnding::Lf,
|
||||
None,
|
||||
);
|
||||
assert_eq!(count, 0);
|
||||
assert_eq!(output, format!("{contents}"));
|
||||
@@ -2855,6 +2871,7 @@ mod tests {
|
||||
&[],
|
||||
&noqa_line_for,
|
||||
LineEnding::Lf,
|
||||
None,
|
||||
);
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(output, "x = 1 # noqa: F841\n");
|
||||
@@ -2885,6 +2902,7 @@ mod tests {
|
||||
&[],
|
||||
&noqa_line_for,
|
||||
LineEnding::Lf,
|
||||
None,
|
||||
);
|
||||
assert_eq!(count, 1);
|
||||
assert_eq!(output, "x = 1 # noqa: E741, F841\n");
|
||||
@@ -2915,6 +2933,7 @@ mod tests {
|
||||
&[],
|
||||
&noqa_line_for,
|
||||
LineEnding::Lf,
|
||||
None,
|
||||
);
|
||||
assert_eq!(count, 0);
|
||||
assert_eq!(output, "x = 1 # noqa");
|
||||
|
||||
@@ -261,16 +261,6 @@ pub(crate) const fn is_b006_unsafe_fix_preserve_assignment_expr_enabled(
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/20520
|
||||
pub(crate) const fn is_fix_read_whole_file_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/20520
|
||||
pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
@@ -279,3 +269,13 @@ pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSett
|
||||
pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/21374
|
||||
pub(crate) const fn is_extended_snmp_api_path_detection_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
// https://github.com/astral-sh/ruff/pull/21395
|
||||
pub(crate) const fn is_enumerate_for_loop_int_index_enabled(settings: &LinterSettings) -> bool {
|
||||
settings.preview.is_enabled()
|
||||
}
|
||||
|
||||
@@ -513,6 +513,9 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
/// def foo(x: MyAny): ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-annotations.allow-star-arg-any`
|
||||
///
|
||||
/// ## References
|
||||
/// - [Typing spec: `Any`](https://typing.python.org/en/latest/spec/special-types.html#any)
|
||||
/// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
|
||||
|
||||
@@ -104,6 +104,8 @@ mod tests {
|
||||
#[test_case(Rule::SuspiciousURLOpenUsage, Path::new("S310.py"))]
|
||||
#[test_case(Rule::SuspiciousNonCryptographicRandomUsage, Path::new("S311.py"))]
|
||||
#[test_case(Rule::SuspiciousTelnetUsage, Path::new("S312.py"))]
|
||||
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"))]
|
||||
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -4,6 +4,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_extended_snmp_api_path_detection_enabled;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of SNMPv1 or SNMPv2.
|
||||
@@ -47,10 +48,17 @@ pub(crate) fn snmp_insecure_version(checker: &Checker, call: &ast::ExprCall) {
|
||||
.semantic()
|
||||
.resolve_qualified_name(&call.func)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["pysnmp", "hlapi", "CommunityData"]
|
||||
)
|
||||
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["pysnmp", "hlapi", .., "CommunityData"]
|
||||
)
|
||||
} else {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["pysnmp", "hlapi", "CommunityData"]
|
||||
)
|
||||
}
|
||||
})
|
||||
{
|
||||
if let Some(keyword) = call.arguments.find_keyword("mpModel") {
|
||||
|
||||
@@ -4,6 +4,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::Violation;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_extended_snmp_api_path_detection_enabled;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of the SNMPv3 protocol without encryption.
|
||||
@@ -47,10 +48,17 @@ pub(crate) fn snmp_weak_cryptography(checker: &Checker, call: &ast::ExprCall) {
|
||||
.semantic()
|
||||
.resolve_qualified_name(&call.func)
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["pysnmp", "hlapi", "UsmUserData"]
|
||||
)
|
||||
if is_extended_snmp_api_path_detection_enabled(checker.settings()) {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["pysnmp", "hlapi", .., "UsmUserData"]
|
||||
)
|
||||
} else {
|
||||
matches!(
|
||||
qualified_name.segments(),
|
||||
["pysnmp", "hlapi", "UsmUserData"]
|
||||
)
|
||||
}
|
||||
})
|
||||
{
|
||||
checker.report_diagnostic(SnmpWeakCryptography, call.func.range());
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:3:25
|
||||
|
|
||||
1 | from pysnmp.hlapi import CommunityData
|
||||
2 |
|
||||
3 | CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
4 | CommunityData("public", mpModel=1) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:4:25
|
||||
|
|
||||
3 | CommunityData("public", mpModel=0) # S508
|
||||
4 | CommunityData("public", mpModel=1) # S508
|
||||
| ^^^^^^^^^
|
||||
5 |
|
||||
6 | CommunityData("public", mpModel=2) # OK
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:18:46
|
||||
|
|
||||
16 | import pysnmp.hlapi.auth
|
||||
17 |
|
||||
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:19:58
|
||||
|
|
||||
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:20:53
|
||||
|
|
||||
18 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
||||
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:21:45
|
||||
|
|
||||
19 | pysnmp.hlapi.v1arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:22:58
|
||||
|
|
||||
20 | pysnmp.hlapi.v1arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
||||
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:23:53
|
||||
|
|
||||
21 | pysnmp.hlapi.v1arch.CommunityData("public", mpModel=0) # S508
|
||||
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
||||
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:24:45
|
||||
|
|
||||
22 | pysnmp.hlapi.v3arch.asyncio.auth.CommunityData("public", mpModel=0) # S508
|
||||
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
||||
|
|
||||
|
||||
S508 The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able.
|
||||
--> S508.py:25:43
|
||||
|
|
||||
23 | pysnmp.hlapi.v3arch.asyncio.CommunityData("public", mpModel=0) # S508
|
||||
24 | pysnmp.hlapi.v3arch.CommunityData("public", mpModel=0) # S508
|
||||
25 | pysnmp.hlapi.auth.CommunityData("public", mpModel=0) # S508
|
||||
| ^^^^^^^^^
|
||||
26 |
|
||||
27 | pysnmp.hlapi.asyncio.CommunityData("public", mpModel=2) # OK
|
||||
|
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:4:12
|
||||
|
|
||||
4 | insecure = UsmUserData("securityName") # S509
|
||||
| ^^^^^^^^^^^
|
||||
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
||||
|
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:5:16
|
||||
|
|
||||
4 | insecure = UsmUserData("securityName") # S509
|
||||
5 | auth_no_priv = UsmUserData("securityName", "authName") # S509
|
||||
| ^^^^^^^^^^^
|
||||
6 |
|
||||
7 | less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
||||
|
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:15:1
|
||||
|
|
||||
13 | import pysnmp.hlapi.auth
|
||||
14 |
|
||||
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
|
||||
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
||||
|
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:16:1
|
||||
|
|
||||
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
|
||||
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
||||
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
||||
|
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:17:1
|
||||
|
|
||||
15 | pysnmp.hlapi.asyncio.UsmUserData("user") # S509
|
||||
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
|
||||
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
||||
|
|
||||
|
||||
S509 You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure.
|
||||
--> S509.py:18:1
|
||||
|
|
||||
16 | pysnmp.hlapi.v3arch.asyncio.UsmUserData("user") # S509
|
||||
17 | pysnmp.hlapi.v3arch.asyncio.auth.UsmUserData("user") # S509
|
||||
18 | pysnmp.hlapi.auth.UsmUserData("user") # S509
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
19 |
|
||||
20 | pysnmp.hlapi.asyncio.UsmUserData("user", "authkey", "privkey") # OK
|
||||
|
|
||||
@@ -61,6 +61,7 @@ mod tests {
|
||||
|
||||
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
|
||||
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
|
||||
#[test_case(Rule::EnumerateForLoop, Path::new("SIM113.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::preview::is_enumerate_for_loop_int_index_enabled;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::statement_visitor::{StatementVisitor, walk_stmt};
|
||||
use ruff_python_ast::{self as ast, Expr, Int, Number, Operator, Stmt};
|
||||
use ruff_python_semantic::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -11,6 +13,9 @@ use crate::checkers::ast::Checker;
|
||||
/// Checks for `for` loops with explicit loop-index variables that can be replaced
|
||||
/// with `enumerate()`.
|
||||
///
|
||||
/// In [preview], this rule checks for index variables initialized with any integer rather than only
|
||||
/// a literal zero.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When iterating over a sequence, it's often desirable to keep track of the
|
||||
/// index of each element alongside the element itself. Prefer the `enumerate`
|
||||
@@ -35,6 +40,8 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `enumerate`](https://docs.python.org/3/library/functions.html#enumerate)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
#[violation_metadata(stable_since = "v0.2.0")]
|
||||
pub(crate) struct EnumerateForLoop {
|
||||
@@ -82,17 +89,21 @@ pub(crate) fn enumerate_for_loop(checker: &Checker, for_stmt: &ast::StmtFor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ensure that the index variable was initialized to 0.
|
||||
// Ensure that the index variable was initialized to 0 (or instance of `int` if preview is enabled).
|
||||
let Some(value) = typing::find_binding_value(binding, checker.semantic()) else {
|
||||
continue;
|
||||
};
|
||||
if !matches!(
|
||||
if !(matches!(
|
||||
value,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: Number::Int(Int::ZERO),
|
||||
..
|
||||
})
|
||||
) {
|
||||
) || matches!(
|
||||
ResolvedPythonType::from(value),
|
||||
ResolvedPythonType::Atom(PythonType::Number(NumberLike::Integer))
|
||||
) && is_enumerate_for_loop_int_index_enabled(checker.settings()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -1101,6 +1101,7 @@ help: Replace with `f"{x=}"`
|
||||
204 + print(f"{x=}") # SIM222
|
||||
205 | (lambda: 1) or True # SIM222
|
||||
206 | (i for i in range(1)) or "bar" # SIM222
|
||||
207 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM222 [*] Use `lambda: 1` instead of `lambda: 1 or ...`
|
||||
@@ -1119,6 +1120,8 @@ help: Replace with `lambda: 1`
|
||||
- (lambda: 1) or True # SIM222
|
||||
205 + lambda: 1 # SIM222
|
||||
206 | (i for i in range(1)) or "bar" # SIM222
|
||||
207 |
|
||||
208 | # https://github.com/astral-sh/ruff/issues/21136
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM222 [*] Use `(i for i in range(1))` instead of `(i for i in range(1)) or ...`
|
||||
@@ -1128,6 +1131,8 @@ SIM222 [*] Use `(i for i in range(1))` instead of `(i for i in range(1)) or ...`
|
||||
205 | (lambda: 1) or True # SIM222
|
||||
206 | (i for i in range(1)) or "bar" # SIM222
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
207 |
|
||||
208 | # https://github.com/astral-sh/ruff/issues/21136
|
||||
|
|
||||
help: Replace with `(i for i in range(1))`
|
||||
203 | x = 1
|
||||
@@ -1135,4 +1140,7 @@ help: Replace with `(i for i in range(1))`
|
||||
205 | (lambda: 1) or True # SIM222
|
||||
- (i for i in range(1)) or "bar" # SIM222
|
||||
206 + (i for i in range(1)) # SIM222
|
||||
207 |
|
||||
208 | # https://github.com/astral-sh/ruff/issues/21136
|
||||
209 | def get_items():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
|
||||
---
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:6:9
|
||||
|
|
||||
4 | for x in range(5):
|
||||
5 | g(x, idx)
|
||||
6 | idx += 1
|
||||
| ^^^^^^^^
|
||||
7 | h(x)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:17:9
|
||||
|
|
||||
15 | if g(x):
|
||||
16 | break
|
||||
17 | idx += 1
|
||||
| ^^^^^^^^
|
||||
18 | sum += h(x, idx)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:27:9
|
||||
|
|
||||
25 | g(x)
|
||||
26 | h(x, y)
|
||||
27 | idx += 1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:36:9
|
||||
|
|
||||
34 | for x in range(5):
|
||||
35 | sum += h(x, idx)
|
||||
36 | idx += 1
|
||||
| ^^^^^^^^
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:44:9
|
||||
|
|
||||
42 | for x in range(5):
|
||||
43 | g(x, idx)
|
||||
44 | idx += 1
|
||||
| ^^^^^^^^
|
||||
45 | h(x)
|
||||
|
|
||||
|
||||
SIM113 Use `enumerate()` for index variable `idx` in `for` loop
|
||||
--> SIM113.py:54:9
|
||||
|
|
||||
52 | for x in range(5):
|
||||
53 | g(x, idx)
|
||||
54 | idx += 1
|
||||
| ^^^^^^^^
|
||||
55 | h(x)
|
||||
|
|
||||
@@ -661,19 +661,31 @@ fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec<Paramet
|
||||
.is_some_and(|first_char| !first_char.is_whitespace())
|
||||
{
|
||||
if let Some(before_colon) = entry.split(':').next() {
|
||||
let param = before_colon.trim_end();
|
||||
let param_name = param.trim_start_matches('*');
|
||||
if is_identifier(param_name) {
|
||||
let param_start = line_start + indentation.text_len();
|
||||
let param_end = param_start + param.text_len();
|
||||
let param_line = before_colon.trim_end();
|
||||
|
||||
entries.push(ParameterEntry {
|
||||
name: param_name,
|
||||
range: TextRange::new(
|
||||
content_start + param_start,
|
||||
content_start + param_end,
|
||||
),
|
||||
});
|
||||
// Split on commas to handle comma-separated parameters
|
||||
let mut current_offset = TextSize::from(0);
|
||||
for param_part in param_line.split(',') {
|
||||
let param_part_trimmed = param_part.trim();
|
||||
let param_name = param_part_trimmed.trim_start_matches('*');
|
||||
if is_identifier(param_name) {
|
||||
// Calculate the position of this specific parameter part within the line
|
||||
// Account for leading whitespace that gets trimmed
|
||||
let param_start_in_line = current_offset
|
||||
+ (param_part.text_len() - param_part_trimmed.text_len());
|
||||
let param_start =
|
||||
line_start + indentation.text_len() + param_start_in_line;
|
||||
|
||||
entries.push(ParameterEntry {
|
||||
name: param_name,
|
||||
range: TextRange::at(
|
||||
content_start + param_start,
|
||||
param_part_trimmed.text_len(),
|
||||
),
|
||||
});
|
||||
}
|
||||
// Update offset for next iteration: add the part length plus comma length
|
||||
current_offset = current_offset + param_part.text_len() + ','.text_len();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -710,12 +722,30 @@ fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName
|
||||
/// ```
|
||||
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
|
||||
let mut entries: Vec<QualifiedName> = Vec::new();
|
||||
for potential in content.lines() {
|
||||
let Some(colon_idx) = potential.find(':') else {
|
||||
continue;
|
||||
};
|
||||
let entry = potential[..colon_idx].trim();
|
||||
entries.push(QualifiedName::user_defined(entry));
|
||||
let mut lines = content.lines().peekable();
|
||||
let Some(first) = lines.peek() else {
|
||||
return entries;
|
||||
};
|
||||
let indentation = &first[..first.len() - first.trim_start().len()];
|
||||
for potential in lines {
|
||||
if let Some(entry) = potential.strip_prefix(indentation) {
|
||||
if let Some(first_char) = entry.chars().next() {
|
||||
if !first_char.is_whitespace() {
|
||||
if let Some(colon_idx) = entry.find(':') {
|
||||
let entry = entry[..colon_idx].trim();
|
||||
if !entry.is_empty() {
|
||||
entries.push(QualifiedName::user_defined(entry));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If we can't strip the expected indentation, check if this is a dedented line
|
||||
// (not blank) - if so, break early as we've reached the end of this section
|
||||
if !potential.trim().is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
@@ -739,6 +769,12 @@ fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
|
||||
let indentation = &dashes[..dashes.len() - dashes.trim_start().len()];
|
||||
for potential in lines {
|
||||
if let Some(entry) = potential.strip_prefix(indentation) {
|
||||
// Check for Sphinx directives (lines starting with ..) - these indicate the end of the
|
||||
// section. In numpy-style, exceptions are dedented to the same level as sphinx
|
||||
// directives.
|
||||
if entry.starts_with("..") {
|
||||
break;
|
||||
}
|
||||
if let Some(first_char) = entry.chars().next() {
|
||||
if !first_char.is_whitespace() {
|
||||
entries.push(QualifiedName::user_defined(entry.trim_end()));
|
||||
|
||||
@@ -95,3 +95,23 @@ DOC502 Raised exception is not explicitly raised: `DivisionByZero`
|
||||
82 | return distance / time
|
||||
|
|
||||
help: Remove `DivisionByZero` from the docstring
|
||||
|
||||
DOC502 Raised exception is not explicitly raised: `ZeroDivisionError`
|
||||
--> DOC502_numpy.py:139:5
|
||||
|
|
||||
137 | # of the exceptions
|
||||
138 | def foo():
|
||||
139 | / """First line.
|
||||
140 | |
|
||||
141 | | Raises
|
||||
142 | | ------
|
||||
143 | | ValueError
|
||||
144 | | some text
|
||||
145 | | .. math:: e^{xception}
|
||||
146 | | ZeroDivisionError
|
||||
147 | | Will not be raised, DOC502
|
||||
148 | | """
|
||||
| |_______^
|
||||
149 | raise ValueError
|
||||
|
|
||||
help: Remove `ZeroDivisionError` from the docstring
|
||||
|
||||
@@ -187,3 +187,36 @@ DOC102 Documented parameter `a` is not in the function's signature
|
||||
302 | b
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `x1` is not in the function's signature
|
||||
--> DOC102_numpy.py:380:5
|
||||
|
|
||||
378 | Parameters
|
||||
379 | ----------
|
||||
380 | x1, x2 : object
|
||||
| ^^
|
||||
381 | Objects.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `x2` is not in the function's signature
|
||||
--> DOC102_numpy.py:380:9
|
||||
|
|
||||
378 | Parameters
|
||||
379 | ----------
|
||||
380 | x1, x2 : object
|
||||
| ^^
|
||||
381 | Objects.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
DOC102 Documented parameter `extra_param` is not in the function's signature
|
||||
--> DOC102_numpy.py:418:5
|
||||
|
|
||||
416 | x1, x2 : str
|
||||
417 | String parameters for processing.
|
||||
418 | extra_param : str
|
||||
| ^^^^^^^^^^^
|
||||
419 | Extra parameter not in signature.
|
||||
|
|
||||
help: Remove the extraneous parameter from the docstring
|
||||
|
||||
@@ -99,6 +99,7 @@ mod tests {
|
||||
#[test_case(Rule::UTF8EncodingDeclaration, Path::new("UP009_many_empty_lines.py"))]
|
||||
#[test_case(Rule::UnicodeKindPrefix, Path::new("UP025.py"))]
|
||||
#[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029_0.py"))]
|
||||
#[test_case(Rule::UnnecessaryBuiltinImport, Path::new("UP029_2.py"))]
|
||||
#[test_case(Rule::UnnecessaryClassParentheses, Path::new("UP039.py"))]
|
||||
#[test_case(Rule::UnnecessaryDefaultTypeArgs, Path::new("UP043.py"))]
|
||||
#[test_case(Rule::UnnecessaryEncodeUTF8, Path::new("UP012.py"))]
|
||||
|
||||
@@ -766,11 +766,12 @@ pub(crate) fn deprecated_import(checker: &Checker, import_from_stmt: &StmtImport
|
||||
}
|
||||
|
||||
for operation in fixer.with_renames() {
|
||||
checker.report_diagnostic(
|
||||
let mut diagnostic = checker.report_diagnostic(
|
||||
DeprecatedImport {
|
||||
deprecation: Deprecation::WithRename(operation),
|
||||
},
|
||||
import_from_stmt.range(),
|
||||
);
|
||||
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,13 @@ pub(crate) fn unnecessary_builtin_import(
|
||||
stmt: &Stmt,
|
||||
module: &str,
|
||||
names: &[Alias],
|
||||
level: u32,
|
||||
) {
|
||||
// Ignore relative imports (they're importing from local modules, not Python's builtins).
|
||||
if level > 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore irrelevant modules.
|
||||
if !matches!(
|
||||
module,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user