Compare commits

..

20 Commits

Author SHA1 Message Date
Douglas Creager
f33ca3a622 minimize more 2025-10-17 16:23:47 -04:00
Douglas Creager
a79ba2036b propagate impossible deeply 2025-10-17 16:23:46 -04:00
Douglas Creager
d6f28b7428 keep all minimizations 2025-10-17 16:23:46 -04:00
Douglas Creager
9df9adae1e minimize before display 2025-10-17 16:23:46 -04:00
Douglas Creager
eec4e2ed11 add impossible terminal 2025-10-17 16:23:46 -04:00
Douglas Creager
a44fbd6658 debug display simplification 2025-10-17 16:23:46 -04:00
Douglas Creager
22075d5ed7 debug 2025-10-17 16:23:46 -04:00
Douglas Creager
5d451979c4 shannon 2025-10-17 16:23:46 -04:00
Douglas Creager
73773b4ea4 more intersection replacements 2025-10-17 16:23:46 -04:00
Douglas Creager
7cfdc4a550 replace with intersection 2025-10-17 16:23:46 -04:00
Douglas Creager
2f0e7d6af7 fix pos/neg implication 2025-10-17 16:23:46 -04:00
Douglas Creager
8d44f8b7b5 remove old simplify 2025-10-17 16:23:46 -04:00
Douglas Creager
c0faa2dc3d use simplify_new 2025-10-17 16:23:46 -04:00
Douglas Creager
f4fff7fb24 new simplification 2025-10-17 16:23:46 -04:00
Douglas Creager
a6bd68886f xor 2025-10-17 16:23:46 -04:00
Douglas Creager
5c2c3f00ff constraint implication check 2025-10-17 16:23:46 -04:00
Douglas Creager
5affc120b3 order typevars near each other 2025-10-17 16:23:46 -04:00
Douglas Creager
1e284933ec normalize bounds 2025-10-17 16:23:46 -04:00
Douglas Creager
c529ee4f80 simplify individual clauses for display 2025-10-17 16:23:46 -04:00
Douglas Creager
f88ff62da5 add BDD graph display 2025-10-17 16:23:46 -04:00
298 changed files with 4815 additions and 11289 deletions

View File

@@ -8,3 +8,7 @@ benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --"
# See: https://github.com/astral-sh/ruff/issues/11503
[target.'cfg(all(target_env="msvc", target_os = "windows"))']
rustflags = ["-C", "target-feature=+crt-static"]
[target.'wasm32-unknown-unknown']
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
rustflags = ["--cfg", 'getrandom_backend="wasm_js"']

View File

@@ -22,7 +22,7 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.14"
PYTHON_VERSION: "3.13"
NEXTEST_PROFILE: ci
jobs:
@@ -277,8 +277,7 @@ jobs:
run: cargo test -p ty_python_semantic --test mdtest || true
- name: "Run tests"
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
# Dogfood ty on py-fuzzer
- run: uv run --project=./python/py-fuzzer cargo run -p ty check --project=./python/py-fuzzer
# Check for broken links in the documentation.
- run: cargo doc --all --no-deps
env:
@@ -334,14 +333,9 @@ jobs:
- name: "Run tests"
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
cargo-test-other:
strategy:
matrix:
platform:
- ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}
- macos-latest
name: "cargo test (${{ matrix.platform }})"
runs-on: ${{ matrix.platform }}
cargo-test-windows:
name: "cargo test (windows)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}
needs: determine_changes
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20
@@ -360,6 +354,37 @@ jobs:
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: "true"
- name: "Run tests"
env:
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: |
cargo nextest run --all-features --profile ci
cargo test --all-features --doc
cargo-test-macos:
name: "cargo test (macos)"
runs-on: macos-latest
needs: determine_changes
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
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install uv"
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: "true"
- name: "Run tests"
run: |
cargo nextest run --all-features --profile ci
@@ -473,10 +498,9 @@ jobs:
chmod +x "${DOWNLOAD_PATH}/ruff"
(
uv run \
uvx \
--python="${PYTHON_VERSION}" \
--project=./python/py-fuzzer \
--locked \
--from=./python/py-fuzzer \
fuzz \
--test-executable="${DOWNLOAD_PATH}/ruff" \
--bin=ruff \
@@ -494,7 +518,6 @@ jobs:
with:
persist-credentials: false
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- name: "Install Rust toolchain"
run: rustup component add rustfmt
# Run all code generation scripts, and verify that the current output is
@@ -509,11 +532,6 @@ jobs:
./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ --prefix TST
./scripts/add_rule.py --name FirstRule --prefix TST --code 001 --linter test
- run: cargo check
# Lint/format/type-check py-fuzzer
# (dogfooding with ty is done in a separate job)
- run: uv run --directory=./python/py-fuzzer mypy
- run: uv run --directory=./python/py-fuzzer ruff format --check
- run: uv run --directory=./python/py-fuzzer ruff check
ecosystem:
name: "ecosystem"
@@ -529,11 +547,9 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
# TODO: figure out why `ruff-ecosystem` crashes on Python 3.14
python-version: "3.13"
activate-environment: true
python-version: ${{ env.PYTHON_VERSION }}
- uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
name: Download comparison Ruff binary
@@ -552,7 +568,7 @@ jobs:
- name: Install ruff-ecosystem
run: |
uv pip install ./python/ruff-ecosystem
pip install ./python/ruff-ecosystem
- name: Run `ruff check` stable ecosystem check
if: ${{ needs.determine_changes.outputs.linter == 'true' }}
@@ -650,7 +666,7 @@ jobs:
- determine_changes
# Only runs on pull requests, since that is the only we way we can find the base version for comparison.
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && github.event_name == 'pull_request' && (needs.determine_changes.outputs.ty == 'true' || needs.determine_changes.outputs.py-fuzzer == 'true') }}
timeout-minutes: ${{ github.repository == 'astral-sh/ruff' && 5 || 20 }}
timeout-minutes: 5
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
@@ -678,16 +694,15 @@ jobs:
chmod +x "${PWD}/ty" "${NEW_TY}/ty"
(
uv run \
uvx \
--python="${PYTHON_VERSION}" \
--project=./python/py-fuzzer \
--locked \
--from=./python/py-fuzzer \
fuzz \
--test-executable="${NEW_TY}/ty" \
--baseline-executable="${PWD}/ty" \
--only-new-bugs \
--bin=ty \
0-1000
0-500
)
cargo-shear:
@@ -788,6 +803,9 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: "3.13"
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
@@ -798,15 +816,12 @@ jobs:
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
python-version: 3.13
activate-environment: true
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt
run: uv pip install -r docs/requirements-insiders.txt --system
- name: "Install dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
run: uv pip install -r docs/requirements.txt
run: uv pip install -r docs/requirements.txt --system
- name: "Update README File"
run: python scripts/transform_readme.py --target mkdocs
- name: "Generate docs"

View File

@@ -48,10 +48,9 @@ jobs:
run: |
# shellcheck disable=SC2046
(
uv run \
--python=3.14 \
--project=./python/py-fuzzer \
--locked \
uvx \
--python=3.12 \
--from=./python/py-fuzzer \
fuzz \
--test-executable=target/debug/ruff \
--bin=ruff \

View File

@@ -18,8 +18,6 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
permissions: {}
jobs:
publish:
runs-on: ubuntu-latest
@@ -34,7 +32,7 @@ jobs:
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
cache: "npm" # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
cache: "npm"
cache-dependency-path: playground/package-lock.json
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies"

View File

@@ -38,7 +38,6 @@ jobs:
- uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
with:
node-version: 22
cache: "npm" # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
- name: "Install Node dependencies"
run: npm ci

View File

@@ -1,6 +1,7 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist
#
# Copyright 2022-2024, axodotdev
# Copyright 2025 Astral Software Inc.
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
@@ -68,7 +69,7 @@ jobs:
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.0/cargo-dist-installer.sh | sh"
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.5-prerelease.1/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@6027e3dd177782cd8ab9af838c04fd81a07f1d47
with:

View File

@@ -34,13 +34,10 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- name: Install Rust toolchain
run: rustup show

View File

@@ -30,13 +30,10 @@ jobs:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
with:
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
workspaces: "ruff"
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
- name: Install Rust toolchain
run: rustup show

13
.github/zizmor.yml vendored
View File

@@ -9,18 +9,13 @@ rules:
cache-poisoning:
ignore:
- build-docker.yml
- publish-playground.yml
- ty-ecosystem-analyzer.yaml
- ty-ecosystem-report.yaml
excessive-permissions:
# it's hard to test what the impact of removing these ignores would be
# without actually running the release workflow...
ignore:
- build-docker.yml
- publish-playground.yml
- publish-docs.yml
secrets-inherit:
# `cargo dist` makes extensive use of `secrets: inherit`,
# and we can't easily fix that until an upstream release changes that.
disable: true
template-injection:
ignore:
# like with `secrets-inherit`, `cargo dist` introduces some
# template injections. We've manually audited these usages for safety.
- release.yml

View File

@@ -35,7 +35,6 @@ repos:
rev: 0.7.22
hooks:
- id: mdformat
language: python # means renovate will also update `additional_dependencies`
additional_dependencies:
- mdformat-mkdocs==4.0.0
- mdformat-footnote==0.1.1
@@ -59,7 +58,6 @@ repos:
rev: 1.19.1
hooks:
- id: blacken-docs
language: python # means renovate will also update `additional_dependencies`
args: ["--pyi", "--line-length", "130"]
files: '^crates/.*/resources/mdtest/.*\.md'
exclude: |
@@ -101,8 +99,8 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/zizmorcore/zizmor-pre-commit
rev: v1.15.2
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.11.0
hooks:
- id: zizmor

134
Cargo.lock generated
View File

@@ -433,9 +433,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.49"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f"
checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae"
dependencies = [
"clap_builder",
"clap_derive",
@@ -443,9 +443,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.49"
version = "4.5.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730"
checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9"
dependencies = [
"anstream",
"anstyle",
@@ -486,9 +486,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.49"
version = "4.5.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c"
dependencies = [
"heck",
"proc-macro2",
@@ -633,7 +633,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -642,7 +642,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1007,7 +1007,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1093,7 +1093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -1262,20 +1262,20 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.3.4"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasip2",
"wasi 0.14.7+wasi-0.2.4",
"wasm-bindgen",
]
@@ -1287,9 +1287,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
[[package]]
name = "globset"
version = "0.4.17"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab69130804d941f8075cfd713bf8848a2c3b3f201a9457a11e6f87e1ab62305"
checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
dependencies = [
"aho-corasick",
"bstr",
@@ -1690,7 +1690,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1754,7 +1754,7 @@ dependencies = [
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1789,7 +1789,7 @@ version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"getrandom 0.3.3",
"libc",
]
@@ -2072,7 +2072,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"log",
"wasi",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
@@ -2627,9 +2627,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.41"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
@@ -2724,7 +2724,7 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom 0.3.4",
"getrandom 0.3.3",
]
[[package]]
@@ -2767,26 +2767,6 @@ dependencies = [
"thiserror 2.0.16",
]
[[package]]
name = "ref-cast"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "regex"
version = "1.11.3"
@@ -2801,9 +2781,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.13"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad"
dependencies = [
"aho-corasick",
"memchr",
@@ -3217,7 +3197,6 @@ dependencies = [
"salsa",
"schemars",
"serde",
"serde_json",
"thiserror 2.0.16",
]
@@ -3451,7 +3430,7 @@ version = "0.14.1"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.4",
"getrandom 0.3.3",
"js-sys",
"log",
"ruff_formatter",
@@ -3505,7 +3484,6 @@ dependencies = [
"rustc-hash",
"schemars",
"serde",
"serde_json",
"shellexpand",
"strum",
"tempfile",
@@ -3545,7 +3523,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -3563,7 +3541,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
dependencies = [
"boxcar",
"compact_str",
@@ -3587,12 +3565,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=d38145c29574758de7ffbe8a13cd4584c3b09161#d38145c29574758de7ffbe8a13cd4584c3b09161"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
dependencies = [
"proc-macro2",
"quote",
@@ -3611,12 +3589,11 @@ dependencies = [
[[package]]
name = "schemars"
version = "1.0.4"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
dependencies = [
"dyn-clone",
"ref-cast",
"schemars_derive",
"serde",
"serde_json",
@@ -3624,9 +3601,9 @@ dependencies = [
[[package]]
name = "schemars_derive"
version = "1.0.4"
version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
dependencies = [
"proc-macro2",
"quote",
@@ -3648,9 +3625,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.228"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd"
dependencies = [
"serde_core",
"serde_derive",
@@ -3669,18 +3646,18 @@ dependencies = [
[[package]]
name = "serde_core"
version = "1.0.228"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
version = "1.0.226"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33"
dependencies = [
"proc-macro2",
"quote",
@@ -3818,9 +3795,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "snapbox"
version = "0.6.22"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "805d09a74586d9b17061e5be6ee5f8cc37e5982c349948114ffc5f68093fe5ec"
checksum = "96dcfc4581e3355d70ac2ee14cfdf81dce3d85c85f1ed9e2c1d3013f53b3436b"
dependencies = [
"anstream",
"anstyle",
@@ -3833,7 +3810,7 @@ dependencies = [
"similar",
"snapbox-macros",
"wait-timeout",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -3938,10 +3915,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53"
dependencies = [
"fastrand",
"getrandom 0.3.4",
"getrandom 0.3.3",
"once_cell",
"rustix",
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]
@@ -4376,7 +4353,6 @@ dependencies = [
"tracing",
"ty_project",
"ty_python_semantic",
"ty_vendored",
]
[[package]]
@@ -4408,7 +4384,6 @@ dependencies = [
"salsa",
"schemars",
"serde",
"serde_json",
"thiserror 2.0.16",
"toml",
"tracing",
@@ -4434,12 +4409,10 @@ dependencies = [
"glob",
"hashbrown 0.16.0",
"indexmap",
"indoc",
"insta",
"itertools 0.14.0",
"memchr",
"ordermap",
"pretty_assertions",
"quickcheck",
"quickcheck_macros",
"ruff_annotate_snippets",
@@ -4458,7 +4431,6 @@ dependencies = [
"salsa",
"schemars",
"serde",
"serde_json",
"smallvec",
"static_assertions",
"strsim",
@@ -4507,6 +4479,7 @@ dependencies = [
"ty_ide",
"ty_project",
"ty_python_semantic",
"ty_vendored",
]
[[package]]
@@ -4566,7 +4539,7 @@ version = "0.0.0"
dependencies = [
"console_error_panic_hook",
"console_log",
"getrandom 0.3.4",
"getrandom 0.3.3",
"js-sys",
"log",
"ruff_db",
@@ -4759,7 +4732,7 @@ version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2"
dependencies = [
"getrandom 0.3.4",
"getrandom 0.3.3",
"js-sys",
"rand 0.9.2",
"uuid-macro-internal",
@@ -4871,6 +4844,15 @@ version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasi"
version = "0.14.7+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c"
dependencies = [
"wasip2",
]
[[package]]
name = "wasip2"
version = "1.0.1+wasi-0.2.4"
@@ -5022,7 +5004,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
"windows-sys 0.52.0",
"windows-sys 0.61.0",
]
[[package]]

View File

@@ -146,13 +146,13 @@ 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 = "d38145c29574758de7ffbe8a13cd4584c3b09161", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ef9f9329be6923acd050c8dddd172e3bc93e8051", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
"inventory",
] }
schemars = { version = "1.0.4" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.4" }

View File

@@ -89,7 +89,8 @@ creator of [isort](https://github.com/PyCQA/isort):
> Just switched my first project to Ruff. Only one downside so far: it's so fast I couldn't believe
> it was working till I intentionally introduced some errors.
[**Tim Abbott**](https://github.com/zulip/zulip/pull/23431#issuecomment-1302557034), lead developer of [Zulip](https://github.com/zulip/zulip) (also [here](https://github.com/astral-sh/ruff/issues/465#issuecomment-1317400028)):
[**Tim Abbott**](https://github.com/astral-sh/ruff/issues/465#issuecomment-1317400028), lead
developer of [Zulip](https://github.com/zulip/zulip):
> This is just ridiculously fast... `ruff` is amazing.

View File

@@ -879,7 +879,19 @@ impl From<&FormatCommandError> for Diagnostic {
| FormatCommandError::Write(_, source_error) => {
Diagnostic::new(DiagnosticId::Io, Severity::Error, source_error)
}
FormatCommandError::Format(_, format_module_error) => format_module_error.into(),
FormatCommandError::Format(_, format_module_error) => match format_module_error {
FormatModuleError::ParseError(parse_error) => Diagnostic::new(
DiagnosticId::InternalError,
Severity::Error,
&parse_error.error,
),
FormatModuleError::FormatError(format_error) => {
Diagnostic::new(DiagnosticId::InternalError, Severity::Error, format_error)
}
FormatModuleError::PrintError(print_error) => {
Diagnostic::new(DiagnosticId::InternalError, Severity::Error, print_error)
}
},
FormatCommandError::RangeFormatNotebook(_) => Diagnostic::new(
DiagnosticId::InvalidCliOption,
Severity::Error,

View File

@@ -7,7 +7,6 @@ use serde::{Serialize, Serializer};
use strum::IntoEnumIterator;
use ruff_linter::FixAvailability;
use ruff_linter::codes::RuleGroup;
use ruff_linter::registry::{Linter, Rule, RuleNamespace};
use crate::args::HelpFormat;
@@ -20,11 +19,9 @@ struct Explanation<'a> {
summary: &'a str,
message_formats: &'a [&'a str],
fix: String,
fix_availability: FixAvailability,
#[expect(clippy::struct_field_names)]
explanation: Option<&'a str>,
preview: bool,
status: RuleGroup,
}
impl<'a> Explanation<'a> {
@@ -39,10 +36,8 @@ impl<'a> Explanation<'a> {
summary: rule.message_formats()[0],
message_formats: rule.message_formats(),
fix,
fix_availability: rule.fixable(),
explanation: rule.explanation(),
preview: rule.is_preview(),
status: rule.group(),
}
}
}

View File

@@ -15,7 +15,6 @@ use std::{
};
use tempfile::TempDir;
mod format;
mod lint;
const BIN_NAME: &str = "ruff";
@@ -58,16 +57,6 @@ impl CliTest {
Self::with_settings(|_, settings| settings)
}
pub(crate) fn with_files<'a>(
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> anyhow::Result<Self> {
let case = Self::new()?;
for file in files {
case.write_file(file.0, file.1)?;
}
Ok(case)
}
pub(crate) fn with_settings(
setup_settings: impl FnOnce(&Path, insta::Settings) -> insta::Settings,
) -> Result<Self> {
@@ -185,10 +174,4 @@ impl CliTest {
command
}
pub(crate) fn format_command(&self) -> Command {
let mut command = self.command();
command.args(["format", "--no-cache"]);
command
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -951,16 +951,6 @@ fn rule_f401() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401"]));
}
#[test]
fn rule_f401_output_json() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401", "--output-format", "json"]));
}
#[test]
fn rule_f401_output_text() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "F401", "--output-format", "text"]));
}
#[test]
fn rule_invalid_rule_name() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404"]), @r"
@@ -975,34 +965,6 @@ fn rule_invalid_rule_name() {
");
}
#[test]
fn rule_invalid_rule_name_output_json() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404", "--output-format", "json"]), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'RUF404' for '[RULE]'
For more information, try '--help'.
");
}
#[test]
fn rule_invalid_rule_name_output_text() {
assert_cmd_snapshot!(ruff_cmd().args(["rule", "RUF404", "--output-format", "text"]), @r"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: invalid value 'RUF404' for '[RULE]'
For more information, try '--help'.
");
}
#[test]
fn show_statistics() {
let mut cmd = RuffCheck::default()

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:
@@ -7,7 +7,6 @@ info:
- "--no-cache"
- "--output-format"
- grouped
- "--preview"
- "--check"
- input.py
---

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:
@@ -7,7 +7,6 @@ info:
- "--no-cache"
- "--output-format"
- pylint
- "--preview"
- "--check"
- input.py
---

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/tests/cli/format.rs
source: crates/ruff/tests/format.rs
info:
program: ruff
args:

View File

@@ -1,30 +0,0 @@
---
source: crates/ruff/tests/integration_test.rs
info:
program: ruff
args:
- rule
- F401
- "--output-format"
- json
---
success: true
exit_code: 0
----- stdout -----
{
"name": "unused-import",
"code": "F401",
"linter": "Pyflakes",
"summary": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability",
"message_formats": [
"`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability",
"`{name}` imported but unused; consider removing, adding to `__all__`, or using a redundant alias",
"`{name}` imported but unused"
],
"fix": "Fix is sometimes available.",
"fix_availability": "Sometimes",
"explanation": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Preview\nWhen [preview] is enabled (and certain simplifying assumptions\nare met), we analyze all import statements for a given module\nwhen determining whether an import is used, rather than simply\nthe last of these statements. This can result in both different and\nmore import statements being marked as unused.\n\nFor example, if a module consists of\n\n```python\nimport a\nimport a.b\n```\n\nthen both statements are marked as unused under [preview], whereas\nonly the second is marked as unused under stable behavior.\n\nAs another example, if a module consists of\n\n```python\nimport a.b\nimport a\n\na.b.foo()\n```\n\nthen a diagnostic will only be emitted for the first line under [preview],\nwhereas a diagnostic would only be emitted for the second line under\nstable behavior.\n\nNote that this behavior is somewhat subjective and is designed\nto conform to the developer's intuition rather than Python's actual\nexecution. To wit, the statement `import a.b` automatically executes\n`import a`, so in some sense `import a` is _always_ redundant\nin the presence of `import a.b`.\n\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\nSee [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)\nfor more details on how Ruff\ndetermines whether an import is first or third-party.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)\n\n[preview]: https://docs.astral.sh/ruff/preview/\n",
"preview": false,
"status": "Stable"
}
----- stderr -----

View File

@@ -1,140 +0,0 @@
---
source: crates/ruff/tests/integration_test.rs
info:
program: ruff
args:
- rule
- F401
- "--output-format"
- text
---
success: true
exit_code: 0
----- stdout -----
# unused-import (F401)
Derived from the **Pyflakes** linter.
Fix is sometimes available.
## What it does
Checks for unused imports.
## Why is this bad?
Unused imports add a performance overhead at runtime, and risk creating
import cycles. They also increase the cognitive load of reading the code.
If an import statement is used to check for the availability or existence
of a module, consider using `importlib.util.find_spec` instead.
If an import statement is used to re-export a symbol as part of a module's
public interface, consider using a "redundant" import alias, which
instructs Ruff (and other tools) to respect the re-export, and avoid
marking it as unused, as in:
```python
from module import member as member
```
Alternatively, you can use `__all__` to declare a symbol as part of the module's
interface, as in:
```python
# __init__.py
import some_module
__all__ = ["some_module"]
```
## Preview
When [preview] is enabled (and certain simplifying assumptions
are met), we analyze all import statements for a given module
when determining whether an import is used, rather than simply
the last of these statements. This can result in both different and
more import statements being marked as unused.
For example, if a module consists of
```python
import a
import a.b
```
then both statements are marked as unused under [preview], whereas
only the second is marked as unused under stable behavior.
As another example, if a module consists of
```python
import a.b
import a
a.b.foo()
```
then a diagnostic will only be emitted for the first line under [preview],
whereas a diagnostic would only be emitted for the second line under
stable behavior.
Note that this behavior is somewhat subjective and is designed
to conform to the developer's intuition rather than Python's actual
execution. To wit, the statement `import a.b` automatically executes
`import a`, so in some sense `import a` is _always_ redundant
in the presence of `import a.b`.
## Fix safety
Fixes to remove unused imports are safe, except in `__init__.py` files.
Applying fixes to `__init__.py` files is currently in preview. The fix offered depends on the
type of the unused import. Ruff will suggest a safe fix to export first-party imports with
either a redundant alias or, if already present in the file, an `__all__` entry. If multiple
`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix
to remove third-party and standard library imports -- the fix is unsafe because the module's
interface changes.
See [this FAQ section](https://docs.astral.sh/ruff/faq/#how-does-ruff-determine-which-of-my-imports-are-first-party-third-party-etc)
for more details on how Ruff
determines whether an import is first or third-party.
## Example
```python
import numpy as np # unused import
def area(radius):
return 3.14 * radius**2
```
Use instead:
```python
def area(radius):
return 3.14 * radius**2
```
To check the availability of a module, use `importlib.util.find_spec`:
```python
from importlib.util import find_spec
if find_spec("numpy") is not None:
print("numpy is installed")
else:
print("numpy is not installed")
```
## Options
- `lint.ignore-init-module-imports`
- `lint.pyflakes.allowed-unused-imports`
## References
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)
- [Typing documentation: interface conventions](https://typing.python.org/en/latest/spec/distributing.html#library-interface-public-and-private-symbols)
[preview]: https://docs.astral.sh/ruff/preview/
----- stderr -----

View File

@@ -667,7 +667,7 @@ fn attrs(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
110,
100,
);
bench_project(&benchmark, criterion);
@@ -684,7 +684,7 @@ fn anyio(criterion: &mut Criterion) {
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY313,
},
150,
100,
);
bench_project(&benchmark, criterion);

View File

@@ -210,7 +210,7 @@ static TANJUN: Benchmark = Benchmark::new(
max_dep_date: "2025-06-17",
python_version: PythonVersion::PY312,
},
320,
100,
);
static STATIC_FRAME: Benchmark = Benchmark::new(
@@ -226,7 +226,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
max_dep_date: "2025-08-09",
python_version: PythonVersion::PY311,
},
750,
630,
);
#[track_caller]

View File

@@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::{Result, bail};
use pretty_assertions::StrComparison;
use schemars::generate::SchemaSettings;
use schemars::schema_for;
use crate::ROOT_DIR;
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
@@ -17,9 +17,7 @@ pub(crate) struct Args {
}
pub(crate) fn main(args: &Args) -> Result<()> {
let settings = SchemaSettings::draft07();
let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<Options>();
let schema = schema_for!(Options);
let schema_string = serde_json::to_string_pretty(&schema).unwrap();
let filename = "ruff.schema.json";
let schema_path = PathBuf::from(ROOT_DIR).join(filename);

View File

@@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::{Result, bail};
use pretty_assertions::StrComparison;
use schemars::generate::SchemaSettings;
use schemars::schema_for;
use crate::ROOT_DIR;
use crate::generate_all::{Mode, REGENERATE_ALL_COMMAND};
@@ -17,9 +17,7 @@ pub(crate) struct Args {
}
pub(crate) fn main(args: &Args) -> Result<()> {
let settings = SchemaSettings::draft07();
let generator = settings.into_generator();
let schema = generator.into_root_schema_for::<Options>();
let schema = schema_for!(Options);
let schema_string = serde_json::to_string_pretty(&schema).unwrap();
let filename = "ty.schema.json";
let schema_path = PathBuf::from(ROOT_DIR).join(filename);

View File

@@ -1,87 +0,0 @@
"""Test FAST002 ellipsis handling."""
from fastapi import Body, Cookie, FastAPI, Header, Query
app = FastAPI()
# Cases that should be fixed - ellipsis should be removed
@app.get("/test1")
async def test_ellipsis_query(
# This should become: param: Annotated[str, Query(description="Test param")]
param: str = Query(..., description="Test param"),
) -> str:
return param
@app.get("/test2")
async def test_ellipsis_header(
# This should become: auth: Annotated[str, Header(description="Auth header")]
auth: str = Header(..., description="Auth header"),
) -> str:
return auth
@app.post("/test3")
async def test_ellipsis_body(
# This should become: data: Annotated[dict, Body(description="Request body")]
data: dict = Body(..., description="Request body"),
) -> dict:
return data
@app.get("/test4")
async def test_ellipsis_cookie(
# This should become: session: Annotated[str, Cookie(description="Session ID")]
session: str = Cookie(..., description="Session ID"),
) -> str:
return session
@app.get("/test5")
async def test_simple_ellipsis(
# This should become: id: Annotated[str, Query()]
id: str = Query(...),
) -> str:
return id
@app.get("/test6")
async def test_multiple_kwargs_with_ellipsis(
# This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
param: str = Query(..., description="Test", min_length=1, max_length=10),
) -> str:
return param
# Cases with actual default values - these should preserve the default
@app.get("/test7")
async def test_with_default_value(
# This should become: param: Annotated[str, Query(description="Test")] = "default"
param: str = Query("default", description="Test"),
) -> str:
return param
@app.get("/test8")
async def test_with_default_none(
# This should become: param: Annotated[str | None, Query(description="Test")] = None
param: str | None = Query(None, description="Test"),
) -> str:
return param or "empty"
@app.get("/test9")
async def test_mixed_parameters(
# First param should be fixed with default preserved
optional_param: str = Query("default", description="Optional"),
# Second param should not be fixed because of the preceding default
required_param: str = Query(..., description="Required"),
# Third param should be fixed with default preserved
another_optional_param: int = Query(42, description="Another optional"),
) -> str:
return f"{required_param}-{optional_param}-{another_optional_param}"

View File

@@ -256,12 +256,3 @@ async def f4():
@app.get("/f5/{param: int}")
async def f5():
return locals()
# https://github.com/astral-sh/ruff/issues/20941
@app.get("/imports/{import}")
async def get_import():
...
@app.get("/debug/{__debug__}")
async def get_debug():
...

View File

@@ -1,10 +1,12 @@
from itertools import count, cycle, repeat
# Errors
zip()
zip(range(3))
zip("a", "b")
zip("a", "b", *zip("c"))
zip(zip("a", "b"), strict=False)
zip(zip("a", strict=True),"b")
zip(zip("a"), strict=False)
zip(zip("a", strict=True))
# OK
zip(range(3), strict=True)
@@ -25,10 +27,3 @@ zip([1, 2, 3], repeat(1, times=4))
import builtins
# Still an error even though it uses the qualified name
builtins.zip([1, 2, 3])
# Regression https://github.com/astral-sh/ruff/issues/20997
# Ok
zip()
zip(range(3))
# Error
zip(*lot_of_iterators)

View File

@@ -31,6 +31,3 @@ map(lambda x, y: x + y, [1, 2, 3], cycle([1, 2, 3]))
map(lambda x, y: x + y, [1, 2, 3], repeat(1))
map(lambda x, y: x + y, [1, 2, 3], repeat(1, times=None))
map(lambda x, y: x + y, [1, 2, 3], count())
# Regression https://github.com/astral-sh/ruff/issues/20997
map(f, *lots_of_iterators)

View File

@@ -2,52 +2,3 @@ _(f"{'value'}")
# Don't trigger for t-strings
_(t"{'value'}")
gettext(f"{'value'}")
ngettext(f"{'value'}", f"{'values'}", 2)
_gettext(f"{'value'}") # no lint
gettext_fn(f"{'value'}") # no lint
# https://github.com/astral-sh/ruff/issues/19028 - import aliases
import gettext as gettext_mod
from gettext import (
gettext as gettext_fn,
ngettext as ngettext_fn,
)
name = "Guido"
gettext_mod.gettext(f"Hello, {name}!")
gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2)
gettext_fn(f"Hello, {name}!")
ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2)
# https://github.com/astral-sh/ruff/issues/19028 - deferred translations
def _(message): return message
animals = [_(f"{'mollusk'}"),
_(f"{'albatross'}"),
_(f"{'rat'}"),
_(f"{'penguin'}"),
_(f"{'python'}"),]
del _
for a in animals:
print(_(a))
print(_(f"{a}"))
# https://github.com/astral-sh/ruff/issues/19028
# - installed translations/i18n functions dynamically assigned to builtins
import builtins
gettext_mod.install("domain")
builtins.__dict__["gettext"] = gettext_fn
builtins._(f"{'value'}")
builtins.gettext(f"{'value'}")
builtins.ngettext(f"{'value'}", f"{'values'}", 2)

View File

@@ -1,49 +1 @@
_("{}".format("line"))
gettext("{}".format("line"))
ngettext("{}".format("line"), "{}".format("lines"), 2)
_gettext("{}".format("line")) # no lint
gettext_fn("{}".format("line")) # no lint
# https://github.com/astral-sh/ruff/issues/19028
import gettext as gettext_mod
from gettext import (
gettext as gettext_fn,
ngettext as ngettext_fn,
)
name = "Guido"
gettext_mod.gettext("Hello, {}!".format(name))
gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
gettext_fn("Hello, {}!".format(name))
ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
# https://github.com/astral-sh/ruff/issues/19028 - deferred translations
def _(message): return message
animals = [_("{}".format("mollusk")),
_("{}".format("albatross")),
_("{}".format("rat")),
_("{}".format("penguin")),
_("{}".format("python")),]
del _
for a in animals:
print(_(a))
print(_("{}".format(a)))
# https://github.com/astral-sh/ruff/issues/19028
# - installed translations/i18n functions dynamically assigned to builtins
import builtins
gettext_mod.install("domain")
builtins.__dict__["gettext"] = gettext_fn
builtins._("{}".format("line"))
builtins.gettext("{}".format("line"))
builtins.ngettext("{}".format("line"), "{}".format("lines"), 2)

View File

@@ -1,49 +1 @@
_("%s" % "line")
gettext("%s" % "line")
ngettext("%s" % "line", "%s" % "lines", 2)
_gettext("%s" % "line") # no lint
gettext_fn("%s" % "line") # no lint
# https://github.com/astral-sh/ruff/issues/19028
import gettext as gettext_mod
from gettext import (
gettext as gettext_fn,
ngettext as ngettext_fn,
)
name = "Guido"
gettext_mod.gettext("Hello, %s!" % name)
gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2)
gettext_fn("Hello, %s!" % name)
ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2)
# https://github.com/astral-sh/ruff/issues/19028 - deferred translations
def _(message): return message
animals = [_("%s" %"mollusk"),
_("%s" %"albatross"),
_("%s" %"rat"),
_("%s" %"penguin"),
_("%s" %"python"),]
del _
for a in animals:
print(_(a))
print(_("%s" % a))
# https://github.com/astral-sh/ruff/issues/19028
# - installed translations/i18n functions dynamically assigned to builtins
import builtins
gettext_mod.install("domain")
builtins.__dict__["gettext"] = gettext_fn
builtins._("%s" % "line")
builtins.gettext("%s" % "line")
builtins.ngettext("%s" % "line", "%s" % "lines", 2)

View File

@@ -34,7 +34,3 @@ def foo():
# https://github.com/astral-sh/ruff/issues/18776
flag_stars = {}
for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):...
# Regression test for https://github.com/astral-sh/ruff/issues/18778
d = {}
for country, stars in zip(d.keys(*x), d.values("hello")):...

View File

@@ -269,10 +269,3 @@ raise ValueError(
# Not a valid type annotation but this test shouldn't result in a panic.
# Refer: https://github.com/astral-sh/ruff/issues/11736
x: "'{} + {}'.format(x, y)"
# Regression https://github.com/astral-sh/ruff/issues/21000
# Fix should parenthesize walrus
if __name__ == "__main__":
number = 0
string = "{}".format(number := number + 1)
print(string)

View File

@@ -26,8 +26,3 @@
"{.real}".format({1, 2})
"{.real}".format({1: 2, 3: 4})
"{}".format((i for i in range(2)))
# https://github.com/astral-sh/ruff/issues/21017
"{.real}".format(1_2)
"{0.real}".format(1_2)
"{a.real}".format(a=1_2)

View File

@@ -1034,7 +1034,6 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
Rule::FormatInGetTextFuncCall,
Rule::PrintfInGetTextFuncCall,
]) && flake8_gettext::is_gettext_func_call(
checker,
func,
&checker.settings().flake8_gettext.functions_names,
) {

View File

@@ -723,9 +723,7 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::IrrefutableCasePattern(_)
| SemanticSyntaxErrorKind::SingleStarredAssignment
| SemanticSyntaxErrorKind::WriteToDebug(_)
| SemanticSyntaxErrorKind::DifferentMatchPatternBindings
| SemanticSyntaxErrorKind::InvalidExpression(..)
| SemanticSyntaxErrorKind::GlobalParameter(_)
| SemanticSyntaxErrorKind::DuplicateMatchKey(_)
| SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_)
| SemanticSyntaxErrorKind::InvalidStarExpression
@@ -847,26 +845,6 @@ impl SemanticSyntaxContext for Checker<'_> {
}
false
}
fn is_bound_parameter(&self, name: &str) -> bool {
for scope in self.semantic.current_scopes() {
match scope.kind {
ScopeKind::Class(_) => return false,
ScopeKind::Function(ast::StmtFunctionDef { parameters, .. })
| ScopeKind::Lambda(ast::ExprLambda {
parameters: Some(parameters),
..
}) => return parameters.includes(name),
ScopeKind::Lambda(_)
| ScopeKind::Generator { .. }
| ScopeKind::Module
| ScopeKind::Type
| ScopeKind::DunderClassCell => {}
}
}
false
}
}
impl<'a> Visitor<'a> for Checker<'a> {

View File

@@ -130,7 +130,6 @@ pub(crate) fn check_noqa(
let edit = delete_comment(directive.range(), locator);
let mut diagnostic = context
.report_diagnostic(UnusedNOQA { codes: None }, directive.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
diagnostic.set_fix(Fix::safe_edit(edit));
}
}
@@ -227,7 +226,6 @@ pub(crate) fn check_noqa(
},
directive.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
diagnostic.set_fix(Fix::safe_edit(edit));
}
}

View File

@@ -5,7 +5,6 @@
use std::fmt::Formatter;
use ruff_db::diagnostic::SecondaryCode;
use serde::Serialize;
use strum_macros::EnumIter;
use crate::registry::Linter;
@@ -75,7 +74,7 @@ impl serde::Serialize for NoqaCode {
}
}
#[derive(Debug, Copy, Clone, Serialize)]
#[derive(Debug, Copy, Clone)]
pub enum RuleGroup {
/// The rule is stable.
Stable,

View File

@@ -1040,42 +1040,6 @@ mod tests {
PythonVersion::PY310,
"DuplicateMatchKey"
)]
#[test_case(
"global_parameter",
"
def f(a):
global a
def g(a):
if True:
global a
def h(a):
def inner():
global a
def i(a):
try:
global a
except Exception:
pass
def f(a):
a = 1
global a
def f(a):
a = 1
a = 2
global a
def f(a):
class Inner:
global a # ok
",
PythonVersion::PY310,
"GlobalParameter"
)]
#[test_case(
"duplicate_match_class_attribute",
"

View File

@@ -274,8 +274,3 @@ pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -
pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19045
pub(crate) const fn is_extended_i18n_function_matching_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -257,8 +257,9 @@ pub struct PreviewOptions {
#[cfg(feature = "schemars")]
mod schema {
use itertools::Itertools;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde_json::Value;
use schemars::_serde_json::Value;
use schemars::JsonSchema;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use strum::IntoEnumIterator;
use crate::RuleSelector;
@@ -266,65 +267,64 @@ mod schema {
use crate::rule_selector::{Linter, RuleCodePrefix};
impl JsonSchema for RuleSelector {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("RuleSelector")
fn schema_name() -> String {
"RuleSelector".to_string()
}
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
let enum_values: Vec<String> = [
// Include the non-standard "ALL" selectors.
"ALL".to_string(),
// Include the legacy "C" and "T" selectors.
"C".to_string(),
"T".to_string(),
// Include some common redirect targets for those legacy selectors.
"C9".to_string(),
"T1".to_string(),
"T2".to_string(),
]
.into_iter()
.chain(
RuleCodePrefix::iter()
.map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
format!("{prefix}{code}")
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(
[
// Include the non-standard "ALL" selectors.
"ALL".to_string(),
// Include the legacy "C" and "T" selectors.
"C".to_string(),
"T".to_string(),
// Include some common redirect targets for those legacy selectors.
"C9".to_string(),
"T1".to_string(),
"T2".to_string(),
]
.into_iter()
.chain(
RuleCodePrefix::iter()
.map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
format!("{prefix}{code}")
})
.chain(Linter::iter().filter_map(|l| {
let prefix = l.common_prefix();
(!prefix.is_empty()).then(|| prefix.to_string())
})),
)
.filter(|p| {
// Exclude any prefixes where all of the rules are removed
if let Ok(Self::Rule { prefix, .. } | Self::Prefix { prefix, .. }) =
RuleSelector::parse_no_redirect(p)
{
!prefix.rules().all(|rule| rule.is_removed())
} else {
true
}
})
.chain(Linter::iter().filter_map(|l| {
let prefix = l.common_prefix();
(!prefix.is_empty()).then(|| prefix.to_string())
})),
)
.filter(|p| {
// Exclude any prefixes where all of the rules are removed
if let Ok(Self::Rule { prefix, .. } | Self::Prefix { prefix, .. }) =
RuleSelector::parse_no_redirect(p)
{
!prefix.rules().all(|rule| rule.is_removed())
} else {
true
}
.filter(|_rule| {
// Filter out all test-only rules
#[cfg(any(feature = "test-rules", test))]
#[expect(clippy::used_underscore_binding)]
if _rule.starts_with("RUF9") || _rule == "PLW0101" {
return false;
}
true
})
.sorted()
.map(Value::String)
.collect(),
),
..SchemaObject::default()
})
.filter(|_rule| {
// Filter out all test-only rules
#[cfg(any(feature = "test-rules", test))]
#[expect(clippy::used_underscore_binding)]
if _rule.starts_with("RUF9") || _rule == "PLW0101" {
return false;
}
true
})
.sorted()
.collect();
let mut schema = schemars::json_schema!({ "type": "string" });
schema.ensure_object().insert(
"enum".to_string(),
Value::Array(enum_values.into_iter().map(Value::String).collect()),
);
schema
}
}
}

View File

@@ -18,7 +18,6 @@ mod tests {
#[test_case(Rule::FastApiRedundantResponseModel, Path::new("FAST001.py"))]
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_2.py"))]
#[test_case(Rule::FastApiUnusedPathParameter, Path::new("FAST003.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.name(), path.to_string_lossy());
@@ -57,7 +56,6 @@ mod tests {
// since `typing.Annotated` was added in Python 3.9
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_0.py"))]
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_1.py"))]
#[test_case(Rule::FastApiNonAnnotatedDependency, Path::new("FAST002_2.py"))]
fn rules_py38(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}_py38", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -275,42 +275,19 @@ fn create_diagnostic(
.collect::<Vec<_>>()
.join(", ");
// Check if the default argument is ellipsis (...), which in FastAPI means "required"
let is_default_argument_ellipsis = matches!(
dependency_call.default_argument.value(),
ast::Expr::EllipsisLiteral(_)
);
if is_default_argument_ellipsis && seen_default {
// For ellipsis after a parameter with default, can't remove the default
return Ok(None);
}
if !is_default_argument_ellipsis {
// For actual default values, mark that we've seen a default
seen_default = true;
}
let base_format = format!(
"{parameter_name}: {binding}[{annotation}, {default_}({kwarg_list})]",
seen_default = true;
format!(
"{parameter_name}: {binding}[{annotation}, {default_}({kwarg_list})] \
= {default_value}",
parameter_name = parameter.name,
annotation = checker.locator().slice(parameter.annotation.range()),
default_ = checker
.locator()
.slice(map_callable(parameter.default).range()),
);
if is_default_argument_ellipsis {
// For ellipsis, don't add a default value since the parameter
// should remain required after conversion to Annotated
base_format
} else {
// For actual default values, preserve them
let default_value = checker
default_value = checker
.locator()
.slice(dependency_call.default_argument.value().range());
format!("{base_format} = {default_value}")
}
.slice(dependency_call.default_argument.value().range()),
)
}
_ => {
if seen_default {

View File

@@ -7,7 +7,6 @@ use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast as ast;
use ruff_python_ast::{Arguments, Expr, ExprCall, ExprSubscript, Parameter, ParameterWithDefault};
use ruff_python_semantic::{BindingKind, Modules, ScopeKind, SemanticModel};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_text_size::{Ranged, TextSize};
use crate::Fix;
@@ -192,7 +191,7 @@ pub(crate) fn fastapi_unused_path_parameter(
.add_start(TextSize::from(range.start as u32 + 1))
.sub_end(TextSize::from((path.len() - range.end + 1) as u32)),
);
if !is_positional && is_identifier(path_param) && path_param != "__debug__" {
if !is_positional {
diagnostic.set_fix(Fix::unsafe_edit(add_parameter(
path_param,
&function_def.parameters,

View File

@@ -1,303 +0,0 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
---
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:14:5
|
12 | async def test_ellipsis_query(
13 | # This should become: param: Annotated[str, Query(description="Test param")]
14 | param: str = Query(..., description="Test param"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15 | ) -> str:
16 | return param
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
12 | @app.get("/test1")
13 | async def test_ellipsis_query(
14 | # This should become: param: Annotated[str, Query(description="Test param")]
- param: str = Query(..., description="Test param"),
15 + param: Annotated[str, Query(description="Test param")],
16 | ) -> str:
17 | return param
18 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:22:5
|
20 | async def test_ellipsis_header(
21 | # This should become: auth: Annotated[str, Header(description="Auth header")]
22 | auth: str = Header(..., description="Auth header"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | ) -> str:
24 | return auth
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
20 | @app.get("/test2")
21 | async def test_ellipsis_header(
22 | # This should become: auth: Annotated[str, Header(description="Auth header")]
- auth: str = Header(..., description="Auth header"),
23 + auth: Annotated[str, Header(description="Auth header")],
24 | ) -> str:
25 | return auth
26 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:30:5
|
28 | async def test_ellipsis_body(
29 | # This should become: data: Annotated[dict, Body(description="Request body")]
30 | data: dict = Body(..., description="Request body"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 | ) -> dict:
32 | return data
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
28 | @app.post("/test3")
29 | async def test_ellipsis_body(
30 | # This should become: data: Annotated[dict, Body(description="Request body")]
- data: dict = Body(..., description="Request body"),
31 + data: Annotated[dict, Body(description="Request body")],
32 | ) -> dict:
33 | return data
34 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:38:5
|
36 | async def test_ellipsis_cookie(
37 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
38 | session: str = Cookie(..., description="Session ID"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 | ) -> str:
40 | return session
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
36 | @app.get("/test4")
37 | async def test_ellipsis_cookie(
38 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
- session: str = Cookie(..., description="Session ID"),
39 + session: Annotated[str, Cookie(description="Session ID")],
40 | ) -> str:
41 | return session
42 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:46:5
|
44 | async def test_simple_ellipsis(
45 | # This should become: id: Annotated[str, Query()]
46 | id: str = Query(...),
| ^^^^^^^^^^^^^^^^^^^^
47 | ) -> str:
48 | return id
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
44 | @app.get("/test5")
45 | async def test_simple_ellipsis(
46 | # This should become: id: Annotated[str, Query()]
- id: str = Query(...),
47 + id: Annotated[str, Query()],
48 | ) -> str:
49 | return id
50 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:54:5
|
52 | async def test_multiple_kwargs_with_ellipsis(
53 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
54 | param: str = Query(..., description="Test", min_length=1, max_length=10),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
55 | ) -> str:
56 | return param
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
52 | @app.get("/test6")
53 | async def test_multiple_kwargs_with_ellipsis(
54 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
- param: str = Query(..., description="Test", min_length=1, max_length=10),
55 + param: Annotated[str, Query(description="Test", min_length=1, max_length=10)],
56 | ) -> str:
57 | return param
58 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:65:5
|
63 | async def test_with_default_value(
64 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
65 | param: str = Query("default", description="Test"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 | ) -> str:
67 | return param
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
63 | @app.get("/test7")
64 | async def test_with_default_value(
65 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
- param: str = Query("default", description="Test"),
66 + param: Annotated[str, Query(description="Test")] = "default",
67 | ) -> str:
68 | return param
69 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:73:5
|
71 | async def test_with_default_none(
72 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
73 | param: str | None = Query(None, description="Test"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74 | ) -> str:
75 | return param or "empty"
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
71 | @app.get("/test8")
72 | async def test_with_default_none(
73 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
- param: str | None = Query(None, description="Test"),
74 + param: Annotated[str | None, Query(description="Test")] = None,
75 | ) -> str:
76 | return param or "empty"
77 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:81:5
|
79 | async def test_mixed_parameters(
80 | # First param should be fixed with default preserved
81 | optional_param: str = Query("default", description="Optional"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | # Second param should not be fixed because of the preceding default
83 | required_param: str = Query(..., description="Required"),
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
79 | @app.get("/test9")
80 | async def test_mixed_parameters(
81 | # First param should be fixed with default preserved
- optional_param: str = Query("default", description="Optional"),
82 + optional_param: Annotated[str, Query(description="Optional")] = "default",
83 | # Second param should not be fixed because of the preceding default
84 | required_param: str = Query(..., description="Required"),
85 | # Third param should be fixed with default preserved
note: This is an unsafe fix and may change runtime behavior
FAST002 FastAPI dependency without `Annotated`
--> FAST002_2.py:83:5
|
81 | optional_param: str = Query("default", description="Optional"),
82 | # Second param should not be fixed because of the preceding default
83 | required_param: str = Query(..., description="Required"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
84 | # Third param should be fixed with default preserved
85 | another_optional_param: int = Query(42, description="Another optional"),
|
help: Replace with `typing.Annotated`
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:85:5
|
83 | required_param: str = Query(..., description="Required"),
84 | # Third param should be fixed with default preserved
85 | another_optional_param: int = Query(42, description="Another optional"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
86 | ) -> str:
87 | return f"{required_param}-{optional_param}-{another_optional_param}"
|
help: Replace with `typing.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
83 | # Second param should not be fixed because of the preceding default
84 | required_param: str = Query(..., description="Required"),
85 | # Third param should be fixed with default preserved
- another_optional_param: int = Query(42, description="Another optional"),
86 + another_optional_param: Annotated[int, Query(description="Another optional")] = 42,
87 | ) -> str:
88 | return f"{required_param}-{optional_param}-{another_optional_param}"
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,303 +0,0 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
---
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:14:5
|
12 | async def test_ellipsis_query(
13 | # This should become: param: Annotated[str, Query(description="Test param")]
14 | param: str = Query(..., description="Test param"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
15 | ) -> str:
16 | return param
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
12 | @app.get("/test1")
13 | async def test_ellipsis_query(
14 | # This should become: param: Annotated[str, Query(description="Test param")]
- param: str = Query(..., description="Test param"),
15 + param: Annotated[str, Query(description="Test param")],
16 | ) -> str:
17 | return param
18 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:22:5
|
20 | async def test_ellipsis_header(
21 | # This should become: auth: Annotated[str, Header(description="Auth header")]
22 | auth: str = Header(..., description="Auth header"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | ) -> str:
24 | return auth
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
20 | @app.get("/test2")
21 | async def test_ellipsis_header(
22 | # This should become: auth: Annotated[str, Header(description="Auth header")]
- auth: str = Header(..., description="Auth header"),
23 + auth: Annotated[str, Header(description="Auth header")],
24 | ) -> str:
25 | return auth
26 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:30:5
|
28 | async def test_ellipsis_body(
29 | # This should become: data: Annotated[dict, Body(description="Request body")]
30 | data: dict = Body(..., description="Request body"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31 | ) -> dict:
32 | return data
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
28 | @app.post("/test3")
29 | async def test_ellipsis_body(
30 | # This should become: data: Annotated[dict, Body(description="Request body")]
- data: dict = Body(..., description="Request body"),
31 + data: Annotated[dict, Body(description="Request body")],
32 | ) -> dict:
33 | return data
34 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:38:5
|
36 | async def test_ellipsis_cookie(
37 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
38 | session: str = Cookie(..., description="Session ID"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
39 | ) -> str:
40 | return session
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
36 | @app.get("/test4")
37 | async def test_ellipsis_cookie(
38 | # This should become: session: Annotated[str, Cookie(description="Session ID")]
- session: str = Cookie(..., description="Session ID"),
39 + session: Annotated[str, Cookie(description="Session ID")],
40 | ) -> str:
41 | return session
42 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:46:5
|
44 | async def test_simple_ellipsis(
45 | # This should become: id: Annotated[str, Query()]
46 | id: str = Query(...),
| ^^^^^^^^^^^^^^^^^^^^
47 | ) -> str:
48 | return id
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
44 | @app.get("/test5")
45 | async def test_simple_ellipsis(
46 | # This should become: id: Annotated[str, Query()]
- id: str = Query(...),
47 + id: Annotated[str, Query()],
48 | ) -> str:
49 | return id
50 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:54:5
|
52 | async def test_multiple_kwargs_with_ellipsis(
53 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
54 | param: str = Query(..., description="Test", min_length=1, max_length=10),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
55 | ) -> str:
56 | return param
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
52 | @app.get("/test6")
53 | async def test_multiple_kwargs_with_ellipsis(
54 | # This should become: param: Annotated[str, Query(description="Test", min_length=1, max_length=10)]
- param: str = Query(..., description="Test", min_length=1, max_length=10),
55 + param: Annotated[str, Query(description="Test", min_length=1, max_length=10)],
56 | ) -> str:
57 | return param
58 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:65:5
|
63 | async def test_with_default_value(
64 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
65 | param: str = Query("default", description="Test"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
66 | ) -> str:
67 | return param
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
63 | @app.get("/test7")
64 | async def test_with_default_value(
65 | # This should become: param: Annotated[str, Query(description="Test")] = "default"
- param: str = Query("default", description="Test"),
66 + param: Annotated[str, Query(description="Test")] = "default",
67 | ) -> str:
68 | return param
69 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:73:5
|
71 | async def test_with_default_none(
72 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
73 | param: str | None = Query(None, description="Test"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
74 | ) -> str:
75 | return param or "empty"
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
71 | @app.get("/test8")
72 | async def test_with_default_none(
73 | # This should become: param: Annotated[str | None, Query(description="Test")] = None
- param: str | None = Query(None, description="Test"),
74 + param: Annotated[str | None, Query(description="Test")] = None,
75 | ) -> str:
76 | return param or "empty"
77 |
note: This is an unsafe fix and may change runtime behavior
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:81:5
|
79 | async def test_mixed_parameters(
80 | # First param should be fixed with default preserved
81 | optional_param: str = Query("default", description="Optional"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
82 | # Second param should not be fixed because of the preceding default
83 | required_param: str = Query(..., description="Required"),
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
79 | @app.get("/test9")
80 | async def test_mixed_parameters(
81 | # First param should be fixed with default preserved
- optional_param: str = Query("default", description="Optional"),
82 + optional_param: Annotated[str, Query(description="Optional")] = "default",
83 | # Second param should not be fixed because of the preceding default
84 | required_param: str = Query(..., description="Required"),
85 | # Third param should be fixed with default preserved
note: This is an unsafe fix and may change runtime behavior
FAST002 FastAPI dependency without `Annotated`
--> FAST002_2.py:83:5
|
81 | optional_param: str = Query("default", description="Optional"),
82 | # Second param should not be fixed because of the preceding default
83 | required_param: str = Query(..., description="Required"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
84 | # Third param should be fixed with default preserved
85 | another_optional_param: int = Query(42, description="Another optional"),
|
help: Replace with `typing_extensions.Annotated`
FAST002 [*] FastAPI dependency without `Annotated`
--> FAST002_2.py:85:5
|
83 | required_param: str = Query(..., description="Required"),
84 | # Third param should be fixed with default preserved
85 | another_optional_param: int = Query(42, description="Another optional"),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
86 | ) -> str:
87 | return f"{required_param}-{optional_param}-{another_optional_param}"
|
help: Replace with `typing_extensions.Annotated`
1 | """Test FAST002 ellipsis handling."""
2 |
3 | from fastapi import Body, Cookie, FastAPI, Header, Query
4 + from typing_extensions import Annotated
5 |
6 | app = FastAPI()
7 |
--------------------------------------------------------------------------------
83 | # Second param should not be fixed because of the preceding default
84 | required_param: str = Query(..., description="Required"),
85 | # Third param should be fixed with default preserved
- another_optional_param: int = Query(42, description="Another optional"),
86 + another_optional_param: Annotated[int, Query(description="Another optional")] = 42,
87 | ) -> str:
88 | return f"{required_param}-{optional_param}-{another_optional_param}"
note: This is an unsafe fix and may change runtime behavior

View File

@@ -363,26 +363,3 @@ help: Add `id` to function signature
206 |
207 | # No errors
note: This is an unsafe fix and may change runtime behavior
FAST003 Parameter `import` appears in route path, but not in `get_import` signature
--> FAST003.py:261:20
|
260 | # https://github.com/astral-sh/ruff/issues/20941
261 | @app.get("/imports/{import}")
| ^^^^^^^^
262 | async def get_import():
263 | ...
|
help: Add `import` to function signature
FAST003 Parameter `__debug__` appears in route path, but not in `get_debug` signature
--> FAST003.py:265:18
|
263 | ...
264 |
265 | @app.get("/debug/{__debug__}")
| ^^^^^^^^^^^
266 | async def get_debug():
267 | ...
|
help: Add `__debug__` to function signature

View File

@@ -9,7 +9,7 @@ use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
/// Checks for `map` calls without an explicit `strict` parameter when called with two or more iterables, or any starred argument.
/// Checks for `map` calls without an explicit `strict` parameter when called with two or more iterables.
///
/// This rule applies to Python 3.14 and later, where `map` accepts a `strict` keyword
/// argument. For details, see: [Whats New in Python 3.14](https://docs.python.org/dev/whatsnew/3.14.html).
@@ -62,12 +62,7 @@ pub(crate) fn map_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
if semantic.match_builtin_expr(&call.func, "map")
&& call.arguments.find_keyword("strict").is_none()
&& (
// at least 2 iterables (+ 1 function)
call.arguments.args.len() >= 3
// or a starred argument
|| call.arguments.args.iter().any(ast::Expr::is_starred_expr)
)
&& call.arguments.args.len() >= 3 // function + at least 2 iterables
&& !any_infinite_iterables(call.arguments.args.iter().skip(1), semantic)
{
checker

View File

@@ -9,7 +9,7 @@ use crate::rules::flake8_bugbear::helpers::any_infinite_iterables;
use crate::{AlwaysFixableViolation, Applicability, Fix};
/// ## What it does
/// Checks for `zip` calls without an explicit `strict` parameter when called with two or more iterables, or any starred argument.
/// Checks for `zip` calls without an explicit `strict` parameter.
///
/// ## Why is this bad?
/// By default, if the iterables passed to `zip` are of different lengths, the
@@ -58,12 +58,6 @@ pub(crate) fn zip_without_explicit_strict(checker: &Checker, call: &ast::ExprCal
if semantic.match_builtin_expr(&call.func, "zip")
&& call.arguments.find_keyword("strict").is_none()
&& (
// at least 2 iterables
call.arguments.args.len() >= 2
// or a starred argument
|| call.arguments.args.iter().any(ast::Expr::is_starred_expr)
)
&& !any_infinite_iterables(call.arguments.args.iter(), semantic)
{
checker

View File

@@ -1,140 +1,204 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
assertion_line: 156
---
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:4:1
|
3 | # Errors
4 | zip("a", "b")
| ^^^^^^^^^^^^^
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
4 | zip()
| ^^^^^
5 | zip(range(3))
6 | zip("a", "b")
|
help: Add explicit value for parameter `strict=`
1 | from itertools import count, cycle, repeat
2 |
3 | # Errors
- zip("a", "b")
4 + zip("a", "b", strict=False)
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
- zip()
4 + zip(strict=False)
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:5:1
|
3 | # Errors
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
| ^^^^^^^^^^^^^^^^^^^^^^^^
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
4 | zip()
5 | zip(range(3))
| ^^^^^^^^^^^^^
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
|
help: Add explicit value for parameter `strict=`
2 |
3 | # Errors
4 | zip("a", "b")
- zip("a", "b", *zip("c"))
5 + zip("a", "b", *zip("c"), strict=False)
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
8 |
4 | zip()
- zip(range(3))
5 + zip(range(3), strict=False)
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:6:5
--> B905.py:6:1
|
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
| ^^^^^^^^^^^^^
7 | zip(zip("a", strict=True),"b")
4 | zip()
5 | zip(range(3))
6 | zip("a", "b")
| ^^^^^^^^^^^^^
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
|
help: Add explicit value for parameter `strict=`
3 | # Errors
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
- zip(zip("a", "b"), strict=False)
6 + zip(zip("a", "b", strict=False), strict=False)
7 | zip(zip("a", strict=True),"b")
8 |
9 | # OK
4 | zip()
5 | zip(range(3))
- zip("a", "b")
6 + zip("a", "b", strict=False)
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:7:1
|
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
7 | zip(zip("a", strict=True),"b")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
8 |
9 | # OK
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
| ^^^^^^^^^^^^^^^^^^^^^^^^
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
|
help: Add explicit value for parameter `strict=`
4 | zip("a", "b")
5 | zip("a", "b", *zip("c"))
6 | zip(zip("a", "b"), strict=False)
- zip(zip("a", strict=True),"b")
7 + zip(zip("a", strict=True),"b", strict=False)
8 |
9 | # OK
10 | zip(range(3), strict=True)
4 | zip()
5 | zip(range(3))
6 | zip("a", "b")
- zip("a", "b", *zip("c"))
7 + zip("a", "b", *zip("c"), strict=False)
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
10 |
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:22:1
--> B905.py:7:16
|
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
| ^^^^^^^^
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
|
help: Add explicit value for parameter `strict=`
4 | zip()
5 | zip(range(3))
6 | zip("a", "b")
- zip("a", "b", *zip("c"))
7 + zip("a", "b", *zip("c", strict=False))
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
10 |
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:8:5
|
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
| ^^^^^^^^
9 | zip(zip("a", strict=True))
|
help: Add explicit value for parameter `strict=`
5 | zip(range(3))
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
- zip(zip("a"), strict=False)
8 + zip(zip("a", strict=False), strict=False)
9 | zip(zip("a", strict=True))
10 |
11 | # OK
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:9:1
|
21 | # Errors (limited iterators).
22 | zip([1, 2, 3], repeat(1, 1))
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
10 |
11 | # OK
|
help: Add explicit value for parameter `strict=`
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
- zip(zip("a", strict=True))
9 + zip(zip("a", strict=True), strict=False)
10 |
11 | # OK
12 | zip(range(3), strict=True)
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:24:1
|
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23 | zip([1, 2, 3], repeat(1, times=4))
25 | zip([1, 2, 3], repeat(1, times=4))
|
help: Add explicit value for parameter `strict=`
19 | zip([1, 2, 3], repeat(1, times=None))
20 |
21 | # Errors (limited iterators).
21 | zip([1, 2, 3], repeat(1, times=None))
22 |
23 | # Errors (limited iterators).
- zip([1, 2, 3], repeat(1, 1))
22 + zip([1, 2, 3], repeat(1, 1), strict=False)
23 | zip([1, 2, 3], repeat(1, times=4))
24 |
25 | import builtins
24 + zip([1, 2, 3], repeat(1, 1), strict=False)
25 | zip([1, 2, 3], repeat(1, times=4))
26 |
27 | import builtins
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:23:1
--> B905.py:25:1
|
21 | # Errors (limited iterators).
22 | zip([1, 2, 3], repeat(1, 1))
23 | zip([1, 2, 3], repeat(1, times=4))
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
25 | zip([1, 2, 3], repeat(1, times=4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
24 |
25 | import builtins
26 |
27 | import builtins
|
help: Add explicit value for parameter `strict=`
20 |
21 | # Errors (limited iterators).
22 | zip([1, 2, 3], repeat(1, 1))
22 |
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
- zip([1, 2, 3], repeat(1, times=4))
23 + zip([1, 2, 3], repeat(1, times=4), strict=False)
24 |
25 | import builtins
26 | # Still an error even though it uses the qualified name
25 + zip([1, 2, 3], repeat(1, times=4), strict=False)
26 |
27 | import builtins
28 | # Still an error even though it uses the qualified name
note: This is an unsafe fix and may change runtime behavior
B905 [*] `zip()` without an explicit `strict=` parameter
--> B905.py:34:1
--> B905.py:29:1
|
32 | zip(range(3))
33 | # Error
34 | zip(*lot_of_iterators)
| ^^^^^^^^^^^^^^^^^^^^^^
27 | import builtins
28 | # Still an error even though it uses the qualified name
29 | builtins.zip([1, 2, 3])
| ^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add explicit value for parameter `strict=`
31 | zip()
32 | zip(range(3))
33 | # Error
- zip(*lot_of_iterators)
34 + zip(*lot_of_iterators, strict=False)
26 |
27 | import builtins
28 | # Still an error even though it uses the qualified name
- builtins.zip([1, 2, 3])
29 + builtins.zip([1, 2, 3], strict=False)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
assertion_line: 112
---
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:5:1
@@ -146,18 +147,3 @@ help: Add explicit value for parameter `strict=`
19 | # OK
20 | map(lambda x: x, [1, 2, 3], strict=True)
note: This is an unsafe fix and may change runtime behavior
B912 [*] `map()` without an explicit `strict=` parameter
--> B912.py:36:1
|
35 | # Regression https://github.com/astral-sh/ruff/issues/20997
36 | map(f, *lots_of_iterators)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Add explicit value for parameter `strict=`
33 | map(lambda x, y: x + y, [1, 2, 3], count())
34 |
35 | # Regression https://github.com/astral-sh/ruff/issues/20997
- map(f, *lots_of_iterators)
36 + map(f, *lots_of_iterators, strict=False)
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,50 +1,17 @@
//! Rules from [flake8-gettext](https://pypi.org/project/flake8-gettext/).
use crate::checkers::ast::Checker;
use crate::preview::is_extended_i18n_function_matching_enabled;
use ruff_python_ast::name::Name;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::Modules;
pub(crate) mod rules;
pub mod settings;
/// Returns true if the [`Expr`] is an internationalization function call.
pub(crate) fn is_gettext_func_call(
checker: &Checker,
func: &Expr,
functions_names: &[Name],
) -> bool {
if func
.as_name_expr()
.map(ast::ExprName::id)
.is_some_and(|id| functions_names.contains(id))
{
return true;
pub(crate) fn is_gettext_func_call(func: &Expr, functions_names: &[Name]) -> bool {
if let Expr::Name(ast::ExprName { id, .. }) = func {
functions_names.contains(id)
} else {
false
}
if !is_extended_i18n_function_matching_enabled(checker.settings()) {
return false;
}
let semantic = checker.semantic();
let Some(qualified_name) = semantic.resolve_qualified_name(func) else {
return false;
};
if semantic.seen_module(Modules::BUILTINS)
&& matches!(
qualified_name.segments(),
["builtins", id] if functions_names.contains(&Name::new(id)),
)
{
return true;
}
matches!(
qualified_name.segments(),
["gettext", "gettext" | "ngettext"]
)
}
#[cfg(test)]
@@ -55,7 +22,6 @@ mod tests {
use test_case::test_case;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::test::test_path;
use crate::{assert_diagnostics, settings};
@@ -71,20 +37,4 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::FStringInGetTextFuncCall, Path::new("INT001.py"))]
#[test_case(Rule::FormatInGetTextFuncCall, Path::new("INT002.py"))]
#[test_case(Rule::PrintfInGetTextFuncCall, Path::new("INT003.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("preview__{}_{}", rule_code.name(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_gettext").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -9,85 +9,3 @@ INT001 f-string is resolved before function call; consider `_("string %s") % arg
2 |
3 | # Don't trigger for t-strings
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:6:9
|
4 | _(t"{'value'}")
5 |
6 | gettext(f"{'value'}")
| ^^^^^^^^^^^^
7 | ngettext(f"{'value'}", f"{'values'}", 2)
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:7:10
|
6 | gettext(f"{'value'}")
7 | ngettext(f"{'value'}", f"{'values'}", 2)
| ^^^^^^^^^^^^
8 |
9 | _gettext(f"{'value'}") # no lint
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:31:14
|
29 | def _(message): return message
30 |
31 | animals = [_(f"{'mollusk'}"),
| ^^^^^^^^^^^^^^
32 | _(f"{'albatross'}"),
33 | _(f"{'rat'}"),
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:32:14
|
31 | animals = [_(f"{'mollusk'}"),
32 | _(f"{'albatross'}"),
| ^^^^^^^^^^^^^^^^
33 | _(f"{'rat'}"),
34 | _(f"{'penguin'}"),
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:33:14
|
31 | animals = [_(f"{'mollusk'}"),
32 | _(f"{'albatross'}"),
33 | _(f"{'rat'}"),
| ^^^^^^^^^^
34 | _(f"{'penguin'}"),
35 | _(f"{'python'}"),]
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:34:14
|
32 | _(f"{'albatross'}"),
33 | _(f"{'rat'}"),
34 | _(f"{'penguin'}"),
| ^^^^^^^^^^^^^^
35 | _(f"{'python'}"),]
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:35:14
|
33 | _(f"{'rat'}"),
34 | _(f"{'penguin'}"),
35 | _(f"{'python'}"),]
| ^^^^^^^^^^^^^
36 |
37 | del _
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:41:13
|
39 | for a in animals:
40 | print(_(a))
41 | print(_(f"{a}"))
| ^^^^^^
|

View File

@@ -6,88 +6,4 @@ INT002 `format` method argument is resolved before function call; consider `_("s
|
1 | _("{}".format("line"))
| ^^^^^^^^^^^^^^^^^^^
2 | gettext("{}".format("line"))
3 | ngettext("{}".format("line"), "{}".format("lines"), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:2:9
|
1 | _("{}".format("line"))
2 | gettext("{}".format("line"))
| ^^^^^^^^^^^^^^^^^^^
3 | ngettext("{}".format("line"), "{}".format("lines"), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:3:10
|
1 | _("{}".format("line"))
2 | gettext("{}".format("line"))
3 | ngettext("{}".format("line"), "{}".format("lines"), 2)
| ^^^^^^^^^^^^^^^^^^^
4 |
5 | _gettext("{}".format("line")) # no lint
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:27:14
|
25 | def _(message): return message
26 |
27 | animals = [_("{}".format("mollusk")),
| ^^^^^^^^^^^^^^^^^^^^^^
28 | _("{}".format("albatross")),
29 | _("{}".format("rat")),
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:28:14
|
27 | animals = [_("{}".format("mollusk")),
28 | _("{}".format("albatross")),
| ^^^^^^^^^^^^^^^^^^^^^^^^
29 | _("{}".format("rat")),
30 | _("{}".format("penguin")),
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:29:14
|
27 | animals = [_("{}".format("mollusk")),
28 | _("{}".format("albatross")),
29 | _("{}".format("rat")),
| ^^^^^^^^^^^^^^^^^^
30 | _("{}".format("penguin")),
31 | _("{}".format("python")),]
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:30:14
|
28 | _("{}".format("albatross")),
29 | _("{}".format("rat")),
30 | _("{}".format("penguin")),
| ^^^^^^^^^^^^^^^^^^^^^^
31 | _("{}".format("python")),]
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:31:14
|
29 | _("{}".format("rat")),
30 | _("{}".format("penguin")),
31 | _("{}".format("python")),]
| ^^^^^^^^^^^^^^^^^^^^^
32 |
33 | del _
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:37:13
|
35 | for a in animals:
36 | print(_(a))
37 | print(_("{}".format(a)))
| ^^^^^^^^^^^^^^
|

View File

@@ -1,162 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_gettext/mod.rs
---
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:1:3
|
1 | _(f"{'value'}")
| ^^^^^^^^^^^^
2 |
3 | # Don't trigger for t-strings
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:6:9
|
4 | _(t"{'value'}")
5 |
6 | gettext(f"{'value'}")
| ^^^^^^^^^^^^
7 | ngettext(f"{'value'}", f"{'values'}", 2)
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:7:10
|
6 | gettext(f"{'value'}")
7 | ngettext(f"{'value'}", f"{'values'}", 2)
| ^^^^^^^^^^^^
8 |
9 | _gettext(f"{'value'}") # no lint
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:22:21
|
20 | name = "Guido"
21 |
22 | gettext_mod.gettext(f"Hello, {name}!")
| ^^^^^^^^^^^^^^^^^
23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2)
24 | gettext_fn(f"Hello, {name}!")
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:23:22
|
22 | gettext_mod.gettext(f"Hello, {name}!")
23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2)
| ^^^^^^^^^^^^^^^^^
24 | gettext_fn(f"Hello, {name}!")
25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2)
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:24:12
|
22 | gettext_mod.gettext(f"Hello, {name}!")
23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2)
24 | gettext_fn(f"Hello, {name}!")
| ^^^^^^^^^^^^^^^^^
25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2)
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:25:13
|
23 | gettext_mod.ngettext(f"Hello, {name}!", f"Hello, {name}s!", 2)
24 | gettext_fn(f"Hello, {name}!")
25 | ngettext_fn(f"Hello, {name}!", f"Hello, {name}s!", 2)
| ^^^^^^^^^^^^^^^^^
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:31:14
|
29 | def _(message): return message
30 |
31 | animals = [_(f"{'mollusk'}"),
| ^^^^^^^^^^^^^^
32 | _(f"{'albatross'}"),
33 | _(f"{'rat'}"),
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:32:14
|
31 | animals = [_(f"{'mollusk'}"),
32 | _(f"{'albatross'}"),
| ^^^^^^^^^^^^^^^^
33 | _(f"{'rat'}"),
34 | _(f"{'penguin'}"),
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:33:14
|
31 | animals = [_(f"{'mollusk'}"),
32 | _(f"{'albatross'}"),
33 | _(f"{'rat'}"),
| ^^^^^^^^^^
34 | _(f"{'penguin'}"),
35 | _(f"{'python'}"),]
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:34:14
|
32 | _(f"{'albatross'}"),
33 | _(f"{'rat'}"),
34 | _(f"{'penguin'}"),
| ^^^^^^^^^^^^^^
35 | _(f"{'python'}"),]
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:35:14
|
33 | _(f"{'rat'}"),
34 | _(f"{'penguin'}"),
35 | _(f"{'python'}"),]
| ^^^^^^^^^^^^^
36 |
37 | del _
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:41:13
|
39 | for a in animals:
40 | print(_(a))
41 | print(_(f"{a}"))
| ^^^^^^
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:51:12
|
49 | builtins.__dict__["gettext"] = gettext_fn
50 |
51 | builtins._(f"{'value'}")
| ^^^^^^^^^^^^
52 | builtins.gettext(f"{'value'}")
53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2)
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:52:18
|
51 | builtins._(f"{'value'}")
52 | builtins.gettext(f"{'value'}")
| ^^^^^^^^^^^^
53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2)
|
INT001 f-string is resolved before function call; consider `_("string %s") % arg`
--> INT001.py:53:19
|
51 | builtins._(f"{'value'}")
52 | builtins.gettext(f"{'value'}")
53 | builtins.ngettext(f"{'value'}", f"{'values'}", 2)
| ^^^^^^^^^^^^
|

View File

@@ -1,162 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_gettext/mod.rs
---
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:1:3
|
1 | _("{}".format("line"))
| ^^^^^^^^^^^^^^^^^^^
2 | gettext("{}".format("line"))
3 | ngettext("{}".format("line"), "{}".format("lines"), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:2:9
|
1 | _("{}".format("line"))
2 | gettext("{}".format("line"))
| ^^^^^^^^^^^^^^^^^^^
3 | ngettext("{}".format("line"), "{}".format("lines"), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:3:10
|
1 | _("{}".format("line"))
2 | gettext("{}".format("line"))
3 | ngettext("{}".format("line"), "{}".format("lines"), 2)
| ^^^^^^^^^^^^^^^^^^^
4 |
5 | _gettext("{}".format("line")) # no lint
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:18:21
|
16 | name = "Guido"
17 |
18 | gettext_mod.gettext("Hello, {}!".format(name))
| ^^^^^^^^^^^^^^^^^^^^^^^^^
19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
20 | gettext_fn("Hello, {}!".format(name))
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:19:22
|
18 | gettext_mod.gettext("Hello, {}!".format(name))
19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^
20 | gettext_fn("Hello, {}!".format(name))
21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:20:12
|
18 | gettext_mod.gettext("Hello, {}!".format(name))
19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
20 | gettext_fn("Hello, {}!".format(name))
| ^^^^^^^^^^^^^^^^^^^^^^^^^
21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:21:13
|
19 | gettext_mod.ngettext("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
20 | gettext_fn("Hello, {}!".format(name))
21 | ngettext_fn("Hello, {}!".format(name), "Hello, {}s!".format(name), 2)
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:27:14
|
25 | def _(message): return message
26 |
27 | animals = [_("{}".format("mollusk")),
| ^^^^^^^^^^^^^^^^^^^^^^
28 | _("{}".format("albatross")),
29 | _("{}".format("rat")),
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:28:14
|
27 | animals = [_("{}".format("mollusk")),
28 | _("{}".format("albatross")),
| ^^^^^^^^^^^^^^^^^^^^^^^^
29 | _("{}".format("rat")),
30 | _("{}".format("penguin")),
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:29:14
|
27 | animals = [_("{}".format("mollusk")),
28 | _("{}".format("albatross")),
29 | _("{}".format("rat")),
| ^^^^^^^^^^^^^^^^^^
30 | _("{}".format("penguin")),
31 | _("{}".format("python")),]
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:30:14
|
28 | _("{}".format("albatross")),
29 | _("{}".format("rat")),
30 | _("{}".format("penguin")),
| ^^^^^^^^^^^^^^^^^^^^^^
31 | _("{}".format("python")),]
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:31:14
|
29 | _("{}".format("rat")),
30 | _("{}".format("penguin")),
31 | _("{}".format("python")),]
| ^^^^^^^^^^^^^^^^^^^^^
32 |
33 | del _
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:37:13
|
35 | for a in animals:
36 | print(_(a))
37 | print(_("{}".format(a)))
| ^^^^^^^^^^^^^^
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:47:12
|
45 | builtins.__dict__["gettext"] = gettext_fn
46 |
47 | builtins._("{}".format("line"))
| ^^^^^^^^^^^^^^^^^^^
48 | builtins.gettext("{}".format("line"))
49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:48:18
|
47 | builtins._("{}".format("line"))
48 | builtins.gettext("{}".format("line"))
| ^^^^^^^^^^^^^^^^^^^
49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2)
|
INT002 `format` method argument is resolved before function call; consider `_("string %s") % arg`
--> INT002.py:49:19
|
47 | builtins._("{}".format("line"))
48 | builtins.gettext("{}".format("line"))
49 | builtins.ngettext("{}".format("line"), "{}".format("lines"), 2)
| ^^^^^^^^^^^^^^^^^^^
|

View File

@@ -1,162 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_gettext/mod.rs
---
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:1:3
|
1 | _("%s" % "line")
| ^^^^^^^^^^^^^
2 | gettext("%s" % "line")
3 | ngettext("%s" % "line", "%s" % "lines", 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:2:9
|
1 | _("%s" % "line")
2 | gettext("%s" % "line")
| ^^^^^^^^^^^^^
3 | ngettext("%s" % "line", "%s" % "lines", 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:3:10
|
1 | _("%s" % "line")
2 | gettext("%s" % "line")
3 | ngettext("%s" % "line", "%s" % "lines", 2)
| ^^^^^^^^^^^^^
4 |
5 | _gettext("%s" % "line") # no lint
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:18:21
|
16 | name = "Guido"
17 |
18 | gettext_mod.gettext("Hello, %s!" % name)
| ^^^^^^^^^^^^^^^^^^^
19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2)
20 | gettext_fn("Hello, %s!" % name)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:19:22
|
18 | gettext_mod.gettext("Hello, %s!" % name)
19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2)
| ^^^^^^^^^^^^^^^^^^^
20 | gettext_fn("Hello, %s!" % name)
21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:20:12
|
18 | gettext_mod.gettext("Hello, %s!" % name)
19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2)
20 | gettext_fn("Hello, %s!" % name)
| ^^^^^^^^^^^^^^^^^^^
21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:21:13
|
19 | gettext_mod.ngettext("Hello, %s!" % name, "Hello, %ss!" % name, 2)
20 | gettext_fn("Hello, %s!" % name)
21 | ngettext_fn("Hello, %s!" % name, "Hello, %ss!" % name, 2)
| ^^^^^^^^^^^^^^^^^^^
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:27:14
|
25 | def _(message): return message
26 |
27 | animals = [_("%s" %"mollusk"),
| ^^^^^^^^^^^^^^^
28 | _("%s" %"albatross"),
29 | _("%s" %"rat"),
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:28:14
|
27 | animals = [_("%s" %"mollusk"),
28 | _("%s" %"albatross"),
| ^^^^^^^^^^^^^^^^^
29 | _("%s" %"rat"),
30 | _("%s" %"penguin"),
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:29:14
|
27 | animals = [_("%s" %"mollusk"),
28 | _("%s" %"albatross"),
29 | _("%s" %"rat"),
| ^^^^^^^^^^^
30 | _("%s" %"penguin"),
31 | _("%s" %"python"),]
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:30:14
|
28 | _("%s" %"albatross"),
29 | _("%s" %"rat"),
30 | _("%s" %"penguin"),
| ^^^^^^^^^^^^^^^
31 | _("%s" %"python"),]
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:31:14
|
29 | _("%s" %"rat"),
30 | _("%s" %"penguin"),
31 | _("%s" %"python"),]
| ^^^^^^^^^^^^^^
32 |
33 | del _
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:37:13
|
35 | for a in animals:
36 | print(_(a))
37 | print(_("%s" % a))
| ^^^^^^^^
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:47:12
|
45 | builtins.__dict__["gettext"] = gettext_fn
46 |
47 | builtins._("%s" % "line")
| ^^^^^^^^^^^^^
48 | builtins.gettext("%s" % "line")
49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:48:18
|
47 | builtins._("%s" % "line")
48 | builtins.gettext("%s" % "line")
| ^^^^^^^^^^^^^
49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:49:19
|
47 | builtins._("%s" % "line")
48 | builtins.gettext("%s" % "line")
49 | builtins.ngettext("%s" % "line", "%s" % "lines", 2)
| ^^^^^^^^^^^^^
|

View File

@@ -6,88 +6,4 @@ INT003 printf-style format is resolved before function call; consider `_("string
|
1 | _("%s" % "line")
| ^^^^^^^^^^^^^
2 | gettext("%s" % "line")
3 | ngettext("%s" % "line", "%s" % "lines", 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:2:9
|
1 | _("%s" % "line")
2 | gettext("%s" % "line")
| ^^^^^^^^^^^^^
3 | ngettext("%s" % "line", "%s" % "lines", 2)
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:3:10
|
1 | _("%s" % "line")
2 | gettext("%s" % "line")
3 | ngettext("%s" % "line", "%s" % "lines", 2)
| ^^^^^^^^^^^^^
4 |
5 | _gettext("%s" % "line") # no lint
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:27:14
|
25 | def _(message): return message
26 |
27 | animals = [_("%s" %"mollusk"),
| ^^^^^^^^^^^^^^^
28 | _("%s" %"albatross"),
29 | _("%s" %"rat"),
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:28:14
|
27 | animals = [_("%s" %"mollusk"),
28 | _("%s" %"albatross"),
| ^^^^^^^^^^^^^^^^^
29 | _("%s" %"rat"),
30 | _("%s" %"penguin"),
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:29:14
|
27 | animals = [_("%s" %"mollusk"),
28 | _("%s" %"albatross"),
29 | _("%s" %"rat"),
| ^^^^^^^^^^^
30 | _("%s" %"penguin"),
31 | _("%s" %"python"),]
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:30:14
|
28 | _("%s" %"albatross"),
29 | _("%s" %"rat"),
30 | _("%s" %"penguin"),
| ^^^^^^^^^^^^^^^
31 | _("%s" %"python"),]
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:31:14
|
29 | _("%s" %"rat"),
30 | _("%s" %"penguin"),
31 | _("%s" %"python"),]
| ^^^^^^^^^^^^^^
32 |
33 | del _
|
INT003 printf-style format is resolved before function call; consider `_("string %s") % arg`
--> INT003.py:37:13
|
35 | for a in animals:
36 | print(_(a))
37 | print(_("%s" % a))
| ^^^^^^^^
|

View File

@@ -78,18 +78,13 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall)
let [arg1, arg2] = &args[..] else {
return;
};
let Some((var1, attr1, args1)) = get_var_attr_args(arg1) else {
let Some((var1, attr1)) = get_var_attr(arg1) else {
return;
};
let Some((var2, attr2, args2)) = get_var_attr_args(arg2) else {
let Some((var2, attr2)) = get_var_attr(arg2) else {
return;
};
if var1.id != var2.id
|| attr1 != "keys"
|| attr2 != "values"
|| !args1.is_empty()
|| !args2.is_empty()
{
if var1.id != var2.id || attr1 != "keys" || attr2 != "values" {
return;
}
if !checker.semantic().match_builtin_expr(func, "zip") {
@@ -127,11 +122,8 @@ pub(crate) fn zip_dict_keys_and_values(checker: &Checker, expr: &ast::ExprCall)
)));
}
fn get_var_attr_args(expr: &Expr) -> Option<(&ExprName, &Identifier, &Arguments)> {
let Expr::Call(ast::ExprCall {
func, arguments, ..
}) = expr
else {
fn get_var_attr(expr: &Expr) -> Option<(&ExprName, &Identifier)> {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return None;
};
let Expr::Attribute(ExprAttribute { value, attr, .. }) = func.as_ref() else {
@@ -140,5 +132,5 @@ fn get_var_attr_args(expr: &Expr) -> Option<(&ExprName, &Identifier, &Arguments)
let Expr::Name(var_name) = value.as_ref() else {
return None;
};
Some((var_name, attr, arguments))
Some((var_name, attr))
}

View File

@@ -81,8 +81,6 @@ SIM911 [*] Use ` flag_stars.items()` instead of `(zip)(flag_stars.keys(), flag_s
35 | flag_stars = {}
36 | for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):...
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
37 |
38 | # Regression test for https://github.com/astral-sh/ruff/issues/18778
|
help: Replace `(zip)(flag_stars.keys(), flag_stars.values())` with ` flag_stars.items()`
33 |
@@ -90,6 +88,3 @@ help: Replace `(zip)(flag_stars.keys(), flag_stars.values())` with ` flag_stars.
35 | flag_stars = {}
- for country, stars in(zip)(flag_stars.keys(), flag_stars.values()):...
36 + for country, stars in flag_stars.items():...
37 |
38 | # Regression test for https://github.com/astral-sh/ruff/issues/18778
39 | d = {}

View File

@@ -165,17 +165,16 @@ pub(crate) fn subscript(checker: &Checker, value: &Expr, expr: &Expr) {
match attr.as_str() {
// PD007
"ix" if checker.is_rule_enabled(Rule::PandasUseOfDotIx) => {
let mut diagnostic = checker.report_diagnostic(PandasUseOfDotIx, range);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
checker.report_diagnostic(PandasUseOfDotIx, range)
}
// PD008
"at" if checker.is_rule_enabled(Rule::PandasUseOfDotAt) => {
checker.report_diagnostic(PandasUseOfDotAt, range);
checker.report_diagnostic(PandasUseOfDotAt, range)
}
// PD009
"iat" if checker.is_rule_enabled(Rule::PandasUseOfDotIat) => {
checker.report_diagnostic(PandasUseOfDotIat, range);
checker.report_diagnostic(PandasUseOfDotIat, range)
}
_ => (),
}
_ => return,
};
}

View File

@@ -191,7 +191,6 @@ pub(crate) fn redefined_while_unused(checker: &Checker, scope_id: ScopeId, scope
},
binding.range(),
);
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Unnecessary);
diagnostic.secondary_annotation(
format_args!("previous definition of `{name}` here"),

View File

@@ -145,6 +145,7 @@ fn parenthesize(expr: &Expr, text: &str, context: FormatContext) -> bool {
Expr::BinOp(_)
| Expr::UnaryOp(_)
| Expr::BoolOp(_)
| Expr::Named(_)
| Expr::Compare(_)
| Expr::If(_)
| Expr::Lambda(_)
@@ -160,17 +161,12 @@ fn parenthesize(expr: &Expr, text: &str, context: FormatContext) -> bool {
value: ast::Number::Int(..),
..
}),
) => text
.chars()
// Ignore digit separators so decimal literals like `1_2` still count as pure digits.
.filter(|c| *c != '_')
.all(|c| c.is_ascii_digit()),
) => text.chars().all(|c| c.is_ascii_digit()),
// E.g., `{x, y}` should be parenthesized in `f"{(x, y)}"`.
(
_,
Expr::Generator(_)
| Expr::Dict(_)
| Expr::Named(_)
| Expr::Set(_)
| Expr::SetComp(_)
| Expr::DictComp(_),

View File

@@ -1320,8 +1320,6 @@ UP032 [*] Use f-string instead of `format` call
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
271 | x: "'{} + {}'.format(x, y)"
| ^^^^^^^^^^^^^^^^^^^^^^
272 |
273 | # Regression https://github.com/astral-sh/ruff/issues/21000
|
help: Convert to f-string
268 |
@@ -1329,23 +1327,3 @@ help: Convert to f-string
270 | # Refer: https://github.com/astral-sh/ruff/issues/11736
- x: "'{} + {}'.format(x, y)"
271 + x: "f'{x} + {y}'"
272 |
273 | # Regression https://github.com/astral-sh/ruff/issues/21000
274 | # Fix should parenthesize walrus
UP032 [*] Use f-string instead of `format` call
--> UP032_0.py:277:14
|
275 | if __name__ == "__main__":
276 | number = 0
277 | string = "{}".format(number := number + 1)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
278 | print(string)
|
help: Convert to f-string
274 | # Fix should parenthesize walrus
275 | if __name__ == "__main__":
276 | number = 0
- string = "{}".format(number := number + 1)
277 + string = f"{(number := number + 1)}"
278 | print(string)

View File

@@ -385,7 +385,6 @@ help: Convert to f-string
26 + f"{({1, 2}).real}"
27 | "{.real}".format({1: 2, 3: 4})
28 | "{}".format((i for i in range(2)))
29 |
UP032 [*] Use f-string instead of `format` call
--> UP032_2.py:27:1
@@ -403,8 +402,6 @@ help: Convert to f-string
- "{.real}".format({1: 2, 3: 4})
27 + f"{({1: 2, 3: 4}).real}"
28 | "{}".format((i for i in range(2)))
29 |
30 | # https://github.com/astral-sh/ruff/issues/21017
UP032 [*] Use f-string instead of `format` call
--> UP032_2.py:28:1
@@ -413,8 +410,6 @@ UP032 [*] Use f-string instead of `format` call
27 | "{.real}".format({1: 2, 3: 4})
28 | "{}".format((i for i in range(2)))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
29 |
30 | # https://github.com/astral-sh/ruff/issues/21017
|
help: Convert to f-string
25 | "{.real}".format([1, 2])
@@ -422,56 +417,3 @@ help: Convert to f-string
27 | "{.real}".format({1: 2, 3: 4})
- "{}".format((i for i in range(2)))
28 + f"{(i for i in range(2))}"
29 |
30 | # https://github.com/astral-sh/ruff/issues/21017
31 | "{.real}".format(1_2)
UP032 [*] Use f-string instead of `format` call
--> UP032_2.py:31:1
|
30 | # https://github.com/astral-sh/ruff/issues/21017
31 | "{.real}".format(1_2)
| ^^^^^^^^^^^^^^^^^^^^^
32 | "{0.real}".format(1_2)
33 | "{a.real}".format(a=1_2)
|
help: Convert to f-string
28 | "{}".format((i for i in range(2)))
29 |
30 | # https://github.com/astral-sh/ruff/issues/21017
- "{.real}".format(1_2)
31 + f"{(1_2).real}"
32 | "{0.real}".format(1_2)
33 | "{a.real}".format(a=1_2)
UP032 [*] Use f-string instead of `format` call
--> UP032_2.py:32:1
|
30 | # https://github.com/astral-sh/ruff/issues/21017
31 | "{.real}".format(1_2)
32 | "{0.real}".format(1_2)
| ^^^^^^^^^^^^^^^^^^^^^^
33 | "{a.real}".format(a=1_2)
|
help: Convert to f-string
29 |
30 | # https://github.com/astral-sh/ruff/issues/21017
31 | "{.real}".format(1_2)
- "{0.real}".format(1_2)
32 + f"{(1_2).real}"
33 | "{a.real}".format(a=1_2)
UP032 [*] Use f-string instead of `format` call
--> UP032_2.py:33:1
|
31 | "{.real}".format(1_2)
32 | "{0.real}".format(1_2)
33 | "{a.real}".format(a=1_2)
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Convert to f-string
30 | # https://github.com/astral-sh/ruff/issues/21017
31 | "{.real}".format(1_2)
32 | "{0.real}".format(1_2)
- "{a.real}".format(a=1_2)
33 + f"{(1_2).real}"

View File

@@ -617,12 +617,12 @@ impl TryFrom<String> for RequiredVersion {
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for RequiredVersion {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("RequiredVersion")
fn schema_name() -> String {
"RequiredVersion".to_string()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
<String as schemars::JsonSchema>::json_schema(generator)
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
generator.subschema_for::<String>()
}
}

View File

@@ -1,56 +0,0 @@
---
source: crates/ruff_linter/src/linter.rs
---
invalid-syntax: name `a` is parameter and global
--> <filename>:3:12
|
2 | def f(a):
3 | global a
| ^
4 |
5 | def g(a):
|
invalid-syntax: name `a` is parameter and global
--> <filename>:7:16
|
5 | def g(a):
6 | if True:
7 | global a
| ^
8 |
9 | def h(a):
|
invalid-syntax: name `a` is parameter and global
--> <filename>:15:16
|
13 | def i(a):
14 | try:
15 | global a
| ^
16 | except Exception:
17 | pass
|
invalid-syntax: name `a` is parameter and global
--> <filename>:21:12
|
19 | def f(a):
20 | a = 1
21 | global a
| ^
22 |
23 | def f(a):
|
invalid-syntax: name `a` is parameter and global
--> <filename>:26:12
|
24 | a = 1
25 | a = 2
26 | global a
| ^
27 |
28 | def f(a):
|

View File

@@ -1,14 +1,12 @@
use std::fmt::{Debug, Display};
use serde::Serialize;
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::SourceFile;
use ruff_text_size::TextRange;
use crate::{codes::Rule, message::create_lint_diagnostic};
#[derive(Debug, Copy, Clone, Serialize)]
#[derive(Debug, Copy, Clone)]
pub enum FixAvailability {
Sometimes,
Always,

View File

@@ -30,11 +30,10 @@ rustc-hash = { workspace = true }
salsa = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
thiserror = { workspace = true }
[features]
schemars = ["dep:schemars", "dep:serde_json"]
schemars = ["dep:schemars"]
cache = ["dep:ruff_cache", "dep:ruff_macros"]
serde = [
"dep:serde",

View File

@@ -559,73 +559,23 @@ InterpolatedStringLiteralElement = { variant = "Literal" }
[Pattern]
doc = "See also [pattern](https://docs.python.org/3/library/ast.html#ast.pattern)"
[Pattern.nodes.PatternMatchValue]
doc = "See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)"
fields = [{ name = "value", type = "Box<Expr>" }]
[Pattern.nodes.PatternMatchSingleton]
doc = "See also [MatchSingleton](https://docs.python.org/3/library/ast.html#ast.MatchSingleton)"
fields = [{ name = "value", type = "Singleton" }]
[Pattern.nodes.PatternMatchSequence]
doc = "See also [MatchSequence](https://docs.python.org/3/library/ast.html#ast.MatchSequence)"
fields = [{ name = "patterns", type = "Pattern*" }]
[Pattern.nodes.PatternMatchMapping]
doc = "See also [MatchMapping](https://docs.python.org/3/library/ast.html#ast.MatchMapping)"
fields = [
{ name = "keys", type = "Expr*" },
{ name = "patterns", type = "Pattern*" },
{ name = "rest", type = "Identifier?" },
]
custom_source_order = true
[Pattern.nodes.PatternMatchClass]
doc = "See also [MatchClass](https://docs.python.org/3/library/ast.html#ast.MatchClass)"
fields = [
{ name = "cls", type = "Box<Expr>" },
{ name = "arguments", type = "PatternArguments" },
]
[Pattern.nodes.PatternMatchStar]
doc = "See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar)"
fields = [{ name = "name", type = "Identifier?" }]
[Pattern.nodes.PatternMatchAs]
doc = "See also [MatchAs](https://docs.python.org/3/library/ast.html#ast.MatchAs)"
fields = [
{ name = "pattern", type = "Box<Pattern>?" },
{ name = "name", type = "Identifier?" },
]
[Pattern.nodes.PatternMatchOr]
doc = "See also [MatchOr](https://docs.python.org/3/library/ast.html#ast.MatchOr)"
fields = [{ name = "patterns", type = "Pattern*" }]
[Pattern.nodes]
PatternMatchValue = {}
PatternMatchSingleton = {}
PatternMatchSequence = {}
PatternMatchMapping = {}
PatternMatchClass = {}
PatternMatchStar = {}
PatternMatchAs = {}
PatternMatchOr = {}
[TypeParam]
doc = "See also [type_param](https://docs.python.org/3/library/ast.html#ast.type_param)"
[TypeParam.nodes.TypeParamTypeVar]
doc = "See also [TypeVar](https://docs.python.org/3/library/ast.html#ast.TypeVar)"
fields = [
{ name = "name", type = "Identifier" },
{ name = "bound", type = "Box<Expr>?" },
{ name = "default", type = "Box<Expr>?" },
]
[TypeParam.nodes.TypeParamTypeVarTuple]
doc = "See also [TypeVarTuple](https://docs.python.org/3/library/ast.html#ast.TypeVarTuple)"
fields = [
{ name = "name", type = "Identifier" },
{ name = "default", type = "Box<Expr>?" },
]
[TypeParam.nodes.TypeParamParamSpec]
doc = "See also [ParamSpec](https://docs.python.org/3/library/ast.html#ast.ParamSpec)"
fields = [
{ name = "name", type = "Identifier" },
{ name = "default", type = "Box<Expr>?" },
]
[TypeParam.nodes]
TypeParamTypeVar = {}
TypeParamTypeVarTuple = {}
TypeParamParamSpec = {}
[ungrouped.nodes]
InterpolatedStringFormatSpec = {}

View File

@@ -38,8 +38,6 @@ types_requiring_crate_prefix = {
"WithItem",
"MatchCase",
"Alias",
"Singleton",
"PatternArguments",
}

View File

@@ -9637,113 +9637,6 @@ pub struct ExprIpyEscapeCommand {
pub value: Box<str>,
}
/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchValue {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub value: Box<Expr>,
}
/// See also [MatchSingleton](https://docs.python.org/3/library/ast.html#ast.MatchSingleton)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchSingleton {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub value: crate::Singleton,
}
/// See also [MatchSequence](https://docs.python.org/3/library/ast.html#ast.MatchSequence)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchSequence {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub patterns: Vec<Pattern>,
}
/// See also [MatchMapping](https://docs.python.org/3/library/ast.html#ast.MatchMapping)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchMapping {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub keys: Vec<Expr>,
pub patterns: Vec<Pattern>,
pub rest: Option<crate::Identifier>,
}
/// See also [MatchClass](https://docs.python.org/3/library/ast.html#ast.MatchClass)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchClass {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub cls: Box<Expr>,
pub arguments: crate::PatternArguments,
}
/// See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchStar {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub name: Option<crate::Identifier>,
}
/// See also [MatchAs](https://docs.python.org/3/library/ast.html#ast.MatchAs)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchAs {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub pattern: Option<Box<Pattern>>,
pub name: Option<crate::Identifier>,
}
/// See also [MatchOr](https://docs.python.org/3/library/ast.html#ast.MatchOr)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchOr {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub patterns: Vec<Pattern>,
}
/// See also [TypeVar](https://docs.python.org/3/library/ast.html#ast.TypeVar)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct TypeParamTypeVar {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub name: crate::Identifier,
pub bound: Option<Box<Expr>>,
pub default: Option<Box<Expr>>,
}
/// See also [TypeVarTuple](https://docs.python.org/3/library/ast.html#ast.TypeVarTuple)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct TypeParamTypeVarTuple {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub name: crate::Identifier,
pub default: Option<Box<Expr>>,
}
/// See also [ParamSpec](https://docs.python.org/3/library/ast.html#ast.ParamSpec)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct TypeParamParamSpec {
pub node_index: crate::AtomicNodeIndex,
pub range: ruff_text_size::TextRange,
pub name: crate::Identifier,
pub default: Option<Box<Expr>>,
}
impl ModModule {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
@@ -10692,182 +10585,3 @@ impl ExprIpyEscapeCommand {
} = self;
}
}
impl PatternMatchValue {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let PatternMatchValue {
value,
range: _,
node_index: _,
} = self;
visitor.visit_expr(value);
}
}
impl PatternMatchSingleton {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let PatternMatchSingleton {
value,
range: _,
node_index: _,
} = self;
visitor.visit_singleton(value);
}
}
impl PatternMatchSequence {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let PatternMatchSequence {
patterns,
range: _,
node_index: _,
} = self;
for elm in patterns {
visitor.visit_pattern(elm);
}
}
}
impl PatternMatchClass {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let PatternMatchClass {
cls,
arguments,
range: _,
node_index: _,
} = self;
visitor.visit_expr(cls);
visitor.visit_pattern_arguments(arguments);
}
}
impl PatternMatchStar {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let PatternMatchStar {
name,
range: _,
node_index: _,
} = self;
if let Some(name) = name {
visitor.visit_identifier(name);
}
}
}
impl PatternMatchAs {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let PatternMatchAs {
pattern,
name,
range: _,
node_index: _,
} = self;
if let Some(pattern) = pattern {
visitor.visit_pattern(pattern);
}
if let Some(name) = name {
visitor.visit_identifier(name);
}
}
}
impl PatternMatchOr {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let PatternMatchOr {
patterns,
range: _,
node_index: _,
} = self;
for elm in patterns {
visitor.visit_pattern(elm);
}
}
}
impl TypeParamTypeVar {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let TypeParamTypeVar {
name,
bound,
default,
range: _,
node_index: _,
} = self;
visitor.visit_identifier(name);
if let Some(bound) = bound {
visitor.visit_expr(bound);
}
if let Some(default) = default {
visitor.visit_expr(default);
}
}
}
impl TypeParamTypeVarTuple {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let TypeParamTypeVarTuple {
name,
default,
range: _,
node_index: _,
} = self;
visitor.visit_identifier(name);
if let Some(default) = default {
visitor.visit_expr(default);
}
}
}
impl TypeParamParamSpec {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let TypeParamParamSpec {
name,
default,
range: _,
node_index: _,
} = self;
visitor.visit_identifier(name);
if let Some(default) = default {
visitor.visit_expr(default);
}
}
}

View File

@@ -11,11 +11,6 @@ use crate::generated::ExprName;
#[cfg_attr(feature = "cache", derive(ruff_macros::CacheKey))]
#[cfg_attr(feature = "salsa", derive(salsa::Update))]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
#[cfg_attr(
feature = "schemars",
derive(schemars::JsonSchema),
schemars(with = "String")
)]
pub struct Name(compact_str::CompactString);
impl Name {
@@ -206,6 +201,35 @@ impl PartialEq<Name> for &String {
}
}
#[cfg(feature = "schemars")]
impl schemars::JsonSchema for Name {
fn is_referenceable() -> bool {
String::is_referenceable()
}
fn schema_name() -> String {
String::schema_name()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
String::schema_id()
}
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
String::json_schema(generator)
}
fn _schemars_private_non_optional_json_schema(
generator: &mut schemars::r#gen::SchemaGenerator,
) -> schemars::schema::Schema {
String::_schemars_private_non_optional_json_schema(generator)
}
fn _schemars_private_is_option() -> bool {
String::_schemars_private_is_option()
}
}
/// A representation of a qualified name, like `typing.List`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct QualifiedName<'a>(SegmentsVec<'a>);

View File

@@ -235,6 +235,50 @@ impl ast::ExceptHandlerExceptHandler {
}
}
impl ast::PatternMatchValue {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::PatternMatchValue {
value,
range: _,
node_index: _,
} = self;
visitor.visit_expr(value);
}
}
impl ast::PatternMatchSingleton {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::PatternMatchSingleton {
value,
range: _,
node_index: _,
} = self;
visitor.visit_singleton(value);
}
}
impl ast::PatternMatchSequence {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::PatternMatchSequence {
patterns,
range: _,
node_index: _,
} = self;
for pattern in patterns {
visitor.visit_pattern(pattern);
}
}
}
impl ast::PatternMatchMapping {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
@@ -267,6 +311,76 @@ impl ast::PatternMatchMapping {
}
}
impl ast::PatternMatchClass {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::PatternMatchClass {
cls,
arguments: parameters,
range: _,
node_index: _,
} = self;
visitor.visit_expr(cls);
visitor.visit_pattern_arguments(parameters);
}
}
impl ast::PatternMatchStar {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::PatternMatchStar {
range: _,
node_index: _,
name,
} = self;
if let Some(name) = name {
visitor.visit_identifier(name);
}
}
}
impl ast::PatternMatchAs {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::PatternMatchAs {
pattern,
range: _,
node_index: _,
name,
} = self;
if let Some(pattern) = pattern {
visitor.visit_pattern(pattern);
}
if let Some(name) = name {
visitor.visit_identifier(name);
}
}
}
impl ast::PatternMatchOr {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::PatternMatchOr {
patterns,
range: _,
node_index: _,
} = self;
for pattern in patterns {
visitor.visit_pattern(pattern);
}
}
}
impl ast::PatternArguments {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
@@ -506,6 +620,67 @@ impl ast::TypeParams {
}
}
impl ast::TypeParamTypeVar {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::TypeParamTypeVar {
bound,
default,
name,
range: _,
node_index: _,
} = self;
visitor.visit_identifier(name);
if let Some(expr) = bound {
visitor.visit_expr(expr);
}
if let Some(expr) = default {
visitor.visit_expr(expr);
}
}
}
impl ast::TypeParamTypeVarTuple {
#[inline]
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::TypeParamTypeVarTuple {
range: _,
node_index: _,
name,
default,
} = self;
visitor.visit_identifier(name);
if let Some(expr) = default {
visitor.visit_expr(expr);
}
}
}
impl ast::TypeParamParamSpec {
#[inline]
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where
V: SourceOrderVisitor<'a> + ?Sized,
{
let ast::TypeParamParamSpec {
range: _,
node_index: _,
name,
default,
} = self;
visitor.visit_identifier(name);
if let Some(expr) = default {
visitor.visit_expr(expr);
}
}
}
impl ast::FString {
pub(crate) fn visit_source_order<'a, V>(&'a self, visitor: &mut V)
where

View File

@@ -3,7 +3,7 @@
use crate::AtomicNodeIndex;
use crate::generated::{
ExprBytesLiteral, ExprDict, ExprFString, ExprList, ExprName, ExprSet, ExprStringLiteral,
ExprTString, ExprTuple, PatternMatchAs, PatternMatchOr, StmtClassDef,
ExprTString, ExprTuple, StmtClassDef,
};
use std::borrow::Cow;
use std::fmt;
@@ -2848,10 +2848,58 @@ pub enum IrrefutablePatternKind {
Wildcard,
}
/// An AST node to represent the arguments to a [`crate::PatternMatchClass`], i.e., the
/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchValue {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub value: Box<Expr>,
}
/// See also [MatchSingleton](https://docs.python.org/3/library/ast.html#ast.MatchSingleton)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchSingleton {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub value: Singleton,
}
/// See also [MatchSequence](https://docs.python.org/3/library/ast.html#ast.MatchSequence)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchSequence {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub patterns: Vec<Pattern>,
}
/// See also [MatchMapping](https://docs.python.org/3/library/ast.html#ast.MatchMapping)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchMapping {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub keys: Vec<Expr>,
pub patterns: Vec<Pattern>,
pub rest: Option<Identifier>,
}
/// See also [MatchClass](https://docs.python.org/3/library/ast.html#ast.MatchClass)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchClass {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub cls: Box<Expr>,
pub arguments: PatternArguments,
}
/// An AST node to represent the arguments to a [`PatternMatchClass`], i.e., the
/// parenthesized contents in `case Point(1, x=0, y=0)`.
///
/// Like [`Arguments`], but for [`crate::PatternMatchClass`].
/// Like [`Arguments`], but for [`PatternMatchClass`].
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternArguments {
@@ -2861,10 +2909,10 @@ pub struct PatternArguments {
pub keywords: Vec<PatternKeyword>,
}
/// An AST node to represent the keyword arguments to a [`crate::PatternMatchClass`], i.e., the
/// An AST node to represent the keyword arguments to a [`PatternMatchClass`], i.e., the
/// `x=0` and `y=0` in `case Point(x=0, y=0)`.
///
/// Like [`Keyword`], but for [`crate::PatternMatchClass`].
/// Like [`Keyword`], but for [`PatternMatchClass`].
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternKeyword {
@@ -2874,6 +2922,34 @@ pub struct PatternKeyword {
pub pattern: Pattern,
}
/// See also [MatchStar](https://docs.python.org/3/library/ast.html#ast.MatchStar)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchStar {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub name: Option<Identifier>,
}
/// See also [MatchAs](https://docs.python.org/3/library/ast.html#ast.MatchAs)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchAs {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub pattern: Option<Box<Pattern>>,
pub name: Option<Identifier>,
}
/// See also [MatchOr](https://docs.python.org/3/library/ast.html#ast.MatchOr)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct PatternMatchOr {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub patterns: Vec<Pattern>,
}
impl TypeParam {
pub const fn name(&self) -> &Identifier {
match self {
@@ -2892,6 +2968,37 @@ impl TypeParam {
}
}
/// See also [TypeVar](https://docs.python.org/3/library/ast.html#ast.TypeVar)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct TypeParamTypeVar {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub name: Identifier,
pub bound: Option<Box<Expr>>,
pub default: Option<Box<Expr>>,
}
/// See also [ParamSpec](https://docs.python.org/3/library/ast.html#ast.ParamSpec)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct TypeParamParamSpec {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub name: Identifier,
pub default: Option<Box<Expr>>,
}
/// See also [TypeVarTuple](https://docs.python.org/3/library/ast.html#ast.TypeVarTuple)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct TypeParamTypeVarTuple {
pub range: TextRange,
pub node_index: AtomicNodeIndex,
pub name: Identifier,
pub default: Option<Box<Expr>>,
}
/// See also [decorator](https://docs.python.org/3/library/ast.html#ast.decorator)
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]

View File

@@ -188,39 +188,42 @@ mod serde {
#[cfg(feature = "schemars")]
mod schemars {
use super::PythonVersion;
use schemars::{JsonSchema, Schema, SchemaGenerator};
use serde_json::Value;
use schemars::_serde_json::Value;
use schemars::JsonSchema;
use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation};
impl JsonSchema for PythonVersion {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("PythonVersion")
fn schema_name() -> String {
"PythonVersion".to_string()
}
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
let mut any_of: Vec<Value> = vec![
schemars::json_schema!({
"type": "string",
"pattern": r"^\d+\.\d+$",
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
let sub_schemas = std::iter::once(Schema::Object(SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
string: Some(Box::new(schemars::schema::StringValidation {
pattern: Some(r"^\d+\.\d+$".to_string()),
..Default::default()
})),
..Default::default()
}))
.chain(Self::iter().map(|v| {
Schema::Object(SchemaObject {
const_value: Some(Value::String(v.to_string())),
metadata: Some(Box::new(Metadata {
description: Some(format!("Python {v}")),
..Metadata::default()
})),
..SchemaObject::default()
})
.into(),
];
}));
for version in Self::iter() {
let mut schema = schemars::json_schema!({
"const": version.to_string(),
});
schema.ensure_object().insert(
"description".to_string(),
Value::String(format!("Python {version}")),
);
any_of.push(schema.into());
}
let mut schema = Schema::default();
schema
.ensure_object()
.insert("anyOf".to_string(), Value::Array(any_of));
schema
Schema::Object(SchemaObject {
subschemas: Some(Box::new(SubschemaValidation {
any_of: Some(sub_schemas.collect()),
..Default::default()
})),
..SchemaObject::default()
})
}
}
}

View File

@@ -34,7 +34,6 @@ rustc-hash = { workspace = true }
salsa = { workspace = true }
serde = { workspace = true, optional = true }
schemars = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
smallvec = { workspace = true }
static_assertions = { workspace = true }
thiserror = { workspace = true }
@@ -67,7 +66,7 @@ serde = [
"ruff_source_file/serde",
"ruff_python_ast/serde",
]
schemars = ["dep:schemars", "dep:serde_json", "ruff_formatter/schemars"]
schemars = ["dep:schemars", "ruff_formatter/schemars"]
[lints]
workspace = true

View File

@@ -1,4 +1,3 @@
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_db::source::source_text;
@@ -11,7 +10,7 @@ use ruff_formatter::{FormatError, Formatted, PrintError, Printed, SourceCode, fo
use ruff_python_ast::{AnyNodeRef, Mod};
use ruff_python_parser::{ParseError, ParseOptions, Parsed, parse};
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::Ranged;
use crate::comments::{
Comments, SourceComment, has_skip_comment, leading_comments, trailing_comments,
@@ -118,33 +117,6 @@ pub enum FormatModuleError {
PrintError(#[from] PrintError),
}
impl FormatModuleError {
pub fn range(&self) -> Option<TextRange> {
match self {
FormatModuleError::ParseError(parse_error) => Some(parse_error.range()),
FormatModuleError::FormatError(_) | FormatModuleError::PrintError(_) => None,
}
}
}
impl From<&FormatModuleError> for Diagnostic {
fn from(error: &FormatModuleError) -> Self {
match error {
FormatModuleError::ParseError(parse_error) => Diagnostic::new(
DiagnosticId::InternalError,
Severity::Error,
&parse_error.error,
),
FormatModuleError::FormatError(format_error) => {
Diagnostic::new(DiagnosticId::InternalError, Severity::Error, format_error)
}
FormatModuleError::PrintError(print_error) => {
Diagnostic::new(DiagnosticId::InternalError, Severity::Error, print_error)
}
}
}
}
#[tracing::instrument(name = "format", level = Level::TRACE, skip_all)]
pub fn format_module_source(
source: &str,

View File

@@ -403,12 +403,15 @@ pub enum DocstringCodeLineWidth {
#[cfg(feature = "schemars")]
mod schema {
use ruff_formatter::LineWidth;
use schemars::{Schema, SchemaGenerator};
use serde_json::Value;
use schemars::r#gen::SchemaGenerator;
use schemars::schema::{Metadata, Schema, SubschemaValidation};
/// A dummy type that is used to generate a schema for `DocstringCodeLineWidth::Dynamic`.
pub(super) fn dynamic(_: &mut SchemaGenerator) -> Schema {
schemars::json_schema!({ "const": "dynamic" })
Schema::Object(schemars::schema::SchemaObject {
const_value: Some("dynamic".to_string().into()),
..Default::default()
})
}
// We use a manual schema for `fixed` even thought it isn't strictly necessary according to the
@@ -419,14 +422,19 @@ mod schema {
// `allOf`. There's no semantic difference between `allOf` and `oneOf` for single element lists.
pub(super) fn fixed(generator: &mut SchemaGenerator) -> Schema {
let schema = generator.subschema_for::<LineWidth>();
let mut schema_object = Schema::default();
let map = schema_object.ensure_object();
map.insert(
"description".to_string(),
Value::String("Wrap docstring code examples at a fixed line width.".to_string()),
);
map.insert("oneOf".to_string(), Value::Array(vec![schema.into()]));
schema_object
Schema::Object(schemars::schema::SchemaObject {
metadata: Some(Box::new(Metadata {
description: Some(
"Wrap docstring code examples at a fixed line width.".to_string(),
),
..Metadata::default()
})),
subschemas: Some(Box::new(SubschemaValidation {
one_of: Some(vec![schema]),
..SubschemaValidation::default()
})),
..Default::default()
})
}
}

View File

@@ -404,18 +404,10 @@ fn ensure_stability_when_formatting_twice(
let reformatted = match format_module_source(formatted_code, options.clone()) {
Ok(reformatted) => reformatted,
Err(err) => {
let mut diag = Diagnostic::from(&err);
if let Some(range) = err.range() {
let file =
SourceFileBuilder::new(input_path.to_string_lossy(), formatted_code).finish();
let span = Span::from(file).with_range(range);
diag.annotate(Annotation::primary(span));
}
panic!(
"Expected formatted code of {} to be valid syntax: {err}:\
\n---\n{formatted_code}---\n{}",
input_path.display(),
diag.display(&DummyFileResolver, &DisplayDiagnosticConfig::default()),
\n---\n{formatted_code}---\n",
input_path.display()
);
}
};

View File

@@ -62,7 +62,7 @@ To run the fuzzer, execute the following command
(requires [`uv`](https://github.com/astral-sh/uv) to be installed):
```sh
uv run --project=./python/py-fuzzer fuzz
uvx --from ./python/py-fuzzer fuzz
```
Refer to the [py-fuzzer](https://github.com/astral-sh/ruff/blob/main/python/py-fuzzer/fuzz.py)

View File

@@ -1,13 +0,0 @@
match x:
case [a] | [b]: ...
case [a] | []: ...
case (x, y) | (x,): ...
case [a, _] | [a, b]: ...
case (x, (y | z)): ...
case [a] | [b] | [c]: ...
case [] | [a]: ...
case [a] | [C(x)]: ...
case [[a] | [b]]: ...
case [C(a)] | [C(b)]: ...
case [C(D(a))] | [C(D(b))]: ...
case [(a, b)] | [(c, d)]: ...

View File

@@ -1,4 +0,0 @@
# parse_options: {"target-version": "3.11"}
# nested interpolations also need to be checked
f'{1: abcd "{'aa'}" }'
f'{1: abcd "{"\n"}" }'

View File

@@ -1,6 +0,0 @@
match x:
case [a] | [a]: ...
case (x, y) | (x, y): ...
case (x, (y | y)): ...
case [a, _] | [a, _]: ...
case [a] | [C(a)]: ...

View File

@@ -489,23 +489,6 @@ impl Tokens {
TokenIterWithContext::new(&self.raw)
}
/// Performs a binary search to find the index of the **first** token that starts at the given `offset`.
///
/// Unlike `binary_search_by_key`, this method ensures that if multiple tokens start at the same offset,
/// it returns the index of the first one. Multiple tokens can start at the same offset in cases where
/// zero-length tokens are involved (like `Dedent` or `Newline` at the end of the file).
pub fn binary_search_by_start(&self, offset: TextSize) -> Result<usize, usize> {
let partition_point = self.partition_point(|token| token.start() < offset);
let after = &self[partition_point..];
if after.first().is_some_and(|first| first.start() == offset) {
Ok(partition_point)
} else {
Err(partition_point)
}
}
/// Returns a slice of [`Token`] that are within the given `range`.
///
/// The start and end offset of the given range should be either:
@@ -549,7 +532,30 @@ impl Tokens {
pub fn in_range(&self, range: TextRange) -> &[Token] {
let tokens_after_start = self.after(range.start());
Self::before_impl(tokens_after_start, range.end())
match tokens_after_start.binary_search_by_key(&range.end(), Ranged::end) {
Ok(idx) => {
// If we found the token with the end offset, that token should be included in the
// return slice.
&tokens_after_start[..=idx]
}
Err(idx) => {
if let Some(token) = tokens_after_start.get(idx) {
// If it's equal to the start offset, then it's at a token boundary which is
// valid. If it's less than the start offset, then it's in the gap between the
// tokens which is valid as well.
assert!(
range.end() <= token.start(),
"End offset {:?} is inside a token range {:?}",
range.end(),
token.range()
);
}
// This index is where the token with the offset _could_ be, so that token should
// be excluded from the return slice.
&tokens_after_start[..idx]
}
}
}
/// Searches the token(s) at `offset`.
@@ -591,7 +597,7 @@ impl Tokens {
/// assert_eq!(collect_tokens(TextSize::new(57)), vec! []);
/// ```
pub fn at_offset(&self, offset: TextSize) -> TokenAt {
match self.binary_search_by_start(offset) {
match self.binary_search_by_key(&offset, ruff_text_size::Ranged::start) {
// The token at `index` starts exactly at `offset.
// ```python
// object.attribute
@@ -643,25 +649,28 @@ impl Tokens {
/// If the given offset is inside a token range at any point
/// other than the start of the range.
pub fn before(&self, offset: TextSize) -> &[Token] {
Self::before_impl(&self.raw, offset)
}
match self.binary_search_by(|token| token.start().cmp(&offset)) {
Ok(idx) => &self[..idx],
Err(idx) => {
// We can't use `saturating_sub` here because a file could contain a BOM header, in
// which case the token starts at offset 3 for UTF-8 encoded file content.
if idx > 0 {
if let Some(prev) = self.get(idx - 1) {
// If it's equal to the end offset, then it's at a token boundary which is
// valid. If it's greater than the end offset, then it's in the gap between
// the tokens which is valid as well.
assert!(
offset >= prev.end(),
"Offset {:?} is inside a token range {:?}",
offset,
prev.range()
);
}
}
fn before_impl(tokens: &[Token], offset: TextSize) -> &[Token] {
let partition_point = tokens.partition_point(|token| token.start() < offset);
let before = &tokens[..partition_point];
if let Some(last) = before.last() {
// If it's equal to the end offset, then it's at a token boundary which is
// valid. If it's greater than the end offset, then it's in the gap between
// the tokens which is valid as well.
assert!(
offset >= last.end(),
"Offset {:?} is inside a token range {:?}",
offset,
last.range()
);
&self[..idx]
}
}
before
}
/// Returns a slice of tokens after the given [`TextSize`] offset.
@@ -675,21 +684,28 @@ impl Tokens {
/// If the given offset is inside a token range at any point
/// other than the start of the range.
pub fn after(&self, offset: TextSize) -> &[Token] {
let partition_point = self.partition_point(|token| token.end() <= offset);
let after = &self[partition_point..];
match self.binary_search_by(|token| token.start().cmp(&offset)) {
Ok(idx) => &self[idx..],
Err(idx) => {
// We can't use `saturating_sub` here because a file could contain a BOM header, in
// which case the token starts at offset 3 for UTF-8 encoded file content.
if idx > 0 {
if let Some(prev) = self.get(idx - 1) {
// If it's equal to the end offset, then it's at a token boundary which is
// valid. If it's greater than the end offset, then it's in the gap between
// the tokens which is valid as well.
assert!(
offset >= prev.end(),
"Offset {:?} is inside a token range {:?}",
offset,
prev.range()
);
}
}
if let Some(first) = after.first() {
// valid. If it's greater than the end offset, then it's in the gap between
// the tokens which is valid as well.
assert!(
offset <= first.start(),
"Offset {:?} is inside a token range {:?}",
offset,
first.range()
);
&self[idx..]
}
}
after
}
}
@@ -1083,7 +1099,7 @@ mod tests {
}
#[test]
#[should_panic(expected = "Offset 6 is inside a token range 4..7")]
#[should_panic(expected = "End offset 6 is inside a token range 4..7")]
fn tokens_in_range_end_offset_inside_token() {
let tokens = new_tokens(TEST_CASE_WITH_GAP.into_iter());
tokens.in_range(TextRange::new(0.into(), 6.into()));

View File

@@ -1539,9 +1539,103 @@ impl<'src> Parser<'src> {
flags = flags.with_unclosed(true);
}
// test_ok pep701_f_string_py312
// # parse_options: {"target-version": "3.12"}
// f'Magic wand: { bag['wand'] }' # nested quotes
// f"{'\n'.join(a)}" # escape sequence
// f'''A complex trick: {
// bag['bag'] # comment
// }'''
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
// f"test {a \
// } more" # line continuation
// test_ok pep750_t_string_py314
// # parse_options: {"target-version": "3.14"}
// t'Magic wand: { bag['wand'] }' # nested quotes
// t"{'\n'.join(a)}" # escape sequence
// t'''A complex trick: {
// bag['bag'] # comment
// }'''
// t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting
// t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes
// t"test {a \
// } more" # line continuation
// test_ok pep701_f_string_py311
// # parse_options: {"target-version": "3.11"}
// f"outer {'# not a comment'}"
// f'outer {x:{"# not a comment"} }'
// f"""{f'''{f'{"# not a comment"}'}'''}"""
// f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
// f"escape outside of \t {expr}\n"
// f"test\"abcd"
// f"{1:\x64}" # escapes are valid in the format spec
// f"{1:\"d\"}" # this also means that escaped outer quotes are valid
// test_err pep701_f_string_py311
// # parse_options: {"target-version": "3.11"}
// f'Magic wand: { bag['wand'] }' # nested quotes
// f"{'\n'.join(a)}" # escape sequence
// f'''A complex trick: {
// bag['bag'] # comment
// }'''
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
// f"test {a \
// } more" # line continuation
// f"""{f"""{x}"""}""" # mark the whole triple quote
// f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors
// test_err nested_quote_in_format_spec_py312
// # parse_options: {"target-version": "3.12"}
// f"{1:""}" # this is a ParseError on all versions
// test_ok non_nested_quote_in_format_spec_py311
// # parse_options: {"target-version": "3.11"}
// f"{1:''}" # but this is okay on all versions
let range = self.node_range(start);
if !self.options.target_version.supports_pep_701()
&& matches!(kind, InterpolatedStringKind::FString)
{
let quote_bytes = flags.quote_str().as_bytes();
let quote_len = flags.quote_len();
for expr in elements.interpolations() {
// We need to check the whole expression range, including any leading or trailing
// debug text, but exclude the format spec, where escapes and escaped, reused quotes
// are allowed.
let range = expr
.format_spec
.as_ref()
.map(|format_spec| TextRange::new(expr.start(), format_spec.start()))
.unwrap_or(expr.range);
for slash_position in memchr::memchr_iter(b'\\', self.source[range].as_bytes()) {
let slash_position = TextSize::try_from(slash_position).unwrap();
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash),
TextRange::at(range.start() + slash_position, '\\'.text_len()),
);
}
if let Some(quote_position) =
memchr::memmem::find(self.source[range].as_bytes(), quote_bytes)
{
let quote_position = TextSize::try_from(quote_position).unwrap();
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote),
TextRange::at(range.start() + quote_position, quote_len),
);
}
}
self.check_fstring_comments(range);
}
InterpolatedStringData {
elements,
range: self.node_range(start),
range,
flags,
}
}
@@ -1827,110 +1921,12 @@ impl<'src> Parser<'src> {
);
}
// test_ok pep701_f_string_py312
// # parse_options: {"target-version": "3.12"}
// f'Magic wand: { bag['wand'] }' # nested quotes
// f"{'\n'.join(a)}" # escape sequence
// f'''A complex trick: {
// bag['bag'] # comment
// }'''
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
// f"test {a \
// } more" # line continuation
// test_ok pep750_t_string_py314
// # parse_options: {"target-version": "3.14"}
// t'Magic wand: { bag['wand'] }' # nested quotes
// t"{'\n'.join(a)}" # escape sequence
// t'''A complex trick: {
// bag['bag'] # comment
// }'''
// t"{t"{t"{t"{t"{t"{1+1}"}"}"}"}"}" # arbitrary nesting
// t"{t'''{"nested"} inner'''} outer" # nested (triple) quotes
// t"test {a \
// } more" # line continuation
// test_ok pep701_f_string_py311
// # parse_options: {"target-version": "3.11"}
// f"outer {'# not a comment'}"
// f'outer {x:{"# not a comment"} }'
// f"""{f'''{f'{"# not a comment"}'}'''}"""
// f"""{f'''# before expression {f'# aro{f"#{1+1}#"}und #'}'''} # after expression"""
// f"escape outside of \t {expr}\n"
// f"test\"abcd"
// f"{1:\x64}" # escapes are valid in the format spec
// f"{1:\"d\"}" # this also means that escaped outer quotes are valid
// test_err pep701_f_string_py311
// # parse_options: {"target-version": "3.11"}
// f'Magic wand: { bag['wand'] }' # nested quotes
// f"{'\n'.join(a)}" # escape sequence
// f'''A complex trick: {
// bag['bag'] # comment
// }'''
// f"{f"{f"{f"{f"{f"{1+1}"}"}"}"}"}" # arbitrary nesting
// f"{f'''{"nested"} inner'''} outer" # nested (triple) quotes
// f"test {a \
// } more" # line continuation
// f"""{f"""{x}"""}""" # mark the whole triple quote
// f"{'\n'.join(['\t', '\v', '\r'])}" # multiple escape sequences, multiple errors
// test_err pep701_nested_interpolation_py311
// # parse_options: {"target-version": "3.11"}
// # nested interpolations also need to be checked
// f'{1: abcd "{'aa'}" }'
// f'{1: abcd "{"\n"}" }'
// test_err nested_quote_in_format_spec_py312
// # parse_options: {"target-version": "3.12"}
// f"{1:""}" # this is a ParseError on all versions
// test_ok non_nested_quote_in_format_spec_py311
// # parse_options: {"target-version": "3.11"}
// f"{1:''}" # but this is okay on all versions
let range = self.node_range(start);
if !self.options.target_version.supports_pep_701()
&& matches!(string_kind, InterpolatedStringKind::FString)
{
// We need to check the whole expression range, including any leading or trailing
// debug text, but exclude the format spec, where escapes and escaped, reused quotes
// are allowed.
let range = format_spec
.as_ref()
.map(|format_spec| TextRange::new(range.start(), format_spec.start()))
.unwrap_or(range);
let quote_bytes = flags.quote_str().as_bytes();
let quote_len = flags.quote_len();
for slash_position in memchr::memchr_iter(b'\\', self.source[range].as_bytes()) {
let slash_position = TextSize::try_from(slash_position).unwrap();
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::Backslash),
TextRange::at(range.start() + slash_position, '\\'.text_len()),
);
}
if let Some(quote_position) =
memchr::memmem::find(self.source[range].as_bytes(), quote_bytes)
{
let quote_position = TextSize::try_from(quote_position).unwrap();
self.add_unsupported_syntax_error(
UnsupportedSyntaxErrorKind::Pep701FString(FStringKind::NestedQuote),
TextRange::at(range.start() + quote_position, quote_len),
);
}
self.check_fstring_comments(range);
}
ast::InterpolatedElement {
expression: Box::new(value.expr),
debug_text,
conversion,
format_spec,
range,
range: self.node_range(start),
node_index: AtomicNodeIndex::NONE,
}
}

View File

@@ -133,17 +133,6 @@ impl SemanticSyntaxChecker {
}
Self::duplicate_parameter_name(parameters, ctx);
}
Stmt::Global(ast::StmtGlobal { names, .. }) => {
for name in names {
if ctx.is_bound_parameter(name) {
Self::add_error(
ctx,
SemanticSyntaxErrorKind::GlobalParameter(name.to_string()),
name.range,
);
}
}
}
Stmt::ClassDef(ast::StmtClassDef { type_params, .. })
| Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => {
if let Some(type_params) = type_params {
@@ -1148,12 +1137,6 @@ impl Display for SemanticSyntaxError {
}
SemanticSyntaxErrorKind::BreakOutsideLoop => f.write_str("`break` outside loop"),
SemanticSyntaxErrorKind::ContinueOutsideLoop => f.write_str("`continue` outside loop"),
SemanticSyntaxErrorKind::GlobalParameter(name) => {
write!(f, "name `{name}` is parameter and global")
}
SemanticSyntaxErrorKind::DifferentMatchPatternBindings => {
write!(f, "alternative patterns bind different names")
}
}
}
}
@@ -1533,27 +1516,6 @@ pub enum SemanticSyntaxErrorKind {
/// Represents the use of a `continue` statement outside of a loop.
ContinueOutsideLoop,
/// Represents a function parameter that is also declared as `global`.
///
/// Declaring a parameter as `global` is invalid, since parameters are already
/// bound in the local scope of the function. Using `global` on them introduces
/// ambiguity and will result in a `SyntaxError`.
GlobalParameter(String),
/// Represents the use of alternative patterns in a `match` statement that bind different names.
///
/// Python requires all alternatives in an OR pattern (`|`) to bind the same set of names.
/// Using different names results in a `SyntaxError`.
///
/// ## Example:
///
/// ```python
/// match 5:
/// case [x] | [y]: # error
/// ...
/// ```
DifferentMatchPatternBindings,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)]
@@ -1796,9 +1758,7 @@ impl<'a, Ctx: SemanticSyntaxContext> MatchPatternVisitor<'a, Ctx> {
self.insert(name);
}
}
Pattern::MatchOr(ast::PatternMatchOr {
patterns, range, ..
}) => {
Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => {
// each of these patterns should be visited separately because patterns can only be
// duplicated within a single arm of the or pattern. For example, the case below is
// a valid pattern.
@@ -1806,48 +1766,12 @@ impl<'a, Ctx: SemanticSyntaxContext> MatchPatternVisitor<'a, Ctx> {
// test_ok multiple_assignment_in_case_pattern
// match 2:
// case Class(x) | [x] | x: ...
let mut previous_names: Option<FxHashSet<&ast::name::Name>> = None;
for pattern in patterns {
let mut visitor = Self {
names: FxHashSet::default(),
ctx: self.ctx,
};
visitor.visit_pattern(pattern);
let Some(prev) = &previous_names else {
previous_names = Some(visitor.names);
continue;
};
if prev.symmetric_difference(&visitor.names).next().is_some() {
// test_err different_match_pattern_bindings
// match x:
// case [a] | [b]: ...
// case [a] | []: ...
// case (x, y) | (x,): ...
// case [a, _] | [a, b]: ...
// case (x, (y | z)): ...
// case [a] | [b] | [c]: ...
// case [] | [a]: ...
// case [a] | [C(x)]: ...
// case [[a] | [b]]: ...
// case [C(a)] | [C(b)]: ...
// case [C(D(a))] | [C(D(b))]: ...
// case [(a, b)] | [(c, d)]: ...
// test_ok different_match_pattern_bindings
// match x:
// case [a] | [a]: ...
// case (x, y) | (x, y): ...
// case (x, (y | y)): ...
// case [a, _] | [a, _]: ...
// case [a] | [C(a)]: ...
SemanticSyntaxChecker::add_error(
self.ctx,
SemanticSyntaxErrorKind::DifferentMatchPatternBindings,
*range,
);
break;
}
}
}
}
@@ -2075,8 +1999,6 @@ pub trait SemanticSyntaxContext {
fn report_semantic_error(&self, error: SemanticSyntaxError);
fn in_loop_context(&self) -> bool;
fn is_bound_parameter(&self, name: &str) -> bool;
}
/// Modified version of [`std::str::EscapeDefault`] that does not escape single or double quotes.

View File

@@ -486,7 +486,7 @@ impl TokenKind {
///
/// [`as_unary_operator`]: TokenKind::as_unary_operator
#[inline]
pub const fn as_unary_arithmetic_operator(self) -> Option<UnaryOp> {
pub(crate) const fn as_unary_arithmetic_operator(self) -> Option<UnaryOp> {
Some(match self {
TokenKind::Plus => UnaryOp::UAdd,
TokenKind::Minus => UnaryOp::USub,
@@ -501,7 +501,7 @@ impl TokenKind {
///
/// [`as_unary_arithmetic_operator`]: TokenKind::as_unary_arithmetic_operator
#[inline]
pub const fn as_unary_operator(self) -> Option<UnaryOp> {
pub(crate) const fn as_unary_operator(self) -> Option<UnaryOp> {
Some(match self {
TokenKind::Plus => UnaryOp::UAdd,
TokenKind::Minus => UnaryOp::USub,
@@ -514,7 +514,7 @@ impl TokenKind {
/// Returns the [`BoolOp`] that corresponds to this token kind, if it is a boolean operator,
/// otherwise return [None].
#[inline]
pub const fn as_bool_operator(self) -> Option<BoolOp> {
pub(crate) const fn as_bool_operator(self) -> Option<BoolOp> {
Some(match self {
TokenKind::And => BoolOp::And,
TokenKind::Or => BoolOp::Or,
@@ -528,7 +528,7 @@ impl TokenKind {
/// Use [`as_augmented_assign_operator`] to match against an augmented assignment token.
///
/// [`as_augmented_assign_operator`]: TokenKind::as_augmented_assign_operator
pub const fn as_binary_operator(self) -> Option<Operator> {
pub(crate) const fn as_binary_operator(self) -> Option<Operator> {
Some(match self {
TokenKind::Plus => Operator::Add,
TokenKind::Minus => Operator::Sub,
@@ -550,7 +550,7 @@ impl TokenKind {
/// Returns the [`Operator`] that corresponds to this token kind, if it is
/// an augmented assignment operator, or [`None`] otherwise.
#[inline]
pub const fn as_augmented_assign_operator(self) -> Option<Operator> {
pub(crate) const fn as_augmented_assign_operator(self) -> Option<Operator> {
Some(match self {
TokenKind::PlusEqual => Operator::Add,
TokenKind::MinusEqual => Operator::Sub,

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