Compare commits

..

1 Commits

Author SHA1 Message Date
Ibraheem Ahmed
2f4dcbf651 prefer declared type of generic classes 2025-10-31 11:01:16 -04:00
341 changed files with 3277 additions and 7342 deletions

View File

@@ -256,15 +256,15 @@ jobs:
- 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@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
@@ -277,8 +277,8 @@ jobs:
run: cargo test -p ty_python_semantic --test mdtest || true
- name: "Run tests"
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
- name: Dogfood ty on py-fuzzer
run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
# Dogfood ty on py-fuzzer
- run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:
@@ -320,15 +320,15 @@ jobs:
- 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@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: "true"
- name: "Run tests"
@@ -353,11 +353,11 @@ jobs:
- 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@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: "true"
- name: "Run tests"
@@ -378,7 +378,7 @@ jobs:
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
cache: "npm"
@@ -437,10 +437,8 @@ jobs:
workspaces: "fuzz -> target"
- 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@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
- 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
@@ -460,7 +458,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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -496,7 +494,7 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -531,7 +529,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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
python-version: ${{ env.PYTHON_VERSION }}
activate-environment: true
@@ -647,36 +645,36 @@ jobs:
name: "Fuzz for new ty panics"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
needs:
- cargo-test-linux
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 10 || 20 }}
timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 5 || 20 }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
persist-credentials: false
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
name: Download new ty binary
id: ty-new
with:
name: ty
path: target/debug
- uses: dawidd6/action-download-artifact@20319c5641d495c8a52e688b7dc5fada6c3a9fbc # v8
name: Download baseline ty binary
with:
name: ty
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Fuzz
env:
FORCE_COLOR: 1
NEW_TY: ${{ steps.ty-new.outputs.download-path }}
run: |
echo "new commit"
git rev-list --format=%s --max-count=1 "$GITHUB_SHA"
cargo build --profile=profiling --bin=ty
mv target/profiling/ty ty-new
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
echo "old commit (merge base)"
git rev-list --format=%s --max-count=1 old_commit
cargo build --profile=profiling --bin=ty
mv target/profiling/ty ty-old
# Make executable, since artifact download doesn't preserve this
chmod +x "${PWD}/ty" "${NEW_TY}/ty"
(
uv run \
@@ -684,8 +682,8 @@ jobs:
--project=./python/py-fuzzer \
--locked \
fuzz \
--test-executable=ty-new \
--baseline-executable=ty-old \
--test-executable="${NEW_TY}/ty" \
--baseline-executable="${PWD}/ty" \
--only-new-bugs \
--bin=ty \
0-1000
@@ -700,7 +698,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@afcf9780305558bcc9e4bc94b7589ab2bb8b6106 # v1.15.9
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -713,12 +711,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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- 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
- name: "Ensure there are no changes"
@@ -760,9 +756,9 @@ 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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
- name: "Cache pre-commit"
@@ -800,7 +796,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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
python-version: 3.13
activate-environment: true
@@ -904,7 +900,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
cache: "npm"
@@ -942,18 +938,18 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
@@ -980,18 +976,18 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark --bench ty
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1
@@ -1018,18 +1014,18 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: astral-sh/setup-uv@85856786d1ce8acfbcc2f13a5f3fbd6b938f9f41 # v7.1.2
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-codspeed
- name: "Build benchmarks"
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@6b43a0cd438f6ca5ad26f9ed03ed159ed2df7da9 # v4.1.1

View File

@@ -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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
@@ -85,7 +85,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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:

View File

@@ -31,7 +31,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
package-manager-cache: false

View File

@@ -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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
pattern: wheels-*

View File

@@ -35,7 +35,7 @@ jobs:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
package-manager-cache: false

View File

@@ -45,7 +45,7 @@ jobs:
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
mv /tmp/package.json crates/ruff_wasm/pkg
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
registry-url: "https://registry.npmjs.org"

View File

@@ -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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- 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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- 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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -207,12 +207,12 @@ 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@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@81ee1d48d9194cdcab880cbdc7d36e87d39874cb # v2.62.45
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
- name: Update snapshots

View File

@@ -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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact

View File

@@ -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@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact

View File

@@ -24,7 +24,7 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
CONFORMANCE_SUITE_COMMIT: 9f6d8ced7cd1c8d92687a4e9c96d7716452e471e
CONFORMANCE_SUITE_COMMIT: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
jobs:
typing_conformance:

View File

@@ -1,50 +1,5 @@
# Changelog
## 0.14.4
Released on 2025-11-06.
### Preview features
- [formatter] Allow newlines after function headers without docstrings ([#21110](https://github.com/astral-sh/ruff/pull/21110))
- [formatter] Avoid extra parentheses for long `match` patterns with `as` captures ([#21176](https://github.com/astral-sh/ruff/pull/21176))
- \[`refurb`\] Expand fix safety for keyword arguments and `Decimal`s (`FURB164`) ([#21259](https://github.com/astral-sh/ruff/pull/21259))
- \[`refurb`\] Preserve argument ordering in autofix (`FURB103`) ([#20790](https://github.com/astral-sh/ruff/pull/20790))
### Bug fixes
- [server] Fix missing diagnostics for notebooks ([#21156](https://github.com/astral-sh/ruff/pull/21156))
- \[`flake8-bugbear`\] Ignore non-NFKC attribute names in `B009` and `B010` ([#21131](https://github.com/astral-sh/ruff/pull/21131))
- \[`refurb`\] Fix false negative for underscores before sign in `Decimal` constructor (`FURB157`) ([#21190](https://github.com/astral-sh/ruff/pull/21190))
- \[`ruff`\] Fix false positives on starred arguments (`RUF057`) ([#21256](https://github.com/astral-sh/ruff/pull/21256))
### Rule changes
- \[`airflow`\] extend deprecated argument `concurrency` in `airflow..DAG` (`AIR301`) ([#21220](https://github.com/astral-sh/ruff/pull/21220))
### Documentation
- Improve `extend` docs ([#21135](https://github.com/astral-sh/ruff/pull/21135))
- \[`flake8-comprehensions`\] Fix typo in `C416` documentation ([#21184](https://github.com/astral-sh/ruff/pull/21184))
- Revise Ruff setup instructions for Zed editor ([#20935](https://github.com/astral-sh/ruff/pull/20935))
### Other changes
- Make `ruff analyze graph` work with jupyter notebooks ([#21161](https://github.com/astral-sh/ruff/pull/21161))
### Contributors
- [@chirizxc](https://github.com/chirizxc)
- [@Lee-W](https://github.com/Lee-W)
- [@musicinmybrain](https://github.com/musicinmybrain)
- [@MichaReiser](https://github.com/MichaReiser)
- [@tjkuson](https://github.com/tjkuson)
- [@danparizher](https://github.com/danparizher)
- [@renovate](https://github.com/renovate)
- [@ntBre](https://github.com/ntBre)
- [@gauthsvenkat](https://github.com/gauthsvenkat)
- [@LoicRiegel](https://github.com/LoicRiegel)
## 0.14.3
Released on 2025-10-30.

384
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package]
# Please update rustfmt.toml when bumping the Rust edition
edition = "2024"
rust-version = "1.89"
rust-version = "1.88"
homepage = "https://docs.astral.sh/ruff"
documentation = "https://docs.astral.sh/ruff"
repository = "https://github.com/astral-sh/ruff"
@@ -103,7 +103,7 @@ hashbrown = { version = "0.16.0", default-features = false, features = [
"inline-more",
] }
heck = "0.5.0"
ignore = { version = "0.4.24" }
ignore = { version = "0.4.22" }
imara-diff = { version = "0.1.5" }
imperative = { version = "1.0.4" }
indexmap = { version = "2.6.0" }
@@ -124,7 +124,7 @@ lsp-server = { version = "0.7.6" }
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
"proposed",
] }
matchit = { version = "0.9.0" }
matchit = { version = "0.8.1" }
memchr = { version = "2.7.1" }
mimalloc = { version = "0.1.39" }
natord = { version = "1.0.9" }
@@ -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 = "cdd0b85516a52c18b8a6d17a2279a96ed6c3e198", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -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.3/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.3/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.3
hooks:
# Run the linter.
- id: ruff-check

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.14.4"
version = "0.14.3"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -7,7 +7,6 @@ use path_absolutize::CWD;
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_graph::{Direction, ImportMap, ModuleDb, ModuleImports};
use ruff_linter::package::PackageRoot;
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{warn_user, warn_user_once};
use ruff_python_ast::{PySourceType, SourceType};
use ruff_workspace::resolver::{ResolvedFile, match_exclusion, python_files_in_path};
@@ -128,6 +127,10 @@ pub(crate) fn analyze_graph(
},
Some(language) => PySourceType::from(language),
};
if matches!(source_type, PySourceType::Ipynb) {
debug!("Ignoring Jupyter notebook: {}", path.display());
continue;
}
// Convert to system paths.
let Ok(package) = package.map(SystemPathBuf::from_path_buf).transpose() else {
@@ -144,34 +147,13 @@ pub(crate) fn analyze_graph(
let root = root.clone();
let result = inner_result.clone();
scope.spawn(move |_| {
// Extract source code (handles both .py and .ipynb files)
let source_kind = match SourceKind::from_path(path.as_std_path(), source_type) {
Ok(Some(source_kind)) => source_kind,
Ok(None) => {
debug!("Skipping non-Python notebook: {path}");
return;
}
Err(err) => {
warn!("Failed to read source for {path}: {err}");
return;
}
};
let source_code = source_kind.source_code();
// Identify any imports via static analysis.
let mut imports = ModuleImports::detect(
&db,
source_code,
source_type,
&path,
package.as_deref(),
string_imports,
)
.unwrap_or_else(|err| {
warn!("Failed to generate import map for {path}: {err}");
ModuleImports::default()
});
let mut imports =
ModuleImports::detect(&db, &path, package.as_deref(), string_imports)
.unwrap_or_else(|err| {
warn!("Failed to generate import map for {path}: {err}");
ModuleImports::default()
});
debug!("Discovered {} imports for {}", imports.len(), path);

View File

@@ -370,7 +370,7 @@ pub(crate) fn format_source(
let line_index = LineIndex::from_source_text(unformatted);
let byte_range = range.to_text_range(unformatted, &line_index);
format_range(unformatted, byte_range, options).map(|formatted_range| {
let mut formatted = unformatted.clone();
let mut formatted = unformatted.to_string();
formatted.replace_range(
std::ops::Range::<usize>::from(formatted_range.source_range()),
formatted_range.as_code(),

View File

@@ -653,133 +653,3 @@ fn venv() -> Result<()> {
Ok(())
}
#[test]
fn notebook_basic() -> Result<()> {
let tempdir = TempDir::new()?;
let root = ChildPath::new(tempdir.path());
root.child("ruff").child("__init__.py").write_str("")?;
root.child("ruff")
.child("a.py")
.write_str(indoc::indoc! {r#"
def helper():
pass
"#})?;
// Create a basic notebook with a simple import
root.child("notebook.ipynb").write_str(indoc::indoc! {r#"
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ruff.a import helper"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
"#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"notebook.ipynb": [
"ruff/a.py"
],
"ruff/__init__.py": [],
"ruff/a.py": []
}
----- stderr -----
"###);
});
Ok(())
}
#[test]
fn notebook_with_magic() -> Result<()> {
let tempdir = TempDir::new()?;
let root = ChildPath::new(tempdir.path());
root.child("ruff").child("__init__.py").write_str("")?;
root.child("ruff")
.child("a.py")
.write_str(indoc::indoc! {r#"
def helper():
pass
"#})?;
// Create a notebook with IPython magic commands and imports
root.child("notebook.ipynb").write_str(indoc::indoc! {r#"
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"%load_ext autoreload\n",
"%autoreload 2"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from ruff.a import helper"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"version": "3.12.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
"#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"notebook.ipynb": [
"ruff/a.py"
],
"ruff/__init__.py": [],
"ruff/a.py": []
}
----- stderr -----
"###);
});
Ok(())
}

View File

@@ -146,7 +146,7 @@ static FREQTRADE: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
525,
400,
);
static PANDAS: Benchmark = Benchmark::new(

View File

@@ -723,11 +723,10 @@ impl ruff_cache::CacheKey for SystemPathBuf {
/// A slice of a virtual path on [`System`](super::System) (akin to [`str`]).
#[repr(transparent)]
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct SystemVirtualPath(str);
impl SystemVirtualPath {
pub const fn new(path: &str) -> &SystemVirtualPath {
pub fn new(path: &str) -> &SystemVirtualPath {
// SAFETY: SystemVirtualPath is marked as #[repr(transparent)] so the conversion from a
// *const str to a *const SystemVirtualPath is valid.
unsafe { &*(path as *const str as *const SystemVirtualPath) }
@@ -768,8 +767,8 @@ pub struct SystemVirtualPathBuf(String);
impl SystemVirtualPathBuf {
#[inline]
pub const fn as_path(&self) -> &SystemVirtualPath {
SystemVirtualPath::new(self.0.as_str())
pub fn as_path(&self) -> &SystemVirtualPath {
SystemVirtualPath::new(&self.0)
}
}
@@ -853,12 +852,6 @@ impl ruff_cache::CacheKey for SystemVirtualPathBuf {
}
}
impl Borrow<SystemVirtualPath> for SystemVirtualPathBuf {
fn borrow(&self) -> &SystemVirtualPath {
self.as_path()
}
}
/// Deduplicates identical paths and removes nested paths.
///
/// # Examples

View File

@@ -62,7 +62,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
generate_set(
output,
Set::Named {
name: set_name.clone(),
name: set_name.to_string(),
set: *sub_set,
},
parents,

View File

@@ -104,7 +104,7 @@ fn generate_set(output: &mut String, set: Set, parents: &mut Vec<Set>) {
generate_set(
output,
Set::Named {
name: set_name.clone(),
name: set_name.to_string(),
set: *sub_set,
},
parents,

View File

@@ -1006,7 +1006,7 @@ impl<Context> std::fmt::Debug for Align<'_, Context> {
/// Block indents indent a block of code, such as in a function body, and therefore insert a line
/// break before and after the content.
///
/// Doesn't create an indentation if the passed in content is empty.
/// Doesn't create an indentation if the passed in content is [`FormatElement.is_empty`].
///
/// # Examples
///

View File

@@ -487,7 +487,7 @@ pub trait FormatElements {
/// Represents the width by adding 1 to the actual width so that the width can be represented by a [`NonZeroU32`],
/// allowing [`TextWidth`] or [`Option<Width>`] fit in 4 bytes rather than 8.
///
/// This means that 2^32 cannot be precisely represented and instead has the same value as 2^32-1.
/// This means that 2^32 can not be precisely represented and instead has the same value as 2^32-1.
/// This imprecision shouldn't matter in practice because either text are longer than any configured line width
/// and thus, the text should break.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]

View File

@@ -3,9 +3,8 @@ use std::collections::{BTreeMap, BTreeSet};
use anyhow::Result;
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_python_ast::PySourceType;
use ruff_python_ast::helpers::to_module_path;
use ruff_python_parser::{ParseOptions, parse};
use ruff_python_parser::{Mode, ParseOptions, parse};
use crate::collector::Collector;
pub use crate::db::ModuleDb;
@@ -25,14 +24,13 @@ impl ModuleImports {
/// Detect the [`ModuleImports`] for a given Python file.
pub fn detect(
db: &ModuleDb,
source: &str,
source_type: PySourceType,
path: &SystemPath,
package: Option<&SystemPath>,
string_imports: StringImports,
) -> Result<Self> {
// Parse the source code.
let parsed = parse(source, ParseOptions::from(source_type))?;
// Read and parse the source code.
let source = std::fs::read_to_string(path)?;
let parsed = parse(&source, ParseOptions::from(Mode::Module))?;
let module_path =
package.and_then(|package| to_module_path(package.as_std_path(), path.as_std_path()));

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.14.4"
version = "0.14.3"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -22,7 +22,6 @@ DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
DAG(dag_id="class_timetable", timetable=NullTimetable())
DAG(dag_id="class_concurrency", concurrency=12)
DAG(dag_id="class_fail_stop", fail_stop=True)

View File

@@ -70,12 +70,3 @@ builtins.getattr(foo, "bar")
# Regression test for: https://github.com/astral-sh/ruff/issues/18353
setattr(foo, "__debug__", 0)
# Regression test for: https://github.com/astral-sh/ruff/issues/21126
# Non-NFKC attribute names should be marked as unsafe. Python normalizes identifiers in
# attribute access (obj.attr) using NFKC, but does not normalize string
# arguments passed to getattr/setattr. Rewriting `getattr(ns, "ſ")` to
# `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior.
# Example: the long s character "ſ" normalizes to "s" under NFKC.
getattr(foo, "ſ")
setattr(foo, "ſ", 1)

View File

@@ -145,11 +145,3 @@ with open("file.txt", "w") as f:
with open("file.txt", "w") as f:
for line in text:
f.write(line)
# See: https://github.com/astral-sh/ruff/issues/20785
import json
data = {"price": 100}
with open("test.json", "wb") as f:
f.write(json.dumps(data, indent=4).encode("utf-8"))

View File

@@ -85,9 +85,3 @@ Decimal("1234_5678") # Safe fix: preserves non-thousands separators
Decimal("0001_2345")
Decimal("000_1_2345")
Decimal("000_000")
# Test cases for underscores before sign
# https://github.com/astral-sh/ruff/issues/21186
Decimal("_-1") # Should flag as verbose
Decimal("_+1") # Should flag as verbose
Decimal("_-1_000") # Should flag as verbose

View File

@@ -64,8 +64,3 @@ _ = Decimal.from_float(True)
_ = Decimal.from_float(float("-nan"))
_ = Decimal.from_float(float("\x2dnan"))
_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
# See: https://github.com/astral-sh/ruff/issues/21257
# fixes must be safe
_ = Fraction.from_float(f=4.2)
_ = Fraction.from_decimal(dec=4)

View File

@@ -81,7 +81,3 @@ round(# a comment
round(
17 # a comment
)
# See: https://github.com/astral-sh/ruff/issues/21209
print(round(125, **{"ndigits": -2}))
print(round(125, *[-2]))

View File

@@ -43,6 +43,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pycodestyle::rules::ambiguous_variable_name(checker, name, name.range());
}
}
if checker.is_rule_enabled(Rule::NonlocalWithoutBinding) {
pylint::rules::nonlocal_without_binding(checker, nonlocal);
}
if checker.is_rule_enabled(Rule::NonlocalAndGlobal) {
pylint::rules::nonlocal_and_global(checker, nonlocal);
}

View File

@@ -73,8 +73,7 @@ use crate::rules::pyflakes::rules::{
UndefinedLocalWithNestedImportStarUsage, YieldOutsideFunction,
};
use crate::rules::pylint::rules::{
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, NonlocalWithoutBinding,
YieldFromInAsyncFunction,
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
};
use crate::rules::{flake8_pyi, flake8_type_checking, pyflakes, pyupgrade};
use crate::settings::rule_table::RuleTable;
@@ -642,10 +641,6 @@ impl SemanticSyntaxContext for Checker<'_> {
self.semantic.global(name)
}
fn has_nonlocal_binding(&self, name: &str) -> bool {
self.semantic.nonlocal(name).is_some()
}
fn report_semantic_error(&self, error: SemanticSyntaxError) {
match error.kind {
SemanticSyntaxErrorKind::LateFutureImport => {
@@ -722,12 +717,6 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(pyflakes::rules::ContinueOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => {
// PLE0117
if self.is_rule_enabled(Rule::NonlocalWithoutBinding) {
self.report_diagnostic(NonlocalWithoutBinding { name }, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)

View File

@@ -4,4 +4,4 @@ expression: content
---
syntax_errors.py:
1:15 invalid-syntax: Expected one or more symbol names after import
3:12 invalid-syntax: Expected `)`, found newline
3:12 invalid-syntax: Expected ')', found newline

View File

@@ -196,7 +196,6 @@ fn check_call_arguments(checker: &Checker, qualified_name: &QualifiedName, argum
match qualified_name.segments() {
["airflow", .., "DAG" | "dag"] => {
// with replacement
diagnostic_for_argument(checker, arguments, "concurrency", Some("max_active_tasks"));
diagnostic_for_argument(checker, arguments, "fail_stop", Some("fail_fast"));
diagnostic_for_argument(checker, arguments, "schedule_interval", Some("schedule"));
diagnostic_for_argument(checker, arguments, "timetable", Some("schedule"));

View File

@@ -28,8 +28,6 @@ AIR301 [*] `timetable` is removed in Airflow 3.0
22 |
23 | DAG(dag_id="class_timetable", timetable=NullTimetable())
| ^^^^^^^^^
24 |
25 | DAG(dag_id="class_concurrency", concurrency=12)
|
help: Use `schedule` instead
20 |
@@ -38,271 +36,249 @@ help: Use `schedule` instead
- DAG(dag_id="class_timetable", timetable=NullTimetable())
23 + DAG(dag_id="class_timetable", schedule=NullTimetable())
24 |
25 | DAG(dag_id="class_concurrency", concurrency=12)
26 |
AIR301 [*] `concurrency` is removed in Airflow 3.0
--> AIR301_args.py:25:33
|
23 | DAG(dag_id="class_timetable", timetable=NullTimetable())
24 |
25 | DAG(dag_id="class_concurrency", concurrency=12)
| ^^^^^^^^^^^
26 |
27 | DAG(dag_id="class_fail_stop", fail_stop=True)
|
help: Use `max_active_tasks` instead
22 |
23 | DAG(dag_id="class_timetable", timetable=NullTimetable())
24 |
- DAG(dag_id="class_concurrency", concurrency=12)
25 + DAG(dag_id="class_concurrency", max_active_tasks=12)
26 |
27 | DAG(dag_id="class_fail_stop", fail_stop=True)
28 |
25 |
26 | DAG(dag_id="class_fail_stop", fail_stop=True)
AIR301 [*] `fail_stop` is removed in Airflow 3.0
--> AIR301_args.py:27:31
--> AIR301_args.py:26:31
|
25 | DAG(dag_id="class_concurrency", concurrency=12)
26 |
27 | DAG(dag_id="class_fail_stop", fail_stop=True)
26 | DAG(dag_id="class_fail_stop", fail_stop=True)
| ^^^^^^^^^
28 |
29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
27 |
28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
|
help: Use `fail_fast` instead
23 | DAG(dag_id="class_timetable", timetable=NullTimetable())
24 |
25 | DAG(dag_id="class_concurrency", concurrency=12)
26 |
25 |
- DAG(dag_id="class_fail_stop", fail_stop=True)
27 + DAG(dag_id="class_fail_stop", fail_fast=True)
28 |
29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
30 |
26 + DAG(dag_id="class_fail_stop", fail_fast=True)
27 |
28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
29 |
AIR301 `default_view` is removed in Airflow 3.0
--> AIR301_args.py:29:34
--> AIR301_args.py:28:34
|
27 | DAG(dag_id="class_fail_stop", fail_stop=True)
28 |
29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
26 | DAG(dag_id="class_fail_stop", fail_stop=True)
27 |
28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
| ^^^^^^^^^^^^
30 |
31 | DAG(dag_id="class_orientation", orientation="BT")
29 |
30 | DAG(dag_id="class_orientation", orientation="BT")
|
AIR301 `orientation` is removed in Airflow 3.0
--> AIR301_args.py:31:33
--> AIR301_args.py:30:33
|
29 | DAG(dag_id="class_default_view", default_view="dag_default_view")
30 |
31 | DAG(dag_id="class_orientation", orientation="BT")
28 | DAG(dag_id="class_default_view", default_view="dag_default_view")
29 |
30 | DAG(dag_id="class_orientation", orientation="BT")
| ^^^^^^^^^^^
32 |
33 | allow_future_exec_dates_dag = DAG(dag_id="class_allow_future_exec_dates")
31 |
32 | allow_future_exec_dates_dag = DAG(dag_id="class_allow_future_exec_dates")
|
AIR301 [*] `schedule_interval` is removed in Airflow 3.0
--> AIR301_args.py:42:6
--> AIR301_args.py:41:6
|
42 | @dag(schedule_interval="0 * * * *")
41 | @dag(schedule_interval="0 * * * *")
| ^^^^^^^^^^^^^^^^^
43 | def decorator_schedule_interval():
44 | pass
42 | def decorator_schedule_interval():
43 | pass
|
help: Use `schedule` instead
39 | pass
38 | pass
39 |
40 |
41 |
- @dag(schedule_interval="0 * * * *")
42 + @dag(schedule="0 * * * *")
43 | def decorator_schedule_interval():
44 | pass
45 |
41 + @dag(schedule="0 * * * *")
42 | def decorator_schedule_interval():
43 | pass
44 |
AIR301 [*] `timetable` is removed in Airflow 3.0
--> AIR301_args.py:47:6
--> AIR301_args.py:46:6
|
47 | @dag(timetable=NullTimetable())
46 | @dag(timetable=NullTimetable())
| ^^^^^^^^^
48 | def decorator_timetable():
49 | pass
47 | def decorator_timetable():
48 | pass
|
help: Use `schedule` instead
44 | pass
43 | pass
44 |
45 |
46 |
- @dag(timetable=NullTimetable())
47 + @dag(schedule=NullTimetable())
48 | def decorator_timetable():
49 | pass
50 |
46 + @dag(schedule=NullTimetable())
47 | def decorator_timetable():
48 | pass
49 |
AIR301 [*] `execution_date` is removed in Airflow 3.0
--> AIR301_args.py:55:62
--> AIR301_args.py:54:62
|
53 | def decorator_deprecated_operator_args():
54 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
55 | task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
52 | def decorator_deprecated_operator_args():
53 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
54 | task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
| ^^^^^^^^^^^^^^
56 | )
57 | trigger_dagrun_op2 = TriggerDagRunOperator(
55 | )
56 | trigger_dagrun_op2 = TriggerDagRunOperator(
|
help: Use `logical_date` instead
52 | @dag()
53 | def decorator_deprecated_operator_args():
54 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
51 | @dag()
52 | def decorator_deprecated_operator_args():
53 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
- task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
55 + task_id="trigger_dagrun_op1", trigger_dag_id="test", logical_date="2024-12-04"
56 | )
57 | trigger_dagrun_op2 = TriggerDagRunOperator(
58 | task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
54 + task_id="trigger_dagrun_op1", trigger_dag_id="test", logical_date="2024-12-04"
55 | )
56 | trigger_dagrun_op2 = TriggerDagRunOperator(
57 | task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
AIR301 [*] `execution_date` is removed in Airflow 3.0
--> AIR301_args.py:58:62
--> AIR301_args.py:57:62
|
56 | )
57 | trigger_dagrun_op2 = TriggerDagRunOperator(
58 | task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
55 | )
56 | trigger_dagrun_op2 = TriggerDagRunOperator(
57 | task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
| ^^^^^^^^^^^^^^
59 | )
58 | )
|
help: Use `logical_date` instead
55 | task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
56 | )
57 | trigger_dagrun_op2 = TriggerDagRunOperator(
54 | task_id="trigger_dagrun_op1", trigger_dag_id="test", execution_date="2024-12-04"
55 | )
56 | trigger_dagrun_op2 = TriggerDagRunOperator(
- task_id="trigger_dagrun_op2", trigger_dag_id="test", execution_date="2024-12-04"
58 + task_id="trigger_dagrun_op2", trigger_dag_id="test", logical_date="2024-12-04"
59 | )
60 |
61 | branch_dt_op = datetime.BranchDateTimeOperator(
57 + task_id="trigger_dagrun_op2", trigger_dag_id="test", logical_date="2024-12-04"
58 | )
59 |
60 | branch_dt_op = datetime.BranchDateTimeOperator(
AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0
--> AIR301_args.py:62:33
--> AIR301_args.py:61:33
|
61 | branch_dt_op = datetime.BranchDateTimeOperator(
62 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
60 | branch_dt_op = datetime.BranchDateTimeOperator(
61 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
| ^^^^^^^^^^^^^^^^^^^^^^
63 | )
64 | branch_dt_op2 = BranchDateTimeOperator(
62 | )
63 | branch_dt_op2 = BranchDateTimeOperator(
|
help: Use `use_task_logical_date` instead
59 | )
60 |
61 | branch_dt_op = datetime.BranchDateTimeOperator(
58 | )
59 |
60 | branch_dt_op = datetime.BranchDateTimeOperator(
- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
62 + task_id="branch_dt_op", use_task_logical_date=True, task_concurrency=5
63 | )
64 | branch_dt_op2 = BranchDateTimeOperator(
65 | task_id="branch_dt_op2",
61 + task_id="branch_dt_op", use_task_logical_date=True, task_concurrency=5
62 | )
63 | branch_dt_op2 = BranchDateTimeOperator(
64 | task_id="branch_dt_op2",
AIR301 [*] `task_concurrency` is removed in Airflow 3.0
--> AIR301_args.py:62:62
--> AIR301_args.py:61:62
|
61 | branch_dt_op = datetime.BranchDateTimeOperator(
62 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
60 | branch_dt_op = datetime.BranchDateTimeOperator(
61 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
| ^^^^^^^^^^^^^^^^
63 | )
64 | branch_dt_op2 = BranchDateTimeOperator(
62 | )
63 | branch_dt_op2 = BranchDateTimeOperator(
|
help: Use `max_active_tis_per_dag` instead
59 | )
60 |
61 | branch_dt_op = datetime.BranchDateTimeOperator(
58 | )
59 |
60 | branch_dt_op = datetime.BranchDateTimeOperator(
- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
62 + task_id="branch_dt_op", use_task_execution_day=True, max_active_tis_per_dag=5
63 | )
64 | branch_dt_op2 = BranchDateTimeOperator(
65 | task_id="branch_dt_op2",
61 + task_id="branch_dt_op", use_task_execution_day=True, max_active_tis_per_dag=5
62 | )
63 | branch_dt_op2 = BranchDateTimeOperator(
64 | task_id="branch_dt_op2",
AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0
--> AIR301_args.py:66:9
--> AIR301_args.py:65:9
|
64 | branch_dt_op2 = BranchDateTimeOperator(
65 | task_id="branch_dt_op2",
66 | use_task_execution_day=True,
63 | branch_dt_op2 = BranchDateTimeOperator(
64 | task_id="branch_dt_op2",
65 | use_task_execution_day=True,
| ^^^^^^^^^^^^^^^^^^^^^^
67 | sla=timedelta(seconds=10),
68 | )
66 | sla=timedelta(seconds=10),
67 | )
|
help: Use `use_task_logical_date` instead
63 | )
64 | branch_dt_op2 = BranchDateTimeOperator(
65 | task_id="branch_dt_op2",
62 | )
63 | branch_dt_op2 = BranchDateTimeOperator(
64 | task_id="branch_dt_op2",
- use_task_execution_day=True,
66 + use_task_logical_date=True,
67 | sla=timedelta(seconds=10),
68 | )
69 |
65 + use_task_logical_date=True,
66 | sla=timedelta(seconds=10),
67 | )
68 |
AIR301 [*] `use_task_execution_day` is removed in Airflow 3.0
--> AIR301_args.py:93:9
--> AIR301_args.py:92:9
|
91 | follow_task_ids_if_true=None,
92 | week_day=1,
93 | use_task_execution_day=True,
90 | follow_task_ids_if_true=None,
91 | week_day=1,
92 | use_task_execution_day=True,
| ^^^^^^^^^^^^^^^^^^^^^^
94 | )
93 | )
|
help: Use `use_task_logical_date` instead
90 | follow_task_ids_if_false=None,
91 | follow_task_ids_if_true=None,
92 | week_day=1,
89 | follow_task_ids_if_false=None,
90 | follow_task_ids_if_true=None,
91 | week_day=1,
- use_task_execution_day=True,
93 + use_task_logical_date=True,
94 | )
95 |
96 | trigger_dagrun_op >> trigger_dagrun_op2
92 + use_task_logical_date=True,
93 | )
94 |
95 | trigger_dagrun_op >> trigger_dagrun_op2
AIR301 `filename_template` is removed in Airflow 3.0
--> AIR301_args.py:103:15
--> AIR301_args.py:102:15
|
102 | # deprecated filename_template argument in FileTaskHandler
103 | S3TaskHandler(filename_template="/tmp/test")
101 | # deprecated filename_template argument in FileTaskHandler
102 | S3TaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^
104 | HdfsTaskHandler(filename_template="/tmp/test")
105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
103 | HdfsTaskHandler(filename_template="/tmp/test")
104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
|
AIR301 `filename_template` is removed in Airflow 3.0
--> AIR301_args.py:104:17
--> AIR301_args.py:103:17
|
102 | # deprecated filename_template argument in FileTaskHandler
103 | S3TaskHandler(filename_template="/tmp/test")
104 | HdfsTaskHandler(filename_template="/tmp/test")
101 | # deprecated filename_template argument in FileTaskHandler
102 | S3TaskHandler(filename_template="/tmp/test")
103 | HdfsTaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^
105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
106 | GCSTaskHandler(filename_template="/tmp/test")
104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
105 | GCSTaskHandler(filename_template="/tmp/test")
|
AIR301 `filename_template` is removed in Airflow 3.0
--> AIR301_args.py:105:26
--> AIR301_args.py:104:26
|
103 | S3TaskHandler(filename_template="/tmp/test")
104 | HdfsTaskHandler(filename_template="/tmp/test")
105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
102 | S3TaskHandler(filename_template="/tmp/test")
103 | HdfsTaskHandler(filename_template="/tmp/test")
104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^
106 | GCSTaskHandler(filename_template="/tmp/test")
105 | GCSTaskHandler(filename_template="/tmp/test")
|
AIR301 `filename_template` is removed in Airflow 3.0
--> AIR301_args.py:106:16
--> AIR301_args.py:105:16
|
104 | HdfsTaskHandler(filename_template="/tmp/test")
105 | ElasticsearchTaskHandler(filename_template="/tmp/test")
106 | GCSTaskHandler(filename_template="/tmp/test")
103 | HdfsTaskHandler(filename_template="/tmp/test")
104 | ElasticsearchTaskHandler(filename_template="/tmp/test")
105 | GCSTaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^
107 |
108 | FabAuthManager(None)
106 |
107 | FabAuthManager(None)
|
AIR301 `appbuilder` is removed in Airflow 3.0
--> AIR301_args.py:108:15
--> AIR301_args.py:107:15
|
106 | GCSTaskHandler(filename_template="/tmp/test")
107 |
108 | FabAuthManager(None)
105 | GCSTaskHandler(filename_template="/tmp/test")
106 |
107 | FabAuthManager(None)
| ^^^^^^
|
help: The constructor takes no parameter now

View File

@@ -3,7 +3,6 @@ use ruff_python_ast::{self as ast, Expr};
use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use ruff_source_file::LineRanges;
use ruff_text_size::Ranged;
use unicode_normalization::UnicodeNormalization;
use crate::checkers::ast::Checker;
use crate::fix::edits::pad;
@@ -30,21 +29,6 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
/// obj.foo
/// ```
///
/// ## Fix safety
/// The fix is marked as unsafe for attribute names that are not in NFKC (Normalization Form KC)
/// normalization. Python normalizes identifiers using NFKC when using attribute access syntax
/// (e.g., `obj.attr`), but does not normalize string arguments passed to `getattr`. Rewriting
/// `getattr(obj, "ſ")` to `obj.ſ` would be interpreted as `obj.s` at runtime, changing behavior.
///
/// For example, the long s character `"ſ"` normalizes to `"s"` under NFKC, so:
/// ```python
/// # This accesses an attribute with the exact name "ſ" (if it exists)
/// value = getattr(obj, "ſ")
///
/// # But this would normalize to "s" and access a different attribute
/// obj.ſ # This is interpreted as obj.s, not obj.ſ
/// ```
///
/// ## References
/// - [Python documentation: `getattr`](https://docs.python.org/3/library/functions.html#getattr)
#[derive(ViolationMetadata)]
@@ -85,14 +69,8 @@ pub(crate) fn getattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr,
return;
}
// Mark fixes as unsafe for non-NFKC attribute names. Python normalizes identifiers using NFKC, so using
// attribute syntax (e.g., `obj.attr`) would normalize the name and potentially change
// program behavior.
let attr_name = value.to_str();
let is_unsafe = attr_name.nfkc().collect::<String>() != attr_name;
let mut diagnostic = checker.report_diagnostic(GetAttrWithConstant, expr.range());
let edit = Edit::range_replacement(
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
pad(
if matches!(
obj,
@@ -110,11 +88,5 @@ pub(crate) fn getattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr,
checker.locator(),
),
expr.range(),
);
let fix = if is_unsafe {
Fix::unsafe_edit(edit)
} else {
Fix::safe_edit(edit)
};
diagnostic.set_fix(fix);
)));
}

View File

@@ -4,7 +4,6 @@ use ruff_text_size::{Ranged, TextRange};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_codegen::Generator;
use ruff_python_stdlib::identifiers::{is_identifier, is_mangled_private};
use unicode_normalization::UnicodeNormalization;
use crate::checkers::ast::Checker;
use crate::{AlwaysFixableViolation, Edit, Fix};
@@ -29,23 +28,6 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
/// obj.foo = 42
/// ```
///
/// ## Fix safety
/// The fix is marked as unsafe for attribute names that are not in NFKC (Normalization Form KC)
/// normalization. Python normalizes identifiers using NFKC when using attribute access syntax
/// (e.g., `obj.attr = value`), but does not normalize string arguments passed to `setattr`.
/// Rewriting `setattr(obj, "ſ", 1)` to `obj.ſ = 1` would be interpreted as `obj.s = 1` at
/// runtime, changing behavior.
///
/// For example, the long s character `"ſ"` normalizes to `"s"` under NFKC, so:
/// ```python
/// # This creates an attribute with the exact name "ſ"
/// setattr(obj, "ſ", 1)
/// getattr(obj, "ſ") # Returns 1
///
/// # But this would normalize to "s" and set a different attribute
/// obj.ſ = 1 # This is interpreted as obj.s = 1, not obj.ſ = 1
/// ```
///
/// ## References
/// - [Python documentation: `setattr`](https://docs.python.org/3/library/functions.html#setattr)
#[derive(ViolationMetadata)]
@@ -107,12 +89,6 @@ pub(crate) fn setattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr,
return;
}
// Mark fixes as unsafe for non-NFKC attribute names. Python normalizes identifiers using NFKC, so using
// attribute syntax (e.g., `obj.attr = value`) would normalize the name and potentially change
// program behavior.
let attr_name = name.to_str();
let is_unsafe = attr_name.nfkc().collect::<String>() != attr_name;
// We can only replace a `setattr` call (which is an `Expr`) with an assignment
// (which is a `Stmt`) if the `Expr` is already being used as a `Stmt`
// (i.e., it's directly within an `Stmt::Expr`).
@@ -124,16 +100,10 @@ pub(crate) fn setattr_with_constant(checker: &Checker, expr: &Expr, func: &Expr,
{
if expr == child.as_ref() {
let mut diagnostic = checker.report_diagnostic(SetAttrWithConstant, expr.range());
let edit = Edit::range_replacement(
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
assignment(obj, name.to_str(), value, checker.generator()),
expr.range(),
);
let fix = if is_unsafe {
Fix::unsafe_edit(edit)
} else {
Fix::safe_edit(edit)
};
diagnostic.set_fix(fix);
)));
}
}
}

View File

@@ -360,21 +360,3 @@ help: Replace `getattr` with attribute access
70 |
71 | # Regression test for: https://github.com/astral-sh/ruff/issues/18353
72 | setattr(foo, "__debug__", 0)
B009 [*] Do not call `getattr` with a constant attribute value. It is not any safer than normal property access.
--> B009_B010.py:80:1
|
78 | # `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior.
79 | # Example: the long s character "ſ" normalizes to "s" under NFKC.
80 | getattr(foo, "ſ")
| ^^^^^^^^^^^^^^^^^
81 | setattr(foo, "ſ", 1)
|
help: Replace `getattr` with attribute access
77 | # arguments passed to getattr/setattr. Rewriting `getattr(ns, "ſ")` to
78 | # `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior.
79 | # Example: the long s character "ſ" normalizes to "s" under NFKC.
- getattr(foo, "ſ")
80 + foo.ſ
81 | setattr(foo, "ſ", 1)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -118,19 +118,3 @@ help: Replace `setattr` with assignment
56 |
57 | # Regression test for: https://github.com/astral-sh/ruff/issues/7455#issuecomment-1722458885
58 | assert getattr(func, '_rpc')is True
B010 [*] Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
--> B009_B010.py:81:1
|
79 | # Example: the long s character "ſ" normalizes to "s" under NFKC.
80 | getattr(foo, "ſ")
81 | setattr(foo, "ſ", 1)
| ^^^^^^^^^^^^^^^^^^^^
|
help: Replace `setattr` with assignment
78 | # `ns.ſ` would be interpreted as `ns.s` at runtime, changing behavior.
79 | # Example: the long s character "ſ" normalizes to "s" under NFKC.
80 | getattr(foo, "ſ")
- setattr(foo, "ſ", 1)
81 + foo.ſ = 1
note: This is an unsafe fix and may change runtime behavior

View File

@@ -43,7 +43,7 @@ use crate::rules::flake8_comprehensions::fixes;
/// >>> {x: y for x, y in d1} # Iterates over the keys of a mapping
/// {1: 2, 4: 5}
/// >>> dict(d1) # Ruff's incorrect suggested fix
/// {(1, 2): 3, (4, 5): 6}
/// (1, 2): 3, (4, 5): 6}
/// >>> dict(d1.keys()) # Correct fix
/// {1: 2, 4: 5}
/// ```

View File

@@ -78,7 +78,7 @@ pub(crate) fn unconventional_import_alias(
let mut diagnostic = checker.report_diagnostic(
UnconventionalImportAlias {
name: qualified_name,
asname: expected_alias.clone(),
asname: expected_alias.to_string(),
},
binding.range(),
);

View File

@@ -6,17 +6,21 @@ use ruff_macros::CacheKey;
#[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum ParametrizeNameType {
#[serde(rename = "csv")]
Csv,
#[serde(rename = "tuple")]
#[default]
Tuple,
#[serde(rename = "list")]
List,
}
impl Default for ParametrizeNameType {
fn default() -> Self {
Self::Tuple
}
}
impl Display for ParametrizeNameType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
@@ -29,15 +33,19 @@ impl Display for ParametrizeNameType {
#[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum ParametrizeValuesType {
#[serde(rename = "tuple")]
Tuple,
#[serde(rename = "list")]
#[default]
List,
}
impl Default for ParametrizeValuesType {
fn default() -> Self {
Self::List
}
}
impl Display for ParametrizeValuesType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
@@ -49,15 +57,19 @@ impl Display for ParametrizeValuesType {
#[derive(Clone, Copy, Debug, CacheKey, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum ParametrizeValuesRowType {
#[serde(rename = "tuple")]
#[default]
Tuple,
#[serde(rename = "list")]
List,
}
impl Default for ParametrizeValuesRowType {
fn default() -> Self {
Self::Tuple
}
}
impl Display for ParametrizeValuesRowType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {

View File

@@ -9,15 +9,19 @@ use ruff_macros::CacheKey;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum Quote {
/// Use double quotes.
#[default]
Double,
/// Use single quotes.
Single,
}
impl Default for Quote {
fn default() -> Self {
Self::Double
}
}
impl From<ruff_python_ast::str::Quote> for Quote {
fn from(value: ruff_python_ast::str::Quote) -> Self {
match value {

View File

@@ -116,7 +116,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) {
let mut diagnostic = checker.report_diagnostic(
ReimplementedBuiltin {
replacement: contents.clone(),
replacement: contents.to_string(),
},
TextRange::new(stmt.start(), terminal.stmt.end()),
);
@@ -212,7 +212,7 @@ pub(crate) fn convert_for_loop_to_any_all(checker: &Checker, stmt: &Stmt) {
let mut diagnostic = checker.report_diagnostic(
ReimplementedBuiltin {
replacement: contents.clone(),
replacement: contents.to_string(),
},
TextRange::new(stmt.start(), terminal.stmt.end()),
);

View File

@@ -47,7 +47,7 @@ pub(crate) fn banned_api<T: Ranged>(checker: &Checker, policy: &NameMatchPolicy,
checker.report_diagnostic(
BannedApi {
name: banned_module,
message: reason.msg.clone(),
message: reason.msg.to_string(),
},
node.range(),
);
@@ -74,8 +74,8 @@ pub(crate) fn banned_attribute_access(checker: &Checker, expr: &Expr) {
{
checker.report_diagnostic(
BannedApi {
name: banned_path.clone(),
message: ban.msg.clone(),
name: banned_path.to_string(),
message: ban.msg.to_string(),
},
expr.range(),
);

View File

@@ -20,17 +20,21 @@ use super::categorize::ImportSection;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[derive(Default)]
pub enum RelativeImportsOrder {
/// Place "closer" imports (fewer `.` characters, most local) before
/// "further" imports (more `.` characters, least local).
ClosestToFurthest,
/// Place "further" imports (more `.` characters, least local) imports
/// before "closer" imports (fewer `.` characters, most local).
#[default]
FurthestToClosest,
}
impl Default for RelativeImportsOrder {
fn default() -> Self {
Self::FurthestToClosest
}
}
impl Display for RelativeImportsOrder {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {

View File

@@ -427,7 +427,7 @@ pub(crate) fn literal_comparisons(checker: &Checker, compare: &ast::ExprCompare)
for diagnostic in &mut diagnostics {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
content.clone(),
content.to_string(),
compare.range(),
)));
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:2:7
|
1 | #: E231
@@ -18,7 +18,7 @@ help: Add missing whitespace
4 | a[b1,:]
5 | #: E231
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:4:5
|
2 | a = (1,2)
@@ -38,7 +38,7 @@ help: Add missing whitespace
6 | a = [{'a':''}]
7 | #: Okay
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:6:10
|
4 | a[b1,:]
@@ -58,7 +58,7 @@ help: Add missing whitespace
8 | a = (4,)
9 | b = (5, )
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:19:10
|
17 | def foo() -> None:
@@ -77,7 +77,7 @@ help: Add missing whitespace
21 |
22 | #: Okay
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:29:20
|
27 | mdtypes_template = {
@@ -96,7 +96,7 @@ help: Add missing whitespace
31 |
32 | # E231
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:33:6
|
32 | # E231
@@ -115,7 +115,7 @@ help: Add missing whitespace
35 | # Okay because it's hard to differentiate between the usages of a colon in a f-string
36 | f"{a:=1}"
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:47:37
|
46 | #: E231
@@ -134,7 +134,7 @@ help: Add missing whitespace
49 | #: Okay
50 | a = (1,)
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:60:13
|
58 | results = {
@@ -154,7 +154,7 @@ help: Add missing whitespace
62 | results_in_tuple = (
63 | {
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:65:17
|
63 | {
@@ -174,7 +174,7 @@ help: Add missing whitespace
67 | )
68 | results_in_list = [
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:71:17
|
69 | {
@@ -194,7 +194,7 @@ help: Add missing whitespace
73 | ]
74 | results_in_list_first = [
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:76:17
|
74 | results_in_list_first = [
@@ -214,7 +214,7 @@ help: Add missing whitespace
78 | ]
79 |
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:82:13
|
80 | x = [
@@ -234,7 +234,7 @@ help: Add missing whitespace
84 | "k3":[2], # E231
85 | "k4": [2],
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:84:13
|
82 | "k1":[2], # E231
@@ -254,7 +254,7 @@ help: Add missing whitespace
86 | "k5": [2],
87 | "k6": [1, 2, 3, 4,5,6,7] # E231
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:87:26
|
85 | "k4": [2],
@@ -274,7 +274,7 @@ help: Add missing whitespace
89 | {
90 | "k1": [
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:87:28
|
85 | "k4": [2],
@@ -294,7 +294,7 @@ help: Add missing whitespace
89 | {
90 | "k1": [
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:87:30
|
85 | "k4": [2],
@@ -314,7 +314,7 @@ help: Add missing whitespace
89 | {
90 | "k1": [
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:92:21
|
90 | "k1": [
@@ -334,7 +334,7 @@ help: Add missing whitespace
94 | {
95 | "kb": [2,3], # E231
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:92:24
|
90 | "k1": [
@@ -354,7 +354,7 @@ help: Add missing whitespace
94 | {
95 | "kb": [2,3], # E231
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:95:25
|
93 | },
@@ -374,7 +374,7 @@ help: Add missing whitespace
97 | {
98 | "ka":[2, 3], # E231
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:98:21
|
96 | },
@@ -394,7 +394,7 @@ help: Add missing whitespace
100 | "kc": [2, 3], # Ok
101 | "kd": [2,3], # E231
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:101:25
|
99 | "kb": [2, 3], # Ok
@@ -414,7 +414,7 @@ help: Add missing whitespace
103 | },
104 | ]
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:102:21
|
100 | "kc": [2, 3], # Ok
@@ -434,7 +434,7 @@ help: Add missing whitespace
104 | ]
105 | }
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:102:24
|
100 | "kc": [2, 3], # Ok
@@ -454,7 +454,7 @@ help: Add missing whitespace
104 | ]
105 | }
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:109:18
|
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
@@ -473,7 +473,7 @@ help: Add missing whitespace
111 | y:B = [[["foo", "bar"]]],
112 | z:object = "fooo",
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:109:40
|
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
@@ -492,7 +492,7 @@ help: Add missing whitespace
111 | y:B = [[["foo", "bar"]]],
112 | z:object = "fooo",
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:109:70
|
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
@@ -511,7 +511,7 @@ help: Add missing whitespace
111 | y:B = [[["foo", "bar"]]],
112 | z:object = "fooo",
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:110:6
|
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
@@ -531,7 +531,7 @@ help: Add missing whitespace
112 | z:object = "fooo",
113 | ):
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:111:6
|
109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
@@ -551,7 +551,7 @@ help: Add missing whitespace
113 | ):
114 | pass
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:112:6
|
110 | x:A = "foo"[::-1],
@@ -571,7 +571,7 @@ help: Add missing whitespace
114 | pass
115 |
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:116:18
|
114 | pass
@@ -591,7 +591,7 @@ help: Add missing whitespace
118 | self,
119 | x:A = "foo"[::-1],
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:116:40
|
114 | pass
@@ -611,7 +611,7 @@ help: Add missing whitespace
118 | self,
119 | x:A = "foo"[::-1],
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:116:70
|
114 | pass
@@ -631,7 +631,7 @@ help: Add missing whitespace
118 | self,
119 | x:A = "foo"[::-1],
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:117:29
|
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
@@ -650,7 +650,7 @@ help: Add missing whitespace
119 | x:A = "foo"[::-1],
120 | y:B = [[["foo", "bar"]]],
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:117:51
|
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
@@ -669,7 +669,7 @@ help: Add missing whitespace
119 | x:A = "foo"[::-1],
120 | y:B = [[["foo", "bar"]]],
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:117:81
|
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
@@ -688,7 +688,7 @@ help: Add missing whitespace
119 | x:A = "foo"[::-1],
120 | y:B = [[["foo", "bar"]]],
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:119:10
|
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
@@ -708,7 +708,7 @@ help: Add missing whitespace
121 | z:object = "fooo",
122 | ):
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:120:10
|
118 | self,
@@ -728,7 +728,7 @@ help: Add missing whitespace
122 | ):
123 | pass
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:121:10
|
119 | x:A = "foo"[::-1],
@@ -748,7 +748,7 @@ help: Add missing whitespace
123 | pass
124 |
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:125:32
|
123 | pass
@@ -768,7 +768,7 @@ help: Add missing whitespace
127 | pass
128 |
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:125:54
|
123 | pass
@@ -788,7 +788,7 @@ help: Add missing whitespace
127 | pass
128 |
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:125:84
|
123 | pass
@@ -808,7 +808,7 @@ help: Add missing whitespace
127 | pass
128 |
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:126:47
|
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
@@ -826,7 +826,7 @@ help: Add missing whitespace
128 |
129 | # Should be no E231 errors on any of these:
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:126:69
|
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
@@ -844,7 +844,7 @@ help: Add missing whitespace
128 |
129 | # Should be no E231 errors on any of these:
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:126:99
|
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
@@ -862,7 +862,7 @@ help: Add missing whitespace
128 |
129 | # Should be no E231 errors on any of these:
E231 [*] Missing whitespace after `,`
E231 [*] Missing whitespace after ','
--> E23.py:147:6
|
146 | # E231
@@ -881,7 +881,7 @@ help: Add missing whitespace
149 | # Okay because it's hard to differentiate between the usages of a colon in a t-string
150 | t"{a:=1}"
E231 [*] Missing whitespace after `:`
E231 [*] Missing whitespace after ':'
--> E23.py:161:37
|
160 | #: E231

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
invalid-syntax: Expected `]`, found `(`
invalid-syntax: Expected ']', found '('
--> E30_syntax_error.py:4:15
|
2 | # parenthesis.
@@ -11,7 +11,7 @@ invalid-syntax: Expected `]`, found `(`
5 | pass
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:13:18
|
12 | class Foo:
@@ -32,7 +32,7 @@ E301 Expected 1 blank line, found 0
|
help: Add missing blank line
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:18:11
|
16 | pass
@@ -41,7 +41,7 @@ invalid-syntax: Expected `)`, found newline
| ^
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:21:9
|
21 | def top(

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
invalid-syntax: Expected `]`, found `(`
invalid-syntax: Expected ']', found '('
--> E30_syntax_error.py:4:15
|
2 | # parenthesis.
@@ -22,7 +22,7 @@ E302 Expected 2 blank lines, found 1
|
help: Add missing blank line(s)
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:13:18
|
12 | class Foo:
@@ -32,7 +32,7 @@ invalid-syntax: Expected `)`, found newline
15 | def method():
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:18:11
|
16 | pass
@@ -41,7 +41,7 @@ invalid-syntax: Expected `)`, found newline
| ^
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:21:9
|
21 | def top(

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
invalid-syntax: Expected `]`, found `(`
invalid-syntax: Expected ']', found '('
--> E30_syntax_error.py:4:15
|
2 | # parenthesis.
@@ -21,7 +21,7 @@ E303 Too many blank lines (3)
|
help: Remove extraneous blank line(s)
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:13:18
|
12 | class Foo:
@@ -31,7 +31,7 @@ invalid-syntax: Expected `)`, found newline
15 | def method():
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:18:11
|
16 | pass
@@ -40,7 +40,7 @@ invalid-syntax: Expected `)`, found newline
| ^
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:21:9
|
21 | def top(

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
invalid-syntax: Expected `]`, found `(`
invalid-syntax: Expected ']', found '('
--> E30_syntax_error.py:4:15
|
2 | # parenthesis.
@@ -11,7 +11,7 @@ invalid-syntax: Expected `]`, found `(`
5 | pass
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:13:18
|
12 | class Foo:
@@ -31,7 +31,7 @@ E305 Expected 2 blank lines after class or function definition, found (1)
|
help: Add missing blank line(s)
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:18:11
|
16 | pass
@@ -40,7 +40,7 @@ invalid-syntax: Expected `)`, found newline
| ^
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:21:9
|
21 | def top(

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
invalid-syntax: Expected `]`, found `(`
invalid-syntax: Expected ']', found '('
--> E30_syntax_error.py:4:15
|
2 | # parenthesis.
@@ -11,7 +11,7 @@ invalid-syntax: Expected `]`, found `(`
5 | pass
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:13:18
|
12 | class Foo:
@@ -21,7 +21,7 @@ invalid-syntax: Expected `)`, found newline
15 | def method():
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:18:11
|
16 | pass
@@ -30,7 +30,7 @@ invalid-syntax: Expected `)`, found newline
| ^
|
invalid-syntax: Expected `)`, found newline
invalid-syntax: Expected ')', found newline
--> E30_syntax_error.py:21:9
|
21 | def top(

View File

@@ -94,7 +94,7 @@ pub(crate) fn capitalized(checker: &Checker, docstring: &Docstring) {
let mut diagnostic = checker.report_diagnostic(
FirstWordUncapitalized {
first_word: first_word.to_string(),
capitalized_word: capitalized_word.clone(),
capitalized_word: capitalized_word.to_string(),
},
docstring.range(),
);

View File

@@ -1,6 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `nonlocal` names without bindings.
@@ -43,3 +46,19 @@ impl Violation for NonlocalWithoutBinding {
format!("Nonlocal name `{name}` found without binding")
}
}
/// PLE0117
pub(crate) fn nonlocal_without_binding(checker: &Checker, nonlocal: &ast::StmtNonlocal) {
if !checker.semantic().scope_id.is_global() {
for name in &nonlocal.names {
if checker.semantic().nonlocal(name).is_none() {
checker.report_diagnostic(
NonlocalWithoutBinding {
name: name.to_string(),
},
name.range(),
);
}
}
}
}

View File

@@ -188,7 +188,7 @@ pub(crate) fn bit_count(checker: &Checker, call: &ExprCall) {
let mut diagnostic = checker.report_diagnostic(
BitCount {
existing: SourceCodeSnippet::from_str(literal_text),
replacement: SourceCodeSnippet::new(replacement.clone()),
replacement: SourceCodeSnippet::new(replacement.to_string()),
},
call.range(),
);

View File

@@ -62,11 +62,40 @@ pub(crate) fn hardcoded_string_charset_literal(checker: &Checker, expr: &ExprStr
struct NamedCharset {
name: &'static str,
bytes: &'static [u8],
ascii_char_set: AsciiCharSet,
}
/// Represents the set of ascii characters in form of a bitset.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct AsciiCharSet(u128);
impl AsciiCharSet {
/// Creates the set of ascii characters from `bytes`.
/// Returns None if there is non-ascii byte.
const fn from_bytes(bytes: &[u8]) -> Option<Self> {
// TODO: simplify implementation, when const-traits are supported
// https://github.com/rust-lang/rust-project-goals/issues/106
let mut bitset = 0;
let mut i = 0;
while i < bytes.len() {
if !bytes[i].is_ascii() {
return None;
}
bitset |= 1 << bytes[i];
i += 1;
}
Some(Self(bitset))
}
}
impl NamedCharset {
const fn new(name: &'static str, bytes: &'static [u8]) -> Self {
Self { name, bytes }
Self {
name,
bytes,
// SAFETY: The named charset is guaranteed to have only ascii bytes.
ascii_char_set: AsciiCharSet::from_bytes(bytes).unwrap(),
}
}
}

View File

@@ -149,9 +149,10 @@ pub(crate) fn unnecessary_from_float(checker: &Checker, call: &ExprCall) {
// Check if we should suppress the fix due to type validation concerns
let is_type_safe = is_valid_argument_type(arg_value, method_name, constructor, checker);
let has_keywords = !call.arguments.keywords.is_empty();
// Determine fix safety
let applicability = if is_type_safe {
let applicability = if is_type_safe && !has_keywords {
Applicability::Safe
} else {
Applicability::Unsafe
@@ -209,27 +210,21 @@ fn is_valid_argument_type(
_ => false,
},
// Fraction.from_decimal accepts int, bool, Decimal
(MethodName::FromDecimal, Constructor::Fraction) => {
// First check if it's a Decimal constructor call
let is_decimal_call = arg_expr
.as_call_expr()
.and_then(|call| semantic.resolve_qualified_name(&call.func))
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["decimal", "Decimal"])
});
if is_decimal_call {
return true;
(MethodName::FromDecimal, Constructor::Fraction) => match resolved_type {
ResolvedPythonType::Atom(PythonType::Number(
NumberLike::Integer | NumberLike::Bool,
)) => true,
ResolvedPythonType::Unknown => is_int,
_ => {
// Check if it's a Decimal instance
arg_expr
.as_call_expr()
.and_then(|call| semantic.resolve_qualified_name(&call.func))
.is_some_and(|qualified_name| {
matches!(qualified_name.segments(), ["decimal", "Decimal"])
})
}
match resolved_type {
ResolvedPythonType::Atom(PythonType::Number(
NumberLike::Integer | NumberLike::Bool,
)) => true,
ResolvedPythonType::Unknown => is_int,
_ => false,
}
}
},
_ => false,
}
}
@@ -279,7 +274,7 @@ fn handle_non_finite_float_special_case(
return None;
}
let Expr::Call(ExprCall {
let Expr::Call(ast::ExprCall {
func, arguments, ..
}) = arg_value
else {

View File

@@ -93,21 +93,16 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
// https://github.com/python/cpython/blob/ac556a2ad1213b8bb81372fe6fb762f5fcb076de/Lib/_pydecimal.py#L6060-L6077
// _after_ trimming whitespace from the string and removing all occurrences of "_".
let original_str = str_literal.to_str().trim_whitespace();
// Strip leading underscores before extracting the sign, as Python's Decimal parser
// removes underscores before parsing the sign.
let sign_check_str = original_str.trim_start_matches('_');
// Extract the unary sign, if any.
let (unary, sign_check_str) = if let Some(trimmed) = sign_check_str.strip_prefix('+') {
let (unary, original_str) = if let Some(trimmed) = original_str.strip_prefix('+') {
("+", trimmed)
} else if let Some(trimmed) = sign_check_str.strip_prefix('-') {
} else if let Some(trimmed) = original_str.strip_prefix('-') {
("-", trimmed)
} else {
("", sign_check_str)
("", original_str)
};
// Save the string after the sign for normalization (before removing underscores)
let str_after_sign_for_normalization = sign_check_str;
let mut rest = Cow::from(sign_check_str);
let has_digit_separators = memchr::memchr(b'_', original_str.as_bytes()).is_some();
let mut rest = Cow::from(original_str);
let has_digit_separators = memchr::memchr(b'_', rest.as_bytes()).is_some();
if has_digit_separators {
rest = Cow::from(rest.replace('_', ""));
}
@@ -128,7 +123,7 @@ pub(crate) fn verbose_decimal_constructor(checker: &Checker, call: &ast::ExprCal
// If the original string had digit separators, normalize them
let rest = if has_digit_separators {
Cow::from(normalize_digit_separators(str_after_sign_for_normalization))
Cow::from(normalize_digit_separators(original_str))
} else {
Cow::from(rest)
};

View File

@@ -5,6 +5,7 @@ use ruff_python_ast::{
relocate::relocate_expr,
visitor::{self, Visitor},
};
use ruff_python_codegen::Generator;
use ruff_text_size::{Ranged, TextRange};

View File

@@ -134,14 +134,3 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo
75 | f.write(foobar)
|
help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
FURB103 `open` and `write` should be replaced by `Path("test.json")....`
--> FURB103.py:154:6
|
152 | data = {"price": 100}
153 |
154 | with open("test.json", "wb") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
155 | f.write(json.dumps(data, indent=4).encode("utf-8"))
|
help: Replace with `Path("test.json")....`

View File

@@ -669,7 +669,6 @@ help: Replace with `1_2345`
85 + Decimal(1_2345)
86 | Decimal("000_1_2345")
87 | Decimal("000_000")
88 |
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:86:9
@@ -687,8 +686,6 @@ help: Replace with `1_2345`
- Decimal("000_1_2345")
86 + Decimal(1_2345)
87 | Decimal("000_000")
88 |
89 | # Test cases for underscores before sign
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:87:9
@@ -697,8 +694,6 @@ FURB157 [*] Verbose expression in `Decimal` constructor
86 | Decimal("000_1_2345")
87 | Decimal("000_000")
| ^^^^^^^^^
88 |
89 | # Test cases for underscores before sign
|
help: Replace with `0`
84 | # Separators _and_ leading zeros
@@ -706,57 +701,3 @@ help: Replace with `0`
86 | Decimal("000_1_2345")
- Decimal("000_000")
87 + Decimal(0)
88 |
89 | # Test cases for underscores before sign
90 | # https://github.com/astral-sh/ruff/issues/21186
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:91:9
|
89 | # Test cases for underscores before sign
90 | # https://github.com/astral-sh/ruff/issues/21186
91 | Decimal("_-1") # Should flag as verbose
| ^^^^^
92 | Decimal("_+1") # Should flag as verbose
93 | Decimal("_-1_000") # Should flag as verbose
|
help: Replace with `-1`
88 |
89 | # Test cases for underscores before sign
90 | # https://github.com/astral-sh/ruff/issues/21186
- Decimal("_-1") # Should flag as verbose
91 + Decimal(-1) # Should flag as verbose
92 | Decimal("_+1") # Should flag as verbose
93 | Decimal("_-1_000") # Should flag as verbose
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:92:9
|
90 | # https://github.com/astral-sh/ruff/issues/21186
91 | Decimal("_-1") # Should flag as verbose
92 | Decimal("_+1") # Should flag as verbose
| ^^^^^
93 | Decimal("_-1_000") # Should flag as verbose
|
help: Replace with `+1`
89 | # Test cases for underscores before sign
90 | # https://github.com/astral-sh/ruff/issues/21186
91 | Decimal("_-1") # Should flag as verbose
- Decimal("_+1") # Should flag as verbose
92 + Decimal(+1) # Should flag as verbose
93 | Decimal("_-1_000") # Should flag as verbose
FURB157 [*] Verbose expression in `Decimal` constructor
--> FURB157.py:93:9
|
91 | Decimal("_-1") # Should flag as verbose
92 | Decimal("_+1") # Should flag as verbose
93 | Decimal("_-1_000") # Should flag as verbose
| ^^^^^^^^^
|
help: Replace with `-1_000`
90 | # https://github.com/astral-sh/ruff/issues/21186
91 | Decimal("_-1") # Should flag as verbose
92 | Decimal("_+1") # Should flag as verbose
- Decimal("_-1_000") # Should flag as verbose
93 + Decimal(-1_000) # Should flag as verbose

View File

@@ -99,6 +99,7 @@ help: Replace with `Fraction` constructor
12 | _ = Fraction.from_decimal(Decimal("-4.2"))
13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 | _ = Decimal.from_float(0.1)
note: This is an unsafe fix and may change runtime behavior
FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
--> FURB164.py:12:5
@@ -119,6 +120,7 @@ help: Replace with `Fraction` constructor
13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 | _ = Decimal.from_float(0.1)
15 | _ = Decimal.from_float(-0.5)
note: This is an unsafe fix and may change runtime behavior
FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
--> FURB164.py:13:5
@@ -482,6 +484,7 @@ help: Replace with `Fraction` constructor
32 | _ = Decimal.from_float(f=4.2)
33 |
34 | # Cases with invalid argument counts - should not get fixes
note: This is an unsafe fix and may change runtime behavior
FURB164 Verbose method `from_float` in `Decimal` construction
--> FURB164.py:32:5
@@ -655,7 +658,6 @@ help: Replace with `Decimal` constructor
64 + _ = Decimal("nan")
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
67 |
FURB164 [*] Verbose method `from_float` in `Decimal` construction
--> FURB164.py:65:5
@@ -673,8 +675,6 @@ help: Replace with `Decimal` constructor
- _ = Decimal.from_float(float("\x2dnan"))
65 + _ = Decimal("nan")
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
FURB164 [*] Verbose method `from_float` in `Decimal` construction
--> FURB164.py:66:5
@@ -683,8 +683,6 @@ FURB164 [*] Verbose method `from_float` in `Decimal` construction
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
|
help: Replace with `Decimal` constructor
63 | # Cases with non-finite floats - should produce safe fixes
@@ -692,38 +690,3 @@ help: Replace with `Decimal` constructor
65 | _ = Decimal.from_float(float("\x2dnan"))
- _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
66 + _ = Decimal("nan")
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
FURB164 [*] Verbose method `from_float` in `Fraction` construction
--> FURB164.py:70:5
|
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
70 | _ = Fraction.from_float(f=4.2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
71 | _ = Fraction.from_decimal(dec=4)
|
help: Replace with `Fraction` constructor
67 |
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
- _ = Fraction.from_float(f=4.2)
70 + _ = Fraction(4.2)
71 | _ = Fraction.from_decimal(dec=4)
FURB164 [*] Verbose method `from_decimal` in `Fraction` construction
--> FURB164.py:71:5
|
69 | # fixes must be safe
70 | _ = Fraction.from_float(f=4.2)
71 | _ = Fraction.from_decimal(dec=4)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `Fraction` constructor
68 | # See: https://github.com/astral-sh/ruff/issues/21257
69 | # fixes must be safe
70 | _ = Fraction.from_float(f=4.2)
- _ = Fraction.from_decimal(dec=4)
71 + _ = Fraction(4)

View File

@@ -257,25 +257,4 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")`
75 + pathlib.Path("file.txt").write_text(foobar, newline="\r\n")
76 |
77 | # Non-errors.
78 |
FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....`
--> FURB103.py:154:6
|
152 | data = {"price": 100}
153 |
154 | with open("test.json", "wb") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
155 | f.write(json.dumps(data, indent=4).encode("utf-8"))
|
help: Replace with `Path("test.json")....`
148 |
149 | # See: https://github.com/astral-sh/ruff/issues/20785
150 | import json
151 + import pathlib
152 |
153 | data = {"price": 100}
154 |
- with open("test.json", "wb") as f:
- f.write(json.dumps(data, indent=4).encode("utf-8"))
155 + pathlib.Path("test.json").write_bytes(json.dumps(data, indent=4).encode("utf-8"))
78 |

View File

@@ -104,14 +104,3 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba
51 | # writes a single time to file and that bit they can replace.
|
help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))`
FURB103 `open` and `write` should be replaced by `Path("test.json")....`
--> FURB103.py:154:6
|
152 | data = {"price": 100}
153 |
154 | with open("test.json", "wb") as f:
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
155 | f.write(json.dumps(data, indent=4).encode("utf-8"))
|
help: Replace with `Path("test.json")....`

View File

@@ -143,15 +143,6 @@ pub(super) fn rounded_and_ndigits<'a>(
return None;
}
// *args
if arguments.args.iter().any(Expr::is_starred_expr) {
return None;
}
// **kwargs
if arguments.keywords.iter().any(|kw| kw.arg.is_none()) {
return None;
}
let rounded = arguments.find_argument_value("number", 0)?;
let ndigits = arguments.find_argument_value("ndigits", 1);

View File

@@ -253,8 +253,6 @@ RUF057 [*] Value being rounded is already an integer
82 | | 17 # a comment
83 | | )
| |_^
84 |
85 | # See: https://github.com/astral-sh/ruff/issues/21209
|
help: Remove unnecessary `round` call
78 | round(# a comment
@@ -264,7 +262,4 @@ help: Remove unnecessary `round` call
- 17 # a comment
- )
81 + 17
82 |
83 | # See: https://github.com/astral-sh/ruff/issues/21209
84 | print(round(125, **{"ndigits": -2}))
note: This is an unsafe fix and may change runtime behavior

View File

@@ -3372,7 +3372,7 @@ impl Arguments {
pub fn arguments_source_order(&self) -> impl Iterator<Item = ArgOrKeyword<'_>> {
let args = self.args.iter().map(ArgOrKeyword::Arg);
let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword);
args.merge_by(keywords, |left, right| left.start() <= right.start())
args.merge_by(keywords, |left, right| left.start() < right.start())
}
pub fn inner_range(&self) -> TextRange {

View File

@@ -335,96 +335,3 @@ def overload4():
# trailing comment
def overload4(a: int): ...
# In preview, we preserve these newlines at the start of functions:
def preserved1():
return 1
def preserved2():
pass
def preserved3():
def inner(): ...
def preserved4():
def inner():
print("with a body")
return 1
return 2
def preserved5():
...
# trailing comment prevents collapsing the stub
def preserved6():
# Comment
return 1
def preserved7():
# comment
# another line
# and a third
return 0
def preserved8(): # this also prevents collapsing the stub
...
# But we still discard these newlines:
def removed1():
"Docstring"
return 1
def removed2():
...
def removed3():
... # trailing same-line comment does not prevent collapsing the stub
# And we discard empty lines after the first:
def partially_preserved1():
return 1
# We only preserve blank lines, not add new ones
def untouched1():
# comment
return 0
def untouched2():
# comment
return 0
def untouched3():
# comment
# another line
# and a third
return 0

View File

@@ -61,9 +61,3 @@ def test6 ():
print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )
def test7 ():
<RANGE_START>print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )

View File

@@ -613,58 +613,3 @@ match guard_comments:
):
pass
# regression tests from https://github.com/astral-sh/ruff/issues/17796
match class_pattern:
case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
pass
case Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as capture:
pass
case Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as capture:
pass
case Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture:
pass
match sequence_pattern_brackets:
case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
pass
case [
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
] as capture:
pass
case [
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
] as capture:
pass
match class_pattern:
# 1
case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture: # 2
# 3
pass # 4
# 5
case Class( # 6
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 7
) as capture: # 8
pass
case Class( # 9
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 10
) as capture: # 11
pass
case Class( # 12
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 13
) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture: # 14
pass
case Class( # 0
# 1
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 2
# 3
) as capture:
pass

View File

@@ -334,7 +334,7 @@ class A: ...
let options = PyFormatOptions::from_source_type(source_type);
let printed = format_range(&source, TextRange::new(start, end), options).unwrap();
let mut formatted = source.clone();
let mut formatted = source.to_string();
formatted.replace_range(
std::ops::Range::<usize>::from(printed.source_range()),
printed.as_code(),

View File

@@ -1,5 +1,5 @@
use ruff_formatter::{FormatOwnedWithRule, FormatRefWithRule, FormatRule, FormatRuleWithOptions};
use ruff_python_ast::{AnyNodeRef, Expr, PatternMatchAs};
use ruff_python_ast::{AnyNodeRef, Expr};
use ruff_python_ast::{MatchCase, Pattern};
use ruff_python_trivia::CommentRanges;
use ruff_python_trivia::{
@@ -14,7 +14,6 @@ use crate::expression::parentheses::{
NeedsParentheses, OptionalParentheses, Parentheses, optional_parentheses, parenthesized,
};
use crate::prelude::*;
use crate::preview::is_avoid_parens_for_long_as_captures_enabled;
pub(crate) mod pattern_arguments;
pub(crate) mod pattern_keyword;
@@ -243,14 +242,8 @@ pub(crate) fn can_pattern_omit_optional_parentheses(
Pattern::MatchValue(_)
| Pattern::MatchSingleton(_)
| Pattern::MatchStar(_)
| Pattern::MatchAs(_)
| Pattern::MatchOr(_) => false,
Pattern::MatchAs(PatternMatchAs { pattern, .. }) => match pattern {
Some(pattern) => {
is_avoid_parens_for_long_as_captures_enabled(context)
&& has_parentheses_and_is_non_empty(pattern, context)
}
None => false,
},
Pattern::MatchSequence(sequence) => {
!sequence.patterns.is_empty() || context.comments().has_dangling(pattern)
}
@@ -306,7 +299,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
}
// `case 4+3j:` or `case 4-3j:
// Cannot contain arbitrary expressions. Limited to complex numbers.
// Can not contain arbitrary expressions. Limited to complex numbers.
Expr::BinOp(_) => {
self.update_max_precedence(OperatorPrecedence::Additive, 1);
}
@@ -325,14 +318,7 @@ impl<'a> CanOmitOptionalParenthesesVisitor<'a> {
// The pattern doesn't start with a parentheses pattern, but with the class's identifier.
self.first.set_if_none(First::Token);
}
Pattern::MatchAs(PatternMatchAs { pattern, .. }) => {
if let Some(pattern) = pattern
&& is_avoid_parens_for_long_as_captures_enabled(context)
{
self.visit_sub_pattern(pattern, context);
}
}
Pattern::MatchStar(_) | Pattern::MatchSingleton(_) => {}
Pattern::MatchStar(_) | Pattern::MatchSingleton(_) | Pattern::MatchAs(_) => {}
Pattern::MatchOr(or_pattern) => {
self.update_max_precedence(
OperatorPrecedence::Or,

View File

@@ -36,19 +36,3 @@ pub(crate) const fn is_remove_parens_around_except_types_enabled(
) -> bool {
context.is_preview()
}
/// Returns `true` if the
/// [`allow_newline_after_block_open`](https://github.com/astral-sh/ruff/pull/21110) preview style
/// is enabled.
pub(crate) const fn is_allow_newline_after_block_open_enabled(context: &PyFormatContext) -> bool {
context.is_preview()
}
/// Returns `true` if the
/// [`avoid_parens_for_long_as_captures`](https://github.com/astral-sh/ruff/pull/21176) preview
/// style is enabled.
pub(crate) const fn is_avoid_parens_for_long_as_captures_enabled(
context: &PyFormatContext,
) -> bool {
context.is_preview()
}

View File

@@ -8,7 +8,7 @@ use ruff_python_trivia::{SimpleToken, SimpleTokenKind, SimpleTokenizer};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::comments::{SourceComment, leading_alternate_branch_comments, trailing_comments};
use crate::statement::suite::{SuiteKind, as_only_an_ellipsis};
use crate::statement::suite::{SuiteKind, contains_only_an_ellipsis};
use crate::verbatim::write_suppressed_clause_header;
use crate::{has_skip_comment, prelude::*};
@@ -449,10 +449,17 @@ impl Format<PyFormatContext<'_>> for FormatClauseBody<'_> {
|| matches!(self.kind, SuiteKind::Function | SuiteKind::Class);
if should_collapse_stub
&& let Some(ellipsis) = as_only_an_ellipsis(self.body, f.context().comments())
&& contains_only_an_ellipsis(self.body, f.context().comments())
&& self.trailing_comments.is_empty()
{
write!(f, [space(), ellipsis.format(), hard_line_break()])
write!(
f,
[
space(),
self.body.format().with_options(self.kind),
hard_line_break()
]
)
} else {
write!(
f,

View File

@@ -13,9 +13,7 @@ use crate::comments::{
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
use crate::other::string_literal::StringLiteralKind;
use crate::prelude::*;
use crate::preview::{
is_allow_newline_after_block_open_enabled, is_blank_line_before_decorated_class_in_stub_enabled,
};
use crate::preview::is_blank_line_before_decorated_class_in_stub_enabled;
use crate::statement::stmt_expr::FormatStmtExpr;
use crate::verbatim::{
suppressed_node, write_suppressed_statements_starting_with_leading_comment,
@@ -171,22 +169,6 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
false,
)
} else {
// Allow an empty line after a function header in preview, if the function has no
// docstring and no initial comment.
let allow_newline_after_block_open =
is_allow_newline_after_block_open_enabled(f.context())
&& matches!(self.kind, SuiteKind::Function)
&& matches!(first, SuiteChildStatement::Other(_));
let start = comments
.leading(first)
.first()
.map_or_else(|| first.start(), Ranged::start);
if allow_newline_after_block_open && lines_before(start, f.context().source()) > 1 {
empty_line().fmt(f)?;
}
first.fmt(f)?;
let empty_line_after_docstring = if matches!(first, SuiteChildStatement::Docstring(_))
@@ -236,7 +218,7 @@ impl FormatRule<Suite, PyFormatContext<'_>> for FormatSuite {
)?;
} else {
// Preserve empty lines after a stub implementation but don't insert a new one if there isn't any present in the source.
// This is useful when having multiple function overloads that should be grouped together by omitting new lines between them.
// This is useful when having multiple function overloads that should be grouped to getter by omitting new lines between them.
let is_preceding_stub_function_without_empty_line = following
.is_function_def_stmt()
&& preceding
@@ -746,21 +728,17 @@ fn stub_suite_can_omit_empty_line(preceding: &Stmt, following: &Stmt, f: &PyForm
/// Returns `true` if a function or class body contains only an ellipsis with no comments.
pub(crate) fn contains_only_an_ellipsis(body: &[Stmt], comments: &Comments) -> bool {
as_only_an_ellipsis(body, comments).is_some()
}
/// Returns `Some(Stmt::Ellipsis)` if a function or class body contains only an ellipsis with no
/// comments.
pub(crate) fn as_only_an_ellipsis<'a>(body: &'a [Stmt], comments: &Comments) -> Option<&'a Stmt> {
if let [node @ Stmt::Expr(ast::StmtExpr { value, .. })] = body
&& value.is_ellipsis_literal_expr()
&& !comments.has_leading(node)
&& !comments.has_trailing_own_line(node)
{
return Some(node);
match body {
[Stmt::Expr(ast::StmtExpr { value, .. })] => {
let [node] = body else {
return false;
};
value.is_ellipsis_literal_expr()
&& !comments.has_leading(node)
&& !comments.has_trailing_own_line(node)
}
_ => false,
}
None
}
/// Returns `true` if a [`Stmt`] is a class or function definition.

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/newlines.py
snapshot_kind: text
---
## Input
```python
@@ -341,99 +342,6 @@ def overload4():
# trailing comment
def overload4(a: int): ...
# In preview, we preserve these newlines at the start of functions:
def preserved1():
return 1
def preserved2():
pass
def preserved3():
def inner(): ...
def preserved4():
def inner():
print("with a body")
return 1
return 2
def preserved5():
...
# trailing comment prevents collapsing the stub
def preserved6():
# Comment
return 1
def preserved7():
# comment
# another line
# and a third
return 0
def preserved8(): # this also prevents collapsing the stub
...
# But we still discard these newlines:
def removed1():
"Docstring"
return 1
def removed2():
...
def removed3():
... # trailing same-line comment does not prevent collapsing the stub
# And we discard empty lines after the first:
def partially_preserved1():
return 1
# We only preserve blank lines, not add new ones
def untouched1():
# comment
return 0
def untouched2():
# comment
return 0
def untouched3():
# comment
# another line
# and a third
return 0
```
## Output
@@ -824,88 +732,6 @@ def overload4():
def overload4(a: int): ...
# In preview, we preserve these newlines at the start of functions:
def preserved1():
return 1
def preserved2():
pass
def preserved3():
def inner(): ...
def preserved4():
def inner():
print("with a body")
return 1
return 2
def preserved5():
...
# trailing comment prevents collapsing the stub
def preserved6():
# Comment
return 1
def preserved7():
# comment
# another line
# and a third
return 0
def preserved8(): # this also prevents collapsing the stub
...
# But we still discard these newlines:
def removed1():
"Docstring"
return 1
def removed2(): ...
def removed3(): ... # trailing same-line comment does not prevent collapsing the stub
# And we discard empty lines after the first:
def partially_preserved1():
return 1
# We only preserve blank lines, not add new ones
def untouched1():
# comment
return 0
def untouched2():
# comment
return 0
def untouched3():
# comment
# another line
# and a third
return 0
```
@@ -913,15 +739,7 @@ def untouched3():
```diff
--- Stable
+++ Preview
@@ -253,6 +253,7 @@
def fakehttp():
+
class FakeHTTPConnection:
if mock_close:
@@ -277,6 +278,7 @@
@@ -277,6 +277,7 @@
def a():
return 1
@@ -929,7 +747,7 @@ def untouched3():
else:
pass
@@ -293,6 +295,7 @@
@@ -293,6 +294,7 @@
def a():
return 1
@@ -937,7 +755,7 @@ def untouched3():
case 1:
def a():
@@ -303,6 +306,7 @@
@@ -303,6 +305,7 @@
def a():
return 1
@@ -945,7 +763,7 @@ def untouched3():
except RuntimeError:
def a():
@@ -313,6 +317,7 @@
@@ -313,6 +316,7 @@
def a():
return 1
@@ -953,7 +771,7 @@ def untouched3():
finally:
def a():
@@ -323,18 +328,22 @@
@@ -323,18 +327,22 @@
def a():
return 1
@@ -976,64 +794,4 @@ def untouched3():
finally:
def a():
@@ -388,18 +397,22 @@
# In preview, we preserve these newlines at the start of functions:
def preserved1():
+
return 1
def preserved2():
+
pass
def preserved3():
+
def inner(): ...
def preserved4():
+
def inner():
print("with a body")
return 1
@@ -408,17 +421,20 @@
def preserved5():
+
...
# trailing comment prevents collapsing the stub
def preserved6():
+
# Comment
return 1
def preserved7():
+
# comment
# another line
# and a third
@@ -427,6 +443,7 @@
def preserved8(): # this also prevents collapsing the stub
+
...
@@ -445,6 +462,7 @@
# And we discard empty lines after the first:
def partially_preserved1():
+
return 1
```

View File

@@ -67,12 +67,6 @@ def test6 ():
print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )
def test7 ():
<RANGE_START>print("Format" )
print(3 + 4)<RANGE_END>
print("Format to fix indentation" )
```
## Outputs
@@ -152,27 +146,6 @@ def test6 ():
print("Format")
print(3 + 4)
print("Format to fix indentation" )
def test7 ():
print("Format")
print(3 + 4)
print("Format to fix indentation" )
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -55,6 +55,7 @@
def test6 ():
+
print("Format")
print(3 + 4)
print("Format to fix indentation" )
```
@@ -252,27 +225,6 @@ def test6 ():
print("Format")
print(3 + 4)
print("Format to fix indentation")
def test7 ():
print("Format")
print(3 + 4)
print("Format to fix indentation")
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -55,6 +55,7 @@
def test6 ():
+
print("Format")
print(3 + 4)
print("Format to fix indentation")
```
@@ -352,25 +304,4 @@ def test6 ():
print("Format")
print(3 + 4)
print("Format to fix indentation")
def test7 ():
print("Format")
print(3 + 4)
print("Format to fix indentation")
```
#### Preview changes
```diff
--- Stable
+++ Preview
@@ -55,6 +55,7 @@
def test6 ():
+
print("Format")
print(3 + 4)
print("Format to fix indentation")
```

View File

@@ -1,6 +1,7 @@
---
source: crates/ruff_python_formatter/tests/fixtures.rs
input_file: crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/match.py
snapshot_kind: text
---
## Input
```python
@@ -619,61 +620,6 @@ match guard_comments:
):
pass
# regression tests from https://github.com/astral-sh/ruff/issues/17796
match class_pattern:
case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
pass
case Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as capture:
pass
case Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as capture:
pass
case Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture:
pass
match sequence_pattern_brackets:
case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
pass
case [
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
] as capture:
pass
case [
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
] as capture:
pass
match class_pattern:
# 1
case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture: # 2
# 3
pass # 4
# 5
case Class( # 6
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 7
) as capture: # 8
pass
case Class( # 9
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 10
) as capture: # 11
pass
case Class( # 12
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 13
) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture: # 14
pass
case Class( # 0
# 1
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 2
# 3
) as capture:
pass
```
## Output
@@ -1339,175 +1285,4 @@ match guard_comments:
# trailing own line comment
):
pass
# regression tests from https://github.com/astral-sh/ruff/issues/17796
match class_pattern:
case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
pass
case (
Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
):
pass
case (
Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as capture
):
pass
case (
Class(
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture
):
pass
match sequence_pattern_brackets:
case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
pass
case (
[xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture
):
pass
case (
[
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
] as capture
):
pass
match class_pattern:
# 1
case (
Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
): # 2
# 3
pass # 4
# 5
case (
Class( # 6
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 7
) as capture
): # 8
pass
case (
Class( # 9
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 10
) as capture
): # 11
pass
case (
Class( # 12
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 13
) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture
): # 14
pass
case (
Class( # 0
# 1
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 2
# 3
) as capture
):
pass
```
## Preview changes
```diff
--- Stable
+++ Preview
@@ -665,15 +665,13 @@
match class_pattern:
case Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture:
pass
- case (
- Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
- ):
+ case Class(
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ ) as capture:
pass
- case (
- Class(
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- ) as capture
- ):
+ case Class(
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ ) as capture:
pass
case (
Class(
@@ -685,37 +683,31 @@
match sequence_pattern_brackets:
case [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture:
pass
- case (
- [xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx] as capture
- ):
+ case [
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ ] as capture:
pass
- case (
- [
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- ] as capture
- ):
+ case [
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ ] as capture:
pass
match class_pattern:
# 1
- case (
- Class(xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx) as capture
- ): # 2
+ case Class(
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+ ) as capture: # 2
# 3
pass # 4
# 5
- case (
- Class( # 6
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 7
- ) as capture
- ): # 8
+ case Class( # 6
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 7
+ ) as capture: # 8
pass
- case (
- Class( # 9
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 10
- ) as capture
- ): # 11
+ case Class( # 9
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 10
+ ) as capture: # 11
pass
case (
Class( # 12
@@ -723,11 +715,9 @@
) as really_really_really_really_really_really_really_really_really_really_really_really_long_capture
): # 14
pass
- case (
- Class( # 0
- # 1
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 2
- # 3
- ) as capture
- ):
+ case Class( # 0
+ # 1
+ xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # 2
+ # 3
+ ) as capture:
pass
```

View File

@@ -78,9 +78,9 @@ pub enum InterpolatedStringErrorType {
impl std::fmt::Display for InterpolatedStringErrorType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::UnclosedLbrace => write!(f, "expecting `}}`"),
Self::UnclosedLbrace => write!(f, "expecting '}}'"),
Self::InvalidConversionFlag => write!(f, "invalid conversion character"),
Self::SingleRbrace => write!(f, "single `}}` is not allowed"),
Self::SingleRbrace => write!(f, "single '}}' is not allowed"),
Self::UnterminatedString => write!(f, "unterminated string"),
Self::UnterminatedTripleQuotedString => write!(f, "unterminated triple-quoted string"),
Self::LambdaWithoutParentheses => {
@@ -232,7 +232,7 @@ impl std::fmt::Display for ParseErrorType {
ParseErrorType::UnexpectedTokenAfterAsync(kind) => {
write!(
f,
"Expected `def`, `with` or `for` to follow `async`, found {kind}",
"Expected 'def', 'with' or 'for' to follow 'async', found {kind}",
)
}
ParseErrorType::InvalidArgumentUnpackingOrder => {
@@ -286,10 +286,10 @@ impl std::fmt::Display for ParseErrorType {
f.write_str("Parameter without a default cannot follow a parameter with a default")
}
ParseErrorType::ExpectedKeywordParam => {
f.write_str("Expected one or more keyword parameter after `*` separator")
f.write_str("Expected one or more keyword parameter after '*' separator")
}
ParseErrorType::VarParameterWithDefault => {
f.write_str("Parameter with `*` or `**` cannot have default value")
f.write_str("Parameter with '*' or '**' cannot have default value")
}
ParseErrorType::InvalidStarPatternUsage => {
f.write_str("Star pattern cannot be used here")

View File

@@ -219,7 +219,7 @@ impl SemanticSyntaxChecker {
AwaitOutsideAsyncFunctionKind::AsyncWith,
);
}
Stmt::Nonlocal(ast::StmtNonlocal { names, range, .. }) => {
Stmt::Nonlocal(ast::StmtNonlocal { range, .. }) => {
// test_ok nonlocal_declaration_at_module_level
// def _():
// nonlocal x
@@ -234,18 +234,6 @@ impl SemanticSyntaxChecker {
*range,
);
}
if !ctx.in_module_scope() {
for name in names {
if !ctx.has_nonlocal_binding(name) {
Self::add_error(
ctx,
SemanticSyntaxErrorKind::NonlocalWithoutBinding(name.to_string()),
name.range,
);
}
}
}
}
Stmt::Break(ast::StmtBreak { range, .. }) => {
if !ctx.in_loop_context() {
@@ -1166,9 +1154,6 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::DifferentMatchPatternBindings => {
write!(f, "alternative patterns bind different names")
}
SemanticSyntaxErrorKind::NonlocalWithoutBinding(name) => {
write!(f, "no binding for nonlocal `{name}` found")
}
}
}
}
@@ -1569,9 +1554,6 @@ pub enum SemanticSyntaxErrorKind {
/// ...
/// ```
DifferentMatchPatternBindings,
/// Represents a nonlocal statement for a name that has no binding in an enclosing scope.
NonlocalWithoutBinding(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
@@ -2022,9 +2004,6 @@ pub trait SemanticSyntaxContext {
/// Return the [`TextRange`] at which a name is declared as `global` in the current scope.
fn global(&self, name: &str) -> Option<TextRange>;
/// Returns `true` if `name` has a binding in an enclosing scope.
fn has_nonlocal_binding(&self, name: &str) -> bool;
/// Returns `true` if the visitor is currently in an async context, i.e. an async function.
fn in_async_context(&self) -> bool;

View File

@@ -635,93 +635,93 @@ impl fmt::Display for TokenKind {
TokenKind::TStringEnd => "TStringEnd",
TokenKind::IpyEscapeCommand => "IPython escape command",
TokenKind::Comment => "comment",
TokenKind::Question => "`?`",
TokenKind::Exclamation => "`!`",
TokenKind::Lpar => "`(`",
TokenKind::Rpar => "`)`",
TokenKind::Lsqb => "`[`",
TokenKind::Rsqb => "`]`",
TokenKind::Lbrace => "`{`",
TokenKind::Rbrace => "`}`",
TokenKind::Equal => "`=`",
TokenKind::ColonEqual => "`:=`",
TokenKind::Dot => "`.`",
TokenKind::Colon => "`:`",
TokenKind::Semi => "`;`",
TokenKind::Comma => "`,`",
TokenKind::Rarrow => "`->`",
TokenKind::Plus => "`+`",
TokenKind::Minus => "`-`",
TokenKind::Star => "`*`",
TokenKind::DoubleStar => "`**`",
TokenKind::Slash => "`/`",
TokenKind::DoubleSlash => "`//`",
TokenKind::Percent => "`%`",
TokenKind::Vbar => "`|`",
TokenKind::Amper => "`&`",
TokenKind::CircumFlex => "`^`",
TokenKind::LeftShift => "`<<`",
TokenKind::RightShift => "`>>`",
TokenKind::Tilde => "`~`",
TokenKind::At => "`@`",
TokenKind::Less => "`<`",
TokenKind::Greater => "`>`",
TokenKind::EqEqual => "`==`",
TokenKind::NotEqual => "`!=`",
TokenKind::LessEqual => "`<=`",
TokenKind::GreaterEqual => "`>=`",
TokenKind::PlusEqual => "`+=`",
TokenKind::MinusEqual => "`-=`",
TokenKind::StarEqual => "`*=`",
TokenKind::DoubleStarEqual => "`**=`",
TokenKind::SlashEqual => "`/=`",
TokenKind::DoubleSlashEqual => "`//=`",
TokenKind::PercentEqual => "`%=`",
TokenKind::VbarEqual => "`|=`",
TokenKind::AmperEqual => "`&=`",
TokenKind::CircumflexEqual => "`^=`",
TokenKind::LeftShiftEqual => "`<<=`",
TokenKind::RightShiftEqual => "`>>=`",
TokenKind::AtEqual => "`@=`",
TokenKind::Ellipsis => "`...`",
TokenKind::False => "`False`",
TokenKind::None => "`None`",
TokenKind::True => "`True`",
TokenKind::And => "`and`",
TokenKind::As => "`as`",
TokenKind::Assert => "`assert`",
TokenKind::Async => "`async`",
TokenKind::Await => "`await`",
TokenKind::Break => "`break`",
TokenKind::Class => "`class`",
TokenKind::Continue => "`continue`",
TokenKind::Def => "`def`",
TokenKind::Del => "`del`",
TokenKind::Elif => "`elif`",
TokenKind::Else => "`else`",
TokenKind::Except => "`except`",
TokenKind::Finally => "`finally`",
TokenKind::For => "`for`",
TokenKind::From => "`from`",
TokenKind::Global => "`global`",
TokenKind::If => "`if`",
TokenKind::Import => "`import`",
TokenKind::In => "`in`",
TokenKind::Is => "`is`",
TokenKind::Lambda => "`lambda`",
TokenKind::Nonlocal => "`nonlocal`",
TokenKind::Not => "`not`",
TokenKind::Or => "`or`",
TokenKind::Pass => "`pass`",
TokenKind::Raise => "`raise`",
TokenKind::Return => "`return`",
TokenKind::Try => "`try`",
TokenKind::While => "`while`",
TokenKind::Match => "`match`",
TokenKind::Type => "`type`",
TokenKind::Case => "`case`",
TokenKind::With => "`with`",
TokenKind::Yield => "`yield`",
TokenKind::Question => "'?'",
TokenKind::Exclamation => "'!'",
TokenKind::Lpar => "'('",
TokenKind::Rpar => "')'",
TokenKind::Lsqb => "'['",
TokenKind::Rsqb => "']'",
TokenKind::Lbrace => "'{'",
TokenKind::Rbrace => "'}'",
TokenKind::Equal => "'='",
TokenKind::ColonEqual => "':='",
TokenKind::Dot => "'.'",
TokenKind::Colon => "':'",
TokenKind::Semi => "';'",
TokenKind::Comma => "','",
TokenKind::Rarrow => "'->'",
TokenKind::Plus => "'+'",
TokenKind::Minus => "'-'",
TokenKind::Star => "'*'",
TokenKind::DoubleStar => "'**'",
TokenKind::Slash => "'/'",
TokenKind::DoubleSlash => "'//'",
TokenKind::Percent => "'%'",
TokenKind::Vbar => "'|'",
TokenKind::Amper => "'&'",
TokenKind::CircumFlex => "'^'",
TokenKind::LeftShift => "'<<'",
TokenKind::RightShift => "'>>'",
TokenKind::Tilde => "'~'",
TokenKind::At => "'@'",
TokenKind::Less => "'<'",
TokenKind::Greater => "'>'",
TokenKind::EqEqual => "'=='",
TokenKind::NotEqual => "'!='",
TokenKind::LessEqual => "'<='",
TokenKind::GreaterEqual => "'>='",
TokenKind::PlusEqual => "'+='",
TokenKind::MinusEqual => "'-='",
TokenKind::StarEqual => "'*='",
TokenKind::DoubleStarEqual => "'**='",
TokenKind::SlashEqual => "'/='",
TokenKind::DoubleSlashEqual => "'//='",
TokenKind::PercentEqual => "'%='",
TokenKind::VbarEqual => "'|='",
TokenKind::AmperEqual => "'&='",
TokenKind::CircumflexEqual => "'^='",
TokenKind::LeftShiftEqual => "'<<='",
TokenKind::RightShiftEqual => "'>>='",
TokenKind::AtEqual => "'@='",
TokenKind::Ellipsis => "'...'",
TokenKind::False => "'False'",
TokenKind::None => "'None'",
TokenKind::True => "'True'",
TokenKind::And => "'and'",
TokenKind::As => "'as'",
TokenKind::Assert => "'assert'",
TokenKind::Async => "'async'",
TokenKind::Await => "'await'",
TokenKind::Break => "'break'",
TokenKind::Class => "'class'",
TokenKind::Continue => "'continue'",
TokenKind::Def => "'def'",
TokenKind::Del => "'del'",
TokenKind::Elif => "'elif'",
TokenKind::Else => "'else'",
TokenKind::Except => "'except'",
TokenKind::Finally => "'finally'",
TokenKind::For => "'for'",
TokenKind::From => "'from'",
TokenKind::Global => "'global'",
TokenKind::If => "'if'",
TokenKind::Import => "'import'",
TokenKind::In => "'in'",
TokenKind::Is => "'is'",
TokenKind::Lambda => "'lambda'",
TokenKind::Nonlocal => "'nonlocal'",
TokenKind::Not => "'not'",
TokenKind::Or => "'or'",
TokenKind::Pass => "'pass'",
TokenKind::Raise => "'raise'",
TokenKind::Return => "'return'",
TokenKind::Try => "'try'",
TokenKind::While => "'while'",
TokenKind::Match => "'match'",
TokenKind::Type => "'type'",
TokenKind::Case => "'case'",
TokenKind::With => "'with'",
TokenKind::Yield => "'yield'",
};
f.write_str(value)
}

View File

@@ -527,10 +527,6 @@ impl SemanticSyntaxContext for SemanticSyntaxCheckerVisitor<'_> {
None
}
fn has_nonlocal_binding(&self, _name: &str) -> bool {
true
}
fn in_async_context(&self) -> bool {
if let Some(scope) = self.scopes.iter().next_back() {
match scope {

View File

@@ -131,7 +131,7 @@ Module(
|
1 | assert *x
2 | assert assert x
| ^^^^^^ Syntax Error: Expected an identifier, but found a keyword `assert` that cannot be used here
| ^^^^^^ Syntax Error: Expected an identifier, but found a keyword 'assert' that cannot be used here
3 | assert yield x
4 | assert x := 1
|

View File

@@ -148,7 +148,7 @@ Module(
|
1 | a = pass = c
| ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here
| ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here
2 | a + b
3 | a = b = pass = c
|
@@ -158,6 +158,6 @@ Module(
1 | a = pass = c
2 | a + b
3 | a = b = pass = c
| ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here
| ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here
4 | a + b
|

View File

@@ -181,7 +181,7 @@ Module(
|
1 | async class Foo: ...
| ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `class`
| ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'class'
2 | async while test: ...
3 | async x = 1
|
@@ -190,7 +190,7 @@ Module(
|
1 | async class Foo: ...
2 | async while test: ...
| ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `while`
| ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'while'
3 | async x = 1
4 | async async def foo(): ...
|
@@ -200,7 +200,7 @@ Module(
1 | async class Foo: ...
2 | async while test: ...
3 | async x = 1
| ^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found name
| ^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found name
4 | async async def foo(): ...
5 | async match test:
|
@@ -210,7 +210,7 @@ Module(
2 | async while test: ...
3 | async x = 1
4 | async async def foo(): ...
| ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `async`
| ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'async'
5 | async match test:
6 | case _: ...
|
@@ -220,6 +220,6 @@ Module(
3 | async x = 1
4 | async async def foo(): ...
5 | async match test:
| ^^^^^ Syntax Error: Expected `def`, `with` or `for` to follow `async`, found `match`
| ^^^^^ Syntax Error: Expected 'def', 'with' or 'for' to follow 'async', found 'match'
6 | case _: ...
|

View File

@@ -245,7 +245,7 @@ Module(
3 | *x += 1
4 | pass += 1
5 | x += pass
| ^^^^ Syntax Error: Expected an identifier, but found a keyword `pass` that cannot be used here
| ^^^^ Syntax Error: Expected an identifier, but found a keyword 'pass' that cannot be used here
6 | (x + y) += 1
|

View File

@@ -121,7 +121,7 @@ Module(
|
1 | class Foo[T1, *T2(a, b):
| ^ Syntax Error: Expected `]`, found `(`
| ^ Syntax Error: Expected ']', found '('
2 | pass
3 | x = 10
|

View File

@@ -68,7 +68,7 @@ Module(
|
1 | call(**x := 1)
| ^^ Syntax Error: Expected `,`, found `:=`
| ^^ Syntax Error: Expected ',', found ':='
|

View File

@@ -61,5 +61,5 @@ Module(
|
1 | # The comma between the first two elements is expected in `parse_list_expression`.
2 | [0, 1 2]
| ^ Syntax Error: Expected `,`, found int
| ^ Syntax Error: Expected ',', found int
|

View File

@@ -77,7 +77,7 @@ Module(
|
1 | (async)
| ^^^^^ Syntax Error: Expected an identifier, but found a keyword `async` that cannot be used here
| ^^^^^ Syntax Error: Expected an identifier, but found a keyword 'async' that cannot be used here
2 | (x async x in iter)
|
@@ -85,5 +85,5 @@ Module(
|
1 | (async)
2 | (x async x in iter)
| ^ Syntax Error: Expected `for`, found name
| ^ Syntax Error: Expected 'for', found name
|

View File

@@ -169,7 +169,7 @@ Module(
|
1 | @def foo(): ...
| ^^^ Syntax Error: Expected an identifier, but found a keyword `def` that cannot be used here
| ^^^ Syntax Error: Expected an identifier, but found a keyword 'def' that cannot be used here
2 | @
3 | def foo(): ...
|

View File

@@ -161,7 +161,7 @@ Module(
|
1 | @x def foo(): ...
| ^^^ Syntax Error: Expected newline, found `def`
| ^^^ Syntax Error: Expected newline, found 'def'
2 | @x async def foo(): ...
3 | @x class Foo: ...
|
@@ -170,7 +170,7 @@ Module(
|
1 | @x def foo(): ...
2 | @x async def foo(): ...
| ^^^^^ Syntax Error: Expected newline, found `async`
| ^^^^^ Syntax Error: Expected newline, found 'async'
3 | @x class Foo: ...
|
@@ -179,5 +179,5 @@ Module(
1 | @x def foo(): ...
2 | @x async def foo(): ...
3 | @x class Foo: ...
| ^^^^^ Syntax Error: Expected newline, found `class`
| ^^^^^ Syntax Error: Expected newline, found 'class'
|

View File

@@ -238,7 +238,7 @@ Module(
3 | call(***x)
4 |
5 | call(**x := 1)
| ^^ Syntax Error: Expected `,`, found `:=`
| ^^ Syntax Error: Expected ',', found ':='
|

View File

@@ -61,5 +61,5 @@ Module(
|
1 | call(x y)
| ^ Syntax Error: Expected `,`, found name
| ^ Syntax Error: Expected ',', found name
|

View File

@@ -76,7 +76,7 @@ Module(
|
1 | call(
| ^ Syntax Error: Expected `)`, found newline
| ^ Syntax Error: Expected ')', found newline
2 |
3 | def foo():
4 | pass

View File

@@ -85,7 +85,7 @@ Module(
|
1 | call(x
| ^ Syntax Error: Expected `)`, found newline
| ^ Syntax Error: Expected ')', found newline
2 |
3 | def foo():
4 | pass

Some files were not shown because too many files have changed in this diff Show More