Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
b1e64a0da4 [ty] Remove Self from generic context when binding Self 2025-09-10 17:08:45 +02:00
246 changed files with 15570 additions and 23307 deletions

View File

@@ -39,11 +39,11 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -68,11 +68,11 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -110,11 +110,11 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
runs-on: macos-14
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: arm64
@@ -166,11 +166,11 @@ jobs:
- target: aarch64-pc-windows-msvc
arch: x64
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }}
@@ -219,11 +219,11 @@ jobs:
- x86_64-unknown-linux-gnu
- i686-unknown-linux-gnu
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -296,11 +296,11 @@ jobs:
arch: riscv64
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -361,11 +361,11 @@ jobs:
- x86_64-unknown-linux-musl
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -427,11 +427,11 @@ jobs:
arch: armv7
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"

View File

@@ -33,7 +33,7 @@ jobs:
- linux/amd64
- linux/arm64
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
submodules: recursive
persist-credentials: false
@@ -113,7 +113,7 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digests-*
@@ -256,7 +256,7 @@ jobs:
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
steps:
- name: Download digests
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
path: /tmp/digests
pattern: digests-*

View File

@@ -43,7 +43,7 @@ jobs:
# Flag that is set to "true" when code related to the playground changes.
playground: ${{ steps.check_playground.outputs.changed }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
persist-credentials: false
@@ -209,7 +209,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Install Rust toolchain"
@@ -223,7 +223,7 @@ jobs:
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -243,7 +243,7 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -252,15 +252,15 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: ty mdtests (GitHub annotations)
@@ -305,7 +305,7 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -314,15 +314,15 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install cargo insta"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-insta
- name: "Install uv"
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: "Run tests"
@@ -338,18 +338,18 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo nextest"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
with:
enable-cache: "true"
- name: "Run tests"
@@ -369,15 +369,15 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
node-version: 20
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
@@ -398,7 +398,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -416,7 +416,7 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: SebRollen/toml-action@b1b3628f55fc3a28208d4203ada8b737e9687876 # v1.2.0
@@ -444,7 +444,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' || needs.determine_changes.outputs.fuzz == 'true' || needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -453,7 +453,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@20aa316bab4942180bbbabe93237858e8d77f1ed # v1.15.5
uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
- 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
@@ -470,11 +470,11 @@ jobs:
env:
FORCE_COLOR: 1
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
with:
@@ -504,7 +504,7 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 5
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -534,14 +534,14 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && needs.determine_changes.outputs.code == 'true' }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download comparison Ruff binary
id: ruff-target
with:
@@ -658,10 +658,10 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download new ty binary
id: ty-new
with:
@@ -674,7 +674,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -701,10 +701,10 @@ jobs:
needs: determine_changes
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@20aa316bab4942180bbbabe93237858e8d77f1ed # v1.15.5
- uses: cargo-bins/cargo-binstall@837578dfb436769f1e6669b2e23ffea9d9d2da8f # v1.15.4
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -714,10 +714,10 @@ jobs:
timeout-minutes: 20
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -741,12 +741,12 @@ jobs:
runs-on: depot-ubuntu-22.04-16
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- name: "Cache pre-commit"
@@ -772,10 +772,10 @@ jobs:
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -787,7 +787,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -814,7 +814,7 @@ jobs:
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.formatter == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
@@ -840,18 +840,18 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
name: "Download ruff-lsp source"
with:
persist-credentials: false
repository: "astral-sh/ruff-lsp"
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
# installation fails on 3.13 and newer
python-version: "3.12"
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download development ruff binary
id: ruff-target
with:
@@ -882,13 +882,13 @@ jobs:
- determine_changes
if: ${{ (needs.determine_changes.outputs.playground == 'true') }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"
@@ -914,18 +914,18 @@ jobs:
timeout-minutes: 20
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-codspeed
@@ -933,9 +933,8 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c # v3.8.1
with:
mode: instrumentation
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
@@ -948,18 +947,18 @@ jobs:
TY_LOG: ruff_benchmark=debug
steps:
- name: "Checkout Branch"
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install codspeed"
uses: taiki-e/install-action@67cc679904bee382389bf22082124fa963c6f6bd # v2.61.3
uses: taiki-e/install-action@6064345e6658255e90e9500fdf9a06ab77e6909c # v2.57.6
with:
tool: cargo-codspeed
@@ -967,13 +966,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@653fdc30e6c40ffd9739e40c8a0576f4f4523ca1 # v4.0.1
env:
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
# appear to provide much useful insight for our walltime benchmarks right now
# (see https://github.com/astral-sh/ruff/pull/20419)
CODSPEED_PERF_ENABLED: false
uses: CodSpeedHQ/action@76578c2a7ddd928664caa737f0e962e3085d4e7c # v3.8.1
with:
mode: walltime
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -31,10 +31,10 @@ jobs:
# Don't run the cron job on forks:
if: ${{ github.repository == 'astral-sh/ruff' || github.event_name != 'schedule' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
@@ -65,7 +65,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -32,14 +32,14 @@ jobs:
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -75,14 +75,14 @@ jobs:
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.RUFF_PRE_COMMIT_PAT }}
script: |

View File

@@ -23,12 +23,12 @@ jobs:
env:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ inputs.ref }}
persist-credentials: true
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: 3.12

View File

@@ -24,12 +24,12 @@ jobs:
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: "npm"

View File

@@ -22,8 +22,8 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*
path: wheels

View File

@@ -30,12 +30,12 @@ jobs:
env:
CF_API_TOKEN_EXISTS: ${{ secrets.CF_API_TOKEN != '' }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Install Rust toolchain"
run: rustup target add wasm32-unknown-unknown
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0

View File

@@ -29,7 +29,7 @@ jobs:
target: [web, bundler, nodejs]
fail-fast: false
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: "Install Rust toolchain"
@@ -45,9 +45,9 @@ 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@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: "Publish (dry-run)"
if: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}

View File

@@ -50,12 +50,12 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
name: Checkout Ruff
with:
path: ruff
persist-credentials: true
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
name: Checkout typeshed
with:
repository: python/typeshed
@@ -65,7 +65,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -112,12 +112,12 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
name: Checkout Ruff
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -150,12 +150,12 @@ jobs:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
name: Checkout Ruff
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
- uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -192,7 +192,7 @@ jobs:
permissions:
issues: write
steps:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |

View File

@@ -26,14 +26,14 @@ jobs:
timeout-minutes: 20
if: contains(github.event.label.name, 'ecosystem-analyzer')
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -64,7 +64,7 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@fc0f612798710b0dd69bb7528bc9b361dc60bd43"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@1f560d07d672effae250e3d271da53d96c5260ff"
ecosystem-analyzer \
--repository ruff \

View File

@@ -22,14 +22,14 @@ jobs:
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6.6.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -32,13 +32,13 @@ jobs:
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: python/typing
ref: ${{ env.CONFORMANCE_SUITE_COMMIT }}

View File

@@ -1,44 +1,5 @@
# Breaking Changes
## 0.13.0
- **Several rules can now add `from __future__ import annotations` automatically**
`TC001`, `TC002`, `TC003`, `RUF013`, and `UP037` now add `from __future__ import annotations` as part of their fixes when the
`lint.future-annotations` setting is enabled. This allows the rules to move
more imports into `TYPE_CHECKING` blocks (`TC001`, `TC002`, and `TC003`),
use PEP 604 union syntax on Python versions before 3.10 (`RUF013`), and
unquote more annotations (`UP037`).
- **Full module paths are now used to verify first-party modules**
Ruff now checks that the full path to a module exists on disk before
categorizing it as a first-party import. This change makes first-party
import detection more accurate, helping to avoid false positives on local
directories with the same name as a third-party dependency, for example. See
the [FAQ
section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) on import categorization for more details.
- **Deprecated rules must now be selected by exact rule code**
Ruff will no longer activate deprecated rules selected by their group name
or prefix. As noted below, the two remaining deprecated rules were also
removed in this release, so this won't affect any current rules, but it will
still affect any deprecations in the future.
- **The deprecated macOS configuration directory fallback has been removed**
Ruff will no longer look for a user-level configuration file at
`~/Library/Application Support/ruff/ruff.toml` on macOS. This feature was
deprecated in v0.5 in favor of using the [XDG
specification](https://specifications.freedesktop.org/basedir-spec/latest/)
(usually resolving to `~/.config/ruff/ruff.toml`), like on Linux. The
fallback and accompanying deprecation warning have now been removed.
- **[`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name) (`PD901`) has been removed**
- **[`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance) (`UP038`) has been removed**
## 0.12.0
- **Detection of more syntax errors**

View File

@@ -1,119 +1,545 @@
# Changelog
## 0.13.0
## 0.12.12
Check out the [blog post](https://astral.sh/blog/ruff-v0.13.0) for a migration
### Preview features
- Show fixes by default ([#19919](https://github.com/astral-sh/ruff/pull/19919))
- \[`airflow`\] Convert `DatasetOrTimeSchedule(datasets=...)` to `AssetOrTimeSchedule(assets=...)` (`AIR311`) ([#20202](https://github.com/astral-sh/ruff/pull/20202))
- \[`airflow`\] Improve the `AIR002` error message ([#20173](https://github.com/astral-sh/ruff/pull/20173))
- \[`airflow`\] Move `airflow.operators.postgres_operator.Mapping` from `AIR302` to `AIR301` ([#20172](https://github.com/astral-sh/ruff/pull/20172))
- \[`flake8-async`\] Implement `blocking-input` rule (`ASYNC250`) ([#20122](https://github.com/astral-sh/ruff/pull/20122))
- \[`flake8-use-pathlib`\] Make `PTH119` and `PTH120` fixes unsafe because they can change behavior ([#20118](https://github.com/astral-sh/ruff/pull/20118))
- \[`pylint`\] Add U+061C to `PLE2502` ([#20106](https://github.com/astral-sh/ruff/pull/20106))
- \[`ruff`\] Fix false negative for empty f-strings in `deque` calls (`RUF037`) ([#20109](https://github.com/astral-sh/ruff/pull/20109))
### Bug fixes
- Less confidently mark f-strings as empty when inferring truthiness ([#20152](https://github.com/astral-sh/ruff/pull/20152))
- \[`fastapi`\] Fix false positive for paths with spaces around parameters (`FAST003`) ([#20077](https://github.com/astral-sh/ruff/pull/20077))
- \[`flake8-comprehensions`\] Skip `C417` when lambda contains `yield`/`yield from` ([#20201](https://github.com/astral-sh/ruff/pull/20201))
- \[`perflint`\] Handle tuples in dictionary comprehensions (`PERF403`) ([#19934](https://github.com/astral-sh/ruff/pull/19934))
### Rule changes
- \[`pycodestyle`\] Preserve return type annotation for `ParamSpec` (`E731`) ([#20108](https://github.com/astral-sh/ruff/pull/20108))
### Documentation
- Add fix safety sections to docs ([#17490](https://github.com/astral-sh/ruff/pull/17490),[#17499](https://github.com/astral-sh/ruff/pull/17499))
## 0.12.11
### Preview features
- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
### Bug fixes
- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
### Rule changes
- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
### Documentation
- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
## 0.12.10
### Preview features
- \[`flake8-simplify`\] Implement fix for `maxsplit` without separator (`SIM905`) ([#19851](https://github.com/astral-sh/ruff/pull/19851))
- \[`flake8-use-pathlib`\] Add fixes for `PTH102` and `PTH103` ([#19514](https://github.com/astral-sh/ruff/pull/19514))
### Bug fixes
- \[`isort`\] Handle multiple continuation lines after module docstring (`I002`) ([#19818](https://github.com/astral-sh/ruff/pull/19818))
- \[`pyupgrade`\] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) ([#19769](https://github.com/astral-sh/ruff/pull/19769))
- \[`pyupgrade`\] Handle nested `Optional`s (`UP045`) ([#19770](https://github.com/astral-sh/ruff/pull/19770))
### Rule changes
- \[`pycodestyle`\] Make `E731` fix unsafe instead of display-only for class assignments ([#19700](https://github.com/astral-sh/ruff/pull/19700))
- \[`pyflakes`\] Add secondary annotation showing previous definition (`F811`) ([#19900](https://github.com/astral-sh/ruff/pull/19900))
### Documentation
- Fix description of global config file discovery strategy ([#19188](https://github.com/astral-sh/ruff/pull/19188))
- Update outdated links to <https://typing.python.org/en/latest/source/stubs.html> ([#19992](https://github.com/astral-sh/ruff/pull/19992))
- \[`flake8-annotations`\] Remove unused import in example (`ANN401`) ([#20000](https://github.com/astral-sh/ruff/pull/20000))
## 0.12.9
### Preview features
- \[`airflow`\] Add check for `airflow.secrets.cache.SecretCache` (`AIR301`) ([#17707](https://github.com/astral-sh/ruff/pull/17707))
- \[`ruff`\] Offer a safe fix for multi-digit zeros (`RUF064`) ([#19847](https://github.com/astral-sh/ruff/pull/19847))
### Bug fixes
- \[`flake8-blind-except`\] Fix `BLE001` false-positive on `raise ... from None` ([#19755](https://github.com/astral-sh/ruff/pull/19755))
- \[`flake8-comprehensions`\] Fix false positive for `C420` with attribute, subscript, or slice assignment targets ([#19513](https://github.com/astral-sh/ruff/pull/19513))
- \[`flake8-simplify`\] Fix handling of U+001C..U+001F whitespace (`SIM905`) ([#19849](https://github.com/astral-sh/ruff/pull/19849))
### Rule changes
- \[`pylint`\] Use lowercase hex characters to match the formatter (`PLE2513`) ([#19808](https://github.com/astral-sh/ruff/pull/19808))
### Documentation
- Fix `lint.future-annotations` link ([#19876](https://github.com/astral-sh/ruff/pull/19876))
### Other changes
- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
```diff
-unused.py:8:19: F401 [*] `pathlib` imported but unused
+F401 [*] `pathlib` imported but unused
+ --> unused.py:8:19
|
7 | # Unused, _not_ marked as required (due to the alias).
8 | import pathlib as non_alias
- | ^^^^^^^^^ F401
+ | ^^^^^^^^^
9 |
10 | # Unused, marked as required.
|
- = help: Remove unused import: `pathlib`
+help: Remove unused import: `pathlib`
```
For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
## 0.12.8
### Preview features
- \[`flake8-use-pathlib`\] Expand `PTH201` to check all `PurePath` subclasses ([#19440](https://github.com/astral-sh/ruff/pull/19440))
### Bug fixes
- \[`flake8-blind-except`\] Change `BLE001` to correctly parse exception tuples ([#19747](https://github.com/astral-sh/ruff/pull/19747))
- \[`flake8-errmsg`\] Exclude `typing.cast` from `EM101` ([#19656](https://github.com/astral-sh/ruff/pull/19656))
- \[`flake8-simplify`\] Fix raw string handling in `SIM905` for embedded quotes ([#19591](https://github.com/astral-sh/ruff/pull/19591))
- \[`flake8-import-conventions`\] Avoid false positives for NFKC-normalized `__debug__` import aliases in `ICN001` ([#19411](https://github.com/astral-sh/ruff/pull/19411))
- \[`isort`\] Fix syntax error after docstring ending with backslash (`I002`) ([#19505](https://github.com/astral-sh/ruff/pull/19505))
- \[`pylint`\] Mark `PLC0207` fixes as unsafe when `*args` unpacking is present ([#19679](https://github.com/astral-sh/ruff/pull/19679))
- \[`pyupgrade`\] Prevent infinite loop with `I002` (`UP010`, `UP035`) ([#19413](https://github.com/astral-sh/ruff/pull/19413))
- \[`ruff`\] Parenthesize generator expressions in f-strings (`RUF010`) ([#19434](https://github.com/astral-sh/ruff/pull/19434))
### Rule changes
- \[`eradicate`\] Don't flag `pyrefly` pragmas as unused code (`ERA001`) ([#19731](https://github.com/astral-sh/ruff/pull/19731))
### Documentation
- Replace "associative" with "commutative" in docs for `RUF036` ([#19706](https://github.com/astral-sh/ruff/pull/19706))
- Fix copy and line separator colors in dark mode ([#19630](https://github.com/astral-sh/ruff/pull/19630))
- Fix link to `typing` documentation ([#19648](https://github.com/astral-sh/ruff/pull/19648))
- \[`refurb`\] Make more examples error out-of-the-box ([#19695](https://github.com/astral-sh/ruff/pull/19695),[#19673](https://github.com/astral-sh/ruff/pull/19673),[#19672](https://github.com/astral-sh/ruff/pull/19672))
### Other changes
- Include column numbers in GitLab output format ([#19708](https://github.com/astral-sh/ruff/pull/19708))
- Always expand tabs to four spaces in diagnostics ([#19618](https://github.com/astral-sh/ruff/pull/19618))
- Update pre-commit's `ruff` id ([#19654](https://github.com/astral-sh/ruff/pull/19654))
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.5
### Preview features
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
### Bug fixes
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
### Rule changes
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
## 0.12.4
### Preview features
- \[`flake8-type-checking`, `pyupgrade`, `ruff`\] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) ([#19100](https://github.com/astral-sh/ruff/pull/19100))
- \[`flake8-use-pathlib`\] Add autofix for `PTH109` ([#19245](https://github.com/astral-sh/ruff/pull/19245))
- \[`pylint`\] Detect indirect `pathlib.Path` usages for `unspecified-encoding` (`PLW1514`) ([#19304](https://github.com/astral-sh/ruff/pull/19304))
### Bug fixes
- \[`flake8-bugbear`\] Fix `B017` false negatives for keyword exception arguments ([#19217](https://github.com/astral-sh/ruff/pull/19217))
- \[`flake8-use-pathlib`\] Fix false negative on direct `Path()` instantiation (`PTH210`) ([#19388](https://github.com/astral-sh/ruff/pull/19388))
- \[`flake8-django`\] Fix `DJ008` false positive for abstract models with type-annotated `abstract` field ([#19221](https://github.com/astral-sh/ruff/pull/19221))
- \[`isort`\] Fix `I002` import insertion after docstring with multiple string statements ([#19222](https://github.com/astral-sh/ruff/pull/19222))
- \[`isort`\] Treat form feed as valid whitespace before a semicolon ([#19343](https://github.com/astral-sh/ruff/pull/19343))
- \[`pydoclint`\] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) ([#19246](https://github.com/astral-sh/ruff/pull/19246))
- \[`refurb`\] `FURB164` fix should validate arguments and should usually be marked unsafe ([#19136](https://github.com/astral-sh/ruff/pull/19136))
### Rule changes
- \[`flake8-use-pathlib`\] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 ([#19331](https://github.com/astral-sh/ruff/pull/19331))
- \[`pep8_naming`\] Avoid false positives on standard library functions with uppercase names (`N802`) ([#18907](https://github.com/astral-sh/ruff/pull/18907))
- \[`pycodestyle`\] Handle brace escapes for t-strings in logical lines ([#19358](https://github.com/astral-sh/ruff/pull/19358))
- \[`pylint`\] Extend invalid string character rules to include t-strings ([#19355](https://github.com/astral-sh/ruff/pull/19355))
- \[`ruff`\] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ ([#19333](https://github.com/astral-sh/ruff/pull/19333))
### Documentation
- \[`flake8-type-checking`\] Make `TC010` docs example more realistic ([#19356](https://github.com/astral-sh/ruff/pull/19356))
- Make more documentation examples error out-of-the-box ([#19288](https://github.com/astral-sh/ruff/pull/19288),[#19272](https://github.com/astral-sh/ruff/pull/19272),[#19291](https://github.com/astral-sh/ruff/pull/19291),[#19296](https://github.com/astral-sh/ruff/pull/19296),[#19292](https://github.com/astral-sh/ruff/pull/19292),[#19295](https://github.com/astral-sh/ruff/pull/19295),[#19297](https://github.com/astral-sh/ruff/pull/19297),[#19309](https://github.com/astral-sh/ruff/pull/19309))
## 0.12.3
### Preview features
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
### Bug fixes
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
### Documentation
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
## 0.12.2
### Preview features
- \[`flake8-pyi`\] Expand `Optional[A]` to `A | None` (`PYI016`) ([#18572](https://github.com/astral-sh/ruff/pull/18572))
- \[`pyupgrade`\] Mark `UP008` fix safe if no comments are in range ([#18683](https://github.com/astral-sh/ruff/pull/18683))
### Bug fixes
- \[`flake8-comprehensions`\] Fix `C420` to prepend whitespace when needed ([#18616](https://github.com/astral-sh/ruff/pull/18616))
- \[`perflint`\] Fix `PERF403` panic on attribute or subscription loop variable ([#19042](https://github.com/astral-sh/ruff/pull/19042))
- \[`pydocstyle`\] Fix `D413` infinite loop for parenthesized docstring ([#18930](https://github.com/astral-sh/ruff/pull/18930))
- \[`pylint`\] Fix `PLW0108` autofix introducing a syntax error when the lambda's body contains an assignment expression ([#18678](https://github.com/astral-sh/ruff/pull/18678))
- \[`refurb`\] Fix false positive on empty tuples (`FURB168`) ([#19058](https://github.com/astral-sh/ruff/pull/19058))
- \[`ruff`\] Allow more `field` calls from `attrs` (`RUF009`) ([#19021](https://github.com/astral-sh/ruff/pull/19021))
- \[`ruff`\] Fix syntax error introduced for an empty string followed by a u-prefixed string (`UP025`) ([#18899](https://github.com/astral-sh/ruff/pull/18899))
### Rule changes
- \[`flake8-executable`\] Allow `uvx` in shebang line (`EXE003`) ([#18967](https://github.com/astral-sh/ruff/pull/18967))
- \[`pandas`\] Avoid flagging `PD002` if `pandas` is not imported ([#18963](https://github.com/astral-sh/ruff/pull/18963))
- \[`pyupgrade`\] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`, `UP045`) ([#18682](https://github.com/astral-sh/ruff/pull/18682))
### Documentation
- Document link between `import-outside-top-level (PLC0415)` and `lint.flake8-tidy-imports.banned-module-level-imports` ([#18733](https://github.com/astral-sh/ruff/pull/18733))
- Fix description of the `format.skip-magic-trailing-comma` example ([#19095](https://github.com/astral-sh/ruff/pull/19095))
- \[`airflow`\] Make `AIR302` example error out-of-the-box ([#18988](https://github.com/astral-sh/ruff/pull/18988))
- \[`airflow`\] Make `AIR312` example error out-of-the-box ([#18989](https://github.com/astral-sh/ruff/pull/18989))
- \[`flake8-annotations`\] Make `ANN401` example error out-of-the-box ([#18974](https://github.com/astral-sh/ruff/pull/18974))
- \[`flake8-async`\] Make `ASYNC100` example error out-of-the-box ([#18993](https://github.com/astral-sh/ruff/pull/18993))
- \[`flake8-async`\] Make `ASYNC105` example error out-of-the-box ([#19002](https://github.com/astral-sh/ruff/pull/19002))
- \[`flake8-async`\] Make `ASYNC110` example error out-of-the-box ([#18975](https://github.com/astral-sh/ruff/pull/18975))
- \[`flake8-async`\] Make `ASYNC210` example error out-of-the-box ([#18977](https://github.com/astral-sh/ruff/pull/18977))
- \[`flake8-async`\] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples error out-of-the-box ([#18978](https://github.com/astral-sh/ruff/pull/18978))
- \[`flake8-async`\] Make `ASYNC251` example error out-of-the-box ([#18990](https://github.com/astral-sh/ruff/pull/18990))
- \[`flake8-bandit`\] Make `S201` example error out-of-the-box ([#19017](https://github.com/astral-sh/ruff/pull/19017))
- \[`flake8-bandit`\] Make `S604` and `S609` examples error out-of-the-box ([#19049](https://github.com/astral-sh/ruff/pull/19049))
- \[`flake8-bugbear`\] Make `B028` example error out-of-the-box ([#19054](https://github.com/astral-sh/ruff/pull/19054))
- \[`flake8-bugbear`\] Make `B911` example error out-of-the-box ([#19051](https://github.com/astral-sh/ruff/pull/19051))
- \[`flake8-datetimez`\] Make `DTZ011` example error out-of-the-box ([#19055](https://github.com/astral-sh/ruff/pull/19055))
- \[`flake8-datetimez`\] Make `DTZ901` example error out-of-the-box ([#19056](https://github.com/astral-sh/ruff/pull/19056))
- \[`flake8-pyi`\] Make `PYI032` example error out-of-the-box ([#19061](https://github.com/astral-sh/ruff/pull/19061))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI014`, `PYI015`) ([#19097](https://github.com/astral-sh/ruff/pull/19097))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI042`) ([#19101](https://github.com/astral-sh/ruff/pull/19101))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI059`) ([#19080](https://github.com/astral-sh/ruff/pull/19080))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI062`) ([#19079](https://github.com/astral-sh/ruff/pull/19079))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT023`) ([#19104](https://github.com/astral-sh/ruff/pull/19104))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT030`) ([#19105](https://github.com/astral-sh/ruff/pull/19105))
- \[`flake8-quotes`\] Make example error out-of-the-box (`Q003`) ([#19106](https://github.com/astral-sh/ruff/pull/19106))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM110`) ([#19113](https://github.com/astral-sh/ruff/pull/19113))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM113`) ([#19109](https://github.com/astral-sh/ruff/pull/19109))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM401`) ([#19110](https://github.com/astral-sh/ruff/pull/19110))
- \[`pyflakes`\] Fix backslash in docs (`F621`) ([#19098](https://github.com/astral-sh/ruff/pull/19098))
- \[`pylint`\] Fix `PLC0415` example ([#18970](https://github.com/astral-sh/ruff/pull/18970))
## 0.12.1
### Preview features
- \[`flake8-errmsg`\] Extend `EM101` to support byte strings ([#18867](https://github.com/astral-sh/ruff/pull/18867))
- \[`flake8-use-pathlib`\] Add autofix for `PTH202` ([#18763](https://github.com/astral-sh/ruff/pull/18763))
- \[`pygrep-hooks`\] Add `AsyncMock` methods to `invalid-mock-access` (`PGH005`) ([#18547](https://github.com/astral-sh/ruff/pull/18547))
- \[`pylint`\] Ignore `__init__.py` files in (`PLC0414`) ([#18400](https://github.com/astral-sh/ruff/pull/18400))
- \[`ruff`\] Trigger `RUF037` for empty string and byte strings ([#18862](https://github.com/astral-sh/ruff/pull/18862))
- [formatter] Fix missing blank lines before decorated classes in `.pyi` files ([#18888](https://github.com/astral-sh/ruff/pull/18888))
### Bug fixes
- Avoid generating diagnostics with per-file ignores ([#18801](https://github.com/astral-sh/ruff/pull/18801))
- Handle parenthesized arguments in `remove_argument` ([#18805](https://github.com/astral-sh/ruff/pull/18805))
- \[`flake8-logging`\] Avoid false positive for `exc_info=True` outside `logger.exception` (`LOG014`) ([#18737](https://github.com/astral-sh/ruff/pull/18737))
- \[`flake8-pytest-style`\] Enforce `pytest` import for decorators ([#18779](https://github.com/astral-sh/ruff/pull/18779))
- \[`flake8-pytest-style`\] Mark autofix for `PT001` and `PT023` as unsafe if there's comments in the decorator ([#18792](https://github.com/astral-sh/ruff/pull/18792))
- \[`flake8-pytest-style`\] `PT001`/`PT023` fix makes syntax error on parenthesized decorator ([#18782](https://github.com/astral-sh/ruff/pull/18782))
- \[`flake8-raise`\] Make fix unsafe if it deletes comments (`RSE102`) ([#18788](https://github.com/astral-sh/ruff/pull/18788))
- \[`flake8-simplify`\] Fix `SIM911` autofix creating a syntax error ([#18793](https://github.com/astral-sh/ruff/pull/18793))
- \[`flake8-simplify`\] Fix false negatives for shadowed bindings (`SIM910`, `SIM911`) ([#18794](https://github.com/astral-sh/ruff/pull/18794))
- \[`flake8-simplify`\] Preserve original behavior for `except ()` and bare `except` (`SIM105`) ([#18213](https://github.com/astral-sh/ruff/pull/18213))
- \[`flake8-pyi`\] Fix `PYI041`'s fix causing `TypeError` with `None | None | ...` ([#18637](https://github.com/astral-sh/ruff/pull/18637))
- \[`perflint`\] Fix `PERF101` autofix creating a syntax error and mark autofix as unsafe if there are comments in the `list` call expr ([#18803](https://github.com/astral-sh/ruff/pull/18803))
- \[`perflint`\] Fix false negative in `PERF401` ([#18866](https://github.com/astral-sh/ruff/pull/18866))
- \[`pylint`\] Avoid flattening nested `min`/`max` when outer call has single argument (`PLW3301`) ([#16885](https://github.com/astral-sh/ruff/pull/16885))
- \[`pylint`\] Fix `PLC2801` autofix creating a syntax error ([#18857](https://github.com/astral-sh/ruff/pull/18857))
- \[`pylint`\] Mark `PLE0241` autofix as unsafe if there's comments in the base classes ([#18832](https://github.com/astral-sh/ruff/pull/18832))
- \[`pylint`\] Suppress `PLE2510`/`PLE2512`/`PLE2513`/`PLE2514`/`PLE2515` autofix if the text contains an odd number of backslashes ([#18856](https://github.com/astral-sh/ruff/pull/18856))
- \[`refurb`\] Detect more exotic float literals in `FURB164` ([#18925](https://github.com/astral-sh/ruff/pull/18925))
- \[`refurb`\] Fix `FURB163` autofix creating a syntax error for `yield` expressions ([#18756](https://github.com/astral-sh/ruff/pull/18756))
- \[`refurb`\] Mark `FURB129` autofix as unsafe if there's comments in the `readlines` call ([#18858](https://github.com/astral-sh/ruff/pull/18858))
- \[`ruff`\] Fix false positives and negatives in `RUF010` ([#18690](https://github.com/astral-sh/ruff/pull/18690))
- Fix casing of `analyze.direction` variant names ([#18892](https://github.com/astral-sh/ruff/pull/18892))
### Rule changes
- Fix f-string interpolation escaping in generated fixes ([#18882](https://github.com/astral-sh/ruff/pull/18882))
- \[`flake8-return`\] Mark `RET501` fix unsafe if comments are inside ([#18780](https://github.com/astral-sh/ruff/pull/18780))
- \[`flake8-async`\] Fix detection for large integer sleep durations in `ASYNC116` rule ([#18767](https://github.com/astral-sh/ruff/pull/18767))
- \[`flake8-async`\] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments ([#18753](https://github.com/astral-sh/ruff/pull/18753))
- \[`flake8-bugbear`\] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments ([#18755](https://github.com/astral-sh/ruff/pull/18755))
- \[`flake8-comprehension`\] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension ([#18768](https://github.com/astral-sh/ruff/pull/18768))
- \[`flake8-comprehensions`\] Handle template strings for comprehension fixes ([#18710](https://github.com/astral-sh/ruff/pull/18710))
- \[`flake8-future-annotations`\] Add autofix (`FA100`) ([#18903](https://github.com/astral-sh/ruff/pull/18903))
- \[`pyflakes`\] Mark `F504`/`F522`/`F523` autofix as unsafe if there's a call with side effect ([#18839](https://github.com/astral-sh/ruff/pull/18839))
- \[`pylint`\] Allow fix with comments and document performance implications (`PLW3301`) ([#18936](https://github.com/astral-sh/ruff/pull/18936))
- \[`pylint`\] Detect more exotic `NaN` literals in `PLW0177` ([#18630](https://github.com/astral-sh/ruff/pull/18630))
- \[`pylint`\] Fix `PLC1802` autofix creating a syntax error and mark autofix as unsafe if there's comments in the `len` call ([#18836](https://github.com/astral-sh/ruff/pull/18836))
- \[`pyupgrade`\] Extend version detection to include `sys.version_info.major` (`UP036`) ([#18633](https://github.com/astral-sh/ruff/pull/18633))
- \[`ruff`\] Add lint rule `RUF064` for calling `chmod` with non-octal integers ([#18541](https://github.com/astral-sh/ruff/pull/18541))
- \[`ruff`\] Added `cls.__dict__.get('__annotations__')` check (`RUF063`) ([#18233](https://github.com/astral-sh/ruff/pull/18233))
- \[`ruff`\] Frozen `dataclass` default should be valid (`RUF009`) ([#18735](https://github.com/astral-sh/ruff/pull/18735))
### Server
- Consider virtual path for various server actions ([#18910](https://github.com/astral-sh/ruff/pull/18910))
### Documentation
- Add fix safety sections ([#18940](https://github.com/astral-sh/ruff/pull/18940),[#18841](https://github.com/astral-sh/ruff/pull/18841),[#18802](https://github.com/astral-sh/ruff/pull/18802),[#18837](https://github.com/astral-sh/ruff/pull/18837),[#18800](https://github.com/astral-sh/ruff/pull/18800),[#18415](https://github.com/astral-sh/ruff/pull/18415),[#18853](https://github.com/astral-sh/ruff/pull/18853),[#18842](https://github.com/astral-sh/ruff/pull/18842))
- Use updated pre-commit id ([#18718](https://github.com/astral-sh/ruff/pull/18718))
- \[`perflint`\] Small docs improvement to `PERF401` ([#18786](https://github.com/astral-sh/ruff/pull/18786))
- \[`pyupgrade`\]: Use `super()`, not `__super__` in error messages (`UP008`) ([#18743](https://github.com/astral-sh/ruff/pull/18743))
- \[`flake8-pie`\] Small docs fix to `PIE794` ([#18829](https://github.com/astral-sh/ruff/pull/18829))
- \[`flake8-pyi`\] Correct `collections-named-tuple` example to use PascalCase assignment ([#16884](https://github.com/astral-sh/ruff/pull/16884))
- \[`flake8-pie`\] Add note on type checking benefits to `unnecessary-dict-kwargs` (`PIE804`) ([#18666](https://github.com/astral-sh/ruff/pull/18666))
- \[`pycodestyle`\] Clarify PEP 8 relationship to `whitespace-around-operator` rules ([#18870](https://github.com/astral-sh/ruff/pull/18870))
### Other changes
- Disallow newlines in format specifiers of single quoted f- or t-strings ([#18708](https://github.com/astral-sh/ruff/pull/18708))
- \[`flake8-logging`\] Add fix safety section to `LOG002` ([#18840](https://github.com/astral-sh/ruff/pull/18840))
- \[`pyupgrade`\] Add fix safety section to `UP010` ([#18838](https://github.com/astral-sh/ruff/pull/18838))
## 0.12.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration
guide and overview of the changes!
### Breaking changes
- **Several rules can now add `from __future__ import annotations` automatically**
- **Detection of more syntax errors**
`TC001`, `TC002`, `TC003`, `RUF013`, and `UP037` now add `from __future__ import annotations` as part of their fixes when the
`lint.future-annotations` setting is enabled. This allows the rules to move
more imports into `TYPE_CHECKING` blocks (`TC001`, `TC002`, and `TC003`),
use PEP 604 union syntax on Python versions before 3.10 (`RUF013`), and
unquote more annotations (`UP037`).
Ruff now detects version-related syntax errors, such as the use of the `match`
statement on Python versions before 3.10, and syntax errors emitted by
CPython's compiler, such as irrefutable `match` patterns before the final
`case` arm.
- **Full module paths are now used to verify first-party modules**
- **New default Python version handling for syntax errors**
Ruff now checks that the full path to a module exists on disk before
categorizing it as a first-party import. This change makes first-party
import detection more accurate, helping to avoid false positives on local
directories with the same name as a third-party dependency, for example. See
the [FAQ
section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc) on import categorization for more details.
Ruff will default to the *latest* supported Python version (3.13) when
checking for the version-related syntax errors mentioned above to prevent
false positives in projects without a Python version configured. The default
in all other cases, like applying lint rules, is unchanged and remains at the
minimum supported Python version (3.9).
- **Deprecated rules must now be selected by exact rule code**
- **Updated f-string formatting**
Ruff will no longer activate deprecated rules selected by their group name
or prefix. As noted below, the two remaining deprecated rules were also
removed in this release, so this won't affect any current rules, but it will
still affect any deprecations in the future.
Ruff now formats multi-line f-strings with format specifiers to avoid adding a
line break after the format specifier. This addresses a change to the Python
grammar in version 3.13.4 that made such a line break a syntax error.
- **The deprecated macOS configuration directory fallback has been removed**
- **`rust-toolchain.toml` is no longer included in source distributions**
Ruff will no longer look for a user-level configuration file at
`~/Library/Application Support/ruff/ruff.toml` on macOS. This feature was
deprecated in v0.5 in favor of using the [XDG
specification](https://specifications.freedesktop.org/basedir-spec/latest/)
(usually resolving to `~/.config/ruff/ruff.toml`), like on Linux. The
fallback and accompanying deprecation warning have now been removed.
The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
minimum supported Rust version (MSRV) for development and building release
artifacts. However, when present in source distributions, it would also cause
downstream package maintainers to pull in the same Rust toolchain, even if
their available toolchain was MSRV-compatible.
### Removed Rules
The following rules have been removed:
- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name) (`PD901`)
- [`non-pep604-isinstance`](https://docs.astral.sh/ruff/rules/non-pep604-isinstance) (`UP038`)
- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
(`S320`)
### Deprecated Rules
The following rules have been deprecated:
- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/)
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`airflow-dag-no-schedule-argument`](https://docs.astral.sh/ruff/rules/airflow-dag-no-schedule-argument)
(`AIR002`)
- [`airflow3-removal`](https://docs.astral.sh/ruff/rules/airflow3-removal) (`AIR301`)
- [`airflow3-moved-to-provider`](https://docs.astral.sh/ruff/rules/airflow3-moved-to-provider)
(`AIR302`)
- [`airflow3-suggested-update`](https://docs.astral.sh/ruff/rules/airflow3-suggested-update)
(`AIR311`)
- [`airflow3-suggested-to-move-to-provider`](https://docs.astral.sh/ruff/rules/airflow3-suggested-to-move-to-provider)
(`AIR312`)
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever) (`ASYNC116`)
- [`f-string-number-format`](https://docs.astral.sh/ruff/rules/f-string-number-format) (`FURB116`)
- [`os-symlink`](https://docs.astral.sh/ruff/rules/os-symlink) (`PTH211`)
- [`generic-not-last-base-class`](https://docs.astral.sh/ruff/rules/generic-not-last-base-class)
(`PYI059`)
- [`redundant-none-literal`](https://docs.astral.sh/ruff/rules/redundant-none-literal) (`PYI061`)
- [`pytest-raises-ambiguous-pattern`](https://docs.astral.sh/ruff/rules/pytest-raises-ambiguous-pattern)
(`RUF043`)
- [`unused-unpacked-variable`](https://docs.astral.sh/ruff/rules/unused-unpacked-variable)
(`RUF059`)
- [`useless-class-metaclass-type`](https://docs.astral.sh/ruff/rules/useless-class-metaclass-type)
(`UP050`)
- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`)
- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`)
- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`)
- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`)
- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`)
- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`)
- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`)
- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`)
- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`)
- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`)
- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`)
- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`)
- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`)
- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`)
- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`)
- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`)
- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`)
- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`)
- [`non-pep604-annotation-optional`] (`UP045`)
- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`)
- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`)
- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`)
The following behaviors have been stabilized:
- [`assert-raises-exception`](https://docs.astral.sh/ruff/rules/assert-raises-exception) (`B017`)
now checks for direct calls to `unittest.TestCase.assert_raises` and `pytest.raises` instead of
only the context manager forms.
- [`missing-trailing-comma`](https://docs.astral.sh/ruff/rules/missing-trailing-comma) (`COM812`)
and [`prohibited-trailing-comma`](https://docs.astral.sh/ruff/rules/prohibited-trailing-comma)
(`COM819`) now check for trailing commas in PEP 695 type parameter lists.
- [`raw-string-in-exception`](https://docs.astral.sh/ruff/rules/raw-string-in-exception) (`EM101`)
now also checks for byte strings in exception messages.
- [`invalid-mock-access`](https://docs.astral.sh/ruff/rules/invalid-mock-access) (`PGH005`) now
checks for `AsyncMock` methods like `not_awaited` in addition to the synchronous variants.
- [`useless-import-alias`](https://docs.astral.sh/ruff/rules/useless-import-alias) (`PLC0414`) no
longer applies to `__init__.py` files, where it conflicted with one of the suggested fixes for
[`unused-import`](https://docs.astral.sh/ruff/rules/unused-import) (`F401`).
- [`bidirectional-unicode`](https://docs.astral.sh/ruff/rules/bidirectional-unicode) (`PLE2502`) now
also checks for U+061C (Arabic Letter Mark).
- The fix for
[`multiple-with-statements`](https://docs.astral.sh/ruff/rules/multiple-with-statements)
(`SIM117`) is now marked as always safe.
- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in
addition to list literals and variables.
- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe.
- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify
expressions to use `or` instead of an `if` expression, where possible.
- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well
as inline comments.
- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings,
as well as lists and tuples of literal strings, as trusted input.
- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that
include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to
plain `bool` annotations.
- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules.
`UP007` now applies only to `typing.Union`, while
[`non-pep604-annotation-optional`] (`UP045`) checks for use of
`typing.Optional`. `UP045` has also been stabilized in this release, but you
may need to update existing `include`, `ignore`, or `noqa` settings to
accommodate this change.
### Preview features
- \[`pyupgrade`\] Enable `UP043` in stub files ([#20027](https://github.com/astral-sh/ruff/pull/20027))
- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368))
- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664))
### Bug fixes
- \[`pyupgrade`\] Apply `UP008` only when the `__class__` cell exists ([#19424](https://github.com/astral-sh/ruff/pull/19424))
- \[`ruff`\] Fix empty f-string detection in `in-empty-collection` (`RUF060`) ([#20249](https://github.com/astral-sh/ruff/pull/20249))
- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706))
- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542))
- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673))
- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704))
- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611))
- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428))
- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472))
- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479))
- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688))
- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592))
- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614))
- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598))
- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631))
- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509))
- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665))
### Rule changes
- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408))
- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532))
- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311))
- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545))
- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478))
- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645))
### Server
- Add support for using uv as an alternative formatter backend ([#19665](https://github.com/astral-sh/ruff/pull/19665))
- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627))
### Documentation
- \[`pep8-naming`\] Fix formatting of `__all__` (`N816`) ([#20301](https://github.com/astral-sh/ruff/pull/20301))
## 0.12.x
See [changelogs/0.12.x](./changelogs/0.12.x.md)
- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709))
- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491))
- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613))
- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615))
## 0.11.x
@@ -158,3 +584,12 @@ See [changelogs/0.2.x](./changelogs/0.2.x.md)
## 0.1.x
See [changelogs/0.1.x](./changelogs/0.1.x.md)
[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument
[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation
[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp
[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional
[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union
[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for
[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true
[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa

198
Cargo.lock generated
View File

@@ -322,11 +322,11 @@ dependencies = [
[[package]]
name = "camino"
version = "1.2.0"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603"
checksum = "dd0b03af37dad7a14518b7691d81acb0f8222604ad3d1b02f6b4bed5188c0cd5"
dependencies = [
"serde_core",
"serde",
]
[[package]]
@@ -376,7 +376,7 @@ dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-link 0.1.3",
"windows-link",
]
[[package]]
@@ -603,7 +603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -612,7 +612,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -826,13 +826,12 @@ dependencies = [
[[package]]
name = "ctrlc"
version = "3.5.0"
version = "3.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3"
checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73"
dependencies = [
"dispatch",
"nix 0.30.1",
"windows-sys 0.61.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -959,12 +958,6 @@ dependencies = [
"windows-sys 0.60.2",
]
[[package]]
name = "dispatch"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "displaydoc"
version = "0.2.5"
@@ -1042,7 +1035,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1493,9 +1486,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.11.1"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [
"equivalent",
"hashbrown 0.15.5",
@@ -1624,7 +1617,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1688,7 +1681,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2133,9 +2126,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordermap"
version = "0.5.10"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dcd63f1ae4b091e314a26627c467dd8810d674ba798abc0e566679955776c63"
checksum = "2fd6fedcd996c8c97932075cc3811d83f53280f48d5620e4e3cab7f6a12678c4"
dependencies = [
"indexmap",
"serde",
@@ -2482,16 +2475,16 @@ dependencies = [
[[package]]
name = "pyproject-toml"
version = "0.13.6"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec768e063102b426e8962989758115e8659485124de9207bc365fab524125d65"
checksum = "7b0f6160dc48298b9260d9b958ad1d7f96f6cd0b9df200b22329204e09334663"
dependencies = [
"indexmap",
"pep440_rs",
"pep508_rs",
"serde",
"thiserror 2.0.16",
"toml",
"toml 0.8.23",
]
[[package]]
@@ -2642,9 +2635,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.11.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
dependencies = [
"either",
"rayon-core",
@@ -2652,9 +2645,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.13.0"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@@ -2728,7 +2721,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.13.0"
version = "0.12.12"
dependencies = [
"anyhow",
"argfile",
@@ -2780,7 +2773,7 @@ dependencies = [
"test-case",
"thiserror 2.0.16",
"tikv-jemallocator",
"toml",
"toml 0.9.5",
"tracing",
"walkdir",
"wild",
@@ -2796,7 +2789,7 @@ dependencies = [
"ruff_annotate_snippets",
"serde",
"snapbox",
"toml",
"toml 0.9.5",
"tryfn",
"unicode-width 0.2.1",
]
@@ -2915,7 +2908,7 @@ dependencies = [
"similar",
"strum",
"tempfile",
"toml",
"toml 0.9.5",
"tracing",
"tracing-indicatif",
"tracing-subscriber",
@@ -2984,7 +2977,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.13.0"
version = "0.12.12"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3016,7 +3009,6 @@ dependencies = [
"ruff_notebook",
"ruff_python_ast",
"ruff_python_codegen",
"ruff_python_importer",
"ruff_python_index",
"ruff_python_literal",
"ruff_python_parser",
@@ -3036,7 +3028,7 @@ dependencies = [
"tempfile",
"test-case",
"thiserror 2.0.16",
"toml",
"toml 0.9.5",
"typed-arena",
"unicode-normalization",
"unicode-width 0.2.1",
@@ -3167,21 +3159,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "ruff_python_importer"
version = "0.0.0"
dependencies = [
"anyhow",
"insta",
"ruff_diagnostics",
"ruff_python_ast",
"ruff_python_codegen",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
]
[[package]]
name = "ruff_python_index"
version = "0.0.0"
@@ -3309,7 +3286,7 @@ dependencies = [
"serde_json",
"shellexpand",
"thiserror 2.0.16",
"toml",
"toml 0.9.5",
"tracing",
"tracing-log",
"tracing-subscriber",
@@ -3338,7 +3315,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.13.0"
version = "0.12.12"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3399,7 +3376,7 @@ dependencies = [
"shellexpand",
"strum",
"tempfile",
"toml",
"toml 0.9.5",
"unicode-normalization",
]
@@ -3435,7 +3412,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3453,7 +3430,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3713cd7eb30821c0c086591832dd6f59f2af7fe7#3713cd7eb30821c0c086591832dd6f59f2af7fe7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
dependencies = [
"boxcar",
"compact_str",
@@ -3477,12 +3454,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3713cd7eb30821c0c086591832dd6f59f2af7fe7#3713cd7eb30821c0c086591832dd6f59f2af7fe7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=3713cd7eb30821c0c086591832dd6f59f2af7fe7#3713cd7eb30821c0c086591832dd6f59f2af7fe7"
source = "git+https://github.com/salsa-rs/salsa.git?rev=a3ffa22cb26756473d56f867aedec3fd907c4dd9#a3ffa22cb26756473d56f867aedec3fd907c4dd9"
dependencies = [
"proc-macro2",
"quote",
@@ -3537,11 +3514,10 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.223"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_core",
"serde_derive",
]
@@ -3556,20 +3532,11 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "serde_core"
version = "1.0.223"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.223"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
@@ -3589,15 +3556,14 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.145"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
@@ -3611,6 +3577,15 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_spanned"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "1.0.0"
@@ -3822,15 +3797,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.22.0"
version = "3.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4022,6 +3997,18 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"toml_edit",
]
[[package]]
name = "toml"
version = "0.9.5"
@@ -4030,7 +4017,7 @@ checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"serde_spanned 1.0.0",
"toml_datetime 0.7.0",
"toml_parser",
"toml_writer",
@@ -4042,6 +4029,9 @@ name = "toml_datetime"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
@@ -4059,6 +4049,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
"indexmap",
"serde",
"serde_spanned 0.6.9",
"toml_datetime 0.6.11",
"winnow",
]
@@ -4200,7 +4192,7 @@ dependencies = [
"ruff_python_trivia",
"salsa",
"tempfile",
"toml",
"toml 0.9.5",
"tracing",
"tracing-flame",
"tracing-subscriber",
@@ -4234,12 +4226,9 @@ dependencies = [
"rayon",
"regex",
"ruff_db",
"ruff_diagnostics",
"ruff_index",
"ruff_memory_usage",
"ruff_python_ast",
"ruff_python_codegen",
"ruff_python_importer",
"ruff_python_parser",
"ruff_python_trivia",
"ruff_source_file",
@@ -4282,7 +4271,7 @@ dependencies = [
"schemars",
"serde",
"thiserror 2.0.16",
"toml",
"toml 0.9.5",
"tracing",
"ty_combine",
"ty_python_semantic",
@@ -4410,7 +4399,7 @@ dependencies = [
"smallvec",
"tempfile",
"thiserror 2.0.16",
"toml",
"toml 0.9.5",
"tracing",
"ty_python_semantic",
"ty_static",
@@ -4519,9 +4508,9 @@ checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561"
[[package]]
name = "unicode-ident"
version = "1.0.19"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unicode-normalization"
@@ -4622,9 +4611,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
@@ -4635,9 +4624,9 @@ dependencies = [
[[package]]
name = "uuid-macro-internal"
version = "1.18.1"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9384a660318abfbd7f8932c34d67e4d1ec511095f95972ddc01e19d7ba8413f"
checksum = "26b682e8c381995ea03130e381928e0e005b7c9eb483c6c8682f50e07b33c2b7"
dependencies = [
"proc-macro2",
"quote",
@@ -4889,7 +4878,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4900,7 +4889,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-link",
"windows-result",
"windows-strings",
]
@@ -4933,19 +4922,13 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]]
name = "windows-link"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link 0.1.3",
"windows-link",
]
[[package]]
@@ -4954,7 +4937,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.1.3",
"windows-link",
]
[[package]]
@@ -4984,15 +4967,6 @@ dependencies = [
"windows-targets 0.53.3",
]
[[package]]
name = "windows-sys"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa"
dependencies = [
"windows-link 0.2.0",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -5015,7 +4989,7 @@ version = "0.53.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
dependencies = [
"windows-link 0.1.3",
"windows-link",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",

View File

@@ -29,7 +29,6 @@ ruff_options_metadata = { path = "crates/ruff_options_metadata" }
ruff_python_ast = { path = "crates/ruff_python_ast" }
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
ruff_python_formatter = { path = "crates/ruff_python_formatter" }
ruff_python_importer = { path = "crates/ruff_python_importer" }
ruff_python_index = { path = "crates/ruff_python_index" }
ruff_python_literal = { path = "crates/ruff_python_literal" }
ruff_python_parser = { path = "crates/ruff_python_parser" }
@@ -144,7 +143,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "3713cd7eb30821c0c086591832dd6f59f2af7fe7", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "a3ffa22cb26756473d56f867aedec3fd907c4dd9", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.13.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.13.0/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.12/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.12/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.0
rev: v0.12.12
hooks:
# Run the linter.
- id: ruff-check
@@ -421,7 +421,7 @@ Ruff is released under the MIT license.
Ruff is used by a number of major open-source projects and companies, including:
- [Albumentations](https://github.com/albumentations-team/AlbumentationsX)
- [Albumentations](https://github.com/albumentations-team/albumentations)
- Amazon ([AWS SAM](https://github.com/aws/serverless-application-model))
- [Anki](https://apps.ankiweb.net/)
- Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python))

View File

@@ -1,551 +0,0 @@
# Changelog 0.12.x
## 0.12.0
Check out the [blog post](https://astral.sh/blog/ruff-v0.12.0) for a migration
guide and overview of the changes!
### Breaking changes
- **Detection of more syntax errors**
Ruff now detects version-related syntax errors, such as the use of the `match`
statement on Python versions before 3.10, and syntax errors emitted by
CPython's compiler, such as irrefutable `match` patterns before the final
`case` arm.
- **New default Python version handling for syntax errors**
Ruff will default to the *latest* supported Python version (3.13) when
checking for the version-related syntax errors mentioned above to prevent
false positives in projects without a Python version configured. The default
in all other cases, like applying lint rules, is unchanged and remains at the
minimum supported Python version (3.9).
- **Updated f-string formatting**
Ruff now formats multi-line f-strings with format specifiers to avoid adding a
line break after the format specifier. This addresses a change to the Python
grammar in version 3.13.4 that made such a line break a syntax error.
- **`rust-toolchain.toml` is no longer included in source distributions**
The `rust-toolchain.toml` is used to specify a higher Rust version than Ruff's
minimum supported Rust version (MSRV) for development and building release
artifacts. However, when present in source distributions, it would also cause
downstream package maintainers to pull in the same Rust toolchain, even if
their available toolchain was MSRV-compatible.
### Removed Rules
The following rules have been removed:
- [`suspicious-xmle-tree-usage`](https://docs.astral.sh/ruff/rules/suspicious-xmle-tree-usage/)
(`S320`)
### Deprecated Rules
The following rules have been deprecated:
- [`pandas-df-variable-name`](https://docs.astral.sh/ruff/rules/pandas-df-variable-name/)
### Stabilization
The following rules have been stabilized and are no longer in preview:
- [`for-loop-writes`](https://docs.astral.sh/ruff/rules/for-loop-writes) (`FURB122`)
- [`check-and-remove-from-set`](https://docs.astral.sh/ruff/rules/check-and-remove-from-set) (`FURB132`)
- [`verbose-decimal-constructor`](https://docs.astral.sh/ruff/rules/verbose-decimal-constructor) (`FURB157`)
- [`fromisoformat-replace-z`](https://docs.astral.sh/ruff/rules/fromisoformat-replace-z) (`FURB162`)
- [`int-on-sliced-str`](https://docs.astral.sh/ruff/rules/int-on-sliced-str) (`FURB166`)
- [`exc-info-outside-except-handler`](https://docs.astral.sh/ruff/rules/exc-info-outside-except-handler) (`LOG014`)
- [`import-outside-top-level`](https://docs.astral.sh/ruff/rules/import-outside-top-level) (`PLC0415`)
- [`unnecessary-dict-index-lookup`](https://docs.astral.sh/ruff/rules/unnecessary-dict-index-lookup) (`PLR1733`)
- [`nan-comparison`](https://docs.astral.sh/ruff/rules/nan-comparison) (`PLW0177`)
- [`eq-without-hash`](https://docs.astral.sh/ruff/rules/eq-without-hash) (`PLW1641`)
- [`pytest-parameter-with-default-argument`](https://docs.astral.sh/ruff/rules/pytest-parameter-with-default-argument) (`PT028`)
- [`pytest-warns-too-broad`](https://docs.astral.sh/ruff/rules/pytest-warns-too-broad) (`PT030`)
- [`pytest-warns-with-multiple-statements`](https://docs.astral.sh/ruff/rules/pytest-warns-with-multiple-statements) (`PT031`)
- [`invalid-formatter-suppression-comment`](https://docs.astral.sh/ruff/rules/invalid-formatter-suppression-comment) (`RUF028`)
- [`dataclass-enum`](https://docs.astral.sh/ruff/rules/dataclass-enum) (`RUF049`)
- [`class-with-mixed-type-vars`](https://docs.astral.sh/ruff/rules/class-with-mixed-type-vars) (`RUF053`)
- [`unnecessary-round`](https://docs.astral.sh/ruff/rules/unnecessary-round) (`RUF057`)
- [`starmap-zip`](https://docs.astral.sh/ruff/rules/starmap-zip) (`RUF058`)
- [`non-pep604-annotation-optional`] (`UP045`)
- [`non-pep695-generic-class`](https://docs.astral.sh/ruff/rules/non-pep695-generic-class) (`UP046`)
- [`non-pep695-generic-function`](https://docs.astral.sh/ruff/rules/non-pep695-generic-function) (`UP047`)
- [`private-type-parameter`](https://docs.astral.sh/ruff/rules/private-type-parameter) (`UP049`)
The following behaviors have been stabilized:
- [`collection-literal-concatenation`] (`RUF005`) now recognizes slices, in
addition to list literals and variables.
- The fix for [`readlines-in-for`] (`FURB129`) is now marked as always safe.
- [`if-else-block-instead-of-if-exp`] (`SIM108`) will now further simplify
expressions to use `or` instead of an `if` expression, where possible.
- [`unused-noqa`] (`RUF100`) now checks for file-level `noqa` comments as well
as inline comments.
- [`subprocess-without-shell-equals-true`] (`S603`) now accepts literal strings,
as well as lists and tuples of literal strings, as trusted input.
- [`boolean-type-hint-positional-argument`] (`FBT001`) now applies to types that
include `bool`, like `bool | int` or `typing.Optional[bool]`, in addition to
plain `bool` annotations.
- [`non-pep604-annotation-union`] (`UP007`) has now been split into two rules.
`UP007` now applies only to `typing.Union`, while
[`non-pep604-annotation-optional`] (`UP045`) checks for use of
`typing.Optional`. `UP045` has also been stabilized in this release, but you
may need to update existing `include`, `ignore`, or `noqa` settings to
accommodate this change.
### Preview features
- \[`ruff`\] Check for non-context-manager use of `pytest.raises`, `pytest.warns`, and `pytest.deprecated_call` (`RUF061`) ([#17368](https://github.com/astral-sh/ruff/pull/17368))
- [syntax-errors] Raise unsupported syntax error for template strings prior to Python 3.14 ([#18664](https://github.com/astral-sh/ruff/pull/18664))
### Bug fixes
- Add syntax error when conversion flag does not immediately follow exclamation mark ([#18706](https://github.com/astral-sh/ruff/pull/18706))
- Add trailing space around `readlines` ([#18542](https://github.com/astral-sh/ruff/pull/18542))
- Fix `\r` and `\r\n` handling in t- and f-string debug texts ([#18673](https://github.com/astral-sh/ruff/pull/18673))
- Hug closing `}` when f-string expression has a format specifier ([#18704](https://github.com/astral-sh/ruff/pull/18704))
- \[`flake8-pyi`\] Avoid syntax error in the case of starred and keyword arguments (`PYI059`) ([#18611](https://github.com/astral-sh/ruff/pull/18611))
- \[`flake8-return`\] Fix `RET504` autofix generating a syntax error ([#18428](https://github.com/astral-sh/ruff/pull/18428))
- \[`pep8-naming`\] Suppress fix for `N804` and `N805` if the recommended name is already used ([#18472](https://github.com/astral-sh/ruff/pull/18472))
- \[`pycodestyle`\] Avoid causing a syntax error in expressions spanning multiple lines (`E731`) ([#18479](https://github.com/astral-sh/ruff/pull/18479))
- \[`pyupgrade`\] Suppress `UP008` if `super` is shadowed ([#18688](https://github.com/astral-sh/ruff/pull/18688))
- \[`refurb`\] Parenthesize lambda and ternary expressions (`FURB122`, `FURB142`) ([#18592](https://github.com/astral-sh/ruff/pull/18592))
- \[`ruff`\] Handle extra arguments to `deque` (`RUF037`) ([#18614](https://github.com/astral-sh/ruff/pull/18614))
- \[`ruff`\] Preserve parentheses around `deque` in fix for `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#18598](https://github.com/astral-sh/ruff/pull/18598))
- \[`ruff`\] Validate arguments before offering a fix (`RUF056`) ([#18631](https://github.com/astral-sh/ruff/pull/18631))
- \[`ruff`\] Skip fix for `RUF059` if dummy name is already bound ([#18509](https://github.com/astral-sh/ruff/pull/18509))
- \[`pylint`\] Fix `PLW0128` to check assignment targets in square brackets and after asterisks ([#18665](https://github.com/astral-sh/ruff/pull/18665))
### Rule changes
- Fix false positive on mutations in `return` statements (`B909`) ([#18408](https://github.com/astral-sh/ruff/pull/18408))
- Treat `ty:` comments as pragma comments ([#18532](https://github.com/astral-sh/ruff/pull/18532))
- \[`flake8-pyi`\] Apply `custom-typevar-for-self` to string annotations (`PYI019`) ([#18311](https://github.com/astral-sh/ruff/pull/18311))
- \[`pyupgrade`\] Don't offer a fix for `Optional[None]` (`UP007`, `UP045)` ([#18545](https://github.com/astral-sh/ruff/pull/18545))
- \[`pyupgrade`\] Fix `super(__class__, self)` detection (`UP008`) ([#18478](https://github.com/astral-sh/ruff/pull/18478))
- \[`refurb`\] Make the fix for `FURB163` unsafe for `log2`, `log10`, `*args`, and deleted comments ([#18645](https://github.com/astral-sh/ruff/pull/18645))
### Server
- Support cancellation requests ([#18627](https://github.com/astral-sh/ruff/pull/18627))
### Documentation
- Drop confusing second `*` from glob pattern example for `per-file-target-version` ([#18709](https://github.com/astral-sh/ruff/pull/18709))
- Update Neovim configuration examples ([#18491](https://github.com/astral-sh/ruff/pull/18491))
- \[`pylint`\] De-emphasize `__hash__ = Parent.__hash__` (`PLW1641`) ([#18613](https://github.com/astral-sh/ruff/pull/18613))
- \[`refurb`\] Add a note about float literal handling (`FURB157`) ([#18615](https://github.com/astral-sh/ruff/pull/18615))
## 0.12.1
### Preview features
- \[`flake8-errmsg`\] Extend `EM101` to support byte strings ([#18867](https://github.com/astral-sh/ruff/pull/18867))
- \[`flake8-use-pathlib`\] Add autofix for `PTH202` ([#18763](https://github.com/astral-sh/ruff/pull/18763))
- \[`pygrep-hooks`\] Add `AsyncMock` methods to `invalid-mock-access` (`PGH005`) ([#18547](https://github.com/astral-sh/ruff/pull/18547))
- \[`pylint`\] Ignore `__init__.py` files in (`PLC0414`) ([#18400](https://github.com/astral-sh/ruff/pull/18400))
- \[`ruff`\] Trigger `RUF037` for empty string and byte strings ([#18862](https://github.com/astral-sh/ruff/pull/18862))
- [formatter] Fix missing blank lines before decorated classes in `.pyi` files ([#18888](https://github.com/astral-sh/ruff/pull/18888))
### Bug fixes
- Avoid generating diagnostics with per-file ignores ([#18801](https://github.com/astral-sh/ruff/pull/18801))
- Handle parenthesized arguments in `remove_argument` ([#18805](https://github.com/astral-sh/ruff/pull/18805))
- \[`flake8-logging`\] Avoid false positive for `exc_info=True` outside `logger.exception` (`LOG014`) ([#18737](https://github.com/astral-sh/ruff/pull/18737))
- \[`flake8-pytest-style`\] Enforce `pytest` import for decorators ([#18779](https://github.com/astral-sh/ruff/pull/18779))
- \[`flake8-pytest-style`\] Mark autofix for `PT001` and `PT023` as unsafe if there's comments in the decorator ([#18792](https://github.com/astral-sh/ruff/pull/18792))
- \[`flake8-pytest-style`\] `PT001`/`PT023` fix makes syntax error on parenthesized decorator ([#18782](https://github.com/astral-sh/ruff/pull/18782))
- \[`flake8-raise`\] Make fix unsafe if it deletes comments (`RSE102`) ([#18788](https://github.com/astral-sh/ruff/pull/18788))
- \[`flake8-simplify`\] Fix `SIM911` autofix creating a syntax error ([#18793](https://github.com/astral-sh/ruff/pull/18793))
- \[`flake8-simplify`\] Fix false negatives for shadowed bindings (`SIM910`, `SIM911`) ([#18794](https://github.com/astral-sh/ruff/pull/18794))
- \[`flake8-simplify`\] Preserve original behavior for `except ()` and bare `except` (`SIM105`) ([#18213](https://github.com/astral-sh/ruff/pull/18213))
- \[`flake8-pyi`\] Fix `PYI041`'s fix causing `TypeError` with `None | None | ...` ([#18637](https://github.com/astral-sh/ruff/pull/18637))
- \[`perflint`\] Fix `PERF101` autofix creating a syntax error and mark autofix as unsafe if there are comments in the `list` call expr ([#18803](https://github.com/astral-sh/ruff/pull/18803))
- \[`perflint`\] Fix false negative in `PERF401` ([#18866](https://github.com/astral-sh/ruff/pull/18866))
- \[`pylint`\] Avoid flattening nested `min`/`max` when outer call has single argument (`PLW3301`) ([#16885](https://github.com/astral-sh/ruff/pull/16885))
- \[`pylint`\] Fix `PLC2801` autofix creating a syntax error ([#18857](https://github.com/astral-sh/ruff/pull/18857))
- \[`pylint`\] Mark `PLE0241` autofix as unsafe if there's comments in the base classes ([#18832](https://github.com/astral-sh/ruff/pull/18832))
- \[`pylint`\] Suppress `PLE2510`/`PLE2512`/`PLE2513`/`PLE2514`/`PLE2515` autofix if the text contains an odd number of backslashes ([#18856](https://github.com/astral-sh/ruff/pull/18856))
- \[`refurb`\] Detect more exotic float literals in `FURB164` ([#18925](https://github.com/astral-sh/ruff/pull/18925))
- \[`refurb`\] Fix `FURB163` autofix creating a syntax error for `yield` expressions ([#18756](https://github.com/astral-sh/ruff/pull/18756))
- \[`refurb`\] Mark `FURB129` autofix as unsafe if there's comments in the `readlines` call ([#18858](https://github.com/astral-sh/ruff/pull/18858))
- \[`ruff`\] Fix false positives and negatives in `RUF010` ([#18690](https://github.com/astral-sh/ruff/pull/18690))
- Fix casing of `analyze.direction` variant names ([#18892](https://github.com/astral-sh/ruff/pull/18892))
### Rule changes
- Fix f-string interpolation escaping in generated fixes ([#18882](https://github.com/astral-sh/ruff/pull/18882))
- \[`flake8-return`\] Mark `RET501` fix unsafe if comments are inside ([#18780](https://github.com/astral-sh/ruff/pull/18780))
- \[`flake8-async`\] Fix detection for large integer sleep durations in `ASYNC116` rule ([#18767](https://github.com/astral-sh/ruff/pull/18767))
- \[`flake8-async`\] Mark autofix for `ASYNC115` as unsafe if the call expression contains comments ([#18753](https://github.com/astral-sh/ruff/pull/18753))
- \[`flake8-bugbear`\] Mark autofix for `B004` as unsafe if the `hasattr` call expr contains comments ([#18755](https://github.com/astral-sh/ruff/pull/18755))
- \[`flake8-comprehension`\] Mark autofix for `C420` as unsafe if there's comments inside the dict comprehension ([#18768](https://github.com/astral-sh/ruff/pull/18768))
- \[`flake8-comprehensions`\] Handle template strings for comprehension fixes ([#18710](https://github.com/astral-sh/ruff/pull/18710))
- \[`flake8-future-annotations`\] Add autofix (`FA100`) ([#18903](https://github.com/astral-sh/ruff/pull/18903))
- \[`pyflakes`\] Mark `F504`/`F522`/`F523` autofix as unsafe if there's a call with side effect ([#18839](https://github.com/astral-sh/ruff/pull/18839))
- \[`pylint`\] Allow fix with comments and document performance implications (`PLW3301`) ([#18936](https://github.com/astral-sh/ruff/pull/18936))
- \[`pylint`\] Detect more exotic `NaN` literals in `PLW0177` ([#18630](https://github.com/astral-sh/ruff/pull/18630))
- \[`pylint`\] Fix `PLC1802` autofix creating a syntax error and mark autofix as unsafe if there's comments in the `len` call ([#18836](https://github.com/astral-sh/ruff/pull/18836))
- \[`pyupgrade`\] Extend version detection to include `sys.version_info.major` (`UP036`) ([#18633](https://github.com/astral-sh/ruff/pull/18633))
- \[`ruff`\] Add lint rule `RUF064` for calling `chmod` with non-octal integers ([#18541](https://github.com/astral-sh/ruff/pull/18541))
- \[`ruff`\] Added `cls.__dict__.get('__annotations__')` check (`RUF063`) ([#18233](https://github.com/astral-sh/ruff/pull/18233))
- \[`ruff`\] Frozen `dataclass` default should be valid (`RUF009`) ([#18735](https://github.com/astral-sh/ruff/pull/18735))
### Server
- Consider virtual path for various server actions ([#18910](https://github.com/astral-sh/ruff/pull/18910))
### Documentation
- Add fix safety sections ([#18940](https://github.com/astral-sh/ruff/pull/18940),[#18841](https://github.com/astral-sh/ruff/pull/18841),[#18802](https://github.com/astral-sh/ruff/pull/18802),[#18837](https://github.com/astral-sh/ruff/pull/18837),[#18800](https://github.com/astral-sh/ruff/pull/18800),[#18415](https://github.com/astral-sh/ruff/pull/18415),[#18853](https://github.com/astral-sh/ruff/pull/18853),[#18842](https://github.com/astral-sh/ruff/pull/18842))
- Use updated pre-commit id ([#18718](https://github.com/astral-sh/ruff/pull/18718))
- \[`perflint`\] Small docs improvement to `PERF401` ([#18786](https://github.com/astral-sh/ruff/pull/18786))
- \[`pyupgrade`\]: Use `super()`, not `__super__` in error messages (`UP008`) ([#18743](https://github.com/astral-sh/ruff/pull/18743))
- \[`flake8-pie`\] Small docs fix to `PIE794` ([#18829](https://github.com/astral-sh/ruff/pull/18829))
- \[`flake8-pyi`\] Correct `collections-named-tuple` example to use PascalCase assignment ([#16884](https://github.com/astral-sh/ruff/pull/16884))
- \[`flake8-pie`\] Add note on type checking benefits to `unnecessary-dict-kwargs` (`PIE804`) ([#18666](https://github.com/astral-sh/ruff/pull/18666))
- \[`pycodestyle`\] Clarify PEP 8 relationship to `whitespace-around-operator` rules ([#18870](https://github.com/astral-sh/ruff/pull/18870))
### Other changes
- Disallow newlines in format specifiers of single quoted f- or t-strings ([#18708](https://github.com/astral-sh/ruff/pull/18708))
- \[`flake8-logging`\] Add fix safety section to `LOG002` ([#18840](https://github.com/astral-sh/ruff/pull/18840))
- \[`pyupgrade`\] Add fix safety section to `UP010` ([#18838](https://github.com/astral-sh/ruff/pull/18838))
## 0.12.2
### Preview features
- \[`flake8-pyi`\] Expand `Optional[A]` to `A | None` (`PYI016`) ([#18572](https://github.com/astral-sh/ruff/pull/18572))
- \[`pyupgrade`\] Mark `UP008` fix safe if no comments are in range ([#18683](https://github.com/astral-sh/ruff/pull/18683))
### Bug fixes
- \[`flake8-comprehensions`\] Fix `C420` to prepend whitespace when needed ([#18616](https://github.com/astral-sh/ruff/pull/18616))
- \[`perflint`\] Fix `PERF403` panic on attribute or subscription loop variable ([#19042](https://github.com/astral-sh/ruff/pull/19042))
- \[`pydocstyle`\] Fix `D413` infinite loop for parenthesized docstring ([#18930](https://github.com/astral-sh/ruff/pull/18930))
- \[`pylint`\] Fix `PLW0108` autofix introducing a syntax error when the lambda's body contains an assignment expression ([#18678](https://github.com/astral-sh/ruff/pull/18678))
- \[`refurb`\] Fix false positive on empty tuples (`FURB168`) ([#19058](https://github.com/astral-sh/ruff/pull/19058))
- \[`ruff`\] Allow more `field` calls from `attrs` (`RUF009`) ([#19021](https://github.com/astral-sh/ruff/pull/19021))
- \[`ruff`\] Fix syntax error introduced for an empty string followed by a u-prefixed string (`UP025`) ([#18899](https://github.com/astral-sh/ruff/pull/18899))
### Rule changes
- \[`flake8-executable`\] Allow `uvx` in shebang line (`EXE003`) ([#18967](https://github.com/astral-sh/ruff/pull/18967))
- \[`pandas`\] Avoid flagging `PD002` if `pandas` is not imported ([#18963](https://github.com/astral-sh/ruff/pull/18963))
- \[`pyupgrade`\] Avoid PEP-604 unions with `typing.NamedTuple` (`UP007`, `UP045`) ([#18682](https://github.com/astral-sh/ruff/pull/18682))
### Documentation
- Document link between `import-outside-top-level (PLC0415)` and `lint.flake8-tidy-imports.banned-module-level-imports` ([#18733](https://github.com/astral-sh/ruff/pull/18733))
- Fix description of the `format.skip-magic-trailing-comma` example ([#19095](https://github.com/astral-sh/ruff/pull/19095))
- \[`airflow`\] Make `AIR302` example error out-of-the-box ([#18988](https://github.com/astral-sh/ruff/pull/18988))
- \[`airflow`\] Make `AIR312` example error out-of-the-box ([#18989](https://github.com/astral-sh/ruff/pull/18989))
- \[`flake8-annotations`\] Make `ANN401` example error out-of-the-box ([#18974](https://github.com/astral-sh/ruff/pull/18974))
- \[`flake8-async`\] Make `ASYNC100` example error out-of-the-box ([#18993](https://github.com/astral-sh/ruff/pull/18993))
- \[`flake8-async`\] Make `ASYNC105` example error out-of-the-box ([#19002](https://github.com/astral-sh/ruff/pull/19002))
- \[`flake8-async`\] Make `ASYNC110` example error out-of-the-box ([#18975](https://github.com/astral-sh/ruff/pull/18975))
- \[`flake8-async`\] Make `ASYNC210` example error out-of-the-box ([#18977](https://github.com/astral-sh/ruff/pull/18977))
- \[`flake8-async`\] Make `ASYNC220`, `ASYNC221`, and `ASYNC222` examples error out-of-the-box ([#18978](https://github.com/astral-sh/ruff/pull/18978))
- \[`flake8-async`\] Make `ASYNC251` example error out-of-the-box ([#18990](https://github.com/astral-sh/ruff/pull/18990))
- \[`flake8-bandit`\] Make `S201` example error out-of-the-box ([#19017](https://github.com/astral-sh/ruff/pull/19017))
- \[`flake8-bandit`\] Make `S604` and `S609` examples error out-of-the-box ([#19049](https://github.com/astral-sh/ruff/pull/19049))
- \[`flake8-bugbear`\] Make `B028` example error out-of-the-box ([#19054](https://github.com/astral-sh/ruff/pull/19054))
- \[`flake8-bugbear`\] Make `B911` example error out-of-the-box ([#19051](https://github.com/astral-sh/ruff/pull/19051))
- \[`flake8-datetimez`\] Make `DTZ011` example error out-of-the-box ([#19055](https://github.com/astral-sh/ruff/pull/19055))
- \[`flake8-datetimez`\] Make `DTZ901` example error out-of-the-box ([#19056](https://github.com/astral-sh/ruff/pull/19056))
- \[`flake8-pyi`\] Make `PYI032` example error out-of-the-box ([#19061](https://github.com/astral-sh/ruff/pull/19061))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI014`, `PYI015`) ([#19097](https://github.com/astral-sh/ruff/pull/19097))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI042`) ([#19101](https://github.com/astral-sh/ruff/pull/19101))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI059`) ([#19080](https://github.com/astral-sh/ruff/pull/19080))
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI062`) ([#19079](https://github.com/astral-sh/ruff/pull/19079))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT023`) ([#19104](https://github.com/astral-sh/ruff/pull/19104))
- \[`flake8-pytest-style`\] Make example error out-of-the-box (`PT030`) ([#19105](https://github.com/astral-sh/ruff/pull/19105))
- \[`flake8-quotes`\] Make example error out-of-the-box (`Q003`) ([#19106](https://github.com/astral-sh/ruff/pull/19106))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM110`) ([#19113](https://github.com/astral-sh/ruff/pull/19113))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM113`) ([#19109](https://github.com/astral-sh/ruff/pull/19109))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM401`) ([#19110](https://github.com/astral-sh/ruff/pull/19110))
- \[`pyflakes`\] Fix backslash in docs (`F621`) ([#19098](https://github.com/astral-sh/ruff/pull/19098))
- \[`pylint`\] Fix `PLC0415` example ([#18970](https://github.com/astral-sh/ruff/pull/18970))
## 0.12.3
### Preview features
- \[`flake8-bugbear`\] Support non-context-manager calls in `B017` ([#19063](https://github.com/astral-sh/ruff/pull/19063))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH100`, `PTH106`, `PTH107`, `PTH108`, `PTH110`, `PTH111`, `PTH112`, `PTH113`, `PTH114`, `PTH115`, `PTH117`, `PTH119`, `PTH120` ([#19213](https://github.com/astral-sh/ruff/pull/19213))
- \[`flake8-use-pathlib`\] Add autofixes for `PTH203`, `PTH204`, `PTH205` ([#18922](https://github.com/astral-sh/ruff/pull/18922))
### Bug fixes
- \[`flake8-return`\] Fix false-positive for variables used inside nested functions in `RET504` ([#18433](https://github.com/astral-sh/ruff/pull/18433))
- Treat form feed as valid whitespace before a line continuation ([#19220](https://github.com/astral-sh/ruff/pull/19220))
- \[`flake8-type-checking`\] Fix syntax error introduced by fix (`TC008`) ([#19150](https://github.com/astral-sh/ruff/pull/19150))
- \[`pyupgrade`\] Keyword arguments in `super` should suppress the `UP008` fix ([#19131](https://github.com/astral-sh/ruff/pull/19131))
### Documentation
- \[`flake8-pyi`\] Make example error out-of-the-box (`PYI007`, `PYI008`) ([#19103](https://github.com/astral-sh/ruff/pull/19103))
- \[`flake8-simplify`\] Make example error out-of-the-box (`SIM116`) ([#19111](https://github.com/astral-sh/ruff/pull/19111))
- \[`flake8-type-checking`\] Make example error out-of-the-box (`TC001`) ([#19151](https://github.com/astral-sh/ruff/pull/19151))
- \[`flake8-use-pathlib`\] Make example error out-of-the-box (`PTH210`) ([#19189](https://github.com/astral-sh/ruff/pull/19189))
- \[`pycodestyle`\] Make example error out-of-the-box (`E272`) ([#19191](https://github.com/astral-sh/ruff/pull/19191))
- \[`pycodestyle`\] Make example not raise unnecessary `SyntaxError` (`E114`) ([#19190](https://github.com/astral-sh/ruff/pull/19190))
- \[`pydoclint`\] Make example error out-of-the-box (`DOC501`) ([#19218](https://github.com/astral-sh/ruff/pull/19218))
- \[`pylint`, `pyupgrade`\] Fix syntax errors in examples (`PLW1501`, `UP028`) ([#19127](https://github.com/astral-sh/ruff/pull/19127))
- \[`pylint`\] Update `missing-maxsplit-arg` docs and error to suggest proper usage (`PLC0207`) ([#18949](https://github.com/astral-sh/ruff/pull/18949))
- \[`flake8-bandit`\] Make example error out-of-the-box (`S412`) ([#19241](https://github.com/astral-sh/ruff/pull/19241))
## 0.12.4
### Preview features
- \[`flake8-type-checking`, `pyupgrade`, `ruff`\] Add `from __future__ import annotations` when it would allow new fixes (`TC001`, `TC002`, `TC003`, `UP037`, `RUF013`) ([#19100](https://github.com/astral-sh/ruff/pull/19100))
- \[`flake8-use-pathlib`\] Add autofix for `PTH109` ([#19245](https://github.com/astral-sh/ruff/pull/19245))
- \[`pylint`\] Detect indirect `pathlib.Path` usages for `unspecified-encoding` (`PLW1514`) ([#19304](https://github.com/astral-sh/ruff/pull/19304))
### Bug fixes
- \[`flake8-bugbear`\] Fix `B017` false negatives for keyword exception arguments ([#19217](https://github.com/astral-sh/ruff/pull/19217))
- \[`flake8-use-pathlib`\] Fix false negative on direct `Path()` instantiation (`PTH210`) ([#19388](https://github.com/astral-sh/ruff/pull/19388))
- \[`flake8-django`\] Fix `DJ008` false positive for abstract models with type-annotated `abstract` field ([#19221](https://github.com/astral-sh/ruff/pull/19221))
- \[`isort`\] Fix `I002` import insertion after docstring with multiple string statements ([#19222](https://github.com/astral-sh/ruff/pull/19222))
- \[`isort`\] Treat form feed as valid whitespace before a semicolon ([#19343](https://github.com/astral-sh/ruff/pull/19343))
- \[`pydoclint`\] Fix `SyntaxError` from fixes with line continuations (`D201`, `D202`) ([#19246](https://github.com/astral-sh/ruff/pull/19246))
- \[`refurb`\] `FURB164` fix should validate arguments and should usually be marked unsafe ([#19136](https://github.com/astral-sh/ruff/pull/19136))
### Rule changes
- \[`flake8-use-pathlib`\] Skip single dots for `invalid-pathlib-with-suffix` (`PTH210`) on versions >= 3.14 ([#19331](https://github.com/astral-sh/ruff/pull/19331))
- \[`pep8_naming`\] Avoid false positives on standard library functions with uppercase names (`N802`) ([#18907](https://github.com/astral-sh/ruff/pull/18907))
- \[`pycodestyle`\] Handle brace escapes for t-strings in logical lines ([#19358](https://github.com/astral-sh/ruff/pull/19358))
- \[`pylint`\] Extend invalid string character rules to include t-strings ([#19355](https://github.com/astral-sh/ruff/pull/19355))
- \[`ruff`\] Allow `strict` kwarg when checking for `starmap-zip` (`RUF058`) in Python 3.14+ ([#19333](https://github.com/astral-sh/ruff/pull/19333))
### Documentation
- \[`flake8-type-checking`\] Make `TC010` docs example more realistic ([#19356](https://github.com/astral-sh/ruff/pull/19356))
- Make more documentation examples error out-of-the-box ([#19288](https://github.com/astral-sh/ruff/pull/19288),[#19272](https://github.com/astral-sh/ruff/pull/19272),[#19291](https://github.com/astral-sh/ruff/pull/19291),[#19296](https://github.com/astral-sh/ruff/pull/19296),[#19292](https://github.com/astral-sh/ruff/pull/19292),[#19295](https://github.com/astral-sh/ruff/pull/19295),[#19297](https://github.com/astral-sh/ruff/pull/19297),[#19309](https://github.com/astral-sh/ruff/pull/19309))
## 0.12.5
### Preview features
- \[`flake8-use-pathlib`\] Add autofix for `PTH101`, `PTH104`, `PTH105`, `PTH121` ([#19404](https://github.com/astral-sh/ruff/pull/19404))
- \[`ruff`\] Support byte strings (`RUF055`) ([#18926](https://github.com/astral-sh/ruff/pull/18926))
### Bug fixes
- Fix `unreachable` panic in parser ([#19183](https://github.com/astral-sh/ruff/pull/19183))
- \[`flake8-pyi`\] Skip fix if all `Union` members are `None` (`PYI016`) ([#19416](https://github.com/astral-sh/ruff/pull/19416))
- \[`perflint`\] Parenthesize generator expressions (`PERF401`) ([#19325](https://github.com/astral-sh/ruff/pull/19325))
- \[`pylint`\] Handle empty comments after line continuation (`PLR2044`) ([#19405](https://github.com/astral-sh/ruff/pull/19405))
### Rule changes
- \[`pep8-naming`\] Fix `N802` false positives for `CGIHTTPRequestHandler` and `SimpleHTTPRequestHandler` ([#19432](https://github.com/astral-sh/ruff/pull/19432))
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.8
### Preview features
- \[`flake8-use-pathlib`\] Expand `PTH201` to check all `PurePath` subclasses ([#19440](https://github.com/astral-sh/ruff/pull/19440))
### Bug fixes
- \[`flake8-blind-except`\] Change `BLE001` to correctly parse exception tuples ([#19747](https://github.com/astral-sh/ruff/pull/19747))
- \[`flake8-errmsg`\] Exclude `typing.cast` from `EM101` ([#19656](https://github.com/astral-sh/ruff/pull/19656))
- \[`flake8-simplify`\] Fix raw string handling in `SIM905` for embedded quotes ([#19591](https://github.com/astral-sh/ruff/pull/19591))
- \[`flake8-import-conventions`\] Avoid false positives for NFKC-normalized `__debug__` import aliases in `ICN001` ([#19411](https://github.com/astral-sh/ruff/pull/19411))
- \[`isort`\] Fix syntax error after docstring ending with backslash (`I002`) ([#19505](https://github.com/astral-sh/ruff/pull/19505))
- \[`pylint`\] Mark `PLC0207` fixes as unsafe when `*args` unpacking is present ([#19679](https://github.com/astral-sh/ruff/pull/19679))
- \[`pyupgrade`\] Prevent infinite loop with `I002` (`UP010`, `UP035`) ([#19413](https://github.com/astral-sh/ruff/pull/19413))
- \[`ruff`\] Parenthesize generator expressions in f-strings (`RUF010`) ([#19434](https://github.com/astral-sh/ruff/pull/19434))
### Rule changes
- \[`eradicate`\] Don't flag `pyrefly` pragmas as unused code (`ERA001`) ([#19731](https://github.com/astral-sh/ruff/pull/19731))
### Documentation
- Replace "associative" with "commutative" in docs for `RUF036` ([#19706](https://github.com/astral-sh/ruff/pull/19706))
- Fix copy and line separator colors in dark mode ([#19630](https://github.com/astral-sh/ruff/pull/19630))
- Fix link to `typing` documentation ([#19648](https://github.com/astral-sh/ruff/pull/19648))
- \[`refurb`\] Make more examples error out-of-the-box ([#19695](https://github.com/astral-sh/ruff/pull/19695),[#19673](https://github.com/astral-sh/ruff/pull/19673),[#19672](https://github.com/astral-sh/ruff/pull/19672))
### Other changes
- Include column numbers in GitLab output format ([#19708](https://github.com/astral-sh/ruff/pull/19708))
- Always expand tabs to four spaces in diagnostics ([#19618](https://github.com/astral-sh/ruff/pull/19618))
- Update pre-commit's `ruff` id ([#19654](https://github.com/astral-sh/ruff/pull/19654))
## 0.12.9
### Preview features
- \[`airflow`\] Add check for `airflow.secrets.cache.SecretCache` (`AIR301`) ([#17707](https://github.com/astral-sh/ruff/pull/17707))
- \[`ruff`\] Offer a safe fix for multi-digit zeros (`RUF064`) ([#19847](https://github.com/astral-sh/ruff/pull/19847))
### Bug fixes
- \[`flake8-blind-except`\] Fix `BLE001` false-positive on `raise ... from None` ([#19755](https://github.com/astral-sh/ruff/pull/19755))
- \[`flake8-comprehensions`\] Fix false positive for `C420` with attribute, subscript, or slice assignment targets ([#19513](https://github.com/astral-sh/ruff/pull/19513))
- \[`flake8-simplify`\] Fix handling of U+001C..U+001F whitespace (`SIM905`) ([#19849](https://github.com/astral-sh/ruff/pull/19849))
### Rule changes
- \[`pylint`\] Use lowercase hex characters to match the formatter (`PLE2513`) ([#19808](https://github.com/astral-sh/ruff/pull/19808))
### Documentation
- Fix `lint.future-annotations` link ([#19876](https://github.com/astral-sh/ruff/pull/19876))
### Other changes
- Build `riscv64` binaries for release ([#19819](https://github.com/astral-sh/ruff/pull/19819))
- Add rule code to error description in GitLab output ([#19896](https://github.com/astral-sh/ruff/pull/19896))
- Improve rendering of the `full` output format ([#19415](https://github.com/astral-sh/ruff/pull/19415))
Below is an example diff for [`F401`](https://docs.astral.sh/ruff/rules/unused-import/):
```diff
-unused.py:8:19: F401 [*] `pathlib` imported but unused
+F401 [*] `pathlib` imported but unused
+ --> unused.py:8:19
|
7 | # Unused, _not_ marked as required (due to the alias).
8 | import pathlib as non_alias
- | ^^^^^^^^^ F401
+ | ^^^^^^^^^
9 |
10 | # Unused, marked as required.
|
- = help: Remove unused import: `pathlib`
+help: Remove unused import: `pathlib`
```
For now, the primary difference is the movement of the filename, line number, and column information to a second line in the header. This new representation will allow us to make further additions to Ruff's diagnostics, such as adding sub-diagnostics and multiple annotations to the same snippet.
## 0.12.10
### Preview features
- \[`flake8-simplify`\] Implement fix for `maxsplit` without separator (`SIM905`) ([#19851](https://github.com/astral-sh/ruff/pull/19851))
- \[`flake8-use-pathlib`\] Add fixes for `PTH102` and `PTH103` ([#19514](https://github.com/astral-sh/ruff/pull/19514))
### Bug fixes
- \[`isort`\] Handle multiple continuation lines after module docstring (`I002`) ([#19818](https://github.com/astral-sh/ruff/pull/19818))
- \[`pyupgrade`\] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) ([#19769](https://github.com/astral-sh/ruff/pull/19769))
- \[`pyupgrade`\] Handle nested `Optional`s (`UP045`) ([#19770](https://github.com/astral-sh/ruff/pull/19770))
### Rule changes
- \[`pycodestyle`\] Make `E731` fix unsafe instead of display-only for class assignments ([#19700](https://github.com/astral-sh/ruff/pull/19700))
- \[`pyflakes`\] Add secondary annotation showing previous definition (`F811`) ([#19900](https://github.com/astral-sh/ruff/pull/19900))
### Documentation
- Fix description of global config file discovery strategy ([#19188](https://github.com/astral-sh/ruff/pull/19188))
- Update outdated links to <https://typing.python.org/en/latest/source/stubs.html> ([#19992](https://github.com/astral-sh/ruff/pull/19992))
- \[`flake8-annotations`\] Remove unused import in example (`ANN401`) ([#20000](https://github.com/astral-sh/ruff/pull/20000))
## 0.12.11
### Preview features
- \[`airflow`\] Extend `AIR311` and `AIR312` rules ([#20082](https://github.com/astral-sh/ruff/pull/20082))
- \[`airflow`\] Replace wrong path `airflow.io.storage` with `airflow.io.store` (`AIR311`) ([#20081](https://github.com/astral-sh/ruff/pull/20081))
- \[`flake8-async`\] Implement `blocking-http-call-httpx-in-async-function` (`ASYNC212`) ([#20091](https://github.com/astral-sh/ruff/pull/20091))
- \[`flake8-logging-format`\] Add auto-fix for f-string logging calls (`G004`) ([#19303](https://github.com/astral-sh/ruff/pull/19303))
- \[`flake8-use-pathlib`\] Add autofix for `PTH211` ([#20009](https://github.com/astral-sh/ruff/pull/20009))
- \[`flake8-use-pathlib`\] Make `PTH100` fix unsafe because it can change behavior ([#20100](https://github.com/astral-sh/ruff/pull/20100))
### Bug fixes
- \[`pyflakes`, `pylint`\] Fix false positives caused by `__class__` cell handling (`F841`, `PLE0117`) ([#20048](https://github.com/astral-sh/ruff/pull/20048))
- \[`pyflakes`\] Fix `allowed-unused-imports` matching for top-level modules (`F401`) ([#20115](https://github.com/astral-sh/ruff/pull/20115))
- \[`ruff`\] Fix false positive for t-strings in `default-factory-kwarg` (`RUF026`) ([#20032](https://github.com/astral-sh/ruff/pull/20032))
- \[`ruff`\] Preserve relative whitespace in multi-line expressions (`RUF033`) ([#19647](https://github.com/astral-sh/ruff/pull/19647))
### Rule changes
- \[`ruff`\] Handle empty t-strings in `unnecessary-empty-iterable-within-deque-call` (`RUF037`) ([#20045](https://github.com/astral-sh/ruff/pull/20045))
### Documentation
- Fix incorrect `D413` links in docstrings convention FAQ ([#20089](https://github.com/astral-sh/ruff/pull/20089))
- \[`flake8-use-pathlib`\] Update links to the table showing the correspondence between `os` and `pathlib` ([#20103](https://github.com/astral-sh/ruff/pull/20103))
## 0.12.12
### Preview features
- Show fixes by default ([#19919](https://github.com/astral-sh/ruff/pull/19919))
- \[`airflow`\] Convert `DatasetOrTimeSchedule(datasets=...)` to `AssetOrTimeSchedule(assets=...)` (`AIR311`) ([#20202](https://github.com/astral-sh/ruff/pull/20202))
- \[`airflow`\] Improve the `AIR002` error message ([#20173](https://github.com/astral-sh/ruff/pull/20173))
- \[`airflow`\] Move `airflow.operators.postgres_operator.Mapping` from `AIR302` to `AIR301` ([#20172](https://github.com/astral-sh/ruff/pull/20172))
- \[`flake8-async`\] Implement `blocking-input` rule (`ASYNC250`) ([#20122](https://github.com/astral-sh/ruff/pull/20122))
- \[`flake8-use-pathlib`\] Make `PTH119` and `PTH120` fixes unsafe because they can change behavior ([#20118](https://github.com/astral-sh/ruff/pull/20118))
- \[`pylint`\] Add U+061C to `PLE2502` ([#20106](https://github.com/astral-sh/ruff/pull/20106))
- \[`ruff`\] Fix false negative for empty f-strings in `deque` calls (`RUF037`) ([#20109](https://github.com/astral-sh/ruff/pull/20109))
### Bug fixes
- Less confidently mark f-strings as empty when inferring truthiness ([#20152](https://github.com/astral-sh/ruff/pull/20152))
- \[`fastapi`\] Fix false positive for paths with spaces around parameters (`FAST003`) ([#20077](https://github.com/astral-sh/ruff/pull/20077))
- \[`flake8-comprehensions`\] Skip `C417` when lambda contains `yield`/`yield from` ([#20201](https://github.com/astral-sh/ruff/pull/20201))
- \[`perflint`\] Handle tuples in dictionary comprehensions (`PERF403`) ([#19934](https://github.com/astral-sh/ruff/pull/19934))
### Rule changes
- \[`pycodestyle`\] Preserve return type annotation for `ParamSpec` (`E731`) ([#20108](https://github.com/astral-sh/ruff/pull/20108))
### Documentation
- Add fix safety sections to docs ([#17490](https://github.com/astral-sh/ruff/pull/17490),[#17499](https://github.com/astral-sh/ruff/pull/17499))
[`boolean-type-hint-positional-argument`]: https://docs.astral.sh/ruff/rules/boolean-type-hint-positional-argument
[`collection-literal-concatenation`]: https://docs.astral.sh/ruff/rules/collection-literal-concatenation
[`if-else-block-instead-of-if-exp`]: https://docs.astral.sh/ruff/rules/if-else-block-instead-of-if-exp
[`non-pep604-annotation-optional`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-optional
[`non-pep604-annotation-union`]: https://docs.astral.sh/ruff/rules/non-pep604-annotation-union
[`readlines-in-for`]: https://docs.astral.sh/ruff/rules/readlines-in-for
[`subprocess-without-shell-equals-true`]: https://docs.astral.sh/ruff/rules/subprocess-without-shell-equals-true
[`unused-noqa`]: https://docs.astral.sh/ruff/rules/unused-noqa

View File

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

View File

@@ -6,14 +6,12 @@ use std::time::Instant;
use anyhow::Result;
use colored::Colorize;
use ignore::Error;
use log::{debug, warn};
use log::{debug, error, warn};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use rustc_hash::FxHashMap;
use ruff_db::diagnostic::{
Annotation, Diagnostic, DiagnosticId, Span, SubDiagnostic, SubDiagnosticSeverity,
};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::panic::catch_unwind;
use ruff_linter::package::PackageRoot;
use ruff_linter::registry::Rule;
@@ -195,24 +193,21 @@ fn lint_path(
match result {
Ok(inner) => inner,
Err(error) => {
let message = match error.payload.as_str() {
Some(summary) => format!("Fatal error while linting: {summary}"),
_ => "Fatal error while linting".to_owned(),
};
let mut diagnostic = Diagnostic::new(
DiagnosticId::Panic,
ruff_db::diagnostic::Severity::Fatal,
message,
let message = r"This indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the following stack trace, we'd be very appreciative!
";
error!(
"{}{}{} {message}\n{error}",
"Panicked while linting ".bold(),
fs::relativize_path(path).bold(),
":".bold()
);
let span = Span::from(SourceFileBuilder::new(path.to_string_lossy(), "").finish());
let mut annotation = Annotation::primary(span);
annotation.set_file_level(true);
diagnostic.annotate(annotation);
diagnostic.sub(SubDiagnostic::new(
SubDiagnosticSeverity::Info,
format!("{error}"),
));
Ok(Diagnostics::new(vec![diagnostic], FxHashMap::default()))
Ok(Diagnostics::default())
}
}
}

View File

@@ -9,11 +9,10 @@ use std::sync::mpsc::channel;
use anyhow::Result;
use clap::CommandFactory;
use colored::Colorize;
use log::{error, warn};
use log::warn;
use notify::{RecursiveMode, Watcher, recommended_watcher};
use args::{GlobalConfigArgs, ServerCommand};
use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_linter::logging::{LogLevel, set_up_logging};
use ruff_linter::settings::flags::FixMode;
use ruff_linter::settings::types::OutputFormat;
@@ -445,27 +444,6 @@ pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result<Exi
}
if !cli.exit_zero {
let max_severity = diagnostics
.inner
.iter()
.map(Diagnostic::severity)
.max()
.unwrap_or(Severity::Info);
if max_severity.is_fatal() {
// When a panic/fatal error is reported, prompt the user to open an issue on github.
// Diagnostics with severity `fatal` will be sorted to the bottom, and printing the
// message here instead of attaching it to the diagnostic ensures that we only print
// it once instead of repeating it for each diagnostic. Prints to stderr to prevent
// the message from being captured by tools parsing the normal output.
let message = "Panic during linting indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
";
error!("{message}");
return Ok(ExitStatus::Error);
}
if cli.diff {
// If we're printing a diff, we always want to exit non-zero if there are
// any fixable violations (since we've printed the diff, but not applied the

View File

@@ -10,12 +10,13 @@ use ruff_linter::linter::FixTable;
use serde::Serialize;
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics,
DisplayGithubDiagnostics, GithubRenderer, SecondaryCode,
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, DisplayDiagnostics, SecondaryCode,
};
use ruff_linter::fs::relativize_path;
use ruff_linter::logging::LogLevel;
use ruff_linter::message::{Emitter, EmitterContext, GroupedEmitter, SarifEmitter, TextEmitter};
use ruff_linter::message::{
Emitter, EmitterContext, GithubEmitter, GroupedEmitter, SarifEmitter, TextEmitter,
};
use ruff_linter::notify_user;
use ruff_linter::settings::flags::{self};
use ruff_linter::settings::types::{OutputFormat, UnsafeFixes};
@@ -225,26 +226,32 @@ impl Printer {
let context = EmitterContext::new(&diagnostics.notebook_indexes);
let fixables = FixableStatistics::try_from(diagnostics, self.unsafe_fixes);
let config = DisplayDiagnosticConfig::default().preview(preview);
match self.format {
OutputFormat::Json => {
let config = config.format(DiagnosticFormat::Json);
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Json)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Rdjson => {
let config = config.format(DiagnosticFormat::Rdjson);
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Rdjson)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::JsonLines => {
let config = config.format(DiagnosticFormat::JsonLines);
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::JsonLines)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Junit => {
let config = config.format(DiagnosticFormat::Junit);
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Junit)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
@@ -283,22 +290,26 @@ impl Printer {
self.write_summary_text(writer, diagnostics)?;
}
OutputFormat::Github => {
let renderer = GithubRenderer::new(&context, "Ruff");
let value = DisplayGithubDiagnostics::new(&renderer, &diagnostics.inner);
write!(writer, "{value}")?;
GithubEmitter.emit(writer, &diagnostics.inner, &context)?;
}
OutputFormat::Gitlab => {
let config = config.format(DiagnosticFormat::Gitlab);
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Gitlab)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Pylint => {
let config = config.format(DiagnosticFormat::Pylint);
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Pylint)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}
OutputFormat::Azure => {
let config = config.format(DiagnosticFormat::Azure);
let config = DisplayDiagnosticConfig::default()
.format(DiagnosticFormat::Azure)
.preview(preview);
let value = DisplayDiagnostics::new(&context, &config, &diagnostics.inner);
write!(writer, "{value}")?;
}

View File

@@ -246,59 +246,6 @@ fn string_detection() -> Result<()> {
Ok(())
}
#[test]
fn string_detection_from_config() -> Result<()> {
let tempdir = TempDir::new()?;
let root = ChildPath::new(tempdir.path());
// Configure string import detection with a lower min-dots via ruff.toml
root.child("ruff.toml").write_str(indoc::indoc! {r#"
[analyze]
detect-string-imports = true
string-imports-min-dots = 1
"#})?;
root.child("ruff").child("__init__.py").write_str("")?;
root.child("ruff")
.child("a.py")
.write_str(indoc::indoc! {r#"
import ruff.b
"#})?;
root.child("ruff")
.child("b.py")
.write_str(indoc::indoc! {r#"
import importlib
importlib.import_module("ruff.c")
"#})?;
root.child("ruff").child("c.py").write_str("")?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": []
}
----- stderr -----
"#);
});
Ok(())
}
#[test]
fn globs() -> Result<()> {
let tempdir = TempDir::new()?;

View File

@@ -5059,59 +5059,6 @@ fn flake8_import_convention_unused_aliased_import_no_conflict() {
);
}
// https://github.com/astral-sh/ruff/issues/19842
#[test]
fn pyupgrade_up026_respects_isort_required_import_fix() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("--isolated")
.arg("check")
.arg("-")
.args(["--select", "I002,UP026"])
.arg("--config")
.arg(r#"lint.isort.required-imports=["import mock"]"#)
.arg("--fix")
.arg("--no-cache")
.pass_stdin("1\n"),
@r"
success: true
exit_code: 0
----- stdout -----
import mock
1
----- stderr -----
Found 1 error (1 fixed, 0 remaining).
"
);
}
// https://github.com/astral-sh/ruff/issues/19842
#[test]
fn pyupgrade_up026_respects_isort_required_import_from_fix() {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.arg("--isolated")
.arg("check")
.arg("-")
.args(["--select", "I002,UP026"])
.arg("--config")
.arg(r#"lint.isort.required-imports = ["from mock import mock"]"#)
.arg("--fix")
.arg("--no-cache")
.pass_stdin("from mock import mock\n"),
@r"
success: true
exit_code: 0
----- stdout -----
from mock import mock
----- stderr -----
All checks passed!
"
);
}
// See: https://github.com/astral-sh/ruff/issues/16177
#[test]
fn flake8_pyi_redundant_none_literal() {
@@ -5891,113 +5838,3 @@ fn show_fixes_in_full_output_with_preview_enabled() {
",
);
}
#[test]
fn rule_panic_mixed_results_concise() -> Result<()> {
let tempdir = TempDir::new()?;
// Create python files
let file_a_path = tempdir.path().join("normal.py");
let file_b_path = tempdir.path().join("panic.py");
fs::write(&file_a_path, b"import os")?;
fs::write(&file_b_path, b"print('hello, world!')")?;
insta::with_settings!({
filters => vec![
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
(r"\\", r"/"),
]
}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--select", "RUF9", "--preview", "--output-format=concise", "--no-cache"])
.args([file_a_path, file_b_path]),
@r"
success: false
exit_code: 2
----- stdout -----
[TMP]/normal.py:1:1: RUF900 Hey this is a stable test rule.
[TMP]/normal.py:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
[TMP]/normal.py:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
[TMP]/normal.py:1:1: RUF903 Hey this is a stable test rule with a display only fix.
[TMP]/normal.py:1:1: RUF911 Hey this is a preview test rule.
[TMP]/normal.py:1:1: RUF950 Hey this is a test rule that was redirected from another.
[TMP]/panic.py: panic: Fatal error while linting: This is a fake panic for testing.
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
error: Panic during linting indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
");
});
Ok(())
}
#[test]
fn rule_panic_mixed_results_full() -> Result<()> {
let tempdir = TempDir::new()?;
// Create python files
let file_a_path = tempdir.path().join("normal.py");
let file_b_path = tempdir.path().join("panic.py");
fs::write(&file_a_path, b"import os")?;
fs::write(&file_b_path, b"print('hello, world!')")?;
insta::with_settings!({
filters => vec![
(tempdir_filter(&tempdir).as_str(), "[TMP]/"),
(r"\\", r"/"),
]
}, {
assert_cmd_snapshot!(
Command::new(get_cargo_bin(BIN_NAME))
.args(["check", "--select", "RUF9", "--preview", "--output-format=full", "--no-cache"])
.args([file_a_path, file_b_path]),
@r"
success: false
exit_code: 2
----- stdout -----
RUF900 Hey this is a stable test rule.
--> [TMP]/normal.py:1:1
RUF901 [*] Hey this is a stable test rule with a safe fix.
--> [TMP]/normal.py:1:1
1 + # fix from stable-test-rule-safe-fix
2 | import os
RUF902 Hey this is a stable test rule with an unsafe fix.
--> [TMP]/normal.py:1:1
RUF903 Hey this is a stable test rule with a display only fix.
--> [TMP]/normal.py:1:1
RUF911 Hey this is a preview test rule.
--> [TMP]/normal.py:1:1
RUF950 Hey this is a test rule that was redirected from another.
--> [TMP]/normal.py:1:1
panic: Fatal error while linting: This is a fake panic for testing.
--> [TMP]/panic.py:1:1
info: panicked at crates/ruff_linter/src/rules/ruff/rules/test_rules.rs:511:9:
This is a fake panic for testing.
run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Found 7 errors.
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
error: Panic during linting indicates a bug in Ruff. If you could open an issue at:
https://github.com/astral-sh/ruff/issues/new?title=%5BLinter%20panic%5D
...with the relevant file contents, the `pyproject.toml` settings, and the stack trace above, we'd be very appreciative!
");
});
Ok(())
}

View File

@@ -8,7 +8,6 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
github::{DisplayGithubDiagnostics, GithubRenderer},
};
use crate::{Db, files::File};
@@ -495,17 +494,22 @@ impl Diagnostic {
self.primary_span()?.range()
}
/// Returns the [`TextRange`] for the diagnostic.
///
/// Panics if the diagnostic has no primary span or if the span has no range.
pub fn expect_range(&self) -> TextRange {
self.range().expect("Expected a range for the primary span")
}
/// Returns the ordering of diagnostics based on the start of their ranges, if they have any.
///
/// Panics if either diagnostic has no primary span, or if its file is not a `SourceFile`.
pub fn ruff_start_ordering(&self, other: &Self) -> std::cmp::Ordering {
let a = (
self.severity().is_fatal(),
self.expect_ruff_source_file(),
self.range().map(|r| r.start()),
);
let b = (
other.severity().is_fatal(),
other.expect_ruff_source_file(),
other.range().map(|r| r.start()),
);
@@ -1450,11 +1454,6 @@ pub enum DiagnosticFormat {
/// [Code Quality]: https://docs.gitlab.com/ci/testing/code_quality/#code-quality-report-format
#[cfg(feature = "serde")]
Gitlab,
/// Print diagnostics in the format used by [GitHub Actions] workflow error annotations.
///
/// [GitHub Actions]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands#setting-an-error-message
Github,
}
/// A representation of the kinds of messages inside a diagnostic.

View File

@@ -25,13 +25,11 @@ use super::{
use azure::AzureRenderer;
use concise::ConciseRenderer;
use github::GithubRenderer;
use pylint::PylintRenderer;
mod azure;
mod concise;
mod full;
pub mod github;
#[cfg(feature = "serde")]
mod gitlab;
#[cfg(feature = "serde")]
@@ -144,9 +142,6 @@ impl std::fmt::Display for DisplayDiagnostics<'_> {
DiagnosticFormat::Gitlab => {
gitlab::GitlabRenderer::new(self.resolver).render(f, self.diagnostics)?;
}
DiagnosticFormat::Github => {
GithubRenderer::new(self.resolver, "ty").render(f, self.diagnostics)?;
}
}
Ok(())

View File

@@ -1,136 +0,0 @@
use crate::diagnostic::{Diagnostic, FileResolver, Severity};
pub struct GithubRenderer<'a> {
resolver: &'a dyn FileResolver,
program: &'a str,
}
impl<'a> GithubRenderer<'a> {
pub fn new(resolver: &'a dyn FileResolver, program: &'a str) -> Self {
Self { resolver, program }
}
pub(super) fn render(
&self,
f: &mut std::fmt::Formatter,
diagnostics: &[Diagnostic],
) -> std::fmt::Result {
for diagnostic in diagnostics {
let severity = match diagnostic.severity() {
Severity::Info => "notice",
Severity::Warning => "warning",
Severity::Error | Severity::Fatal => "error",
};
write!(
f,
"::{severity} title={program} ({code})",
program = self.program,
code = diagnostic.secondary_code_or_id()
)?;
if let Some(span) = diagnostic.primary_span() {
let file = span.file();
write!(f, ",file={file}", file = file.path(self.resolver))?;
let (start_location, end_location) = if self.resolver.is_notebook(file) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
None
} else {
let diagnostic_source = file.diagnostic_source(self.resolver);
let source_code = diagnostic_source.as_source_code();
span.range().map(|range| {
(
source_code.line_column(range.start()),
source_code.line_column(range.end()),
)
})
}
.unwrap_or_default();
write!(
f,
",line={row},col={column},endLine={end_row},endColumn={end_column}::",
row = start_location.line,
column = start_location.column,
end_row = end_location.line,
end_column = end_location.column,
)?;
write!(
f,
"{path}:{row}:{column}: ",
path = file.relative_path(self.resolver).display(),
row = start_location.line,
column = start_location.column,
)?;
} else {
write!(f, "::")?;
}
if let Some(code) = diagnostic.secondary_code() {
write!(f, "{code}")?;
} else {
write!(f, "{id}:", id = diagnostic.id())?;
}
writeln!(f, " {}", diagnostic.body())?;
}
Ok(())
}
}
pub struct DisplayGithubDiagnostics<'a> {
renderer: &'a GithubRenderer<'a>,
diagnostics: &'a [Diagnostic],
}
impl<'a> DisplayGithubDiagnostics<'a> {
pub fn new(renderer: &'a GithubRenderer<'a>, diagnostics: &'a [Diagnostic]) -> Self {
Self {
renderer,
diagnostics,
}
}
}
impl std::fmt::Display for DisplayGithubDiagnostics<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.renderer.render(f, self.diagnostics)
}
}
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat,
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
fn output() {
let (env, diagnostics) = create_diagnostics(DiagnosticFormat::Github);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
#[test]
fn syntax_errors() {
let (env, diagnostics) = create_syntax_error_diagnostics(DiagnosticFormat::Github);
insta::assert_snapshot!(env.render_diagnostics(&diagnostics));
}
#[test]
fn missing_file() {
let mut env = TestEnvironment::new();
env.format(DiagnosticFormat::Github);
let diag = env.err().build();
insta::assert_snapshot!(
env.render(&diag),
@"::error title=ty (test-diagnostic)::test-diagnostic: main diagnostic message",
);
}
}

View File

@@ -1,7 +0,0 @@
---
source: crates/ruff_db/src/diagnostic/render/github.rs
expression: env.render_diagnostics(&diagnostics)
---
::error title=ty (F401),file=fib.py,line=1,col=8,endLine=1,endColumn=10::fib.py:1:8: F401 `os` imported but unused
::error title=ty (F841),file=fib.py,line=6,col=5,endLine=6,endColumn=6::fib.py:6:5: F841 Local variable `x` is assigned to but never used
::error title=ty (F821),file=undef.py,line=1,col=4,endLine=1,endColumn=5::undef.py:1:4: F821 Undefined name `a`

View File

@@ -1,6 +0,0 @@
---
source: crates/ruff_db/src/diagnostic/render/github.rs
expression: env.render_diagnostics(&diagnostics)
---
::error title=ty (invalid-syntax),file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
::error title=ty (invalid-syntax),file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline

View File

@@ -459,12 +459,6 @@ impl File {
self.source_type(db).is_stub()
}
/// Returns `true` if the file is an `__init__.py(i)`
pub fn is_init(self, db: &dyn Db) -> bool {
let path = self.path(db).as_str();
path.ends_with("__init__.py") || path.ends_with("__init__.pyi")
}
pub fn source_type(self, db: &dyn Db) -> PySourceType {
match self.path(db) {
FilePath::System(path) => path

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.13.0"
version = "0.12.12"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -20,7 +20,6 @@ ruff_notebook = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["serde", "cache"] }
ruff_python_codegen = { workspace = true }
ruff_python_importer = { workspace = true }
ruff_python_index = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_semantic = { workspace = true }

View File

@@ -18,18 +18,3 @@ var_string = "true"
Popen(var_string, shell=True)
Popen([var_string], shell=True)
Popen([var_string, ""], shell=True)
# Check dict display with only double-starred expressions can be falsey.
Popen("true", shell={**{}})
Popen("true", shell={**{**{}}})
# Check pattern with merged defaults/configs
class ShellConfig:
def __init__(self):
self.shell_defaults = {}
def fetch_shell_config(self, username):
return {}
def run(self, username):
Popen("true", shell={**self.shell_defaults, **self.fetch_shell_config(username)})

View File

@@ -3,6 +3,3 @@ def foo(shell):
foo(shell=True)
foo(shell={**{}})
foo(shell={**{**{}}})

View File

@@ -6,6 +6,3 @@ subprocess.Popen("/bin/chown root: *", shell=True)
subprocess.Popen(["/usr/local/bin/rsync", "*", "some_where:"], shell=True)
subprocess.Popen("/usr/local/bin/rsync * no_injection_here:")
os.system("tar cf foo.tar bar/*")
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{**{}}})

View File

@@ -39,16 +39,3 @@ hasattr(
"__call__", # comment 4
# comment 5
)
import operator
assert hasattr(operator, "__call__")
assert callable(operator) is False
class A:
def __init__(self): self.__call__ = None
assert hasattr(A(), "__call__")
assert callable(A()) is False

View File

@@ -663,15 +663,4 @@ class C[
type X[T,] = T
def f[T,](): pass
class C[T,]: pass
# t-string examples
kwargs.pop("remove", t"this {trailing_comma}",)
kwargs.pop("remove", t"this {f"{trailing_comma}"}",)
t"""This is a test. {
"Another sentence."
if True else
"Don't add a trailing comma here ->"
}"""
class C[T,]: pass

View File

@@ -187,24 +187,3 @@ _ = (
# leading comment
"end"
)
# https://github.com/astral-sh/ruff/issues/20310
# ISC001
t"The quick " t"brown fox."
# ISC002
t"The quick brown fox jumps over the lazy "\
t"dog."
# ISC003
(
t"The quick brown fox jumps over the lazy "
+ t"dog"
)
# nested examples with both t and f-strings
_ = "a" f"b {t"c" t"d"} e" "f"
_ = t"b {f"c" f"d {t"e" t"f"} g"} h"
_ = f"b {t"abc" \
t"def"} g"

View File

@@ -55,30 +55,3 @@ def foo(some_other):
def foo():
dict = {"Tom": 23, "Maria": 23, "Dog": 11}
age = dict.get("Cat", None)
# https://github.com/astral-sh/ruff/issues/20341
# Method call as key
ages = {"Tom": 23, "Maria": 23, "Dog": 11}
key_source = {"Thomas": "Tom"}
age = ages.get(key_source.get("Thomas", "Tom"), None)
# Property access as key
class Data:
def __init__(self):
self.name = "Tom"
data = Data()
ages = {"Tom": 23, "Maria": 23, "Dog": 11}
age = ages.get(data.name, None)
# Complex expression as key
ages = {"Tom": 23, "Maria": 23, "Dog": 11}
age = ages.get("Tom" if True else "Maria", None)
# Function call as key
def get_key():
return "Tom"
ages = {"Tom": 23, "Maria": 23, "Dog": 11}
age = ages.get(get_key(), None)

View File

@@ -1,24 +0,0 @@
import builtins
from pathlib import Path
_file = "file.txt"
_x = ("r+", -1)
r_plus = "r+"
builtins.open(file=_file)
open(_file, "r+ ", - 1)
open(mode="wb", file=_file)
open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
open(_file, "r+", - 1, None, None, None, True, None)
open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
open(_file, f" {r_plus} ", - 1)
open(buffering=- 1, file=_file, encoding= "utf-8")
# Only diagnostic
open()
open(_file, *_x)
open(_file, "r+", unknown=True)
open(_file, "r+", closefd=False)
open(_file, "r+", None, None, None, None, None, None, None)

View File

@@ -62,4 +62,4 @@ rename(
rename(file, "file_2.py", src_dir_fd=None, dst_dir_fd=None)
rename(file, "file_2.py", src_dir_fd=1)
rename(file, "file_2.py", src_dir_fd=1)

View File

@@ -974,39 +974,3 @@ BANANA = 100
APPLE = 200
# end
# https://github.com/astral-sh/ruff/issues/19752
class foo:
async def recv(self, *, length=65536):
loop = asyncio.get_event_loop()
def callback():
loop.remove_reader(self._fd)
loop.add_reader(self._fd, callback)
# end
# E301
class Foo:
if True:
print("conditional")
def test():
pass
# end
# Test case for nested class scenario
class Bar:
def f():
x = 1
def g():
return 1
return 2
def f():
class Baz:
x = 1
def g():
return 1
return 2
# end

View File

@@ -125,13 +125,4 @@ class D:
@dataclass
class E:
c: C = C()
# https://github.com/astral-sh/ruff/issues/20266
@dataclass(frozen=True)
class C:
@dataclass(frozen=True)
class D:
foo: int = 1
d: D = D() # OK
c: C = C()

View File

@@ -113,18 +113,3 @@ var = bytearray(b"abc")["x"]
x = "x"
var = [1, 2, 3][0:x]
var = [1, 2, 3][x:1]
# https://github.com/astral-sh/ruff/issues/20204
# Should not emit for boolean index and slice bounds
var = [1, 2, 3][False]
var = [1, 2, 3][False:True:True]
# Should emit for invalid access using t-string
var = [1, 2][t"x"]
# Should emit for invalid access using lambda
var = [1, 2, 3][lambda: 0]
# Should emit for invalid access using generator
var = [1, 2, 3][(x for x in ())]

View File

@@ -16,5 +16,3 @@ from collections import defaultdict # noqa: INVALID100, INVALID200, F401
from itertools import chain # noqa: E402, INVALID300, F401
# Test for mixed code types
import json # noqa: E402, INVALID400, V100
# Test for rule redirects
import pandas as pd # noqa: TCH002

View File

@@ -134,7 +134,7 @@ pub(crate) fn deferred_scopes(checker: &Checker) {
);
}
if checker.is_rule_enabled(Rule::FunctionCallInDataclassDefaultArgument) {
ruff::rules::function_call_in_dataclass_default(checker, class_def, scope_id);
ruff::rules::function_call_in_dataclass_default(checker, class_def);
}
if checker.is_rule_enabled(Rule::MutableClassDefault) {
ruff::rules::mutable_class_default(checker, class_def);

View File

@@ -1051,6 +1051,7 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::OsStat,
Rule::OsPathJoin,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Rule::PyPath,
Rule::Glob,
Rule::OsListdir,
@@ -1134,9 +1135,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsSymlink) {
flake8_use_pathlib::rules::os_symlink(checker, call, segments);
}
if checker.is_rule_enabled(Rule::BuiltinOpen) {
flake8_use_pathlib::rules::builtin_open(checker, call, segments);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(
checker, call, segments,

View File

@@ -1,6 +1,7 @@
use ruff_python_ast::helpers;
use ruff_python_ast::types::Node;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::ScopeKind;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -808,6 +809,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyflakes::rules::future_feature_not_defined(checker, alias);
}
} else if &alias.name == "*" {
// F406
if checker.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
checker.report_diagnostic(
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
name: helpers::format_import_from(level, module).to_string(),
},
stmt.range(),
);
}
}
// F403
checker.report_diagnostic_if_enabled(
pyflakes::rules::UndefinedLocalWithImportStar {

View File

@@ -13,7 +13,7 @@ pub(crate) fn unresolved_references(checker: &Checker) {
for reference in checker.semantic.unresolved_references() {
if reference.is_wildcard_import() {
// F405
// F406
checker.report_diagnostic_if_enabled(
pyflakes::rules::UndefinedLocalWithImportStarUsage {
name: reference.name(checker.source()).to_string(),

View File

@@ -69,8 +69,7 @@ use crate::package::PackageRoot;
use crate::preview::is_undefined_export_in_dunder_init_enabled;
use crate::registry::Rule;
use crate::rules::pyflakes::rules::{
LateFutureImport, ReturnOutsideFunction, UndefinedLocalWithNestedImportStarUsage,
YieldOutsideFunction,
LateFutureImport, ReturnOutsideFunction, YieldOutsideFunction,
};
use crate::rules::pylint::rules::{
AwaitOutsideAsync, LoadBeforeGlobalDeclaration, YieldFromInAsyncFunction,
@@ -277,7 +276,7 @@ impl<'a> Checker<'a> {
locator,
stylist,
indexer,
importer: Importer::new(parsed, locator.contents(), stylist),
importer: Importer::new(parsed, locator, stylist),
semantic,
visit: deferred::Visit::default(),
analyze: deferred::Analyze::default(),
@@ -660,14 +659,6 @@ impl SemanticSyntaxContext for Checker<'_> {
self.report_diagnostic(YieldOutsideFunction::new(kind), error.range);
}
}
SemanticSyntaxErrorKind::NonModuleImportStar(name) => {
if self.is_rule_enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
self.report_diagnostic(
UndefinedLocalWithNestedImportStarUsage { name },
error.range,
);
}
}
SemanticSyntaxErrorKind::ReturnOutsideFunction => {
// F706
if self.is_rule_enabled(Rule::ReturnOutsideFunction) {
@@ -3275,11 +3266,6 @@ impl<'a> LintContext<'a> {
pub(crate) const fn settings(&self) -> &LinterSettings {
self.settings
}
#[cfg(any(feature = "test-rules", test))]
pub(crate) const fn source_file(&self) -> &SourceFile {
&self.source_file
}
}
/// An abstraction for mutating a diagnostic.

View File

@@ -6,7 +6,7 @@ use itertools::Itertools;
use rustc_hash::FxHashSet;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::Ranged;
use crate::fix::edits::delete_comment;
use crate::noqa::{
@@ -68,7 +68,7 @@ pub(crate) fn check_noqa(
let noqa_offsets = diagnostic
.parent()
.into_iter()
.chain(diagnostic.range().map(TextRange::start).into_iter())
.chain(std::iter::once(diagnostic.expect_range().start()))
.map(|position| noqa_line_for.resolve(position))
.unique();

View File

@@ -944,7 +944,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathSamefile),
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::BuiltinOpen),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
(Flake8UsePathlib, "124") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::PyPath),
(Flake8UsePathlib, "201") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::PathConstructorCurrentDirectory),
(Flake8UsePathlib, "202") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathGetsize),
@@ -1080,8 +1080,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "950") => (RuleGroup::Stable, rules::ruff::rules::RedirectedToTestRule),
#[cfg(any(feature = "test-rules", test))]
(Ruff, "960") => (RuleGroup::Removed, rules::ruff::rules::RedirectedFromPrefixTestRule),
#[cfg(any(feature = "test-rules", test))]
(Ruff, "990") => (RuleGroup::Preview, rules::ruff::rules::PanicyTestRule),
// flake8-django

View File

@@ -125,27 +125,26 @@ fn extract_noqa_line_for(tokens: &Tokens, locator: &Locator, indexer: &Indexer)
}
// The capacity allocated here might be more than we need if there are
// nested interpolated strings.
let mut interpolated_string_mappings =
Vec::with_capacity(indexer.interpolated_string_ranges().len());
// nested f-strings.
let mut fstring_mappings = Vec::with_capacity(indexer.fstring_ranges().len());
// For nested interpolated strings, we expect `noqa` directives on the last line of the
// outermost interpolated string. The last interpolated string range will be used to skip over
// the inner interpolated strings.
let mut last_interpolated_string_range: TextRange = TextRange::default();
for interpolated_string_range in indexer.interpolated_string_ranges().values() {
if !locator.contains_line_break(*interpolated_string_range) {
// For nested f-strings, we expect `noqa` directives on the last line of the
// outermost f-string. The last f-string range will be used to skip over
// the inner f-strings.
let mut last_fstring_range: TextRange = TextRange::default();
for fstring_range in indexer.fstring_ranges().values() {
if !locator.contains_line_break(*fstring_range) {
continue;
}
if last_interpolated_string_range.contains_range(*interpolated_string_range) {
if last_fstring_range.contains_range(*fstring_range) {
continue;
}
let new_range = TextRange::new(
locator.line_start(interpolated_string_range.start()),
interpolated_string_range.end(),
locator.line_start(fstring_range.start()),
fstring_range.end(),
);
interpolated_string_mappings.push(new_range);
last_interpolated_string_range = new_range;
fstring_mappings.push(new_range);
last_fstring_range = new_range;
}
let mut continuation_mappings = Vec::new();
@@ -173,11 +172,11 @@ fn extract_noqa_line_for(tokens: &Tokens, locator: &Locator, indexer: &Indexer)
// Merge the mappings in sorted order
let mut mappings = NoqaMapping::with_capacity(
continuation_mappings.len() + string_mappings.len() + interpolated_string_mappings.len(),
continuation_mappings.len() + string_mappings.len() + fstring_mappings.len(),
);
let string_mappings = SortedMergeIter {
left: interpolated_string_mappings.into_iter().peekable(),
left: fstring_mappings.into_iter().peekable(),
right: string_mappings.into_iter().peekable(),
};
let all_mappings = SortedMergeIter {
@@ -498,35 +497,12 @@ end'''
NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(70))])
);
let contents = "x = 1
y = t'''abc
def {f'''nested
interpolated string''' f'another nested'}
end'''
";
assert_eq!(
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(6), TextSize::from(82))])
);
let contents = "x = 1
y = f'normal'
z = f'another but {f'nested but {f'still single line'} nested'}'
";
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
let contents = "x = 1
y = t'normal'
z = t'another but {t'nested but {t'still single line'} nested'}'
";
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
let contents = "x = 1
y = f'normal'
z = f'another but {t'nested but {f'still single line'} nested'}'
";
assert_eq!(noqa_mappings(contents), NoqaMapping::default());
let contents = r"x = \
1";
assert_eq!(

View File

@@ -14,7 +14,6 @@ use unicode_normalization::UnicodeNormalization;
use ruff_python_ast::Stmt;
use ruff_python_ast::name::UnqualifiedName;
use ruff_python_codegen::Stylist;
use ruff_text_size::Ranged;
use crate::Locator;
use crate::cst::matchers::match_statement;
@@ -128,10 +127,10 @@ pub(crate) fn remove_imports<'a>(
pub(crate) fn retain_imports(
member_names: &[&str],
stmt: &Stmt,
contents: &str,
locator: &Locator,
stylist: &Stylist,
) -> Result<String> {
let module_text = &contents[stmt.range()];
let module_text = locator.slice(stmt);
let mut tree = match_statement(module_text)?;
let Statement::Simple(body) = &mut tree else {

View File

@@ -370,8 +370,8 @@ pub(crate) fn adjust_indentation(
// If the range includes a multi-line string, use LibCST to ensure that we don't adjust the
// whitespace _within_ the string.
let contains_multiline_string = indexer.multiline_ranges().intersects(range)
|| indexer.interpolated_string_ranges().intersects(range);
let contains_multiline_string =
indexer.multiline_ranges().intersects(range) || indexer.fstring_ranges().intersects(range);
// If the range has mixed indentation, we will use LibCST as well.
let mixed_indentation = contents.universal_newlines().any(|line| {

View File

@@ -1,8 +1,6 @@
//! Insert statements into Python code.
use std::ops::Add;
use ruff_diagnostics::Edit;
use ruff_python_ast::Stmt;
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_codegen::Stylist;
@@ -12,6 +10,9 @@ use ruff_python_trivia::{PythonWhitespace, textwrap::indent};
use ruff_source_file::{LineRanges, UniversalNewlineIterator};
use ruff_text_size::{Ranged, TextSize};
use crate::Edit;
use crate::Locator;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum Placement<'a> {
/// The content will be inserted inline with the existing code (i.e., within semicolon-delimited
@@ -24,7 +25,7 @@ pub(super) enum Placement<'a> {
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Insertion<'a> {
pub(super) struct Insertion<'a> {
/// The content to add before the insertion.
prefix: &'a str,
/// The location at which to insert.
@@ -49,31 +50,33 @@ impl<'a> Insertion<'a> {
///
/// The insertion returned will begin at the start of the `import os` statement, and will
/// include a trailing newline.
pub fn start_of_file(body: &[Stmt], contents: &str, stylist: &Stylist) -> Insertion<'static> {
pub(super) fn start_of_file(
body: &[Stmt],
locator: &Locator,
stylist: &Stylist,
) -> Insertion<'static> {
// Skip over any docstrings.
let mut location = if let Some(mut location) = match_docstring_end(body) {
// If the first token after the docstring is a semicolon, insert after the semicolon as
// an inline statement.
if let Some(offset) = match_semicolon(&contents[location.to_usize()..]) {
if let Some(offset) = match_semicolon(locator.after(location)) {
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
}
// While the first token after the docstring is a continuation character (i.e. "\"), advance
// additional rows to prevent inserting in the same logical line.
while match_continuation(&contents[location.to_usize()..]).is_some() {
location = contents.full_line_end(location);
while match_continuation(locator.after(location)).is_some() {
location = locator.full_line_end(location);
}
// Otherwise, advance to the next row.
contents.full_line_end(location)
locator.full_line_end(location)
} else {
contents.bom_start_offset()
locator.bom_start_offset()
};
// Skip over commented lines, with whitespace separation.
for line in
UniversalNewlineIterator::with_offset(&contents[location.to_usize()..], location)
{
for line in UniversalNewlineIterator::with_offset(locator.after(location), location) {
let trimmed_line = line.trim_whitespace_start();
if trimmed_line.is_empty() {
continue;
@@ -108,13 +111,17 @@ impl<'a> Insertion<'a> {
/// in this case is the line after `import math`, and will include a trailing newline.
///
/// The statement itself is assumed to be at the top-level of the module.
pub fn end_of_statement(stmt: &Stmt, contents: &str, stylist: &Stylist) -> Insertion<'static> {
pub(super) fn end_of_statement(
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
) -> Insertion<'static> {
let location = stmt.end();
if let Some(offset) = match_semicolon(&contents[location.to_usize()..]) {
if let Some(offset) = match_semicolon(locator.after(location)) {
// If the first token after the statement is a semicolon, insert after the semicolon as
// an inline statement.
Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";")
} else if match_continuation(&contents[location.to_usize()..]).is_some() {
} else if match_continuation(locator.after(location)).is_some() {
// If the first token after the statement is a continuation, insert after the statement
// with a semicolon.
Insertion::inline("; ", location, "")
@@ -122,63 +129,12 @@ impl<'a> Insertion<'a> {
// Otherwise, insert on the next line.
Insertion::own_line(
"",
contents.full_line_end(location),
locator.full_line_end(location),
stylist.line_ending().as_str(),
)
}
}
/// Create an [`Insertion`] to insert an additional member to import
/// into a `from <module> import member1, member2, ...` statement.
///
/// For example, given the following code:
///
/// ```python
/// """Hello, world!"""
///
/// from collections import Counter
///
///
/// def foo():
/// pass
/// ```
///
/// The insertion returned will begin after `Counter` but before the
/// newline terminator. Callers can then call [`Insertion::into_edit`]
/// with the additional member to add. A comma delimiter is handled
/// automatically.
///
/// The statement itself is assumed to be at the top-level of the module.
///
/// This returns `None` when `stmt` isn't a `from ... import ...`
/// statement.
pub fn existing_import(stmt: &Stmt, tokens: &Tokens) -> Option<Insertion<'static>> {
let Stmt::ImportFrom(ref import_from) = *stmt else {
return None;
};
if let Some(at) = import_from.names.last().map(Ranged::end) {
return Some(Insertion::inline(", ", at, ""));
}
// Our AST can deal with partial `from ... import`
// statements, so we might not have any members
// yet. In this case, we don't need the comma.
//
// ... however, unless we can be certain that
// inserting this name leads to a valid AST, we
// give up.
let at = import_from.end();
if !matches!(
tokens
.before(at)
.last()
.map(ruff_python_parser::Token::kind),
Some(TokenKind::Import)
) {
return None;
}
Some(Insertion::inline(" ", at, ""))
}
/// Create an [`Insertion`] to insert (e.g.) an import statement at the start of a given
/// block, along with a prefix and suffix to use for the insertion.
///
@@ -193,9 +149,9 @@ impl<'a> Insertion<'a> {
/// include a trailing newline.
///
/// The block itself is assumed to be at the top-level of the module.
pub fn start_of_block(
pub(super) fn start_of_block(
mut location: TextSize,
contents: &'a str,
locator: &Locator<'a>,
stylist: &Stylist,
tokens: &Tokens,
) -> Insertion<'a> {
@@ -248,7 +204,7 @@ impl<'a> Insertion<'a> {
"",
token.start(),
stylist.line_ending().as_str(),
&contents[token.range()],
locator.slice(token),
);
}
_ => {
@@ -264,7 +220,7 @@ impl<'a> Insertion<'a> {
}
/// Convert this [`Insertion`] into an [`Edit`] that inserts the given content.
pub fn into_edit(self, content: &str) -> Edit {
pub(super) fn into_edit(self, content: &str) -> Edit {
let Insertion {
prefix,
location,
@@ -284,7 +240,7 @@ impl<'a> Insertion<'a> {
}
/// Returns `true` if this [`Insertion`] is inline.
pub fn is_inline(&self) -> bool {
pub(super) fn is_inline(&self) -> bool {
matches!(self.placement, Placement::Inline)
}
@@ -365,7 +321,9 @@ mod tests {
use ruff_python_codegen::Stylist;
use ruff_python_parser::parse_module;
use ruff_source_file::LineEnding;
use ruff_text_size::{Ranged, TextSize};
use ruff_text_size::TextSize;
use crate::Locator;
use super::Insertion;
@@ -373,8 +331,9 @@ mod tests {
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion<'_>> {
let parsed = parse_module(contents)?;
let stylist = Stylist::from_tokens(parsed.tokens(), contents);
Ok(Insertion::start_of_file(parsed.suite(), contents, &stylist))
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());
Ok(Insertion::start_of_file(parsed.suite(), &locator, &stylist))
}
let contents = "";
@@ -504,8 +463,9 @@ x = 1
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion<'_> {
let parsed = parse_module(contents).unwrap();
let stylist = Stylist::from_tokens(parsed.tokens(), contents);
Insertion::start_of_block(offset, contents, &stylist, parsed.tokens())
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());
Insertion::start_of_block(offset, &locator, &stylist, parsed.tokens())
}
let contents = "if True: pass";
@@ -524,286 +484,4 @@ if True:
Insertion::indented("", TextSize::from(9), "\n", " ")
);
}
#[test]
fn existing_import_works() {
fn snapshot(content: &str, member: &str) -> String {
let parsed = parse_module(content).unwrap();
let edit = Insertion::existing_import(parsed.suite().first().unwrap(), parsed.tokens())
.unwrap()
.into_edit(member);
let insert_text = edit.content().expect("edit should be non-empty");
let mut content = content.to_string();
content.replace_range(edit.range().to_std_range(), insert_text);
content
}
let source = r#"
from collections import Counter
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import Counter, defaultdict
",
);
let source = r#"
from collections import Counter, OrderedDict
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import Counter, OrderedDict, defaultdict
",
);
let source = r#"
from collections import (Counter)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@"from collections import (Counter, defaultdict)",
);
let source = r#"
from collections import (Counter, OrderedDict)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@"from collections import (Counter, OrderedDict, defaultdict)",
);
let source = r#"
from collections import (Counter,)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@"from collections import (Counter, defaultdict,)",
);
let source = r#"
from collections import (Counter, OrderedDict,)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@"from collections import (Counter, OrderedDict, defaultdict,)",
);
let source = r#"
from collections import (
Counter
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter, defaultdict
)
",
);
let source = r#"
from collections import (
Counter,
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter, defaultdict,
)
",
);
let source = r#"
from collections import (
Counter,
OrderedDict
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter,
OrderedDict, defaultdict
)
",
);
let source = r#"
from collections import (
Counter,
OrderedDict,
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter,
OrderedDict, defaultdict,
)
",
);
let source = r#"
from collections import \
Counter
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import \
Counter, defaultdict
",
);
let source = r#"
from collections import \
Counter, OrderedDict
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import \
Counter, OrderedDict, defaultdict
",
);
let source = r#"
from collections import \
Counter, \
OrderedDict
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import \
Counter, \
OrderedDict, defaultdict
",
);
/*
from collections import (
Collector # comment
)
from collections import (
Collector, # comment
)
from collections import (
Collector # comment
,
)
from collections import (
Collector
# comment
,
)
*/
let source = r#"
from collections import (
Counter # comment
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter, defaultdict # comment
)
",
);
let source = r#"
from collections import (
Counter, # comment
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter, defaultdict, # comment
)
",
);
let source = r#"
from collections import (
Counter # comment
,
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter, defaultdict # comment
,
)
",
);
let source = r#"
from collections import (
Counter
# comment
,
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
Counter, defaultdict
# comment
,
)
",
);
let source = r#"
from collections import (
# comment 1
Counter # comment 2
# comment 3
)
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@r"
from collections import (
# comment 1
Counter, defaultdict # comment 2
# comment 3
)
",
);
let source = r#"
from collections import Counter # comment
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@"from collections import Counter, defaultdict # comment",
);
let source = r#"
from collections import Counter, OrderedDict # comment
"#;
insta::assert_snapshot!(
snapshot(source, "defaultdict"),
@"from collections import Counter, OrderedDict, defaultdict # comment",
);
}
}

View File

@@ -6,12 +6,10 @@
use std::error::Error;
use anyhow::Result;
use libcst_native as cst;
use libcst_native::{ImportAlias, Name as cstName, NameOrAttribute};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Expr, ModModule, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_importer::Insertion;
use ruff_python_parser::{Parsed, Tokens};
use ruff_python_semantic::{
ImportedName, MemberNameImport, ModuleNameImport, NameImport, SemanticModel,
@@ -19,17 +17,22 @@ use ruff_python_semantic::{
use ruff_python_trivia::textwrap::indent;
use ruff_text_size::{Ranged, TextSize};
use crate::Edit;
use crate::Locator;
use crate::cst::matchers::{match_aliases, match_import_from, match_statement};
use crate::fix;
use crate::fix::codemods::CodegenStylist;
use crate::importer::insertion::Insertion;
mod insertion;
pub(crate) struct Importer<'a> {
/// The Python AST to which we are adding imports.
python_ast: &'a [Stmt],
/// The tokens representing the Python AST.
tokens: &'a Tokens,
/// The source code text for `python_ast`.
source: &'a str,
/// The [`Locator`] for the Python AST.
locator: &'a Locator<'a>,
/// The [`Stylist`] for the Python AST.
stylist: &'a Stylist<'a>,
/// The list of visited, top-level runtime imports in the Python AST.
@@ -41,13 +44,13 @@ pub(crate) struct Importer<'a> {
impl<'a> Importer<'a> {
pub(crate) fn new(
parsed: &'a Parsed<ModModule>,
source: &'a str,
locator: &'a Locator<'a>,
stylist: &'a Stylist<'a>,
) -> Self {
Self {
python_ast: parsed.suite(),
tokens: parsed.tokens(),
source,
locator,
stylist,
runtime_imports: Vec::default(),
type_checking_blocks: Vec::default(),
@@ -73,10 +76,11 @@ impl<'a> Importer<'a> {
let required_import = import.to_string();
if let Some(stmt) = self.preceding_import(at) {
// Insert after the last top-level import.
Insertion::end_of_statement(stmt, self.source, self.stylist).into_edit(&required_import)
Insertion::end_of_statement(stmt, self.locator, self.stylist)
.into_edit(&required_import)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
Insertion::start_of_file(self.python_ast, self.locator, self.stylist)
.into_edit(&required_import)
}
}
@@ -95,17 +99,17 @@ impl<'a> Importer<'a> {
let content = fix::codemods::retain_imports(
&import.names,
import.statement,
self.source,
self.locator,
self.stylist,
)?;
// Add the import to the top-level.
let insertion = if let Some(stmt) = self.preceding_import(at) {
// Insert after the last top-level import.
Insertion::end_of_statement(stmt, self.source, self.stylist)
Insertion::end_of_statement(stmt, self.locator, self.stylist)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
Insertion::start_of_file(self.python_ast, self.locator, self.stylist)
};
let add_import_edit = insertion.into_edit(&content);
@@ -127,7 +131,7 @@ impl<'a> Importer<'a> {
let content = fix::codemods::retain_imports(
&import.names,
import.statement,
self.source,
self.locator,
self.stylist,
)?;
@@ -151,7 +155,7 @@ impl<'a> Importer<'a> {
None
} else {
Some(Edit::range_replacement(
self.source[statement.range()].to_string(),
self.locator.slice(statement.range()).to_string(),
statement.range(),
))
}
@@ -182,7 +186,7 @@ impl<'a> Importer<'a> {
None
} else {
Some(Edit::range_replacement(
self.source[type_checking.range()].to_string(),
self.locator.slice(type_checking.range()).to_string(),
type_checking.range(),
))
};
@@ -358,7 +362,7 @@ impl<'a> Importer<'a> {
// By adding this no-op edit, we force the `unused-imports` fix to conflict with the
// `sys-exit-alias` fix, and thus will avoid applying both fixes in the same pass.
let import_edit = Edit::range_replacement(
self.source[imported_name.range()].to_string(),
self.locator.slice(imported_name.range()).to_string(),
imported_name.range(),
);
Ok(Some((import_edit, imported_name.into_name())))
@@ -465,11 +469,11 @@ impl<'a> Importer<'a> {
/// Add the given member to an existing `Stmt::ImportFrom` statement.
fn add_member(&self, stmt: &Stmt, member: &str) -> Result<Edit> {
let mut statement = match_statement(&self.source[stmt.range()])?;
let mut statement = match_statement(self.locator.slice(stmt))?;
let import_from = match_import_from(&mut statement)?;
let aliases = match_aliases(import_from)?;
aliases.push(cst::ImportAlias {
name: cst::NameOrAttribute::N(Box::new(cst::Name {
aliases.push(ImportAlias {
name: NameOrAttribute::N(Box::new(cstName {
value: member,
lpar: vec![],
rpar: vec![],
@@ -487,10 +491,10 @@ impl<'a> Importer<'a> {
fn add_type_checking_block(&self, content: &str, at: TextSize) -> Result<Edit> {
let insertion = if let Some(stmt) = self.preceding_import(at) {
// Insert after the last top-level import.
Insertion::end_of_statement(stmt, self.source, self.stylist)
Insertion::end_of_statement(stmt, self.locator, self.stylist)
} else {
// Insert at the start of the file.
Insertion::start_of_file(self.python_ast, self.source, self.stylist)
Insertion::start_of_file(self.python_ast, self.locator, self.stylist)
};
if insertion.is_inline() {
Err(anyhow::anyhow!(
@@ -503,7 +507,7 @@ impl<'a> Importer<'a> {
/// Add an import statement to an existing `TYPE_CHECKING` block.
fn add_to_type_checking_block(&self, content: &str, at: TextSize) -> Edit {
Insertion::start_of_block(at, self.source, self.stylist, self.tokens).into_edit(content)
Insertion::start_of_block(at, self.locator, self.stylist, self.tokens).into_edit(content)
}
/// Return the import statement that precedes the given position, if any.

View File

@@ -319,9 +319,6 @@ pub fn check_path(
&context,
);
}
Rule::PanicyTestRule => {
test_rules::PanicyTestRule::diagnostic(locator, comment_ranges, &context);
}
_ => unreachable!("All test rules must have an implementation"),
}
}
@@ -516,9 +513,10 @@ fn diagnostics_to_messages(
.map(|error| create_syntax_error_diagnostic(source_file.clone(), error, error)),
)
.chain(diagnostics.into_iter().map(|mut diagnostic| {
if let Some(range) = diagnostic.range() {
diagnostic.set_noqa_offset(directives.noqa_line_for.resolve(range.start()));
}
let noqa_offset = directives
.noqa_line_for
.resolve(diagnostic.expect_range().start());
diagnostic.set_noqa_offset(noqa_offset);
diagnostic
}))
.collect()
@@ -985,7 +983,7 @@ mod tests {
&parsed,
target_version,
);
diagnostics.sort_by(Diagnostic::ruff_start_ordering);
diagnostics.sort_by_key(|diagnostic| diagnostic.expect_range().start());
diagnostics
}

View File

@@ -0,0 +1,90 @@
use std::io::Write;
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::LineColumn;
use crate::fs::relativize_path;
use crate::message::{Emitter, EmitterContext};
/// Generate error workflow command in GitHub Actions format.
/// See: [GitHub documentation](https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message)
#[derive(Default)]
pub struct GithubEmitter;
impl Emitter for GithubEmitter {
fn emit(
&mut self,
writer: &mut dyn Write,
diagnostics: &[Diagnostic],
context: &EmitterContext,
) -> anyhow::Result<()> {
for diagnostic in diagnostics {
let source_location = diagnostic.ruff_start_location().unwrap_or_default();
let filename = diagnostic.expect_ruff_filename();
let location = if context.is_notebook(&filename) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
LineColumn::default()
} else {
source_location
};
let end_location = diagnostic.ruff_end_location().unwrap_or_default();
write!(
writer,
"::error title=Ruff ({code}),file={file},line={row},col={column},endLine={end_row},endColumn={end_column}::",
code = diagnostic.secondary_code_or_id(),
file = filename,
row = source_location.line,
column = source_location.column,
end_row = end_location.line,
end_column = end_location.column,
)?;
write!(
writer,
"{path}:{row}:{column}:",
path = relativize_path(&filename),
row = location.line,
column = location.column,
)?;
if let Some(code) = diagnostic.secondary_code() {
write!(writer, " {code}")?;
} else {
write!(writer, " {id}:", id = diagnostic.id())?;
}
writeln!(writer, " {}", diagnostic.body())?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use insta::assert_snapshot;
use crate::message::GithubEmitter;
use crate::message::tests::{
capture_emitter_output, create_diagnostics, create_syntax_error_diagnostics,
};
#[test]
fn output() {
let mut emitter = GithubEmitter;
let content = capture_emitter_output(&mut emitter, &create_diagnostics());
assert_snapshot!(content);
}
#[test]
fn syntax_errors() {
let mut emitter = GithubEmitter;
let content = capture_emitter_output(&mut emitter, &create_syntax_error_diagnostics());
assert_snapshot!(content);
}
}

View File

@@ -9,6 +9,7 @@ use ruff_db::diagnostic::{
};
use ruff_db::files::File;
pub use github::GithubEmitter;
pub use grouped::GroupedEmitter;
use ruff_notebook::NotebookIndex;
use ruff_source_file::SourceFile;
@@ -19,6 +20,7 @@ pub use text::TextEmitter;
use crate::Fix;
use crate::registry::Rule;
mod github;
mod grouped;
mod sarif;
mod text;

View File

@@ -0,0 +1,8 @@
---
source: crates/ruff_linter/src/message/github.rs
expression: content
snapshot_kind: text
---
::error title=Ruff (F401),file=fib.py,line=1,col=8,endLine=1,endColumn=10::fib.py:1:8: F401 `os` imported but unused
::error title=Ruff (F841),file=fib.py,line=6,col=5,endLine=6,endColumn=6::fib.py:6:5: F841 Local variable `x` is assigned to but never used
::error title=Ruff (F821),file=undef.py,line=1,col=4,endLine=1,endColumn=5::undef.py:1:4: F821 Undefined name `a`

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff_linter/src/message/github.rs
expression: content
---
::error title=Ruff (invalid-syntax),file=syntax_errors.py,line=1,col=15,endLine=2,endColumn=1::syntax_errors.py:1:15: invalid-syntax: Expected one or more symbol names after import
::error title=Ruff (invalid-syntax),file=syntax_errors.py,line=3,col=12,endLine=4,endColumn=1::syntax_errors.py:3:12: invalid-syntax: Expected ')', found newline

View File

@@ -886,10 +886,7 @@ fn find_noqa_comments<'a>(
}
}
let noqa_offset = message
.range()
.map(|range| noqa_line_for.resolve(range.start()))
.unwrap_or_default();
let noqa_offset = noqa_line_for.resolve(message.expect_range().start());
// Or ignored by the directive itself?
if let Some(directive_line) = directives.find_line_with_directive(noqa_offset) {

View File

@@ -218,13 +218,3 @@ pub(crate) const fn is_unnecessary_default_type_args_stubs_enabled(
) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20343
pub(crate) const fn is_sim910_expanded_key_support_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20169
pub(crate) const fn is_fix_builtin_open_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -124,6 +124,4 @@ S602 `subprocess` call with `shell=True` identified, security issue
19 | Popen([var_string], shell=True)
20 | Popen([var_string, ""], shell=True)
| ^^^^^
21 |
22 | # Check dict display with only double-starred expressions can be falsey.
|

View File

@@ -6,6 +6,4 @@ S604 Function call with `shell=True` parameter identified, security issue
|
5 | foo(shell=True)
| ^^^
6 |
7 | foo(shell={**{}})
|

View File

@@ -34,12 +34,10 @@ S609 Possible wildcard injection in call due to `*` usage
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:8:11
|
6 | subprocess.Popen(["/usr/local/bin/rsync", "*", "some_where:"], shell=True)
7 | subprocess.Popen("/usr/local/bin/rsync * no_injection_here:")
8 | os.system("tar cf foo.tar bar/*")
| ^^^^^^^^^^^^^^^^^^^^^^
9 |
10 | subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
|
--> S609.py:8:11
|
6 | subprocess.Popen(["/usr/local/bin/rsync", "*", "some_where:"], shell=True)
7 | subprocess.Popen("/usr/local/bin/rsync * no_injection_here:")
8 | os.system("tar cf foo.tar bar/*")
| ^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -28,29 +28,10 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe because the replacement may not be semantically
/// equivalent to the original expression, potentially changing the behavior of the code.
/// This rule's fix is marked as unsafe if there's comments in the `hasattr` call
/// expression, as comments may be removed.
///
/// For example, an imported module may have a `__call__` attribute but is not considered
/// a callable object:
/// ```python
/// import operator
///
/// assert hasattr(operator, "__call__")
/// assert callable(operator) is False
/// ```
/// Additionally, `__call__` may be defined only as an instance method:
/// ```python
/// class A:
/// def __init__(self):
/// self.__call__ = None
///
///
/// assert hasattr(A(), "__call__")
/// assert callable(A()) is False
/// ```
///
/// Additionally, if there are comments in the `hasattr` call expression, they may be removed:
/// For example, the fix would be marked as unsafe in the following case:
/// ```python
/// hasattr(
/// # comment 1
@@ -122,7 +103,11 @@ pub(crate) fn unreliable_callable_check(
Ok(Fix::applicable_edits(
binding_edit,
import_edit,
Applicability::Unsafe,
if checker.comment_ranges().intersects(expr.range()) {
Applicability::Unsafe
} else {
Applicability::Safe
},
))
});
}

View File

@@ -19,7 +19,6 @@ help: Replace with `callable()`
4 | print("Ooh, callable! Or is it?")
5 | if getattr(o, "__call__", False):
6 | print("Ooh, callable! Or is it?")
note: This is an unsafe fix and may change runtime behavior
B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:5:8
@@ -51,7 +50,6 @@ help: Replace with `callable()`
13 | print("B U G")
14 | if builtins.getattr(o, "__call__", False):
15 | print("B U G")
note: This is an unsafe fix and may change runtime behavior
B004 Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:14:8
@@ -87,7 +85,6 @@ help: Replace with `callable()`
26 | print("STILL a bug!")
27 |
28 |
note: This is an unsafe fix and may change runtime behavior
B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:35:1
@@ -102,8 +99,6 @@ B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
40 | | # comment 5
41 | | )
| |_^
42 |
43 | import operator
|
help: Replace with `callable()`
32 |
@@ -117,43 +112,4 @@ help: Replace with `callable()`
- # comment 5
- )
35 + callable(obj)
36 |
37 | import operator
38 |
note: This is an unsafe fix and may change runtime behavior
B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:45:8
|
43 | import operator
44 |
45 | assert hasattr(operator, "__call__")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
46 | assert callable(operator) is False
|
help: Replace with `callable()`
42 |
43 | import operator
44 |
- assert hasattr(operator, "__call__")
45 + assert callable(operator)
46 | assert callable(operator) is False
47 |
48 |
note: This is an unsafe fix and may change runtime behavior
B004 [*] Using `hasattr(x, "__call__")` to test if x is callable is unreliable. Use `callable(x)` for consistent results.
--> B004.py:53:8
|
53 | assert hasattr(A(), "__call__")
| ^^^^^^^^^^^^^^^^^^^^^^^^
54 | assert callable(A()) is False
|
help: Replace with `callable()`
50 | def __init__(self): self.__call__ = None
51 |
52 |
- assert hasattr(A(), "__call__")
53 + assert callable(A())
54 | assert callable(A()) is False
note: This is an unsafe fix and may change runtime behavior

View File

@@ -250,23 +250,23 @@ pub(crate) fn trailing_commas(
locator: &Locator,
indexer: &Indexer,
) {
let mut interpolated_strings = 0u32;
let mut fstrings = 0u32;
let simple_tokens = tokens.iter().filter_map(|token| {
match token.kind() {
// Completely ignore comments -- they just interfere with the logic.
TokenKind::Comment => None,
// F-strings and t-strings are handled as `String` token type with the complete range
// of the outermost interpolated string. This means that the expression inside the
// interpolated string is not checked for trailing commas.
TokenKind::FStringStart | TokenKind::TStringStart => {
interpolated_strings = interpolated_strings.saturating_add(1);
// F-strings are handled as `String` token type with the complete range
// of the outermost f-string. This means that the expression inside the
// f-string is not checked for trailing commas.
TokenKind::FStringStart => {
fstrings = fstrings.saturating_add(1);
None
}
TokenKind::FStringEnd | TokenKind::TStringEnd => {
interpolated_strings = interpolated_strings.saturating_sub(1);
if interpolated_strings == 0 {
TokenKind::FStringEnd => {
fstrings = fstrings.saturating_sub(1);
if fstrings == 0 {
indexer
.interpolated_string_ranges()
.fstring_ranges()
.outermost(token.start())
.map(|range| SimpleToken::new(TokenType::String, range))
} else {
@@ -274,7 +274,7 @@ pub(crate) fn trailing_commas(
}
}
_ => {
if interpolated_strings == 0 {
if fstrings == 0 {
Some(SimpleToken::from(token.as_tuple()))
} else {
None
@@ -362,7 +362,8 @@ fn check_token(
if let Some(mut diagnostic) =
lint_context.report_diagnostic_if_enabled(ProhibitedTrailingComma, prev.range())
{
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(prev.range)));
let range = diagnostic.expect_range();
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(range)));
return;
}
}

View File

@@ -1016,7 +1016,6 @@ help: Remove trailing comma
664 + type X[T] = T
665 | def f[T,](): pass
666 | class C[T,]: pass
667 |
COM819 [*] Trailing comma prohibited
--> COM81.py:665:8
@@ -1033,8 +1032,6 @@ help: Remove trailing comma
- def f[T,](): pass
665 + def f[T](): pass
666 | class C[T,]: pass
667 |
668 | # t-string examples
COM819 [*] Trailing comma prohibited
--> COM81.py:666:10
@@ -1043,8 +1040,6 @@ COM819 [*] Trailing comma prohibited
665 | def f[T,](): pass
666 | class C[T,]: pass
| ^
667 |
668 | # t-string examples
|
help: Remove trailing comma
663 |
@@ -1052,44 +1047,3 @@ help: Remove trailing comma
665 | def f[T,](): pass
- class C[T,]: pass
666 + class C[T]: pass
667 |
668 | # t-string examples
669 | kwargs.pop("remove", t"this {trailing_comma}",)
COM819 [*] Trailing comma prohibited
--> COM81.py:669:46
|
668 | # t-string examples
669 | kwargs.pop("remove", t"this {trailing_comma}",)
| ^
670 | kwargs.pop("remove", t"this {f"{trailing_comma}"}",)
|
help: Remove trailing comma
666 | class C[T,]: pass
667 |
668 | # t-string examples
- kwargs.pop("remove", t"this {trailing_comma}",)
669 + kwargs.pop("remove", t"this {trailing_comma}")
670 | kwargs.pop("remove", t"this {f"{trailing_comma}"}",)
671 |
672 | t"""This is a test. {
COM819 [*] Trailing comma prohibited
--> COM81.py:670:51
|
668 | # t-string examples
669 | kwargs.pop("remove", t"this {trailing_comma}",)
670 | kwargs.pop("remove", t"this {f"{trailing_comma}"}",)
| ^
671 |
672 | t"""This is a test. {
|
help: Remove trailing comma
667 |
668 | # t-string examples
669 | kwargs.pop("remove", t"this {trailing_comma}",)
- kwargs.pop("remove", t"this {f"{trailing_comma}"}",)
670 + kwargs.pop("remove", t"this {f"{trailing_comma}"}")
671 |
672 | t"""This is a test. {
673 | "Another sentence."

View File

@@ -74,7 +74,6 @@ pub(crate) fn explicit(checker: &Checker, expr: &Expr) {
Expr::StringLiteral(_) | Expr::FString(_),
Expr::StringLiteral(_) | Expr::FString(_)
) | (Expr::BytesLiteral(_), Expr::BytesLiteral(_))
| (Expr::TString(_), Expr::TString(_))
);
if concatable
&& checker

View File

@@ -123,32 +123,21 @@ pub(crate) fn implicit(
let (a_range, b_range) = match (a_token.kind(), b_token.kind()) {
(TokenKind::String, TokenKind::String) => (a_token.range(), b_token.range()),
(TokenKind::String, TokenKind::FStringStart) => {
match indexer
.interpolated_string_ranges()
.innermost(b_token.start())
{
match indexer.fstring_ranges().innermost(b_token.start()) {
Some(b_range) => (a_token.range(), b_range),
None => continue,
}
}
(TokenKind::FStringEnd, TokenKind::String) => {
match indexer
.interpolated_string_ranges()
.innermost(a_token.start())
{
match indexer.fstring_ranges().innermost(a_token.start()) {
Some(a_range) => (a_range, b_token.range()),
None => continue,
}
}
(TokenKind::FStringEnd, TokenKind::FStringStart)
| (TokenKind::TStringEnd, TokenKind::TStringStart) => {
(TokenKind::FStringEnd, TokenKind::FStringStart) => {
match (
indexer
.interpolated_string_ranges()
.innermost(a_token.start()),
indexer
.interpolated_string_ranges()
.innermost(b_token.start()),
indexer.fstring_ranges().innermost(a_token.start()),
indexer.fstring_ranges().innermost(b_token.start()),
) {
(Some(a_range), Some(b_range)) => (a_range, b_range),
_ => continue,

View File

@@ -484,104 +484,3 @@ help: Combine string literals
94 |
95 |
96 | # Mixed literal + non-literal scenarios
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:193:1
|
191 | # https://github.com/astral-sh/ruff/issues/20310
192 | # ISC001
193 | t"The quick " t"brown fox."
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
194 |
195 | # ISC002
|
help: Combine string literals
190 |
191 | # https://github.com/astral-sh/ruff/issues/20310
192 | # ISC001
- t"The quick " t"brown fox."
193 + t"The quick brown fox."
194 |
195 | # ISC002
196 | t"The quick brown fox jumps over the lazy "\
ISC001 Implicitly concatenated string literals on one line
--> ISC.py:206:5
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
| ^^^^^^^^^^^^^^^^^^^^^^
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC.py:206:9
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
| ^^^^^^^^^^^^^^^^^^^^^^
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
|
help: Combine string literals
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:206:14
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
| ^^^^^^^^^
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
|
help: Combine string literals
203 | )
204 |
205 | # nested examples with both t and f-strings
- _ = "a" f"b {t"c" t"d"} e" "f"
206 + _ = "a" f"b {t"cd"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
209 | t"def"} g"
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:207:10
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
| ^^^^^^^^^^^^^^^^^^^^^^^
208 | _ = f"b {t"abc" \
209 | t"def"} g"
|
help: Combine string literals
204 |
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
- _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
207 + _ = t"b {f"cd {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
209 | t"def"} g"
210 |
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:207:20
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
| ^^^^^^^^^
208 | _ = f"b {t"abc" \
209 | t"def"} g"
|
help: Combine string literals
204 |
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
- _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
207 + _ = t"b {f"c" f"d {t"ef"} g"} h"
208 | _ = f"b {t"abc" \
209 | t"def"} g"
210 |

View File

@@ -26,25 +26,3 @@ ISC002 Implicitly concatenated string literals over multiple lines
76 |
77 | # Explicitly concatenated nested f-strings
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:196:1
|
195 | # ISC002
196 | / t"The quick brown fox jumps over the lazy "\
197 | | t"dog."
| |_______^
198 |
199 | # ISC003
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:208:10
|
206 | _ = "a" f"b {t"c" t"d"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
| __________^
209 | | t"def"} g"
| |__________^
|

View File

@@ -337,23 +337,3 @@ help: Remove redundant '+' operator to implicitly concatenate
187 | # leading comment
188 | "end"
189 | )
ISC003 [*] Explicitly concatenated string should be implicitly concatenated
--> ISC.py:201:5
|
199 | # ISC003
200 | (
201 | / t"The quick brown fox jumps over the lazy "
202 | | + t"dog"
| |____________^
203 | )
|
help: Remove redundant '+' operator to implicitly concatenate
199 | # ISC003
200 | (
201 | t"The quick brown fox jumps over the lazy "
- + t"dog"
202 + t"dog"
203 | )
204 |
205 | # nested examples with both t and f-strings

View File

@@ -484,104 +484,3 @@ help: Combine string literals
94 |
95 |
96 | # Mixed literal + non-literal scenarios
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:193:1
|
191 | # https://github.com/astral-sh/ruff/issues/20310
192 | # ISC001
193 | t"The quick " t"brown fox."
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
194 |
195 | # ISC002
|
help: Combine string literals
190 |
191 | # https://github.com/astral-sh/ruff/issues/20310
192 | # ISC001
- t"The quick " t"brown fox."
193 + t"The quick brown fox."
194 |
195 | # ISC002
196 | t"The quick brown fox jumps over the lazy "\
ISC001 Implicitly concatenated string literals on one line
--> ISC.py:206:5
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
| ^^^^^^^^^^^^^^^^^^^^^^
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC.py:206:9
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
| ^^^^^^^^^^^^^^^^^^^^^^
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
|
help: Combine string literals
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:206:14
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
| ^^^^^^^^^
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
|
help: Combine string literals
203 | )
204 |
205 | # nested examples with both t and f-strings
- _ = "a" f"b {t"c" t"d"} e" "f"
206 + _ = "a" f"b {t"cd"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
209 | t"def"} g"
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:207:10
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
| ^^^^^^^^^^^^^^^^^^^^^^^
208 | _ = f"b {t"abc" \
209 | t"def"} g"
|
help: Combine string literals
204 |
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
- _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
207 + _ = t"b {f"cd {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
209 | t"def"} g"
210 |
ISC001 [*] Implicitly concatenated string literals on one line
--> ISC.py:207:20
|
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
| ^^^^^^^^^
208 | _ = f"b {t"abc" \
209 | t"def"} g"
|
help: Combine string literals
204 |
205 | # nested examples with both t and f-strings
206 | _ = "a" f"b {t"c" t"d"} e" "f"
- _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
207 + _ = t"b {f"c" f"d {t"ef"} g"} h"
208 | _ = f"b {t"abc" \
209 | t"def"} g"
210 |

View File

@@ -67,25 +67,3 @@ ISC002 Implicitly concatenated string literals over multiple lines
76 |
77 | # Explicitly concatenated nested f-strings
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:196:1
|
195 | # ISC002
196 | / t"The quick brown fox jumps over the lazy "\
197 | | t"dog."
| |_______^
198 |
199 | # ISC003
|
ISC002 Implicitly concatenated string literals over multiple lines
--> ISC.py:208:10
|
206 | _ = "a" f"b {t"c" t"d"} e" "f"
207 | _ = t"b {f"c" f"d {t"e" t"f"} g"} h"
208 | _ = f"b {t"abc" \
| __________^
209 | | t"def"} g"
| |__________^
|

View File

@@ -60,7 +60,6 @@ mod tests {
}
#[test_case(Rule::SplitStaticString, Path::new("SIM905.py"))]
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -264,10 +264,8 @@ pub(crate) fn dict_get_with_none_default(checker: &Checker, expr: &Expr) {
let Some(key) = args.first() else {
return;
};
if !crate::preview::is_sim910_expanded_key_support_enabled(checker.settings()) {
if !(key.is_literal_expr() || key.is_name_expr()) {
return;
}
if !(key.is_literal_expr() || key.is_name_expr()) {
return;
}
let Some(default) = args.get(1) else {
return;

View File

@@ -167,6 +167,3 @@ help: Replace `dict.get("Cat", None)` with `dict.get("Cat")`
56 | dict = {"Tom": 23, "Maria": 23, "Dog": 11}
- age = dict.get("Cat", None)
57 + age = dict.get("Cat")
58 |
59 |
60 | # https://github.com/astral-sh/ruff/issues/20341

View File

@@ -1,246 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)`
--> SIM910.py:2:1
|
1 | # SIM910
2 | {}.get(key, None)
| ^^^^^^^^^^^^^^^^^
3 |
4 | # SIM910
|
help: Replace `{}.get(key, None)` with `{}.get(key)`
1 | # SIM910
- {}.get(key, None)
2 + {}.get(key)
3 |
4 | # SIM910
5 | {}.get("key", None)
SIM910 [*] Use `{}.get("key")` instead of `{}.get("key", None)`
--> SIM910.py:5:1
|
4 | # SIM910
5 | {}.get("key", None)
| ^^^^^^^^^^^^^^^^^^^
6 |
7 | # OK
|
help: Replace `{}.get("key", None)` with `{}.get("key")`
2 | {}.get(key, None)
3 |
4 | # SIM910
- {}.get("key", None)
5 + {}.get("key")
6 |
7 | # OK
8 | {}.get(key)
SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)`
--> SIM910.py:20:9
|
19 | # SIM910
20 | if a := {}.get(key, None):
| ^^^^^^^^^^^^^^^^^
21 | pass
|
help: Replace `{}.get(key, None)` with `{}.get(key)`
17 | {}.get("key", False)
18 |
19 | # SIM910
- if a := {}.get(key, None):
20 + if a := {}.get(key):
21 | pass
22 |
23 | # SIM910
SIM910 [*] Use `{}.get(key)` instead of `{}.get(key, None)`
--> SIM910.py:24:5
|
23 | # SIM910
24 | a = {}.get(key, None)
| ^^^^^^^^^^^^^^^^^
25 |
26 | # SIM910
|
help: Replace `{}.get(key, None)` with `{}.get(key)`
21 | pass
22 |
23 | # SIM910
- a = {}.get(key, None)
24 + a = {}.get(key)
25 |
26 | # SIM910
27 | ({}).get(key, None)
SIM910 [*] Use `({}).get(key)` instead of `({}).get(key, None)`
--> SIM910.py:27:1
|
26 | # SIM910
27 | ({}).get(key, None)
| ^^^^^^^^^^^^^^^^^^^
28 |
29 | # SIM910
|
help: Replace `({}).get(key, None)` with `({}).get(key)`
24 | a = {}.get(key, None)
25 |
26 | # SIM910
- ({}).get(key, None)
27 + ({}).get(key)
28 |
29 | # SIM910
30 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
SIM910 [*] Use `ages.get("Cat")` instead of `ages.get("Cat", None)`
--> SIM910.py:31:7
|
29 | # SIM910
30 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
31 | age = ages.get("Cat", None)
| ^^^^^^^^^^^^^^^^^^^^^
32 |
33 | # OK
|
help: Replace `ages.get("Cat", None)` with `ages.get("Cat")`
28 |
29 | # SIM910
30 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
- age = ages.get("Cat", None)
31 + age = ages.get("Cat")
32 |
33 | # OK
34 | ages = ["Tom", "Maria", "Dog"]
SIM910 [*] Use `kwargs.get('a')` instead of `kwargs.get('a', None)`
--> SIM910.py:39:9
|
37 | # SIM910
38 | def foo(**kwargs):
39 | a = kwargs.get('a', None)
| ^^^^^^^^^^^^^^^^^^^^^
40 |
41 | # SIM910
|
help: Replace `kwargs.get('a', None)` with `kwargs.get('a')`
36 |
37 | # SIM910
38 | def foo(**kwargs):
- a = kwargs.get('a', None)
39 + a = kwargs.get('a')
40 |
41 | # SIM910
42 | def foo(some_dict: dict):
SIM910 [*] Use `some_dict.get('a')` instead of `some_dict.get('a', None)`
--> SIM910.py:43:9
|
41 | # SIM910
42 | def foo(some_dict: dict):
43 | a = some_dict.get('a', None)
| ^^^^^^^^^^^^^^^^^^^^^^^^
44 |
45 | # OK
|
help: Replace `some_dict.get('a', None)` with `some_dict.get('a')`
40 |
41 | # SIM910
42 | def foo(some_dict: dict):
- a = some_dict.get('a', None)
43 + a = some_dict.get('a')
44 |
45 | # OK
46 | def foo(some_other: object):
SIM910 [*] Use `dict.get("Cat")` instead of `dict.get("Cat", None)`
--> SIM910.py:57:11
|
55 | def foo():
56 | dict = {"Tom": 23, "Maria": 23, "Dog": 11}
57 | age = dict.get("Cat", None)
| ^^^^^^^^^^^^^^^^^^^^^
|
help: Replace `dict.get("Cat", None)` with `dict.get("Cat")`
54 | # https://github.com/astral-sh/ruff/issues/18777
55 | def foo():
56 | dict = {"Tom": 23, "Maria": 23, "Dog": 11}
- age = dict.get("Cat", None)
57 + age = dict.get("Cat")
58 |
59 |
60 | # https://github.com/astral-sh/ruff/issues/20341
SIM910 [*] Use `ages.get(key_source.get("Thomas", "Tom"))` instead of `ages.get(key_source.get("Thomas", "Tom"), None)`
--> SIM910.py:64:7
|
62 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
63 | key_source = {"Thomas": "Tom"}
64 | age = ages.get(key_source.get("Thomas", "Tom"), None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
65 |
66 | # Property access as key
|
help: Replace `ages.get(key_source.get("Thomas", "Tom"), None)` with `ages.get(key_source.get("Thomas", "Tom"))`
61 | # Method call as key
62 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
63 | key_source = {"Thomas": "Tom"}
- age = ages.get(key_source.get("Thomas", "Tom"), None)
64 + age = ages.get(key_source.get("Thomas", "Tom"))
65 |
66 | # Property access as key
67 | class Data:
SIM910 [*] Use `ages.get(data.name)` instead of `ages.get(data.name, None)`
--> SIM910.py:73:7
|
71 | data = Data()
72 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
73 | age = ages.get(data.name, None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^
74 |
75 | # Complex expression as key
|
help: Replace `ages.get(data.name, None)` with `ages.get(data.name)`
70 |
71 | data = Data()
72 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
- age = ages.get(data.name, None)
73 + age = ages.get(data.name)
74 |
75 | # Complex expression as key
76 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
SIM910 [*] Use `ages.get("Tom" if True else "Maria")` instead of `ages.get("Tom" if True else "Maria", None)`
--> SIM910.py:77:7
|
75 | # Complex expression as key
76 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
77 | age = ages.get("Tom" if True else "Maria", None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
78 |
79 | # Function call as key
|
help: Replace `ages.get("Tom" if True else "Maria", None)` with `ages.get("Tom" if True else "Maria")`
74 |
75 | # Complex expression as key
76 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
- age = ages.get("Tom" if True else "Maria", None)
77 + age = ages.get("Tom" if True else "Maria")
78 |
79 | # Function call as key
80 | def get_key():
SIM910 [*] Use `ages.get(get_key())` instead of `ages.get(get_key(), None)`
--> SIM910.py:84:7
|
83 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
84 | age = ages.get(get_key(), None)
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace `ages.get(get_key(), None)` with `ages.get(get_key())`
81 | return "Tom"
82 |
83 | ages = {"Tom": 23, "Maria": 23, "Dog": 11}
- age = ages.get(get_key(), None)
84 + age = ages.get(get_key())

View File

@@ -205,14 +205,3 @@ pub(crate) fn has_unknown_keywords_or_starred_expr(
None => true,
})
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
pub(crate) fn is_argument_non_default(
arguments: &ast::Arguments,
name: &str,
position: usize,
) -> bool {
arguments
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}

View File

@@ -59,7 +59,6 @@ mod tests {
#[test_case(Rule::PyPath, Path::new("py_path_1.py"))]
#[test_case(Rule::PyPath, Path::new("py_path_2.py"))]
#[test_case(Rule::BuiltinOpen, Path::new("PTH123.py"))]
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]
@@ -124,7 +123,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::BuiltinOpen, Path::new("PTH123.py"))]
#[test_case(Rule::PathConstructorCurrentDirectory, Path::new("PTH201.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202.py"))]
#[test_case(Rule::OsPathGetsize, Path::new("PTH202_2.py"))]

View File

@@ -1,191 +0,0 @@
use ruff_diagnostics::{Applicability, Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{ArgOrKeyword, Expr, ExprBooleanLiteral, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_builtin_open_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
has_unknown_keywords_or_starred_expr, is_argument_non_default, is_file_descriptor,
is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
/// ## What it does
/// Checks for uses of the `open()` builtin.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation. When possible,
/// using `Path` object methods such as `Path.open()` can improve readability
/// over the `open` builtin.
///
/// ## Examples
/// ```python
/// with open("f1.py", "wb") as fp:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// with Path("f1.py").open("wb") as fp:
/// ...
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than working directly with strings,
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open)
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct BuiltinOpen;
impl Violation for BuiltinOpen {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`open()` should be replaced by `Path.open()`".to_string()
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `Path.open()`".to_string())
}
}
// PTH123
pub(crate) fn builtin_open(checker: &Checker, call: &ExprCall, segments: &[&str]) {
// `closefd` and `opener` are not supported by pathlib, so check if they
// are set to non-default values.
// https://github.com/astral-sh/ruff/issues/7620
// Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open):
// ```text
// builtins.open(
// file, 0
// mode='r', 1
// buffering=-1, 2
// encoding=None, 3
// errors=None, 4
// newline=None, 5
// closefd=True, 6 <= not supported
// opener=None 7 <= not supported
// )
// ```
// For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open):
// ```text
// Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
// ```
let file_arg = call.arguments.find_argument_value("file", 0);
if call
.arguments
.find_argument_value("closefd", 6)
.is_some_and(|expr| {
!matches!(
expr,
Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. })
)
})
|| is_argument_non_default(&call.arguments, "opener", 7)
|| file_arg.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{
return;
}
if !matches!(segments, ["" | "builtins", "open"]) {
return;
}
let mut diagnostic = checker.report_diagnostic(BuiltinOpen, call.func.range());
if !is_fix_builtin_open_enabled(checker.settings()) {
return;
}
let Some(file) = file_arg else {
return;
};
if has_unknown_keywords_or_starred_expr(
&call.arguments,
&[
"file",
"mode",
"buffering",
"encoding",
"errors",
"newline",
"closefd",
"opener",
],
) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let locator = checker.locator();
let file_code = locator.slice(file.range());
let args = |i: usize, arg: ArgOrKeyword| match arg {
ArgOrKeyword::Arg(expr) => {
if expr.range() == file.range() || i == 6 || i == 7 {
None
} else {
Some(locator.slice(expr.range()))
}
}
ArgOrKeyword::Keyword(kw) => match kw.arg.as_deref() {
Some("mode" | "buffering" | "encoding" | "errors" | "newline") => {
Some(locator.slice(kw))
}
_ => None,
},
};
let open_args = itertools::join(
call.arguments
.arguments_source_order()
.enumerate()
.filter_map(|(i, arg)| args(i, arg)),
", ",
);
let replacement = if is_pathlib_path_call(checker, file) {
format!("{file_code}.open({open_args})")
} else {
format!("{binding}({file_code}).open({open_args})")
};
let range = call.range();
let applicability = if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
};
Ok(Fix::applicable_edits(
Edit::range_replacement(replacement, range),
[import_edit],
applicability,
))
});
}

View File

@@ -1,4 +1,3 @@
pub(crate) use builtin_open::*;
pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_chmod::*;
@@ -30,7 +29,6 @@ pub(crate) use os_unlink::*;
pub(crate) use path_constructor_current_directory::*;
pub(crate) use replaceable_by_pathlib::*;
mod builtin_open;
mod glob_rule;
mod invalid_pathlib_with_suffix;
mod os_chmod;

View File

@@ -1,11 +1,13 @@
use ruff_diagnostics::Applicability;
use ruff_diagnostics::{Edit, Fix};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::{
check_os_pathlib_single_arg_calls, has_unknown_keywords_or_starred_expr,
has_unknown_keywords_or_starred_expr, is_pathlib_path_call,
};
use crate::{FixAvailability, Violation};
@@ -73,17 +75,43 @@ pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&s
return;
}
if call.arguments.len() != 1 {
return;
}
let Some(arg) = call.arguments.find_argument_value("path", 0) else {
return;
};
let arg_code = checker.locator().slice(arg.range());
let range = call.range();
let mut diagnostic = checker.report_diagnostic(OsPathAbspath, call.func.range());
if has_unknown_keywords_or_starred_expr(&call.arguments, &["path"]) {
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"resolve()",
"path",
is_fix_os_path_abspath_enabled(checker.settings()),
OsPathAbspath,
Some(Applicability::Unsafe),
);
if !is_fix_os_path_abspath_enabled(checker.settings()) {
return;
}
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_symbol(
&ImportRequest::import("pathlib", "Path"),
call.start(),
checker.semantic(),
)?;
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.resolve()")
} else {
format!("{binding}({arg_code}).resolve()")
};
Ok(Fix::unsafe_edits(
Edit::range_replacement(replacement, range),
[import_edit],
))
});
}

View File

@@ -1,4 +1,3 @@
use ruff_diagnostics::Applicability;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;
@@ -36,10 +35,7 @@ use crate::{FixAvailability, Violation};
/// especially on older versions of Python.
///
/// ## Fix Safety
/// This rule's fix is always marked as unsafe because the behaviors of
/// `os.path.expanduser` and `Path.expanduser` differ when a user's home
/// directory can't be resolved: `os.path.expanduser` returns the
/// input unchanged, while `Path.expanduser` raises `RuntimeError`.
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
///
/// ## References
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
@@ -75,6 +71,6 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
"path",
is_fix_os_path_expanduser_enabled(checker.settings()),
OsPathExpanduser,
Some(Applicability::Unsafe),
None,
);
}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{Expr, ExprCall};
use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -7,7 +7,7 @@ use crate::rules::flake8_use_pathlib::helpers::{
};
use crate::rules::flake8_use_pathlib::{
rules::Glob,
violations::{Joiner, OsListdir, OsPathJoin, OsPathSplitext, OsStat, PyPath},
violations::{BuiltinOpen, Joiner, OsListdir, OsPathJoin, OsPathSplitext, OsStat, PyPath},
};
pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
@@ -60,6 +60,42 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
),
// PTH122
["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range),
// PTH123
["" | "builtins", "open"] => {
// `closefd` and `opener` are not supported by pathlib, so check if they
// are set to non-default values.
// https://github.com/astral-sh/ruff/issues/7620
// Signature as of Python 3.11 (https://docs.python.org/3/library/functions.html#open):
// ```text
// 0 1 2 3 4 5
// open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None,
// 6 7
// closefd=True, opener=None)
// ^^^^ ^^^^
// ```
// For `pathlib` (https://docs.python.org/3/library/pathlib.html#pathlib.Path.open):
// ```text
// Path.open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
// ```
if call
.arguments
.find_argument_value("closefd", 6)
.is_some_and(|expr| {
!matches!(
expr,
Expr::BooleanLiteral(ExprBooleanLiteral { value: true, .. })
)
})
|| is_argument_non_default(&call.arguments, "opener", 7)
|| call
.arguments
.find_argument_value("file", 0)
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
{
return;
}
checker.report_diagnostic_if_enabled(BuiltinOpen, range)
}
// PTH124
["py", "path", "local"] => checker.report_diagnostic_if_enabled(PyPath, range),
// PTH207
@@ -115,3 +151,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) {
_ => return,
};
}
/// Returns `true` if argument `name` is set to a non-default `None` value.
fn is_argument_non_default(arguments: &ast::Arguments, name: &str, position: usize) -> bool {
arguments
.find_argument_value(name, position)
.is_some_and(|expr| !expr.is_none_literal_expr())
}

View File

@@ -1,143 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:8:1
|
6 | r_plus = "r+"
7 |
8 | builtins.open(file=_file)
| ^^^^^^^^^^^^^
9 |
10 | open(_file, "r+ ", - 1)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:10:1
|
8 | builtins.open(file=_file)
9 |
10 | open(_file, "r+ ", - 1)
| ^^^^
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:11:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
| ^^^^
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:12:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
| ^^^^
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:13:1
|
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
| ^^^^
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:14:1
|
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
| ^^^^
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:15:1
|
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
| ^^^^
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:16:1
|
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
| ^^^^
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:17:1
|
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
| ^^^^
18 |
19 | # Only diagnostic
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:20:1
|
19 | # Only diagnostic
20 | open()
| ^^^^
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:21:1
|
19 | # Only diagnostic
20 | open()
21 | open(_file, *_x)
| ^^^^
22 | open(_file, "r+", unknown=True)
23 | open(_file, "r+", closefd=False)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:22:1
|
20 | open()
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
| ^^^^
23 | open(_file, "r+", closefd=False)
24 | open(_file, "r+", None, None, None, None, None, None, None)
|
help: Replace with `Path.open()`

View File

@@ -305,7 +305,6 @@ PTH123 `open()` should be replaced by `Path.open()`
33 | fp.read()
34 | open(p).close()
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:34:1
@@ -317,7 +316,6 @@ PTH123 `open()` should be replaced by `Path.open()`
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
|
help: Replace with `Path.open()`
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:35:1
@@ -362,7 +360,6 @@ PTH123 `open()` should be replaced by `Path.open()`
47 | open(p, 'r', - 1, None, None, None, True, None)
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:47:1
@@ -373,7 +370,6 @@ PTH123 `open()` should be replaced by `Path.open()`
| ^^^^
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:65:1
@@ -385,7 +381,6 @@ PTH123 `open()` should be replaced by `Path.open()`
66 | byte_str = b"bar"
67 | open(byte_str)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:67:1
@@ -397,7 +392,6 @@ PTH123 `open()` should be replaced by `Path.open()`
68 |
69 | def bytes_str_func() -> bytes:
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:71:1
@@ -409,7 +403,6 @@ PTH123 `open()` should be replaced by `Path.open()`
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|
help: Replace with `Path.open()`
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:108:1

View File

@@ -305,7 +305,6 @@ PTH123 `open()` should be replaced by `Path.open()`
35 | fp.read()
36 | open(p).close()
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> import_from.py:36:1
@@ -315,7 +314,6 @@ PTH123 `open()` should be replaced by `Path.open()`
36 | open(p).close()
| ^^^^
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> import_from.py:43:10
@@ -325,7 +323,6 @@ PTH123 `open()` should be replaced by `Path.open()`
43 | with open(p) as _: ... # Error
| ^^^^
|
help: Replace with `Path.open()`
PTH104 `os.rename()` should be replaced by `Path.rename()`
--> import_from.py:53:1

View File

@@ -1,215 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
---
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:8:1
|
6 | r_plus = "r+"
7 |
8 | builtins.open(file=_file)
| ^^^^^^^^^^^^^
9 |
10 | open(_file, "r+ ", - 1)
|
help: Replace with `Path.open()`
5 | _x = ("r+", -1)
6 | r_plus = "r+"
7 |
- builtins.open(file=_file)
8 + Path(_file).open()
9 |
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:10:1
|
8 | builtins.open(file=_file)
9 |
10 | open(_file, "r+ ", - 1)
| ^^^^
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
|
help: Replace with `Path.open()`
7 |
8 | builtins.open(file=_file)
9 |
- open(_file, "r+ ", - 1)
10 + Path(_file).open("r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:11:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
| ^^^^
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
|
help: Replace with `Path.open()`
8 | builtins.open(file=_file)
9 |
10 | open(_file, "r+ ", - 1)
- open(mode="wb", file=_file)
11 + Path(_file).open(mode="wb")
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:12:1
|
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
| ^^^^
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
|
help: Replace with `Path.open()`
9 |
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
- open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
12 + Path(_file).open(mode="r+", buffering=-1, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:13:1
|
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
| ^^^^
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
|
help: Replace with `Path.open()`
10 | open(_file, "r+ ", - 1)
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
- open(_file, "r+", - 1, None, None, None, True, None)
13 + Path(_file).open("r+", - 1, None, None, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:14:1
|
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
| ^^^^
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
|
help: Replace with `Path.open()`
11 | open(mode="wb", file=_file)
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
- open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
14 + Path(_file).open("r+", -1, None, None, None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:15:1
|
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
| ^^^^
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
12 | open(mode="r+", buffering=-1, file=_file, encoding="utf-8")
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
- open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
15 + Path(_file).open(mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
18 |
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:16:1
|
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
| ^^^^
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
|
help: Replace with `Path.open()`
13 | open(_file, "r+", - 1, None, None, None, True, None)
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
- open(_file, f" {r_plus} ", - 1)
16 + Path(_file).open(f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
18 |
19 | # Only diagnostic
PTH123 [*] `open()` should be replaced by `Path.open()`
--> PTH123.py:17:1
|
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
17 | open(buffering=- 1, file=_file, encoding= "utf-8")
| ^^^^
18 |
19 | # Only diagnostic
|
help: Replace with `Path.open()`
14 | open(_file, "r+", -1, None, None, None, closefd=True, opener=None)
15 | open(_file, mode="r+", buffering=-1, encoding=None, errors=None, newline=None)
16 | open(_file, f" {r_plus} ", - 1)
- open(buffering=- 1, file=_file, encoding= "utf-8")
17 + Path(_file).open(buffering=- 1, encoding= "utf-8")
18 |
19 | # Only diagnostic
20 | open()
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:20:1
|
19 | # Only diagnostic
20 | open()
| ^^^^
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:21:1
|
19 | # Only diagnostic
20 | open()
21 | open(_file, *_x)
| ^^^^
22 | open(_file, "r+", unknown=True)
23 | open(_file, "r+", closefd=False)
|
help: Replace with `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> PTH123.py:22:1
|
20 | open()
21 | open(_file, *_x)
22 | open(_file, "r+", unknown=True)
| ^^^^
23 | open(_file, "r+", closefd=False)
24 | open(_file, "r+", None, None, None, None, None, None, None)
|
help: Replace with `Path.open()`

View File

@@ -260,7 +260,6 @@ help: Replace with `Path(...).expanduser()`
20 | bbb = os.path.isdir(p)
21 | bbbb = os.path.isfile(p)
22 | bbbbb = os.path.islink(p)
note: This is an unsafe fix and may change runtime behavior
PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
--> full_name.py:19:7
@@ -520,7 +519,7 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an
33 | fp.read()
|
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:32:6
|
30 | os.path.samefile(p)
@@ -530,24 +529,8 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
33 | fp.read()
34 | open(p).close()
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
30 | os.path.dirname(p)
31 | os.path.samefile(p)
32 | os.path.splitext(p)
- with open(p) as fp:
33 + with pathlib.Path(p).open() as fp:
34 | fp.read()
35 | open(p).close()
36 | os.getcwdb(p)
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:34:1
|
32 | with open(p) as fp:
@@ -557,22 +540,6 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
35 | os.getcwdb(p)
36 | os.path.join(p, *q)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
32 | os.path.splitext(p)
33 | with open(p) as fp:
34 | fp.read()
- open(p).close()
35 + pathlib.Path(p).open().close()
36 | os.getcwdb(p)
37 | os.path.join(p, *q)
38 | os.sep.join(p, *q)
PTH109 `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:35:1
@@ -607,7 +574,7 @@ PTH118 `os.sep.join()` should be replaced by `Path.joinpath()`
39 | # https://github.com/astral-sh/ruff/issues/7620
|
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:46:1
|
44 | open(p, closefd=False)
@@ -617,24 +584,8 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
47 | open(p, 'r', - 1, None, None, None, True, None)
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
44 |
45 | open(p, closefd=False)
46 | open(p, opener=opener)
- open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
47 + pathlib.Path(p).open(mode='r', buffering=-1, encoding=None, errors=None, newline=None)
48 | open(p, 'r', - 1, None, None, None, True, None)
49 | open(p, 'r', - 1, None, None, None, False, opener)
50 |
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:47:1
|
45 | open(p, opener=opener)
@@ -643,24 +594,8 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
| ^^^^
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
45 | open(p, closefd=False)
46 | open(p, opener=opener)
47 | open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
- open(p, 'r', - 1, None, None, None, True, None)
48 + pathlib.Path(p).open('r', - 1, None, None, None)
49 | open(p, 'r', - 1, None, None, None, False, opener)
50 |
51 | # Cannot be upgraded `pathlib.Open` does not support fds
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:65:1
|
63 | open(f())
@@ -670,24 +605,8 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
66 | byte_str = b"bar"
67 | open(byte_str)
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
63 | return 1
64 | open(f())
65 |
- open(b"foo")
66 + pathlib.Path(b"foo").open()
67 | byte_str = b"bar"
68 | open(byte_str)
69 |
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:67:1
|
65 | open(b"foo")
@@ -697,24 +616,8 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
68 |
69 | def bytes_str_func() -> bytes:
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
65 |
66 | open(b"foo")
67 | byte_str = b"bar"
- open(byte_str)
68 + pathlib.Path(byte_str).open()
69 |
70 | def bytes_str_func() -> bytes:
71 | return b"foo"
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> full_name.py:71:1
|
69 | def bytes_str_func() -> bytes:
@@ -724,22 +627,6 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
72 |
73 | # https://github.com/astral-sh/ruff/issues/17693
|
help: Replace with `Path.open()`
1 | import os
2 | import os.path
3 + import pathlib
4 |
5 | p = "/foo"
6 | q = "bar"
--------------------------------------------------------------------------------
69 |
70 | def bytes_str_func() -> bytes:
71 | return b"foo"
- open(bytes_str_func())
72 + pathlib.Path(bytes_str_func()).open()
73 |
74 | # https://github.com/astral-sh/ruff/issues/17693
75 | os.stat(1)
PTH109 [*] `os.getcwd()` should be replaced by `Path.cwd()`
--> full_name.py:108:1

View File

@@ -260,7 +260,6 @@ help: Replace with `Path(...).expanduser()`
20 | bbb = foo_p.isdir(p)
21 | bbbb = foo_p.isfile(p)
22 | bbbbb = foo_p.islink(p)
note: This is an unsafe fix and may change runtime behavior
PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
--> import_as.py:19:7

View File

@@ -268,7 +268,6 @@ help: Replace with `Path(...).expanduser()`
22 | bbb = isdir(p)
23 | bbbb = isfile(p)
24 | bbbbb = islink(p)
note: This is an unsafe fix and may change runtime behavior
PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
--> import_from.py:21:7
@@ -535,7 +534,7 @@ PTH122 `os.path.splitext()` should be replaced by `Path.suffix`, `Path.stem`, an
35 | fp.read()
|
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> import_from.py:34:6
|
32 | samefile(p)
@@ -545,25 +544,8 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
35 | fp.read()
36 | open(p).close()
|
help: Replace with `Path.open()`
2 | from os import remove, unlink, getcwd, readlink, stat
3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 + import pathlib
6 |
7 | p = "/foo"
8 | q = "bar"
--------------------------------------------------------------------------------
32 | dirname(p)
33 | samefile(p)
34 | splitext(p)
- with open(p) as fp:
35 + with pathlib.Path(p).open() as fp:
36 | fp.read()
37 | open(p).close()
38 |
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> import_from.py:36:1
|
34 | with open(p) as fp:
@@ -571,25 +553,8 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
36 | open(p).close()
| ^^^^
|
help: Replace with `Path.open()`
2 | from os import remove, unlink, getcwd, readlink, stat
3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 + import pathlib
6 |
7 | p = "/foo"
8 | q = "bar"
--------------------------------------------------------------------------------
34 | splitext(p)
35 | with open(p) as fp:
36 | fp.read()
- open(p).close()
37 + pathlib.Path(p).open().close()
38 |
39 |
40 | # https://github.com/astral-sh/ruff/issues/15442
PTH123 [*] `open()` should be replaced by `Path.open()`
PTH123 `open()` should be replaced by `Path.open()`
--> import_from.py:43:10
|
41 | from builtins import open
@@ -597,23 +562,6 @@ PTH123 [*] `open()` should be replaced by `Path.open()`
43 | with open(p) as _: ... # Error
| ^^^^
|
help: Replace with `Path.open()`
2 | from os import remove, unlink, getcwd, readlink, stat
3 | from os.path import abspath, exists, expanduser, isdir, isfile, islink
4 | from os.path import isabs, join, basename, dirname, samefile, splitext
5 + import pathlib
6 |
7 | p = "/foo"
8 | q = "bar"
--------------------------------------------------------------------------------
41 | def _():
42 | from builtins import open
43 |
- with open(p) as _: ... # Error
44 + with pathlib.Path(p).open() as _: ... # Error
45 |
46 |
47 | def _():
PTH104 [*] `os.rename()` should be replaced by `Path.rename()`
--> import_from.py:53:1

View File

@@ -268,7 +268,6 @@ help: Replace with `Path(...).expanduser()`
27 | bbb = xisdir(p)
28 | bbbb = xisfile(p)
29 | bbbbb = xislink(p)
note: This is an unsafe fix and may change runtime behavior
PTH112 [*] `os.path.isdir()` should be replaced by `Path.is_dir()`
--> import_from_as.py:26:7

View File

@@ -174,6 +174,50 @@ impl Violation for OsPathSplitext {
}
}
/// ## What it does
/// Checks for uses of the `open()` builtin.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation. When possible,
/// using `Path` object methods such as `Path.open()` can improve readability
/// over the `open` builtin.
///
/// ## Examples
/// ```python
/// with open("f1.py", "wb") as fp:
/// ...
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// with Path("f1.py").open("wb") as fp:
/// ...
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than working directly with strings,
/// especially on older versions of Python.
///
/// ## References
/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open)
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
/// - [PEP 428 The pathlib module object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#corresponding-tools)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct BuiltinOpen;
impl Violation for BuiltinOpen {
#[derive_message_formats]
fn message(&self) -> String {
"`open()` should be replaced by `Path.open()`".to_string()
}
}
/// ## What it does
/// Checks for uses of the `py.path` library.
///

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