Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
b6954063c1 [ty] Do not function-like property for Callable type relations 2025-10-13 13:15:36 +02:00
499 changed files with 4762 additions and 17824 deletions

View File

@@ -1,12 +1,3 @@
# Define serial test group for running tests sequentially.
[test-groups]
serial = { max-threads = 1 }
# Run ty file watching tests sequentially to avoid race conditions.
[[profile.default.overrides]]
filter = 'binary(file_watching)'
test-group = 'serial'
[profile.ci]
# Print out output for failing tests as soon as they fail, and also at the end
# of the run (for easy scrollability).

View File

@@ -10,7 +10,7 @@ indent_style = space
insert_final_newline = true
indent_size = 2
[*.{rs,py,pyi,toml}]
[*.{rs,py,pyi}]
indent_size = 4
[*.snap]
@@ -18,3 +18,6 @@ trim_trailing_whitespace = false
[*.md]
max_line_length = 100
[*.toml]
indent_size = 4

View File

@@ -12,10 +12,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -23,7 +19,6 @@ env:
RUSTUP_MAX_RETRIES: 10
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.13"
NEXTEST_PROFILE: ci
jobs:
determine_changes:
@@ -147,7 +142,7 @@ jobs:
env:
MERGE_BASE: ${{ steps.merge_base.outputs.sha }}
run: |
if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py-fuzzer/**' \
if git diff --quiet "${MERGE_BASE}...HEAD" -- 'python/py_fuzzer/**' \
; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
@@ -242,7 +237,7 @@ jobs:
cargo-test-linux:
name: "cargo test (linux)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
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
@@ -276,6 +271,9 @@ jobs:
# This step is just to get nice GitHub annotations on the PR diff in the files-changed tab.
run: cargo test -p ty_python_semantic --test mdtest || true
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --all-features --unreferenced reject --test-runner nextest
# Check for broken links in the documentation.
@@ -301,13 +299,9 @@ jobs:
cargo-test-linux-release:
name: "cargo test (linux, release)"
# release builds timeout on GitHub runners, so this job is just skipped on forks in the `if` check
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
!contains(github.event.pull_request.labels.*.name, 'no-test') &&
(needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main')
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
@@ -331,11 +325,14 @@ jobs:
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
cargo-test-windows:
name: "cargo test (windows)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-windows-2022-16' || 'windows-latest' }}
runs-on: depot-windows-2022-16
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
@@ -355,41 +352,15 @@ jobs:
with:
enable-cache: "true"
- name: "Run tests"
shell: bash
env:
NEXTEST_PROFILE: "ci"
# 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
cargo test --all-features --doc
cargo-test-wasm:
name: "cargo test (wasm)"
runs-on: ubuntu-latest
@@ -420,9 +391,26 @@ jobs:
cd crates/ty_wasm
wasm-pack test --node
cargo-build-release:
name: "cargo build (release)"
runs-on: macos-latest
if: ${{ 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: "Build"
run: cargo build --release --locked
cargo-build-msrv:
name: "cargo build (msrv)"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-latest-8
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
@@ -443,6 +431,7 @@ jobs:
- name: "Install mold"
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Build tests"
shell: bash
env:
MSRV: ${{ steps.msrv.outputs.value }}
run: cargo "+${MSRV}" test --no-run --all-features
@@ -535,7 +524,7 @@ jobs:
ecosystem:
name: "ecosystem"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-latest-8' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-latest-8
needs:
- cargo-test-linux
- determine_changes
@@ -660,7 +649,7 @@ jobs:
fuzz-ty:
name: "Fuzz for new ty panics"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
needs:
- cargo-test-linux
- determine_changes
@@ -720,7 +709,7 @@ jobs:
ty-completion-evaluation:
name: "ty completion evaluation"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
needs: determine_changes
if: ${{ needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main' }}
steps:
@@ -732,7 +721,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Run ty completion evaluation"
run: cargo run --release --package ty_completion_eval -- all --threshold 0.4 --tasks /tmp/completion-evaluation-tasks.csv
run: cargo run --release --package ty_completion_eval -- all --threshold 0.1 --tasks /tmp/completion-evaluation-tasks.csv
- name: "Ensure there are no changes"
run: diff ./crates/ty_completion_eval/completion-evaluation-tasks.csv /tmp/completion-evaluation-tasks.csv
@@ -766,7 +755,7 @@ jobs:
pre-commit:
name: "pre-commit"
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-16' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-16
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -940,12 +929,8 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
(
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.formatter == 'true' ||
needs.determine_changes.outputs.linter == 'true'
)
github.ref == 'refs/heads/main' ||
(needs.determine_changes.outputs.formatter == 'true' || needs.determine_changes.outputs.linter == 'true')
timeout-minutes: 20
steps:
- name: "Checkout Branch"
@@ -979,11 +964,8 @@ jobs:
runs-on: ubuntu-24.04
needs: determine_changes
if: |
github.repository == 'astral-sh/ruff' &&
(
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
)
github.ref == 'refs/heads/main' ||
needs.determine_changes.outputs.ty == 'true'
timeout-minutes: 20
steps:
- name: "Checkout Branch"

View File

@@ -19,10 +19,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
defaults:
run:
shell: bash
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
@@ -33,7 +29,7 @@ env:
jobs:
mypy_primer:
name: Run mypy_primer
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -53,6 +49,7 @@ jobs:
run: rustup show
- name: Run mypy_primer
shell: bash
env:
PRIMER_SELECTOR: crates/ty_python_semantic/resources/primer/good.txt
DIFF_FILE: mypy_primer.diff
@@ -75,7 +72,7 @@ jobs:
memory_usage:
name: Run memory statistics
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -95,6 +92,7 @@ jobs:
run: rustup show
- name: Run mypy_primer
shell: bash
env:
TY_MAX_PARALLELISM: 1 # for deterministic memory numbers
TY_MEMORY_REPORT: mypy_primer

View File

@@ -16,10 +16,8 @@ name: Sync typeshed
# 3. Once the Windows worker is done, a MacOS worker:
# a. Checks out the branch created by the Linux worker
# b. Syncs all docstrings available on MacOS that are not available on Linux or Windows
# c. Attempts to update any snapshots that might have changed
# (this sub-step is allowed to fail)
# d. Commits the changes and pushes them to the same upstream branch
# e. Creates a PR against the `main` branch using the branch all three workers have pushed to
# c. Commits the changes and pushes them to the same upstream branch
# d. Creates a PR against the `main` branch using the branch all three workers have pushed to
# 4. If any of steps 1-3 failed, an issue is created in the `astral-sh/ruff` repository
on:
@@ -28,18 +26,8 @@ on:
# Run on the 1st and the 15th of every month:
- cron: "0 0 1,15 * *"
defaults:
run:
shell: bash
env:
# Don't set this flag globally for the workflow: it does strange things
# to the snapshots in the `cargo insta test --accept` step in the MacOS job.
#
# FORCE_COLOR: 1
CARGO_TERM_COLOR: always
NEXTEST_PROFILE: "ci"
FORCE_COLOR: 1
GH_TOKEN: ${{ github.token }}
# The name of the upstream branch that the first worker creates,
@@ -98,8 +86,6 @@ jobs:
git commit -m "Sync typeshed. Source commit: https://github.com/python/typeshed/commit/$(git -C ../typeshed rev-parse HEAD)" --allow-empty
- name: Sync Linux docstrings
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
cd ruff
./scripts/codemod_docstrings.sh
@@ -138,8 +124,7 @@ jobs:
git config --global user.email '<>'
- name: Sync Windows docstrings
id: docstrings
env:
FORCE_COLOR: 1
shell: bash
run: ./scripts/codemod_docstrings.sh
- name: Commit the changes
if: ${{ steps.docstrings.outcome == 'success' }}
@@ -176,63 +161,26 @@ jobs:
git config --global user.name typeshedbot
git config --global user.email '<>'
- name: Sync macOS docstrings
run: ./scripts/codemod_docstrings.sh
- name: Commit and push the changes
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
./scripts/codemod_docstrings.sh
git commit -am "Sync macOS docstrings" --allow-empty
- name: Format the changes
if: ${{ success() }}
env:
FORCE_COLOR: 1
run: |
# Here we just reformat the codemodded stubs so that they are
# consistent with the other typeshed stubs around them.
# Typeshed formats code using black in their CI, so we just invoke
# black on the stubs the same way that typeshed does.
uvx black "${VENDORED_TYPESHED}/stdlib" --config "${VENDORED_TYPESHED}/pyproject.toml" || true
git commit -am "Format codemodded docstrings" --allow-empty
- name: Remove typeshed pyproject.toml file
if: ${{ success() }}
run: |
rm "${VENDORED_TYPESHED}/pyproject.toml"
git commit -am "Remove pyproject.toml file"
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
- name: "Install Rust toolchain"
if: ${{ success() }}
run: rustup show
- name: "Install mold"
if: ${{ success() }}
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
- name: "Install cargo nextest"
if: ${{ success() }}
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-nextest
- name: "Install cargo insta"
if: ${{ success() }}
uses: taiki-e/install-action@522492a8c115f1b6d4d318581f09638e9442547b # v2.62.21
with:
tool: cargo-insta
- name: Update snapshots
if: ${{ success() }}
run: |
# The `cargo insta` docs indicate that `--unreferenced=delete` might be a good option,
# but from local testing it appears to just revert all changes made by `cargo insta test --accept`.
#
# If there were only snapshot-related failures, `cargo insta test --accept` will have exit code 0,
# but if there were also other mdtest failures (for example), it will return a nonzero exit code.
# We don't care about other tests failing here, we just want snapshots updated where possible,
# so we use `|| true` here to ignore the exit code.
cargo insta test --accept --color=always --all-features --test-runner=nextest || true
- name: Commit snapshot changes
if: ${{ success() }}
run: git commit -am "Update snapshots" || echo "No snapshot changes to commit"
- name: Push changes upstream and create a PR
if: ${{ success() }}
run: |
git push
- name: Create a PR
if: ${{ success() }}
run: |
gh pr list --repo "${GITHUB_REPOSITORY}" --head "${UPSTREAM_BRANCH}" --json id --jq length | grep 1 && exit 0 # exit if there is existing pr
gh pr create --title "[ty] Sync vendored typeshed stubs" --body "Close and reopen this PR to trigger CI" --label "ty"

View File

@@ -22,7 +22,7 @@ env:
jobs:
ty-ecosystem-analyzer:
name: Compute diagnostic diff
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
if: contains(github.event.label.name, 'ecosystem-analyzer')
steps:
@@ -64,12 +64,12 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
ecosystem-analyzer \
--repository ruff \
diff \
--profile=profiling \
--profile=release \
--projects-old ruff/projects_old.txt \
--projects-new ruff/projects_new.txt \
--old old_commit \

View File

@@ -19,7 +19,7 @@ env:
jobs:
ty-ecosystem-report:
name: Create ecosystem report
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 20
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -49,13 +49,13 @@ jobs:
cd ..
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@908758da02a73ef3f3308e1dbb2248510029bbe4"
uv tool install "git+https://github.com/astral-sh/ecosystem-analyzer@279f8a15b0e7f77213bf9096dbc2335a19ef89c5"
ecosystem-analyzer \
--verbose \
--repository ruff \
analyze \
--profile=profiling \
--profile=release \
--projects ruff/crates/ty_python_semantic/resources/primer/good.txt \
--output ecosystem-diagnostics.json

View File

@@ -29,7 +29,7 @@ env:
jobs:
typing_conformance:
name: Compute diagnostic diff
runs-on: ${{ github.repository == 'astral-sh/ruff' && 'depot-ubuntu-22.04-32' || 'ubuntu-latest' }}
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

View File

@@ -1,63 +1,5 @@
# Changelog
## 0.14.1
Released on 2025-10-16.
### Preview features
- [formatter] Remove parentheses around multiple exception types on Python 3.14+ ([#20768](https://github.com/astral-sh/ruff/pull/20768))
- \[`flake8-bugbear`\] Omit annotation in preview fix for `B006` ([#20877](https://github.com/astral-sh/ruff/pull/20877))
- \[`flake8-logging-format`\] Avoid dropping implicitly concatenated pieces in the `G004` fix ([#20793](https://github.com/astral-sh/ruff/pull/20793))
- \[`pydoclint`\] Implement `docstring-extraneous-parameter` (`DOC102`) ([#20376](https://github.com/astral-sh/ruff/pull/20376))
- \[`pyupgrade`\] Extend `UP019` to detect `typing_extensions.Text` (`UP019`) ([#20825](https://github.com/astral-sh/ruff/pull/20825))
- \[`pyupgrade`\] Fix false negative for `TypeVar` with default argument in `non-pep695-generic-class` (`UP046`) ([#20660](https://github.com/astral-sh/ruff/pull/20660))
### Bug fixes
- Fix false negatives in `Truthiness::from_expr` for lambdas, generators, and f-strings ([#20704](https://github.com/astral-sh/ruff/pull/20704))
- Fix syntax error false positives for escapes and quotes in f-strings ([#20867](https://github.com/astral-sh/ruff/pull/20867))
- Fix syntax error false positives on parenthesized context managers ([#20846](https://github.com/astral-sh/ruff/pull/20846))
- \[`fastapi`\] Fix false positives for path parameters that FastAPI doesn't recognize (`FAST003`) ([#20687](https://github.com/astral-sh/ruff/pull/20687))
- \[`flake8-pyi`\] Fix operator precedence by adding parentheses when needed (`PYI061`) ([#20508](https://github.com/astral-sh/ruff/pull/20508))
- \[`ruff`\] Suppress diagnostic for f-string interpolations with debug text (`RUF010`) ([#20525](https://github.com/astral-sh/ruff/pull/20525))
### Rule changes
- \[`airflow`\] Add warning to `airflow.datasets.DatasetEvent` usage (`AIR301`) ([#20551](https://github.com/astral-sh/ruff/pull/20551))
- \[`flake8-bugbear`\] Mark `B905` and `B912` fixes as unsafe ([#20695](https://github.com/astral-sh/ruff/pull/20695))
- Use `DiagnosticTag` for more rules - changes display in editors ([#20758](https://github.com/astral-sh/ruff/pull/20758),[#20734](https://github.com/astral-sh/ruff/pull/20734))
### Documentation
- Update Python compatibility from 3.13 to 3.14 in README.md ([#20852](https://github.com/astral-sh/ruff/pull/20852))
- Update `lint.flake8-type-checking.quoted-annotations` docs ([#20765](https://github.com/astral-sh/ruff/pull/20765))
- Update setup instructions for Zed 0.208.0+ ([#20902](https://github.com/astral-sh/ruff/pull/20902))
- \[`flake8-datetimez`\] Clarify docs for several rules ([#20778](https://github.com/astral-sh/ruff/pull/20778))
- Fix typo in `RUF015` description ([#20873](https://github.com/astral-sh/ruff/pull/20873))
### Other changes
- Reduce binary size ([#20863](https://github.com/astral-sh/ruff/pull/20863))
- Improved error recovery for unclosed strings (including f- and t-strings) ([#20848](https://github.com/astral-sh/ruff/pull/20848))
### Contributors
- [@ntBre](https://github.com/ntBre)
- [@Paillat-dev](https://github.com/Paillat-dev)
- [@terror](https://github.com/terror)
- [@pieterh-oai](https://github.com/pieterh-oai)
- [@MichaReiser](https://github.com/MichaReiser)
- [@TaKO8Ki](https://github.com/TaKO8Ki)
- [@ageorgou](https://github.com/ageorgou)
- [@danparizher](https://github.com/danparizher)
- [@mgaitan](https://github.com/mgaitan)
- [@augustelalande](https://github.com/augustelalande)
- [@dylwil3](https://github.com/dylwil3)
- [@Lee-W](https://github.com/Lee-W)
- [@injust](https://github.com/injust)
- [@CarrotManMatt](https://github.com/CarrotManMatt)
## 0.14.0
Released on 2025-10-07.

18
Cargo.lock generated
View File

@@ -2815,7 +2815,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"anyhow",
"argfile",
@@ -3072,7 +3072,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3426,7 +3426,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.14.1"
version = "0.14.0"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3540,8 +3540,8 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"boxcar",
"compact_str",
@@ -3564,13 +3564,13 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
[[package]]
name = "salsa-macros"
version = "0.24.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=ef9f9329be6923acd050c8dddd172e3bc93e8051#ef9f9329be6923acd050c8dddd172e3bc93e8051"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=29ab321b45d00daa4315fa2a06f7207759a8c87e#29ab321b45d00daa4315fa2a06f7207759a8c87e"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
rustc-hash = { version = "2.0.0" }
rustc-stable-hash = { version = "0.1.2" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "ef9f9329be6923acd050c8dddd172e3bc93e8051", default-features = false, features = [
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "29ab321b45d00daa4315fa2a06f7207759a8c87e", default-features = false, features = [
"compact_str",
"macros",
"salsa_unstable",
@@ -268,7 +268,12 @@ large_stack_arrays = "allow"
[profile.release]
lto = "fat"
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
# and runtime performance[1].
#
# [1]: https://github.com/astral-sh/ruff/pull/9031
lto = "thin"
codegen-units = 16
# Some crates don't change as much but benefit more from
@@ -278,8 +283,6 @@ codegen-units = 16
codegen-units = 1
[profile.release.package.ruff_python_ast]
codegen-units = 1
[profile.release.package.salsa]
codegen-units = 1
[profile.dev.package.insta]
opt-level = 3
@@ -295,30 +298,11 @@ opt-level = 3
[profile.dev.package.ruff_python_parser]
opt-level = 1
# This profile is meant to mimic the `release` profile as closely as
# possible, but using settings that are more beneficial for iterative
# development. That is, the `release` profile is intended for actually
# building the release, where as `profiling` is meant for building ty/ruff
# for running benchmarks.
#
# The main differences here are to avoid stripping debug information
# and disabling fat lto. This does result in a mismatch between our release
# configuration and our benchmarking configuration, which is unfortunate.
# But compile times with `lto = fat` are completely untenable.
#
# This setup does risk that we are measuring something in benchmarks
# that we aren't shipping, but in order to make those two the same, we'd
# either need to make compile times way worse for development, or take
# a hit to binary size and a slight hit to runtime performance in our
# release builds.
#
# Use the `--profile profiling` flag to show symbols in release mode.
# e.g. `cargo build --profile profiling`
[profile.profiling]
inherits = "release"
strip = false
debug = "full"
lto = false
debug = 1
# The profile that 'cargo dist' will build with.
[profile.dist]

View File

@@ -28,7 +28,7 @@ An extremely fast Python linter and code formatter, written in Rust.
- ⚡️ 10-100x faster than existing linters (like Flake8) and formatters (like Black)
- 🐍 Installable via `pip`
- 🛠️ `pyproject.toml` support
- 🤝 Python 3.14 compatibility
- 🤝 Python 3.13 compatibility
- ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-flake8), isort, and [Black](https://docs.astral.sh/ruff/faq/#how-does-ruffs-formatter-compare-to-black)
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports)
@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.14.1/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.1/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.14.0/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.14.0/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -182,7 +182,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.1
rev: v0.14.0
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -13,6 +13,7 @@ use log::{debug, warn};
use ruff_db::diagnostic::Diagnostic;
use ruff_linter::codes::Rule;
use ruff_linter::linter::{FixTable, FixerResult, LinterResult, ParseSource, lint_fix, lint_only};
use ruff_linter::message::create_syntax_error_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
use ruff_linter::settings::types::UnsafeFixes;
@@ -102,7 +103,11 @@ impl Diagnostics {
let name = path.map_or_else(|| "-".into(), Path::to_string_lossy);
let dummy = SourceFileBuilder::new(name, "").finish();
Self::new(
vec![Diagnostic::invalid_syntax(dummy, err, TextRange::default())],
vec![create_syntax_error_diagnostic(
dummy,
err,
TextRange::default(),
)],
FxHashMap::default(),
)
}

View File

@@ -7,8 +7,7 @@ use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{
DisplayDiagnostic, DisplayDiagnostics, DummyFileResolver, FileResolver, Input,
ceil_char_boundary,
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
github::{DisplayGithubDiagnostics, GithubRenderer},
};
use crate::{Db, files::File};
@@ -84,14 +83,17 @@ impl Diagnostic {
/// at time of writing, `ruff_db` depends on `ruff_python_parser` instead of
/// the other way around. And since we want to do this conversion in a couple
/// places, it makes sense to centralize it _somewhere_. So it's here for now.
///
/// Note that `message` is stored in the primary annotation, _not_ in the primary diagnostic
/// message.
pub fn invalid_syntax(
span: impl Into<Span>,
message: impl IntoDiagnosticMessage,
range: impl Ranged,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
let span = span.into().with_range(range.range());
diag.annotate(Annotation::primary(span));
diag.annotate(Annotation::primary(span).message(message));
diag
}

View File

@@ -1170,31 +1170,6 @@ pub fn ceil_char_boundary(text: &str, offset: TextSize) -> TextSize {
.unwrap_or_else(|| TextSize::from(upper_bound))
}
/// A stub implementation of [`FileResolver`] intended for testing.
pub struct DummyFileResolver;
impl FileResolver for DummyFileResolver {
fn path(&self, _file: File) -> &str {
unimplemented!()
}
fn input(&self, _file: File) -> Input {
unimplemented!()
}
fn notebook_index(&self, _file: &UnifiedFile) -> Option<NotebookIndex> {
None
}
fn is_notebook(&self, _file: &UnifiedFile) -> bool {
false
}
fn current_directory(&self) -> &Path {
Path::new(".")
}
}
#[cfg(test)]
mod tests {

View File

@@ -193,17 +193,6 @@ impl Files {
roots.at(&absolute)
}
/// The same as [`Self::root`] but panics if no root is found.
#[track_caller]
pub fn expect_root(&self, db: &dyn Db, path: &SystemPath) -> FileRoot {
if let Some(root) = self.root(db, path) {
return root;
}
let roots = self.inner.roots.read().unwrap();
panic!("No root found for path '{path}'. Known roots: {roots:#?}");
}
/// Adds a new root for `path` and returns the root.
///
/// The root isn't added nor is the file root's kind updated if a root for `path` already exists.

View File

@@ -81,8 +81,6 @@ impl FileRoots {
}
}
tracing::debug!("Adding new file root '{path}' of kind {kind:?}");
// normalize the path to use `/` separators and escape the '{' and '}' characters,
// which matchit uses for routing parameters
let mut route = normalized_path.replace('{', "{{").replace('}', "}}");

View File

@@ -93,39 +93,14 @@ fn generate_markdown() -> String {
})
.join("\n");
let status_text = match lint.status() {
ty_python_semantic::lint::LintStatus::Stable { since } => {
format!(
r#"Added in <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>"#
)
}
ty_python_semantic::lint::LintStatus::Preview { since } => {
format!(
r#"Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
ty_python_semantic::lint::LintStatus::Deprecated { since, .. } => {
format!(
r#"Deprecated (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
ty_python_semantic::lint::LintStatus::Removed { since, .. } => {
format!(
r#"Removed (since <a href="https://github.com/astral-sh/ty/releases/tag/{since}">{since}</a>)"#
)
}
};
let _ = writeln!(
&mut output,
r#"<small>
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of '{level}'."><code>{level}</code></a> ·
{status_text} ·
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}" target="_blank">Related issues</a> ·
<a href="https://github.com/astral-sh/ruff/blob/main/{file}#L{line}" target="_blank">View source</a>
Default level: [`{level}`](../rules.md#rule-levels "This lint has a default level of '{level}'.") ·
[Related issues](https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20{encoded_name}) ·
[View source](https://github.com/astral-sh/ruff/blob/main/{file}#L{line})
</small>
{documentation}
"#,
level = lint.default_level(),

View File

@@ -98,10 +98,6 @@ impl Db for ModuleDb {
fn lint_registry(&self) -> &LintRegistry {
default_lint_registry()
}
fn verbose(&self) -> bool {
false
}
}
#[salsa::db]

View File

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

View File

@@ -11,7 +11,7 @@ from airflow import (
)
from airflow.api_connexion.security import requires_access
from airflow.contrib.aws_athena_hook import AWSAthenaHook
from airflow.datasets import DatasetAliasEvent, DatasetEvent
from airflow.datasets import DatasetAliasEvent
from airflow.operators.postgres_operator import Mapping
from airflow.operators.subdag import SubDagOperator
from airflow.secrets.cache import SecretCache
@@ -48,7 +48,6 @@ AWSAthenaHook()
# airflow.datasets
DatasetAliasEvent()
DatasetEvent()
# airflow.operators.subdag.*

View File

@@ -33,10 +33,3 @@ class ShellConfig:
def run(self, username):
Popen("true", shell={**self.shell_defaults, **self.fetch_shell_config(username)})
# Additional truthiness cases for generator, lambda, and f-strings
Popen("true", shell=(i for i in ()))
Popen("true", shell=lambda: 0)
Popen("true", shell=f"{b''}")
x = 1
Popen("true", shell=f"{x=}")

View File

@@ -6,19 +6,3 @@ foo(shell=True)
foo(shell={**{}})
foo(shell={**{**{}}})
# Truthy non-bool values for `shell`
foo(shell=(i for i in ()))
foo(shell=lambda: 0)
# f-strings guaranteed non-empty
foo(shell=f"{b''}")
x = 1
foo(shell=f"{x=}")
# Additional truthiness cases for generator, lambda, and f-strings
foo(shell=(i for i in ()))
foo(shell=lambda: 0)
foo(shell=f"{b''}")
x = 1
foo(shell=f"{x=}")

View File

@@ -9,10 +9,3 @@ os.system("tar cf foo.tar bar/*")
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
subprocess.Popen(["chmod", "+w", "*.py"], shell={**{**{}}})
# Additional truthiness cases for generator, lambda, and f-strings
subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
subprocess.Popen("chmod +w foo*", shell=lambda: 0)
subprocess.Popen("chmod +w foo*", shell=f"{b''}")
x = 1
subprocess.Popen("chmod +w foo*", shell=f"{x=}")

View File

@@ -1,10 +1,10 @@
# The lexer emits a string token if it's unterminated
# The lexer doesn't emit a string token if it's unterminated
"a" "b
"a" "b" "c
"a" """b
c""" "d
# This is also true for
# For f-strings, the `FStringRanges` won't contain the range for
# unterminated f-strings.
f"a" f"b
f"a" f"b" f"c

View File

@@ -78,11 +78,3 @@ b: None | Literal[None] | None
c: (None | Literal[None]) | None
d: None | (Literal[None] | None)
e: None | ((None | Literal[None]) | None) | None
# Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -197,10 +197,3 @@ for x in {**a, **b} or [None]:
# https://github.com/astral-sh/ruff/issues/7127
def f(a: "'b' or 'c'"): ...
# https://github.com/astral-sh/ruff/issues/20703
print(f"{b''}" or "bar") # SIM222
x = 1
print(f"{x=}" or "bar") # SIM222
(lambda: 1) or True # SIM222
(i for i in range(1)) or "bar" # SIM222

View File

@@ -1,264 +0,0 @@
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
a (int): The first number to add.
b (int): The second number to add.
Returns:
int: The sum of the two numbers.
"""
return a + b
# DOC102
def multiply_list_elements(lst):
"""
Multiplies each element in a list by a given multiplier.
Args:
lst (list of int): A list of integers.
multiplier (int): The multiplier for each element in the list.
Returns:
list of int: A new list with each element multiplied.
"""
return [x * multiplier for x in lst]
# DOC102
def find_max_value():
"""
Finds the maximum value in a list of numbers.
Args:
numbers (list of int): A list of integers to search through.
Returns:
int: The maximum value found in the list.
"""
return max(numbers)
# DOC102
def create_user_profile(location="here"):
"""
Creates a user profile with basic information.
Args:
name (str): The name of the user.
age (int): The age of the user.
email (str): The user's email address.
location (str): The location of the user.
Returns:
dict: A dictionary containing the user's profile.
"""
return {
'name': name,
'age': age,
'email': email,
'location': location
}
# DOC102
def calculate_total_price(item_prices, discount):
"""
Calculates the total price after applying tax and a discount.
Args:
item_prices (list of float): A list of prices for each item.
tax_rate (float): The tax rate to apply.
discount (float): The discount to subtract from the total.
Returns:
float: The final total price after tax and discount.
"""
total = sum(item_prices)
total_with_tax = total + (total * tax_rate)
final_total = total_with_tax - discount
return final_total
# DOC102
def send_email(subject, body, bcc_address=None):
"""
Sends an email to the specified recipients.
Args:
subject (str): The subject of the email.
body (str): The content of the email.
to_address (str): The recipient's email address.
cc_address (str, optional): The email address for CC. Defaults to None.
bcc_address (str, optional): The email address for BCC. Defaults to None.
Returns:
bool: True if the email was sent successfully, False otherwise.
"""
return True
# DOC102
def concatenate_strings(*args):
"""
Concatenates multiple strings with a specified separator.
Args:
separator (str): The separator to use between strings.
*args (str): Variable length argument list of strings to concatenate.
Returns:
str: A single concatenated string.
"""
return separator.join(args)
# DOC102
def process_order(order_id):
"""
Processes an order with a list of items and optional order details.
Args:
order_id (int): The unique identifier for the order.
*items (str): Variable length argument list of items in the order.
**details (dict): Additional details such as shipping method and address.
Returns:
dict: A dictionary containing the order summary.
"""
return {
'order_id': order_id,
'items': items,
'details': details
}
class Calculator:
"""
A simple calculator class that can perform basic arithmetic operations.
"""
# DOC102
def __init__(self):
"""
Initializes the calculator with an initial value.
Args:
value (int, optional): The initial value of the calculator. Defaults to 0.
"""
self.value = value
# DOC102
def add(self, number2):
"""
Adds a number to the current value.
Args:
number (int or float): The number to add to the current value.
Returns:
int or float: The updated value after addition.
"""
self.value += number + number2
return self.value
# DOC102
@classmethod
def from_string(cls):
"""
Creates a Calculator instance from a string representation of a number.
Args:
value_str (str): The string representing the initial value.
Returns:
Calculator: A new instance of Calculator initialized with the value from the string.
"""
value = float(value_str)
return cls(value)
# DOC102
@staticmethod
def is_valid_number():
"""
Checks if a given number is valid (int or float).
Args:
number (any): The value to check.
Returns:
bool: True if the number is valid, False otherwise.
"""
return isinstance(number, (int, float))
# OK
def foo(param1, param2, *args, **kwargs):
"""Foo.
Args:
param1 (int): The first parameter.
param2 (:obj:`str`, optional): The second parameter. Defaults to None.
Second line of description: should be indented.
*args: Variable length argument list.
**kwargs: Arbitrary keyword arguments.
"""
return
# OK
def on_server_unloaded(self, server_context: ServerContext) -> None:
''' Execute ``on_server_unloaded`` from ``server_lifecycle.py`` (if
it is defined) when the server cleanly exits. (Before stopping the
server's ``IOLoop``.)
Args:
server_context (ServerContext) :
.. warning::
In practice this code may not run, since servers are often killed
by a signal.
'''
return self._lifecycle_handler.on_server_unloaded(server_context)
# OK
def function_with_kwargs(param1, param2, **kwargs):
"""Function with **kwargs parameter.
Args:
param1 (int): The first parameter.
param2 (str): The second parameter.
extra_param (str): An extra parameter that may be passed via **kwargs.
another_extra (int): Another extra parameter.
"""
return
# OK
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
b: The second number to add.
Returns:
int: The sum of the two numbers.
"""
return
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Args:
a: The first number to add.
b: The second number to add.
Returns:
int: The sum of the two numbers.
"""
return a + b

View File

@@ -1,372 +0,0 @@
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
a : int
The first number to add.
b : int
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
# DOC102
def multiply_list_elements(lst):
"""
Multiplies each element in a list by a given multiplier.
Parameters
----------
lst : list of int
A list of integers.
multiplier : int
The multiplier for each element in the list.
Returns
-------
list of int
A new list with each element multiplied.
"""
return [x * multiplier for x in lst]
# DOC102
def find_max_value():
"""
Finds the maximum value in a list of numbers.
Parameters
----------
numbers : list of int
A list of integers to search through.
Returns
-------
int
The maximum value found in the list.
"""
return max(numbers)
# DOC102
def create_user_profile(location="here"):
"""
Creates a user profile with basic information.
Parameters
----------
name : str
The name of the user.
age : int
The age of the user.
email : str
The user's email address.
location : str, optional
The location of the user, by default "here".
Returns
-------
dict
A dictionary containing the user's profile.
"""
return {
'name': name,
'age': age,
'email': email,
'location': location
}
# DOC102
def calculate_total_price(item_prices, discount):
"""
Calculates the total price after applying tax and a discount.
Parameters
----------
item_prices : list of float
A list of prices for each item.
tax_rate : float
The tax rate to apply.
discount : float
The discount to subtract from the total.
Returns
-------
float
The final total price after tax and discount.
"""
total = sum(item_prices)
total_with_tax = total + (total * tax_rate)
final_total = total_with_tax - discount
return final_total
# DOC102
def send_email(subject, body, bcc_address=None):
"""
Sends an email to the specified recipients.
Parameters
----------
subject : str
The subject of the email.
body : str
The content of the email.
to_address : str
The recipient's email address.
cc_address : str, optional
The email address for CC, by default None.
bcc_address : str, optional
The email address for BCC, by default None.
Returns
-------
bool
True if the email was sent successfully, False otherwise.
"""
return True
# DOC102
def concatenate_strings(*args):
"""
Concatenates multiple strings with a specified separator.
Parameters
----------
separator : str
The separator to use between strings.
*args : str
Variable length argument list of strings to concatenate.
Returns
-------
str
A single concatenated string.
"""
return True
# DOC102
def process_order(order_id):
"""
Processes an order with a list of items and optional order details.
Parameters
----------
order_id : int
The unique identifier for the order.
*items : str
Variable length argument list of items in the order.
**details : dict
Additional details such as shipping method and address.
Returns
-------
dict
A dictionary containing the order summary.
"""
return {
'order_id': order_id,
'items': items,
'details': details
}
class Calculator:
"""
A simple calculator class that can perform basic arithmetic operations.
"""
# DOC102
def __init__(self):
"""
Initializes the calculator with an initial value.
Parameters
----------
value : int, optional
The initial value of the calculator, by default 0.
"""
self.value = value
# DOC102
def add(self, number2):
"""
Adds two numbers to the current value.
Parameters
----------
number : int or float
The first number to add.
number2 : int or float
The second number to add.
Returns
-------
int or float
The updated value after addition.
"""
self.value += number + number2
return self.value
# DOC102
@classmethod
def from_string(cls):
"""
Creates a Calculator instance from a string representation of a number.
Parameters
----------
value_str : str
The string representing the initial value.
Returns
-------
Calculator
A new instance of Calculator initialized with the value from the string.
"""
value = float(value_str)
return cls(value)
# DOC102
@staticmethod
def is_valid_number():
"""
Checks if a given number is valid (int or float).
Parameters
----------
number : any
The value to check.
Returns
-------
bool
True if the number is valid, False otherwise.
"""
return isinstance(number, (int, float))
# OK
def function_with_kwargs(param1, param2, **kwargs):
"""Function with **kwargs parameter.
Parameters
----------
param1 : int
The first parameter.
param2 : str
The second parameter.
extra_param : str
An extra parameter that may be passed via **kwargs.
another_extra : int
Another extra parameter.
"""
return True
# OK
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
b
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
# DOC102
def add_numbers(b):
"""
Adds two numbers and returns the result.
Parameters
----------
a
The first number to add.
b
The second number to add.
Returns
-------
int
The sum of the two numbers.
"""
return a + b
class Foo:
# OK
def send_help(self, *args: Any) -> Any:
"""|coro|
Shows the help command for the specified entity if given.
The entity can be a command or a cog.
If no entity is given, then it'll show help for the
entire bot.
If the entity is a string, then it looks up whether it's a
:class:`Cog` or a :class:`Command`.
.. note::
Due to the way this function works, instead of returning
something similar to :meth:`~.commands.HelpCommand.command_not_found`
this returns :class:`None` on bad input or no help command.
Parameters
----------
entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]]
The entity to show help for.
Returns
-------
Any
The result of the help command, if any.
"""
return
# OK
@classmethod
async def convert(cls, ctx: Context, argument: str) -> Self:
"""|coro|
The method that actually converters an argument to the flag mapping.
Parameters
----------
cls: Type[:class:`FlagConverter`]
The flag converter class.
ctx: :class:`Context`
The invocation context.
argument: :class:`str`
The argument to convert from.
Raises
------
FlagError
A flag related parsing error.
CommandError
A command related error.
Returns
-------
:class:`FlagConverter`
The flag converter instance with all flags parsed.
"""
return

View File

@@ -18,20 +18,3 @@ def print_third_word(word: Hello.Text) -> None:
def print_fourth_word(word: Goodbye) -> None:
print(word)
import typing_extensions
import typing_extensions as TypingExt
from typing_extensions import Text as TextAlias
def print_fifth_word(word: typing_extensions.Text) -> None:
print(word)
def print_sixth_word(word: TypingExt.Text) -> None:
print(word)
def print_seventh_word(word: TextAlias) -> None:
print(word)

View File

@@ -43,7 +43,7 @@ class Foo:
T = typing.TypeVar(*args)
x: typing.TypeAlias = list[T]
# `default` was added in Python 3.13
# `default` should be skipped for now, added in Python 3.13
T = typing.TypeVar("T", default=Any)
x: typing.TypeAlias = list[T]
@@ -90,9 +90,9 @@ PositiveList = TypeAliasType(
"PositiveList2", list[Annotated[T, Gt(0)]], type_params=(T,)
)
# `default` was added in Python 3.13
# `default` should be skipped for now, added in Python 3.13
T = typing.TypeVar("T", default=Any)
AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
AnyList = TypeAliasType("AnyList", list[T], typep_params=(T,))
# unsafe fix if comments within the fix
T = TypeVar("T")
@@ -128,7 +128,3 @@ T: TypeAlias = ( # comment0
str # comment6
# comment7
) # comment8
# Test case for TypeVar with default - should be converted when preview mode is enabled
T_default = TypeVar("T_default", default=int)
DefaultList: TypeAlias = list[T_default]

View File

@@ -122,7 +122,7 @@ class MixedGenerics[U]:
return (u, t)
# default requires 3.13
# TODO(brent) default requires 3.13
V = TypeVar("V", default=Any, bound=str)
@@ -130,14 +130,6 @@ class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
var: V
# Test case for TypeVar with default but no bound
W = TypeVar("W", default=int)
class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
var: W
# nested classes and functions are skipped
class Outer:
class Inner(Generic[T]):

View File

@@ -44,7 +44,9 @@ def any_str_param(s: AnyStr) -> AnyStr:
return s
# default requires 3.13
# these cases are not handled
# TODO(brent) default requires 3.13
V = TypeVar("V", default=Any, bound=str)
@@ -52,8 +54,6 @@ def default_var(v: V) -> V:
return v
# these cases are not handled
def outer():
def inner(t: T) -> T:
return t

View File

@@ -81,7 +81,6 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::UndocumentedPublicPackage,
]);
let enforce_pydoclint = checker.any_rule_enabled(&[
Rule::DocstringExtraneousParameter,
Rule::DocstringMissingReturns,
Rule::DocstringExtraneousReturns,
Rule::DocstringMissingYields,

View File

@@ -50,6 +50,24 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::nonlocal_and_global(checker, nonlocal);
}
}
Stmt::Break(_) => {
if checker.is_rule_enabled(Rule::BreakOutsideLoop) {
pyflakes::rules::break_outside_loop(
checker,
stmt,
&mut checker.semantic.current_statements().skip(1),
);
}
}
Stmt::Continue(_) => {
if checker.is_rule_enabled(Rule::ContinueOutsideLoop) {
pyflakes::rules::continue_outside_loop(
checker,
stmt,
&mut checker.semantic.current_statements().skip(1),
);
}
}
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
is_async,

View File

@@ -697,7 +697,6 @@ impl SemanticSyntaxContext for Checker<'_> {
}
}
SemanticSyntaxErrorKind::FutureFeatureNotDefined(name) => {
// F407
if self.is_rule_enabled(Rule::FutureFeatureNotDefined) {
self.report_diagnostic(
pyflakes::rules::FutureFeatureNotDefined { name },
@@ -705,18 +704,6 @@ impl SemanticSyntaxContext for Checker<'_> {
);
}
}
SemanticSyntaxErrorKind::BreakOutsideLoop => {
// F701
if self.is_rule_enabled(Rule::BreakOutsideLoop) {
self.report_diagnostic(pyflakes::rules::BreakOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::ContinueOutsideLoop => {
// F702
if self.is_rule_enabled(Rule::ContinueOutsideLoop) {
self.report_diagnostic(pyflakes::rules::ContinueOutsideLoop, error.range);
}
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
@@ -824,40 +811,19 @@ impl SemanticSyntaxContext for Checker<'_> {
}
)
}
fn in_loop_context(&self) -> bool {
let mut child = self.semantic.current_statement();
for parent in self.semantic.current_statements().skip(1) {
match parent {
Stmt::For(ast::StmtFor { orelse, .. })
| Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return true;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
false
}
}
impl<'a> Visitor<'a> for Checker<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For functions, defer semantic syntax error checks until the body of the function is
// visited
if !stmt.is_function_def_stmt() {
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
}
// Step 0: Pre-processing
self.semantic.push_node(stmt);
// For Jupyter Notebooks, we'll reset the `IMPORT_BOUNDARY` flag when
// we encounter a cell boundary.
if self.source_type.is_ipynb()

View File

@@ -988,7 +988,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(FastApi, "003") => (RuleGroup::Stable, rules::fastapi::rules::FastApiUnusedPathParameter),
// pydoclint
(Pydoclint, "102") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousParameter),
(Pydoclint, "201") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingReturns),
(Pydoclint, "202") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringExtraneousReturns),
(Pydoclint, "402") => (RuleGroup::Preview, rules::pydoclint::rules::DocstringMissingYields),

View File

@@ -11,7 +11,8 @@ use crate::settings::types::CompiledPerFileIgnoreList;
pub fn get_cwd() -> &'static Path {
#[cfg(target_arch = "wasm32")]
{
Path::new(".")
static CWD: std::sync::LazyLock<PathBuf> = std::sync::LazyLock::new(|| PathBuf::from("."));
&CWD
}
#[cfg(not(target_arch = "wasm32"))]
path_absolutize::path_dedot::CWD.as_path()

View File

@@ -24,6 +24,7 @@ use crate::checkers::tokens::check_tokens;
use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::fix::{FixResult, fix_file};
use crate::message::create_syntax_error_diagnostic;
use crate::noqa::add_noqa;
use crate::package::PackageRoot;
use crate::registry::Rule;
@@ -495,15 +496,15 @@ fn diagnostics_to_messages(
parse_errors
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.chain(unsupported_syntax_errors.iter().map(|syntax_error| {
Diagnostic::invalid_syntax(source_file.clone(), syntax_error, syntax_error)
create_syntax_error_diagnostic(source_file.clone(), syntax_error, syntax_error)
}))
.chain(
semantic_syntax_errors
.iter()
.map(|error| Diagnostic::invalid_syntax(source_file.clone(), error, error)),
.map(|error| create_syntax_error_diagnostic(source_file.clone(), error, error)),
)
.chain(diagnostics.into_iter().map(|mut diagnostic| {
if let Some(range) = diagnostic.range() {

View File

@@ -16,7 +16,7 @@ use ruff_db::files::File;
pub use grouped::GroupedEmitter;
use ruff_notebook::NotebookIndex;
use ruff_source_file::{SourceFile, SourceFileBuilder};
use ruff_text_size::{TextRange, TextSize};
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use sarif::SarifEmitter;
use crate::Fix;
@@ -26,6 +26,24 @@ use crate::settings::types::{OutputFormat, RuffOutputFormat};
mod grouped;
mod sarif;
/// Creates a `Diagnostic` from a syntax error, with the format expected by Ruff.
///
/// This is almost identical to `ruff_db::diagnostic::create_syntax_error_diagnostic`, except the
/// `message` is stored as the primary diagnostic message instead of on the primary annotation.
///
/// TODO(brent) These should be unified at some point, but we keep them separate for now to avoid a
/// ton of snapshot changes while combining ruff's diagnostic type with `Diagnostic`.
pub fn create_syntax_error_diagnostic(
span: impl Into<Span>,
message: impl std::fmt::Display,
range: impl Ranged,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, message);
let span = span.into().with_range(range.range());
diag.annotate(Annotation::primary(span));
diag
}
/// Create a `Diagnostic` from a panic.
pub fn create_panic_diagnostic(error: &PanicError, path: Option<&Path>) -> Diagnostic {
let mut diagnostic = Diagnostic::new(
@@ -242,6 +260,8 @@ mod tests {
use crate::message::{Emitter, EmitterContext, create_lint_diagnostic};
use crate::{Edit, Fix};
use super::create_syntax_error_diagnostic;
pub(super) fn create_syntax_error_diagnostics() -> Vec<Diagnostic> {
let source = r"from os import
@@ -254,7 +274,7 @@ if call(foo
.errors()
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.collect()
}

View File

@@ -242,11 +242,6 @@ pub(crate) const fn is_refined_submodule_import_match_enabled(settings: &LinterS
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/20660
pub(crate) const fn is_type_var_default_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// github.com/astral-sh/ruff/issues/20004
pub(crate) const fn is_b006_check_guaranteed_mutable_expr_enabled(
settings: &LinterSettings,
@@ -270,7 +265,3 @@ pub(crate) const fn is_fix_read_whole_file_enabled(settings: &LinterSettings) ->
pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
pub(crate) const fn is_typing_extensions_str_alias_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -655,11 +655,6 @@ fn check_name(checker: &Checker, expr: &Expr, range: TextRange) {
},
// airflow.datasets
["airflow", "datasets", "DatasetAliasEvent"] => Replacement::None,
["airflow", "datasets", "DatasetEvent"] => Replacement::Message(
"`DatasetEvent` has been made private in Airflow 3. \
Use `dict[str, Any]` for the time being. \
An `AssetEvent` type will be added to the apache-airflow-task-sdk in a future version.",
),
// airflow.hooks
["airflow", "hooks", "base_hook", "BaseHook"] => Replacement::Rename {

View File

@@ -104,49 +104,38 @@ AIR301 `airflow.datasets.DatasetAliasEvent` is removed in Airflow 3.0
49 | # airflow.datasets
50 | DatasetAliasEvent()
| ^^^^^^^^^^^^^^^^^
51 | DatasetEvent()
|
AIR301 `airflow.datasets.DatasetEvent` is removed in Airflow 3.0
--> AIR301_names.py:51:1
|
49 | # airflow.datasets
50 | DatasetAliasEvent()
51 | DatasetEvent()
| ^^^^^^^^^^^^
|
help: `DatasetEvent` has been made private in Airflow 3. Use `dict[str, Any]` for the time being. An `AssetEvent` type will be added to the apache-airflow-task-sdk in a future version.
AIR301 `airflow.operators.subdag.SubDagOperator` is removed in Airflow 3.0
--> AIR301_names.py:55:1
--> AIR301_names.py:54:1
|
54 | # airflow.operators.subdag.*
55 | SubDagOperator()
53 | # airflow.operators.subdag.*
54 | SubDagOperator()
| ^^^^^^^^^^^^^^
56 |
57 | # airflow.operators.postgres_operator
55 |
56 | # airflow.operators.postgres_operator
|
help: The whole `airflow.subdag` module has been removed.
AIR301 `airflow.operators.postgres_operator.Mapping` is removed in Airflow 3.0
--> AIR301_names.py:58:1
--> AIR301_names.py:57:1
|
57 | # airflow.operators.postgres_operator
58 | Mapping()
56 | # airflow.operators.postgres_operator
57 | Mapping()
| ^^^^^^^
59 |
60 | # airflow.secrets
58 |
59 | # airflow.secrets
|
AIR301 [*] `airflow.secrets.cache.SecretCache` is removed in Airflow 3.0
--> AIR301_names.py:65:1
--> AIR301_names.py:64:1
|
64 | # airflow.secrets.cache
65 | SecretCache()
63 | # airflow.secrets.cache
64 | SecretCache()
| ^^^^^^^^^^^
|
help: Use `SecretCache` from `airflow.sdk` instead.
14 | from airflow.datasets import DatasetAliasEvent, DatasetEvent
14 | from airflow.datasets import DatasetAliasEvent
15 | from airflow.operators.postgres_operator import Mapping
16 | from airflow.operators.subdag import SubDagOperator
- from airflow.secrets.cache import SecretCache
@@ -164,211 +153,211 @@ help: Use `SecretCache` from `airflow.sdk` instead.
note: This is an unsafe fix and may change runtime behavior
AIR301 `airflow.triggers.external_task.TaskStateTrigger` is removed in Airflow 3.0
--> AIR301_names.py:69:1
--> AIR301_names.py:68:1
|
68 | # airflow.triggers.external_task
69 | TaskStateTrigger()
67 | # airflow.triggers.external_task
68 | TaskStateTrigger()
| ^^^^^^^^^^^^^^^^
70 |
71 | # airflow.utils.date
69 |
70 | # airflow.utils.date
|
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:72:1
--> AIR301_names.py:71:1
|
71 | # airflow.utils.date
72 | dates.date_range
70 | # airflow.utils.date
71 | dates.date_range
| ^^^^^^^^^^^^^^^^
73 | dates.days_ago
72 | dates.days_ago
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:73:1
--> AIR301_names.py:72:1
|
71 | # airflow.utils.date
72 | dates.date_range
73 | dates.days_ago
70 | # airflow.utils.date
71 | dates.date_range
72 | dates.days_ago
| ^^^^^^^^^^^^^^
74 |
75 | date_range
73 |
74 | date_range
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.date_range` is removed in Airflow 3.0
--> AIR301_names.py:75:1
--> AIR301_names.py:74:1
|
73 | dates.days_ago
74 |
75 | date_range
72 | dates.days_ago
73 |
74 | date_range
| ^^^^^^^^^^
76 | days_ago
77 | infer_time_unit
75 | days_ago
76 | infer_time_unit
|
AIR301 `airflow.utils.dates.days_ago` is removed in Airflow 3.0
--> AIR301_names.py:76:1
--> AIR301_names.py:75:1
|
75 | date_range
76 | days_ago
74 | date_range
75 | days_ago
| ^^^^^^^^
77 | infer_time_unit
78 | parse_execution_date
76 | infer_time_unit
77 | parse_execution_date
|
help: Use `pendulum.today('UTC').add(days=-N, ...)` instead
AIR301 `airflow.utils.dates.infer_time_unit` is removed in Airflow 3.0
--> AIR301_names.py:77:1
--> AIR301_names.py:76:1
|
75 | date_range
76 | days_ago
77 | infer_time_unit
74 | date_range
75 | days_ago
76 | infer_time_unit
| ^^^^^^^^^^^^^^^
78 | parse_execution_date
79 | round_time
77 | parse_execution_date
78 | round_time
|
AIR301 `airflow.utils.dates.parse_execution_date` is removed in Airflow 3.0
--> AIR301_names.py:78:1
--> AIR301_names.py:77:1
|
76 | days_ago
77 | infer_time_unit
78 | parse_execution_date
75 | days_ago
76 | infer_time_unit
77 | parse_execution_date
| ^^^^^^^^^^^^^^^^^^^^
79 | round_time
80 | scale_time_units
78 | round_time
79 | scale_time_units
|
AIR301 `airflow.utils.dates.round_time` is removed in Airflow 3.0
--> AIR301_names.py:79:1
--> AIR301_names.py:78:1
|
77 | infer_time_unit
78 | parse_execution_date
79 | round_time
76 | infer_time_unit
77 | parse_execution_date
78 | round_time
| ^^^^^^^^^^
80 | scale_time_units
79 | scale_time_units
|
AIR301 `airflow.utils.dates.scale_time_units` is removed in Airflow 3.0
--> AIR301_names.py:80:1
--> AIR301_names.py:79:1
|
78 | parse_execution_date
79 | round_time
80 | scale_time_units
77 | parse_execution_date
78 | round_time
79 | scale_time_units
| ^^^^^^^^^^^^^^^^
81 |
82 | # This one was not deprecated.
80 |
81 | # This one was not deprecated.
|
AIR301 `airflow.utils.dag_cycle_tester.test_cycle` is removed in Airflow 3.0
--> AIR301_names.py:87:1
--> AIR301_names.py:86:1
|
86 | # airflow.utils.dag_cycle_tester
87 | test_cycle
85 | # airflow.utils.dag_cycle_tester
86 | test_cycle
| ^^^^^^^^^^
|
AIR301 `airflow.utils.db.create_session` is removed in Airflow 3.0
--> AIR301_names.py:91:1
--> AIR301_names.py:90:1
|
90 | # airflow.utils.db
91 | create_session
89 | # airflow.utils.db
90 | create_session
| ^^^^^^^^^^^^^^
92 |
93 | # airflow.utils.decorators
91 |
92 | # airflow.utils.decorators
|
AIR301 `airflow.utils.decorators.apply_defaults` is removed in Airflow 3.0
--> AIR301_names.py:94:1
--> AIR301_names.py:93:1
|
93 | # airflow.utils.decorators
94 | apply_defaults
92 | # airflow.utils.decorators
93 | apply_defaults
| ^^^^^^^^^^^^^^
95 |
96 | # airflow.utils.file
94 |
95 | # airflow.utils.file
|
help: `apply_defaults` is now unconditionally done and can be safely removed.
AIR301 `airflow.utils.file.mkdirs` is removed in Airflow 3.0
--> AIR301_names.py:97:1
--> AIR301_names.py:96:1
|
96 | # airflow.utils.file
97 | mkdirs
95 | # airflow.utils.file
96 | mkdirs
| ^^^^^^
|
help: Use `pathlib.Path({path}).mkdir` instead
AIR301 `airflow.utils.state.SHUTDOWN` is removed in Airflow 3.0
--> AIR301_names.py:101:1
--> AIR301_names.py:100:1
|
100 | # airflow.utils.state
101 | SHUTDOWN
99 | # airflow.utils.state
100 | SHUTDOWN
| ^^^^^^^^
102 | terminating_states
101 | terminating_states
|
AIR301 `airflow.utils.state.terminating_states` is removed in Airflow 3.0
--> AIR301_names.py:102:1
--> AIR301_names.py:101:1
|
100 | # airflow.utils.state
101 | SHUTDOWN
102 | terminating_states
99 | # airflow.utils.state
100 | SHUTDOWN
101 | terminating_states
| ^^^^^^^^^^^^^^^^^^
103 |
104 | # airflow.utils.trigger_rule
102 |
103 | # airflow.utils.trigger_rule
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.DUMMY` is removed in Airflow 3.0
--> AIR301_names.py:105:1
--> AIR301_names.py:104:1
|
104 | # airflow.utils.trigger_rule
105 | TriggerRule.DUMMY
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
| ^^^^^^^^^^^^^^^^^
106 | TriggerRule.NONE_FAILED_OR_SKIPPED
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
|
AIR301 `airflow.utils.trigger_rule.TriggerRule.NONE_FAILED_OR_SKIPPED` is removed in Airflow 3.0
--> AIR301_names.py:106:1
--> AIR301_names.py:105:1
|
104 | # airflow.utils.trigger_rule
105 | TriggerRule.DUMMY
106 | TriggerRule.NONE_FAILED_OR_SKIPPED
103 | # airflow.utils.trigger_rule
104 | TriggerRule.DUMMY
105 | TriggerRule.NONE_FAILED_OR_SKIPPED
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
AIR301 `airflow.www.auth.has_access` is removed in Airflow 3.0
--> AIR301_names.py:110:1
--> AIR301_names.py:109:1
|
109 | # airflow.www.auth
110 | has_access
108 | # airflow.www.auth
109 | has_access
| ^^^^^^^^^^
111 | has_access_dataset
110 | has_access_dataset
|
AIR301 `airflow.www.auth.has_access_dataset` is removed in Airflow 3.0
--> AIR301_names.py:111:1
--> AIR301_names.py:110:1
|
109 | # airflow.www.auth
110 | has_access
111 | has_access_dataset
108 | # airflow.www.auth
109 | has_access
110 | has_access_dataset
| ^^^^^^^^^^^^^^^^^^
112 |
113 | # airflow.www.utils
111 |
112 | # airflow.www.utils
|
AIR301 `airflow.www.utils.get_sensitive_variables_fields` is removed in Airflow 3.0
--> AIR301_names.py:114:1
--> AIR301_names.py:113:1
|
113 | # airflow.www.utils
114 | get_sensitive_variables_fields
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
115 | should_hide_value_for_key
114 | should_hide_value_for_key
|
AIR301 `airflow.www.utils.should_hide_value_for_key` is removed in Airflow 3.0
--> AIR301_names.py:115:1
--> AIR301_names.py:114:1
|
113 | # airflow.www.utils
114 | get_sensitive_variables_fields
115 | should_hide_value_for_key
112 | # airflow.www.utils
113 | get_sensitive_variables_fields
114 | should_hide_value_for_key
| ^^^^^^^^^^^^^^^^^^^^^^^^^
|

View File

@@ -127,44 +127,3 @@ S602 `subprocess` call with `shell=True` identified, security issue
21 |
22 | # Check dict display with only double-starred expressions can be falsey.
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:38:1
|
37 | # Additional truthiness cases for generator, lambda, and f-strings
38 | Popen("true", shell=(i for i in ()))
| ^^^^^
39 | Popen("true", shell=lambda: 0)
40 | Popen("true", shell=f"{b''}")
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:39:1
|
37 | # Additional truthiness cases for generator, lambda, and f-strings
38 | Popen("true", shell=(i for i in ()))
39 | Popen("true", shell=lambda: 0)
| ^^^^^
40 | Popen("true", shell=f"{b''}")
41 | x = 1
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:40:1
|
38 | Popen("true", shell=(i for i in ()))
39 | Popen("true", shell=lambda: 0)
40 | Popen("true", shell=f"{b''}")
| ^^^^^
41 | x = 1
42 | Popen("true", shell=f"{x=}")
|
S602 `subprocess` call with truthy `shell` seems safe, but may be changed in the future; consider rewriting without `shell`
--> S602.py:42:1
|
40 | Popen("true", shell=f"{b''}")
41 | x = 1
42 | Popen("true", shell=f"{x=}")
| ^^^^^
|

View File

@@ -9,85 +9,3 @@ S604 Function call with `shell=True` parameter identified, security issue
6 |
7 | foo(shell={**{}})
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:11:1
|
10 | # Truthy non-bool values for `shell`
11 | foo(shell=(i for i in ()))
| ^^^
12 | foo(shell=lambda: 0)
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:12:1
|
10 | # Truthy non-bool values for `shell`
11 | foo(shell=(i for i in ()))
12 | foo(shell=lambda: 0)
| ^^^
13 |
14 | # f-strings guaranteed non-empty
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:15:1
|
14 | # f-strings guaranteed non-empty
15 | foo(shell=f"{b''}")
| ^^^
16 | x = 1
17 | foo(shell=f"{x=}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:17:1
|
15 | foo(shell=f"{b''}")
16 | x = 1
17 | foo(shell=f"{x=}")
| ^^^
18 |
19 | # Additional truthiness cases for generator, lambda, and f-strings
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:20:1
|
19 | # Additional truthiness cases for generator, lambda, and f-strings
20 | foo(shell=(i for i in ()))
| ^^^
21 | foo(shell=lambda: 0)
22 | foo(shell=f"{b''}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:21:1
|
19 | # Additional truthiness cases for generator, lambda, and f-strings
20 | foo(shell=(i for i in ()))
21 | foo(shell=lambda: 0)
| ^^^
22 | foo(shell=f"{b''}")
23 | x = 1
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:22:1
|
20 | foo(shell=(i for i in ()))
21 | foo(shell=lambda: 0)
22 | foo(shell=f"{b''}")
| ^^^
23 | x = 1
24 | foo(shell=f"{x=}")
|
S604 Function call with truthy `shell` parameter identified, security issue
--> S604.py:24:1
|
22 | foo(shell=f"{b''}")
23 | x = 1
24 | foo(shell=f"{x=}")
| ^^^
|

View File

@@ -43,44 +43,3 @@ S609 Possible wildcard injection in call due to `*` usage
9 |
10 | subprocess.Popen(["chmod", "+w", "*.py"], shell={**{}})
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:14:18
|
13 | # Additional truthiness cases for generator, lambda, and f-strings
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
| ^^^^^^^^^^^^^^^
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:15:18
|
13 | # Additional truthiness cases for generator, lambda, and f-strings
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
| ^^^^^^^^^^^^^^^
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
17 | x = 1
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:16:18
|
14 | subprocess.Popen("chmod +w foo*", shell=(i for i in ()))
15 | subprocess.Popen("chmod +w foo*", shell=lambda: 0)
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
| ^^^^^^^^^^^^^^^
17 | x = 1
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
|
S609 Possible wildcard injection in call due to `*` usage
--> S609.py:18:18
|
16 | subprocess.Popen("chmod +w foo*", shell=f"{b''}")
17 | x = 1
18 | subprocess.Popen("chmod +w foo*", shell=f"{x=}")
| ^^^^^^^^^^^^^^^
|

View File

@@ -188,10 +188,16 @@ fn move_initialization(
content.push_str(stylist.line_ending().as_str());
content.push_str(stylist.indentation());
if is_b006_unsafe_fix_preserve_assignment_expr_enabled(checker.settings()) {
let annotation = if let Some(ann) = parameter.annotation() {
format!(": {}", locator.slice(ann))
} else {
String::new()
};
let _ = write!(
&mut content,
"{} = {}",
"{}{} = {}",
parameter.parameter.name(),
annotation,
locator.slice(
parenthesized_range(
default.into(),

View File

@@ -16,7 +16,7 @@ help: Replace with `None`; initialize within function
5 + def import_module_wrong(value: dict[str, str] = None):
6 | import os
7 + if value is None:
8 + value = {}
8 + value: dict[str, str] = {}
9 |
10 |
11 | def import_module_with_values_wrong(value: dict[str, str] = {}):
@@ -38,7 +38,7 @@ help: Replace with `None`; initialize within function
10 | import os
11 |
12 + if value is None:
13 + value = {}
13 + value: dict[str, str] = {}
14 | return 2
15 |
16 |
@@ -62,7 +62,7 @@ help: Replace with `None`; initialize within function
17 | import sys
18 | import itertools
19 + if value is None:
20 + value = {}
20 + value: dict[str, str] = {}
21 |
22 |
23 | def from_import_module_wrong(value: dict[str, str] = {}):
@@ -83,7 +83,7 @@ help: Replace with `None`; initialize within function
21 + def from_import_module_wrong(value: dict[str, str] = None):
22 | from os import path
23 + if value is None:
24 + value = {}
24 + value: dict[str, str] = {}
25 |
26 |
27 | def from_imports_module_wrong(value: dict[str, str] = {}):
@@ -106,7 +106,7 @@ help: Replace with `None`; initialize within function
26 | from os import path
27 | from sys import version_info
28 + if value is None:
29 + value = {}
29 + value: dict[str, str] = {}
30 |
31 |
32 | def import_and_from_imports_module_wrong(value: dict[str, str] = {}):
@@ -129,7 +129,7 @@ help: Replace with `None`; initialize within function
31 | import os
32 | from sys import version_info
33 + if value is None:
34 + value = {}
34 + value: dict[str, str] = {}
35 |
36 |
37 | def import_docstring_module_wrong(value: dict[str, str] = {}):
@@ -152,7 +152,7 @@ help: Replace with `None`; initialize within function
36 | """Docstring"""
37 | import os
38 + if value is None:
39 + value = {}
39 + value: dict[str, str] = {}
40 |
41 |
42 | def import_module_wrong(value: dict[str, str] = {}):
@@ -175,7 +175,7 @@ help: Replace with `None`; initialize within function
41 | """Docstring"""
42 | import os; import sys
43 + if value is None:
44 + value = {}
44 + value: dict[str, str] = {}
45 |
46 |
47 | def import_module_wrong(value: dict[str, str] = {}):
@@ -197,7 +197,7 @@ help: Replace with `None`; initialize within function
45 + def import_module_wrong(value: dict[str, str] = None):
46 | """Docstring"""
47 + if value is None:
48 + value = {}
48 + value: dict[str, str] = {}
49 | import os; import sys; x = 1
50 |
51 |
@@ -220,7 +220,7 @@ help: Replace with `None`; initialize within function
51 | """Docstring"""
52 | import os; import sys
53 + if value is None:
54 + value = {}
54 + value: dict[str, str] = {}
55 |
56 |
57 | def import_module_wrong(value: dict[str, str] = {}):
@@ -241,7 +241,7 @@ help: Replace with `None`; initialize within function
55 + def import_module_wrong(value: dict[str, str] = None):
56 | import os; import sys
57 + if value is None:
58 + value = {}
58 + value: dict[str, str] = {}
59 |
60 |
61 | def import_module_wrong(value: dict[str, str] = {}):
@@ -261,7 +261,7 @@ help: Replace with `None`; initialize within function
- def import_module_wrong(value: dict[str, str] = {}):
59 + def import_module_wrong(value: dict[str, str] = None):
60 + if value is None:
61 + value = {}
61 + value: dict[str, str] = {}
62 | import os; import sys; x = 1
63 |
64 |
@@ -282,7 +282,7 @@ help: Replace with `None`; initialize within function
63 + def import_module_wrong(value: dict[str, str] = None):
64 | import os; import sys
65 + if value is None:
66 + value = {}
66 + value: dict[str, str] = {}
67 |
68 |
69 | def import_module_wrong(value: dict[str, str] = {}): import os

View File

@@ -51,7 +51,7 @@ help: Replace with `None`; initialize within function
10 + def baz(a: list = None):
11 | """This one raises a different exception"""
12 + if a is None:
13 + a = []
13 + a: list = []
14 | raise IndexError()
15 |
16 |

View File

@@ -1,31 +1,30 @@
---
source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
---
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:2:1
|
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
| ^^^^^^
3 | "a" "b" "c
4 | "a" """b
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:2:5
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:2:7
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^
3 | "a" "b" "c
4 | "a" """b
|
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:3:1
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^^^^^^
@@ -34,22 +33,10 @@ ISC001 Implicitly concatenated string literals on one line
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:3:5
|
1 | # The lexer emits a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^^^^^
4 | "a" """b
5 | c""" "d
|
help: Combine string literals
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:3:9
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^
@@ -57,6 +44,17 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:3:11
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^
4 | "a" """b
5 | c""" "d
|
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:4:1
|
@@ -66,21 +64,7 @@ ISC001 Implicitly concatenated string literals on one line
5 | | c""" "d
| |____^
6 |
7 | # This is also true for
|
help: Combine string literals
ISC001 Implicitly concatenated string literals on one line
--> ISC_syntax_error.py:4:5
|
2 | "a" "b
3 | "a" "b" "c
4 | "a" """b
| _____^
5 | | c""" "d
| |_______^
6 |
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
help: Combine string literals
@@ -92,13 +76,24 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
| ^^
6 |
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:5:8
|
3 | "a" "b" "c
4 | "a" """b
5 | c""" "d
| ^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:9:8
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -109,7 +104,7 @@ invalid-syntax: f-string: unterminated string
invalid-syntax: Expected FStringEnd, found newline
--> ISC_syntax_error.py:9:9
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -188,6 +183,14 @@ invalid-syntax: f-string: unterminated triple-quoted string
| |__^
|
invalid-syntax: unexpected EOF while parsing
--> ISC_syntax_error.py:30:1
|
28 | "i" "j"
29 | )
| ^
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:30:1
|

View File

@@ -4,17 +4,27 @@ source: crates/ruff_linter/src/rules/flake8_implicit_str_concat/mod.rs
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:2:5
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:2:7
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
| ^
3 | "a" "b" "c
4 | "a" """b
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:3:9
|
1 | # The lexer emits a string token if it's unterminated
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^^
@@ -22,6 +32,17 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:3:11
|
1 | # The lexer doesn't emit a string token if it's unterminated
2 | "a" "b
3 | "a" "b" "c
| ^
4 | "a" """b
5 | c""" "d
|
invalid-syntax: missing closing quote in string literal
--> ISC_syntax_error.py:5:6
|
@@ -30,13 +51,24 @@ invalid-syntax: missing closing quote in string literal
5 | c""" "d
| ^^
6 |
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: Expected a statement
--> ISC_syntax_error.py:5:8
|
3 | "a" "b" "c
4 | "a" """b
5 | c""" "d
| ^
6 |
7 | # For f-strings, the `FStringRanges` won't contain the range for
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:9:8
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -47,7 +79,7 @@ invalid-syntax: f-string: unterminated string
invalid-syntax: Expected FStringEnd, found newline
--> ISC_syntax_error.py:9:9
|
7 | # This is also true for
7 | # For f-strings, the `FStringRanges` won't contain the range for
8 | # unterminated f-strings.
9 | f"a" f"b
| ^
@@ -101,6 +133,14 @@ invalid-syntax: f-string: unterminated triple-quoted string
| |__^
|
invalid-syntax: unexpected EOF while parsing
--> ISC_syntax_error.py:30:1
|
28 | "i" "j"
29 | )
| ^
|
invalid-syntax: f-string: unterminated string
--> ISC_syntax_error.py:30:1
|

View File

@@ -4,8 +4,6 @@ use ruff_python_ast::{
self as ast, Expr, ExprBinOp, ExprContext, ExprNoneLiteral, Operator, PythonVersion,
helpers::{pep_604_union, typing_optional},
name::Name,
operator_precedence::OperatorPrecedence,
parenthesize::parenthesized_range,
};
use ruff_python_semantic::analyze::typing::{traverse_literal, traverse_union};
use ruff_text_size::{Ranged, TextRange};
@@ -240,19 +238,7 @@ fn create_fix(
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
});
let union_expr = pep_604_union(&[new_literal_expr, none_expr]);
// Check if we need parentheses to preserve operator precedence
let content = if needs_parentheses_for_precedence(
semantic,
literal_expr,
checker.comment_ranges(),
checker.source(),
) {
format!("({})", checker.generator().expr(&union_expr))
} else {
checker.generator().expr(&union_expr)
};
let content = checker.generator().expr(&union_expr);
let union_edit = Edit::range_replacement(content, literal_expr.range());
Fix::applicable_edit(union_edit, applicability)
}
@@ -270,37 +256,3 @@ enum UnionKind {
TypingOptional,
BitOr,
}
/// Check if the union expression needs parentheses to preserve operator precedence.
/// This is needed when the union is part of a larger expression where the `|` operator
/// has lower precedence than the surrounding operations (like attribute access).
fn needs_parentheses_for_precedence(
semantic: &ruff_python_semantic::SemanticModel,
literal_expr: &Expr,
comment_ranges: &ruff_python_trivia::CommentRanges,
source: &str,
) -> bool {
// Get the parent expression to check if we're in a context that needs parentheses
let Some(parent_expr) = semantic.current_expression_parent() else {
return false;
};
// Check if the literal expression is already parenthesized
if parenthesized_range(
literal_expr.into(),
parent_expr.into(),
comment_ranges,
source,
)
.is_some()
{
return false; // Already parenthesized, don't add more
}
// Check if the parent expression has higher precedence than the `|` operator
let union_precedence = OperatorPrecedence::BitOr;
let parent_precedence = OperatorPrecedence::from(parent_expr);
// If the parent operation has higher precedence than `|`, we need parentheses
parent_precedence > union_precedence
}

View File

@@ -423,117 +423,5 @@ PYI061 Use `None` rather than `Literal[None]`
79 | d: None | (Literal[None] | None)
80 | e: None | ((None | Literal[None]) | None) | None
| ^^^^
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
|
help: Replace with `None`
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:83:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
| ^^^^
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
|
help: Replace with `Literal[...] | None`
80 | e: None | ((None | Literal[None]) | None) | None
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
- print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
83 + print((Literal[1] | None).__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:84:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
| ^^^^
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
|
help: Replace with `Literal[...] | None`
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
- print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
84 + print((Literal[1] | None).method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:85:18
|
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
| ^^^^
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
|
help: Replace with `Literal[...] | None`
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
- print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
85 + print((Literal[1] | None)[0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:86:18
|
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
| ^^^^
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Literal[...] | None`
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
- print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
86 + print((Literal[1] | None) + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:87:18
|
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
| ^^^^
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Literal[...] | None`
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
- print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
87 + print((Literal[1] | None) * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Literal[...] | None` rather than `Literal[None, ...]`
--> PYI061.py:88:19
|
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
| ^^^^
|
help: Replace with `Literal[...] | None`
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
- print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
88 + print((Literal[1] | None).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -465,153 +465,5 @@ PYI061 Use `None` rather than `Literal[None]`
79 | d: None | (Literal[None] | None)
80 | e: None | ((None | Literal[None]) | None) | None
| ^^^^
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
|
help: Replace with `None`
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:83:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
| ^^^^
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
80 | e: None | ((None | Literal[None]) | None) | None
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
- print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
83 + print(Optional[Literal[1]].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:84:18
|
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
| ^^^^
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
81 |
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
- print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
84 + print(Optional[Literal[1]].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:85:18
|
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
| ^^^^
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
82 | # Test cases for operator precedence issue (https://github.com/astral-sh/ruff/issues/20265)
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
- print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
85 + print(Optional[Literal[1]][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:86:18
|
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
| ^^^^
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
83 | print(Literal[1, None].__dict__) # Should become (Literal[1] | None).__dict__
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
- print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
86 + print(Optional[Literal[1]] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:87:18
|
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
| ^^^^
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
84 | print(Literal[1, None].method()) # Should become (Literal[1] | None).method()
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
- print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
87 + print(Optional[Literal[1]] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
PYI061 [*] Use `Optional[Literal[...]]` rather than `Literal[None, ...]`
--> PYI061.py:88:19
|
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
88 | print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
| ^^^^
|
help: Replace with `Optional[Literal[...]]`
- from typing import Literal, Union
1 + from typing import Literal, Union, Optional
2 |
3 |
4 | def func1(arg1: Literal[None]):
--------------------------------------------------------------------------------
85 | print(Literal[1, None][0]) # Should become (Literal[1] | None)[0]
86 | print(Literal[1, None] + 1) # Should become (Literal[1] | None) + 1
87 | print(Literal[1, None] * 2) # Should become (Literal[1] | None) * 2
- print((Literal[1, None]).__dict__) # Should become ((Literal[1] | None)).__dict__
88 + print((Optional[Literal[1]]).__dict__) # Should become ((Literal[1] | None)).__dict__

View File

@@ -1062,77 +1062,3 @@ help: Replace with `"bar"`
170 |
171 |
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `f"{b''}"` instead of `f"{b''}" or ...`
--> SIM222.py:202:7
|
201 | # https://github.com/astral-sh/ruff/issues/20703
202 | print(f"{b''}" or "bar") # SIM222
| ^^^^^^^^^^^^^^^^^
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
|
help: Replace with `f"{b''}"`
199 | def f(a: "'b' or 'c'"): ...
200 |
201 | # https://github.com/astral-sh/ruff/issues/20703
- print(f"{b''}" or "bar") # SIM222
202 + print(f"{b''}") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `f"{x=}"` instead of `f"{x=}" or ...`
--> SIM222.py:204:7
|
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
| ^^^^^^^^^^^^^^^^
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
|
help: Replace with `f"{x=}"`
201 | # https://github.com/astral-sh/ruff/issues/20703
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
- print(f"{x=}" or "bar") # SIM222
204 + print(f"{x=}") # SIM222
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `lambda: 1` instead of `lambda: 1 or ...`
--> SIM222.py:205:1
|
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
| ^^^^^^^^^^^^^^^^^^^
206 | (i for i in range(1)) or "bar" # SIM222
|
help: Replace with `lambda: 1`
202 | print(f"{b''}" or "bar") # SIM222
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
- (lambda: 1) or True # SIM222
205 + lambda: 1 # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
note: This is an unsafe fix and may change runtime behavior
SIM222 [*] Use `(i for i in range(1))` instead of `(i for i in range(1)) or ...`
--> SIM222.py:206:1
|
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
206 | (i for i in range(1)) or "bar" # SIM222
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Replace with `(i for i in range(1))`
203 | x = 1
204 | print(f"{x=}" or "bar") # SIM222
205 | (lambda: 1) or True # SIM222
- (i for i in range(1)) or "bar" # SIM222
206 + (i for i in range(1)) # SIM222
note: This is an unsafe fix and may change runtime behavior

View File

@@ -28,7 +28,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_google.py"))]
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_google.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_google.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_google.py"))]
@@ -51,7 +50,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::DocstringExtraneousParameter, Path::new("DOC102_numpy.py"))]
#[test_case(Rule::DocstringMissingReturns, Path::new("DOC201_numpy.py"))]
#[test_case(Rule::DocstringExtraneousReturns, Path::new("DOC202_numpy.py"))]
#[test_case(Rule::DocstringMissingYields, Path::new("DOC402_numpy.py"))]

View File

@@ -1,14 +1,14 @@
use itertools::Itertools;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::name::QualifiedName;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, Stmt, visitor};
use ruff_python_semantic::analyze::{function_type, visibility};
use ruff_python_semantic::{Definition, SemanticModel};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_source_file::{LineRanges, NewlineWithTrailingNewline};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
use ruff_source_file::NewlineWithTrailingNewline;
use ruff_text_size::{Ranged, TextRange};
use crate::Violation;
use crate::checkers::ast::Checker;
@@ -18,62 +18,6 @@ use crate::docstrings::styles::SectionStyle;
use crate::registry::Rule;
use crate::rules::pydocstyle::settings::Convention;
/// ## What it does
/// Checks for function docstrings that include parameters which are not
/// in the function signature.
///
/// ## Why is this bad?
/// If a docstring documents a parameter which is not in the function signature,
/// it can be misleading to users and/or a sign of incomplete documentation or
/// refactors.
///
/// ## Example
/// ```python
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
/// Args:
/// distance: Distance traveled.
/// time: Time spent traveling.
/// acceleration: Rate of change of speed.
///
/// Returns:
/// Speed as distance divided by time.
/// """
/// return distance / time
/// ```
///
/// Use instead:
/// ```python
/// def calculate_speed(distance: float, time: float) -> float:
/// """Calculate speed as distance divided by time.
///
/// Args:
/// distance: Distance traveled.
/// time: Time spent traveling.
///
/// Returns:
/// Speed as distance divided by time.
/// """
/// return distance / time
/// ```
#[derive(ViolationMetadata)]
pub(crate) struct DocstringExtraneousParameter {
id: String,
}
impl Violation for DocstringExtraneousParameter {
#[derive_message_formats]
fn message(&self) -> String {
let DocstringExtraneousParameter { id } = self;
format!("Documented parameter `{id}` is not in the function's signature")
}
fn fix_title(&self) -> Option<String> {
Some("Remove the extraneous parameter from the docstring".to_string())
}
}
/// ## What it does
/// Checks for functions with `return` statements that do not have "Returns"
/// sections in their docstrings.
@@ -452,19 +396,6 @@ impl GenericSection {
}
}
/// A parameter in a docstring with its text range.
#[derive(Debug, Clone)]
struct ParameterEntry<'a> {
name: &'a str,
range: TextRange,
}
impl Ranged for ParameterEntry<'_> {
fn range(&self) -> TextRange {
self.range
}
}
/// A "Raises" section in a docstring.
#[derive(Debug)]
struct RaisesSection<'a> {
@@ -483,46 +414,17 @@ impl<'a> RaisesSection<'a> {
/// a "Raises" section.
fn from_section(section: &SectionContext<'a>, style: Option<SectionStyle>) -> Self {
Self {
raised_exceptions: parse_raises(section.following_lines_str(), style),
raised_exceptions: parse_entries(section.following_lines_str(), style),
range: section.range(),
}
}
}
/// An "Args" or "Parameters" section in a docstring.
#[derive(Debug)]
struct ParametersSection<'a> {
parameters: Vec<ParameterEntry<'a>>,
range: TextRange,
}
impl Ranged for ParametersSection<'_> {
fn range(&self) -> TextRange {
self.range
}
}
impl<'a> ParametersSection<'a> {
/// Return the parameters for the docstring, or `None` if the docstring does not contain
/// an "Args" or "Parameters" section.
fn from_section(section: &SectionContext<'a>, style: Option<SectionStyle>) -> Self {
Self {
parameters: parse_parameters(
section.following_lines_str(),
section.following_range().start(),
style,
),
range: section.section_name_range(),
}
}
}
#[derive(Debug, Default)]
struct DocstringSections<'a> {
returns: Option<GenericSection>,
yields: Option<GenericSection>,
raises: Option<RaisesSection<'a>>,
parameters: Option<ParametersSection<'a>>,
}
impl<'a> DocstringSections<'a> {
@@ -530,10 +432,6 @@ impl<'a> DocstringSections<'a> {
let mut docstring_sections = Self::default();
for section in sections {
match section.kind() {
SectionKind::Args | SectionKind::Arguments | SectionKind::Parameters => {
docstring_sections.parameters =
Some(ParametersSection::from_section(&section, style));
}
SectionKind::Raises => {
docstring_sections.raises = Some(RaisesSection::from_section(&section, style));
}
@@ -550,142 +448,18 @@ impl<'a> DocstringSections<'a> {
}
}
/// Parse the entries in a "Parameters" section of a docstring.
///
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
/// entries are found.
fn parse_parameters(
content: &str,
content_start: TextSize,
style: Option<SectionStyle>,
) -> Vec<ParameterEntry<'_>> {
match style {
Some(SectionStyle::Google) => parse_parameters_google(content, content_start),
Some(SectionStyle::Numpy) => parse_parameters_numpy(content, content_start),
None => {
let entries = parse_parameters_google(content, content_start);
if entries.is_empty() {
parse_parameters_numpy(content, content_start)
} else {
entries
}
}
}
}
/// Parses Google-style "Args" sections of the form:
///
/// ```python
/// Args:
/// a (int): The first number to add.
/// b (int): The second number to add.
/// ```
fn parse_parameters_google(content: &str, content_start: TextSize) -> Vec<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = Vec::new();
// Find first entry to determine indentation
let Some(first_arg) = content.lines().next() else {
return entries;
};
let indentation = &first_arg[..first_arg.len() - first_arg.trim_start().len()];
let mut current_pos = TextSize::ZERO;
for line in content.lines() {
let line_start = current_pos;
current_pos = content.full_line_end(line_start);
if let Some(entry) = line.strip_prefix(indentation) {
if entry
.chars()
.next()
.is_some_and(|first_char| !first_char.is_whitespace())
{
let Some((before_colon, _)) = entry.split_once(':') else {
continue;
};
if let Some(param) = before_colon.split_whitespace().next() {
let param_name = param.trim_start_matches('*');
if is_identifier(param_name) {
let param_start = line_start + indentation.text_len();
let param_end = param_start + param.text_len();
entries.push(ParameterEntry {
name: param_name,
range: TextRange::new(
content_start + param_start,
content_start + param_end,
),
});
}
}
}
}
}
entries
}
/// Parses NumPy-style "Parameters" sections of the form:
///
/// ```python
/// Parameters
/// ----------
/// a : int
/// The first number to add.
/// b : int
/// The second number to add.
/// ```
fn parse_parameters_numpy(content: &str, content_start: TextSize) -> Vec<ParameterEntry<'_>> {
let mut entries: Vec<ParameterEntry> = Vec::new();
let mut lines = content.lines();
let Some(dashes) = lines.next() else {
return entries;
};
let indentation = &dashes[..dashes.len() - dashes.trim_start().len()];
let mut current_pos = content.full_line_end(dashes.text_len());
for potential in lines {
let line_start = current_pos;
current_pos = content.full_line_end(line_start);
if let Some(entry) = potential.strip_prefix(indentation) {
if entry
.chars()
.next()
.is_some_and(|first_char| !first_char.is_whitespace())
{
if let Some(before_colon) = entry.split(':').next() {
let param = before_colon.trim_end();
let param_name = param.trim_start_matches('*');
if is_identifier(param_name) {
let param_start = line_start + indentation.text_len();
let param_end = param_start + param.text_len();
entries.push(ParameterEntry {
name: param_name,
range: TextRange::new(
content_start + param_start,
content_start + param_end,
),
});
}
}
}
}
}
entries
}
/// Parse the entries in a "Raises" section of a docstring.
///
/// Attempts to parse using the specified [`SectionStyle`], falling back to the other style if no
/// entries are found.
fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName<'_>> {
fn parse_entries(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName<'_>> {
match style {
Some(SectionStyle::Google) => parse_raises_google(content),
Some(SectionStyle::Numpy) => parse_raises_numpy(content),
Some(SectionStyle::Google) => parse_entries_google(content),
Some(SectionStyle::Numpy) => parse_entries_numpy(content),
None => {
let entries = parse_raises_google(content);
let entries = parse_entries_google(content);
if entries.is_empty() {
parse_raises_numpy(content)
parse_entries_numpy(content)
} else {
entries
}
@@ -693,14 +467,14 @@ fn parse_raises(content: &str, style: Option<SectionStyle>) -> Vec<QualifiedName
}
}
/// Parses Google-style "Raises" section of the form:
/// Parses Google-style docstring sections of the form:
///
/// ```python
/// Raises:
/// FasterThanLightError: If speed is greater than the speed of light.
/// DivisionByZero: If attempting to divide by zero.
/// ```
fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
fn parse_entries_google(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
for potential in content.lines() {
let Some(colon_idx) = potential.find(':') else {
@@ -712,7 +486,7 @@ fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
entries
}
/// Parses NumPy-style "Raises" section of the form:
/// Parses NumPy-style docstring sections of the form:
///
/// ```python
/// Raises
@@ -722,7 +496,7 @@ fn parse_raises_google(content: &str) -> Vec<QualifiedName<'_>> {
/// DivisionByZero
/// If attempting to divide by zero.
/// ```
fn parse_raises_numpy(content: &str) -> Vec<QualifiedName<'_>> {
fn parse_entries_numpy(content: &str) -> Vec<QualifiedName<'_>> {
let mut entries: Vec<QualifiedName> = Vec::new();
let mut lines = content.lines();
let Some(dashes) = lines.next() else {
@@ -1093,17 +867,6 @@ fn is_generator_function_annotated_as_returning_none(
.is_some_and(GeneratorOrIteratorArguments::indicates_none_returned)
}
fn parameters_from_signature<'a>(docstring: &'a Docstring) -> Vec<&'a str> {
let mut parameters = Vec::new();
let Some(function) = docstring.definition.as_function_def() else {
return parameters;
};
for param in &function.parameters {
parameters.push(param.name());
}
parameters
}
fn is_one_line(docstring: &Docstring) -> bool {
let mut non_empty_line_count = 0;
for line in NewlineWithTrailingNewline::from(docstring.body().as_str()) {
@@ -1117,7 +880,7 @@ fn is_one_line(docstring: &Docstring) -> bool {
true
}
/// DOC102, DOC201, DOC202, DOC402, DOC403, DOC501, DOC502
/// DOC201, DOC202, DOC402, DOC403, DOC501, DOC502
pub(crate) fn check_docstring(
checker: &Checker,
definition: &Definition,
@@ -1157,8 +920,6 @@ pub(crate) fn check_docstring(
visitor.finish()
};
let signature_parameters = parameters_from_signature(docstring);
// DOC201
if checker.is_rule_enabled(Rule::DocstringMissingReturns) {
if should_document_returns(function_def)
@@ -1247,25 +1008,6 @@ pub(crate) fn check_docstring(
}
}
// DOC102
if checker.is_rule_enabled(Rule::DocstringExtraneousParameter) {
// Don't report extraneous parameters if the signature defines *args or **kwargs
if function_def.parameters.vararg.is_none() && function_def.parameters.kwarg.is_none() {
if let Some(docstring_params) = docstring_sections.parameters {
for docstring_param in &docstring_params.parameters {
if !signature_parameters.contains(&docstring_param.name) {
checker.report_diagnostic(
DocstringExtraneousParameter {
id: docstring_param.name.to_string(),
},
docstring_param.range(),
);
}
}
}
}
}
// Avoid applying "extraneous" rules to abstract methods. An abstract method's docstring _could_
// document that it raises an exception without including the exception in the implementation.
if !visibility::is_abstract(&function_def.decorator_list, semantic) {

View File

@@ -1,180 +0,0 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_google.py:7:9
|
6 | Args:
7 | a (int): The first number to add.
| ^
8 | b (int): The second number to add.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `multiplier` is not in the function's signature
--> DOC102_google.py:23:9
|
21 | Args:
22 | lst (list of int): A list of integers.
23 | multiplier (int): The multiplier for each element in the list.
| ^^^^^^^^^^
24 |
25 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `numbers` is not in the function's signature
--> DOC102_google.py:37:9
|
36 | Args:
37 | numbers (list of int): A list of integers to search through.
| ^^^^^^^
38 |
39 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `name` is not in the function's signature
--> DOC102_google.py:51:9
|
50 | Args:
51 | name (str): The name of the user.
| ^^^^
52 | age (int): The age of the user.
53 | email (str): The user's email address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `age` is not in the function's signature
--> DOC102_google.py:52:9
|
50 | Args:
51 | name (str): The name of the user.
52 | age (int): The age of the user.
| ^^^
53 | email (str): The user's email address.
54 | location (str): The location of the user.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `email` is not in the function's signature
--> DOC102_google.py:53:9
|
51 | name (str): The name of the user.
52 | age (int): The age of the user.
53 | email (str): The user's email address.
| ^^^^^
54 | location (str): The location of the user.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `tax_rate` is not in the function's signature
--> DOC102_google.py:74:9
|
72 | Args:
73 | item_prices (list of float): A list of prices for each item.
74 | tax_rate (float): The tax rate to apply.
| ^^^^^^^^
75 | discount (float): The discount to subtract from the total.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `to_address` is not in the function's signature
--> DOC102_google.py:94:9
|
92 | subject (str): The subject of the email.
93 | body (str): The content of the email.
94 | to_address (str): The recipient's email address.
| ^^^^^^^^^^
95 | cc_address (str, optional): The email address for CC. Defaults to None.
96 | bcc_address (str, optional): The email address for BCC. Defaults to None.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `cc_address` is not in the function's signature
--> DOC102_google.py:95:9
|
93 | body (str): The content of the email.
94 | to_address (str): The recipient's email address.
95 | cc_address (str, optional): The email address for CC. Defaults to None.
| ^^^^^^^^^^
96 | bcc_address (str, optional): The email address for BCC. Defaults to None.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `items` is not in the function's signature
--> DOC102_google.py:126:9
|
124 | Args:
125 | order_id (int): The unique identifier for the order.
126 | *items (str): Variable length argument list of items in the order.
| ^^^^^^
127 | **details (dict): Additional details such as shipping method and address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `details` is not in the function's signature
--> DOC102_google.py:127:9
|
125 | order_id (int): The unique identifier for the order.
126 | *items (str): Variable length argument list of items in the order.
127 | **details (dict): Additional details such as shipping method and address.
| ^^^^^^^^^
128 |
129 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value` is not in the function's signature
--> DOC102_google.py:150:13
|
149 | Args:
150 | value (int, optional): The initial value of the calculator. Defaults to 0.
| ^^^^^
151 | """
152 | self.value = value
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_google.py:160:13
|
159 | Args:
160 | number (int or float): The number to add to the current value.
| ^^^^^^
161 |
162 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value_str` is not in the function's signature
--> DOC102_google.py:175:13
|
174 | Args:
175 | value_str (str): The string representing the initial value.
| ^^^^^^^^^
176 |
177 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_google.py:190:13
|
189 | Args:
190 | number (any): The value to check.
| ^^^^^^
191 |
192 | Returns:
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_google.py:258:9
|
257 | Args:
258 | a: The first number to add.
| ^
259 | b: The second number to add.
|
help: Remove the extraneous parameter from the docstring

View File

@@ -1,189 +0,0 @@
---
source: crates/ruff_linter/src/rules/pydoclint/mod.rs
---
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_numpy.py:8:5
|
6 | Parameters
7 | ----------
8 | a : int
| ^
9 | The first number to add.
10 | b : int
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `multiplier` is not in the function's signature
--> DOC102_numpy.py:30:5
|
28 | lst : list of int
29 | A list of integers.
30 | multiplier : int
| ^^^^^^^^^^
31 | The multiplier for each element in the list.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `numbers` is not in the function's signature
--> DOC102_numpy.py:48:5
|
46 | Parameters
47 | ----------
48 | numbers : list of int
| ^^^^^^^
49 | A list of integers to search through.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `name` is not in the function's signature
--> DOC102_numpy.py:66:5
|
64 | Parameters
65 | ----------
66 | name : str
| ^^^^
67 | The name of the user.
68 | age : int
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `age` is not in the function's signature
--> DOC102_numpy.py:68:5
|
66 | name : str
67 | The name of the user.
68 | age : int
| ^^^
69 | The age of the user.
70 | email : str
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `email` is not in the function's signature
--> DOC102_numpy.py:70:5
|
68 | age : int
69 | The age of the user.
70 | email : str
| ^^^^^
71 | The user's email address.
72 | location : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `tax_rate` is not in the function's signature
--> DOC102_numpy.py:97:5
|
95 | item_prices : list of float
96 | A list of prices for each item.
97 | tax_rate : float
| ^^^^^^^^
98 | The tax rate to apply.
99 | discount : float
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `to_address` is not in the function's signature
--> DOC102_numpy.py:124:5
|
122 | body : str
123 | The content of the email.
124 | to_address : str
| ^^^^^^^^^^
125 | The recipient's email address.
126 | cc_address : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `cc_address` is not in the function's signature
--> DOC102_numpy.py:126:5
|
124 | to_address : str
125 | The recipient's email address.
126 | cc_address : str, optional
| ^^^^^^^^^^
127 | The email address for CC, by default None.
128 | bcc_address : str, optional
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `items` is not in the function's signature
--> DOC102_numpy.py:168:5
|
166 | order_id : int
167 | The unique identifier for the order.
168 | *items : str
| ^^^^^^
169 | Variable length argument list of items in the order.
170 | **details : dict
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `details` is not in the function's signature
--> DOC102_numpy.py:170:5
|
168 | *items : str
169 | Variable length argument list of items in the order.
170 | **details : dict
| ^^^^^^^^^
171 | Additional details such as shipping method and address.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value` is not in the function's signature
--> DOC102_numpy.py:197:9
|
195 | Parameters
196 | ----------
197 | value : int, optional
| ^^^^^
198 | The initial value of the calculator, by default 0.
199 | """
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_numpy.py:209:9
|
207 | Parameters
208 | ----------
209 | number : int or float
| ^^^^^^
210 | The first number to add.
211 | number2 : int or float
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `value_str` is not in the function's signature
--> DOC102_numpy.py:230:9
|
228 | Parameters
229 | ----------
230 | value_str : str
| ^^^^^^^^^
231 | The string representing the initial value.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `number` is not in the function's signature
--> DOC102_numpy.py:249:9
|
247 | Parameters
248 | ----------
249 | number : any
| ^^^^^^
250 | The value to check.
|
help: Remove the extraneous parameter from the docstring
DOC102 Documented parameter `a` is not in the function's signature
--> DOC102_numpy.py:300:5
|
298 | Parameters
299 | ----------
300 | a
| ^
301 | The first number to add.
302 | b
|
help: Remove the extraneous parameter from the docstring

View File

@@ -1,6 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use crate::Violation;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
/// ## What it does
/// Checks for `break` statements outside of loops.
@@ -26,3 +29,28 @@ impl Violation for BreakOutsideLoop {
"`break` outside loop".to_string()
}
}
/// F701
pub(crate) fn break_outside_loop<'a>(
checker: &Checker,
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) {
let mut child = stmt;
for parent in parents {
match parent {
Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
checker.report_diagnostic(BreakOutsideLoop, stmt.range());
}

View File

@@ -1,6 +1,9 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::{self as ast, Stmt};
use crate::Violation;
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_text_size::Ranged;
use crate::{Violation, checkers::ast::Checker};
/// ## What it does
/// Checks for `continue` statements outside of loops.
@@ -26,3 +29,28 @@ impl Violation for ContinueOutsideLoop {
"`continue` not properly in loop".to_string()
}
}
/// F702
pub(crate) fn continue_outside_loop<'a>(
checker: &Checker,
stmt: &'a Stmt,
parents: &mut impl Iterator<Item = &'a Stmt>,
) {
let mut child = stmt;
for parent in parents {
match parent {
Stmt::For(ast::StmtFor { orelse, .. }) | Stmt::While(ast::StmtWhile { orelse, .. }) => {
if !orelse.contains(child) {
return;
}
}
Stmt::FunctionDef(_) | Stmt::ClassDef(_) => {
break;
}
_ => {}
}
child = parent;
}
checker.report_diagnostic(ContinueOutsideLoop, stmt.range());
}

View File

@@ -23,17 +23,16 @@ invalid-syntax: missing closing quote in string literal
9 | # Unterminated f-string
|
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:7:6
invalid-syntax: Expected a statement
--> invalid_characters_syntax_error.py:7:7
|
5 | b = '␈'
6 | # Unterminated string
7 | b = '␈
| ^
| ^
8 | b = '␈'
9 | # Unterminated f-string
|
help: Replace with escape sequence
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:8:6
@@ -47,18 +46,6 @@ PLE2510 Invalid unescaped character backspace, use "\b" instead
|
help: Replace with escape sequence
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:10:7
|
8 | b = '␈'
9 | # Unterminated f-string
10 | b = f'␈
| ^
11 | b = f'␈'
12 | # Implicitly concatenated
|
help: Replace with escape sequence
invalid-syntax: f-string: unterminated string
--> invalid_characters_syntax_error.py:10:7
|
@@ -122,12 +109,11 @@ invalid-syntax: missing closing quote in string literal
| ^^
|
PLE2510 Invalid unescaped character backspace, use "\b" instead
--> invalid_characters_syntax_error.py:13:15
invalid-syntax: Expected a statement
--> invalid_characters_syntax_error.py:13:16
|
11 | b = f'␈'
12 | # Implicitly concatenated
13 | b = '␈' f'␈' '␈
| ^
| ^
|
help: Replace with escape sequence

View File

@@ -19,7 +19,7 @@ mod tests {
use crate::rules::{isort, pyupgrade};
use crate::settings::types::PreviewMode;
use crate::test::{test_path, test_snippet};
use crate::{assert_diagnostics, assert_diagnostics_diff, settings};
use crate::{assert_diagnostics, settings};
#[test_case(Rule::ConvertNamedTupleFunctionalToClass, Path::new("UP014.py"))]
#[test_case(Rule::ConvertTypedDictFunctionalToClass, Path::new("UP013.py"))]
@@ -126,7 +126,6 @@ mod tests {
}
#[test_case(Rule::SuperCallWithParameters, Path::new("UP008.py"))]
#[test_case(Rule::TypingTextStrAlias, Path::new("UP019.py"))]
fn rules_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview", path.to_string_lossy());
let diagnostics = test_path(
@@ -140,28 +139,6 @@ mod tests {
Ok(())
}
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.py"))]
#[test_case(Rule::NonPEP695TypeAlias, Path::new("UP040.pyi"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))]
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
fn type_var_default_preview(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}__preview_diff", path.to_string_lossy());
assert_diagnostics_diff!(
snapshot,
Path::new("pyupgrade").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Disabled,
..settings::LinterSettings::for_rule(rule_code)
},
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
);
Ok(())
}
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_0.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_1.py"))]
#[test_case(Rule::QuotedAnnotation, Path::new("UP037_2.pyi"))]

View File

@@ -6,7 +6,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::helpers::any_over_expr;
use ruff_python_ast::str::{leading_quote, trailing_quote};
use ruff_python_ast::{self as ast, Expr, Keyword, StringFlags};
use ruff_python_ast::{self as ast, Expr, Keyword};
use ruff_python_literal::format::{
FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate,
};
@@ -430,7 +430,7 @@ pub(crate) fn f_strings(checker: &Checker, call: &ast::ExprCall, summary: &Forma
// dot is the start of an attribute access.
break token.start();
}
TokenKind::String if !token.unwrap_string_flags().is_unclosed() => {
TokenKind::String => {
match FStringConversion::try_convert(token.range(), &mut summary, checker.locator())
{
// If the format string contains side effects that would need to be repeated,

View File

@@ -14,14 +14,13 @@ use ruff_python_ast::{
use ruff_python_semantic::SemanticModel;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_type_var_default_enabled;
pub(crate) use non_pep695_generic_class::*;
pub(crate) use non_pep695_generic_function::*;
pub(crate) use non_pep695_type_alias::*;
pub(crate) use private_type_parameter::*;
use crate::checkers::ast::Checker;
mod non_pep695_generic_class;
mod non_pep695_generic_function;
mod non_pep695_type_alias;
@@ -123,10 +122,6 @@ impl Display for DisplayTypeVar<'_> {
}
}
}
if let Some(default) = self.type_var.default {
f.write_str(" = ")?;
f.write_str(&self.source[default.range()])?;
}
Ok(())
}
@@ -138,63 +133,66 @@ impl<'a> From<&'a TypeVar<'a>> for TypeParam {
name,
restriction,
kind,
default,
default: _, // TODO(brent) see below
}: &'a TypeVar<'a>,
) -> Self {
let default = default.map(|expr| Box::new(expr.clone()));
match kind {
TypeParamKind::TypeVar => TypeParam::TypeVar(TypeParamTypeVar {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
bound: match restriction {
Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())),
Some(TypeVarRestriction::Constraint(constraints)) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: constraints.iter().map(|expr| (*expr).clone()).collect(),
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
Some(TypeVarRestriction::AnyStr) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: vec![
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("str"),
ctx: ast::ExprContext::Load,
}),
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("bytes"),
ctx: ast::ExprContext::Load,
}),
],
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
None => None,
},
default,
}),
TypeParamKind::TypeVar => {
TypeParam::TypeVar(TypeParamTypeVar {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
bound: match restriction {
Some(TypeVarRestriction::Bound(bound)) => Some(Box::new((*bound).clone())),
Some(TypeVarRestriction::Constraint(constraints)) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: constraints.iter().map(|expr| (*expr).clone()).collect(),
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
Some(TypeVarRestriction::AnyStr) => {
Some(Box::new(Expr::Tuple(ast::ExprTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
elts: vec![
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("str"),
ctx: ast::ExprContext::Load,
}),
Expr::Name(ExprName {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
id: Name::from("bytes"),
ctx: ast::ExprContext::Load,
}),
],
ctx: ast::ExprContext::Load,
parenthesized: true,
})))
}
None => None,
},
// We don't handle defaults here yet. Should perhaps be a different rule since
// defaults are only valid in 3.13+.
default: None,
})
}
TypeParamKind::TypeVarTuple => TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
default,
default: None,
}),
TypeParamKind::ParamSpec => TypeParam::ParamSpec(TypeParamParamSpec {
range: TextRange::default(),
node_index: ruff_python_ast::AtomicNodeIndex::NONE,
name: Identifier::new(*name, TextRange::default()),
default,
default: None,
}),
}
}
@@ -320,8 +318,8 @@ pub(crate) fn expr_name_to_type_var<'a>(
.first()
.is_some_and(Expr::is_string_literal_expr)
{
// `default` was added in PEP 696 and Python 3.13. We now support converting
// TypeVars with defaults to PEP 695 type parameters.
// TODO(brent) `default` was added in PEP 696 and Python 3.13 but can't be used in
// generic type parameters before that
//
// ```python
// T = TypeVar("T", default=Any, bound=str)
@@ -369,22 +367,21 @@ fn in_nested_context(checker: &Checker) -> bool {
}
/// Deduplicate `vars`, returning `None` if `vars` is empty or any duplicates are found.
/// Also returns `None` if any `TypeVar` has a default value and preview mode is not enabled.
fn check_type_vars<'a>(vars: Vec<TypeVar<'a>>, checker: &Checker) -> Option<Vec<TypeVar<'a>>> {
fn check_type_vars(vars: Vec<TypeVar<'_>>) -> Option<Vec<TypeVar<'_>>> {
if vars.is_empty() {
return None;
}
// If any type variables have defaults and preview mode is not enabled, skip the rule
if vars.iter().any(|tv| tv.default.is_some())
&& !is_type_var_default_enabled(checker.settings())
{
return None;
}
// If any type variables were not unique, just bail out here. this is a runtime error and we
// can't predict what the user wanted.
(vars.iter().unique_by(|tvar| tvar.name).count() == vars.len()).then_some(vars)
// can't predict what the user wanted. also bail out if any Python 3.13+ default values are
// found on the type parameters
(vars
.iter()
.unique_by(|tvar| tvar.name)
.filter(|tvar| tvar.default.is_none())
.count()
== vars.len())
.then_some(vars)
}
/// Search `class_bases` for a `typing.Generic` base class. Returns the `Generic` expression (if

View File

@@ -186,7 +186,7 @@ pub(crate) fn non_pep695_generic_class(checker: &Checker, class_def: &StmtClassD
//
// just because we can't confirm that `SomethingElse` is a `TypeVar`
if !visitor.any_skipped {
let Some(type_vars) = check_type_vars(visitor.vars, checker) else {
let Some(type_vars) = check_type_vars(visitor.vars) else {
diagnostic.defuse();
return;
};

View File

@@ -154,7 +154,7 @@ pub(crate) fn non_pep695_generic_function(checker: &Checker, function_def: &Stmt
}
}
let Some(type_vars) = check_type_vars(type_vars, checker) else {
let Some(type_vars) = check_type_vars(type_vars) else {
return;
};

View File

@@ -8,7 +8,6 @@ use ruff_python_ast::{Expr, ExprCall, ExprName, Keyword, StmtAnnAssign, StmtAssi
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::preview::is_type_var_default_enabled;
use crate::{Applicability, Edit, Fix, FixAvailability, Violation};
use ruff_python_ast::PythonVersion;
@@ -233,10 +232,8 @@ pub(crate) fn non_pep695_type_alias(checker: &Checker, stmt: &StmtAnnAssign) {
.unique_by(|tvar| tvar.name)
.collect::<Vec<_>>();
// Skip if any TypeVar has defaults and preview mode is not enabled
if vars.iter().any(|tv| tv.default.is_some())
&& !is_type_var_default_enabled(checker.settings())
{
// TODO(brent) handle `default` arg for Python 3.13+
if vars.iter().any(|tv| tv.default.is_some()) {
return;
}

View File

@@ -1,19 +1,15 @@
use ruff_python_ast::Expr;
use std::fmt::{Display, Formatter};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_semantic::Modules;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::preview::is_typing_extensions_str_alias_enabled;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
/// Checks for uses of `typing.Text`.
///
/// In preview mode, also checks for `typing_extensions.Text`.
///
/// ## Why is this bad?
/// `typing.Text` is an alias for `str`, and only exists for Python 2
/// compatibility. As of Python 3.11, `typing.Text` is deprecated. Use `str`
@@ -34,16 +30,14 @@ use crate::{Edit, Fix, FixAvailability, Violation};
/// ## References
/// - [Python documentation: `typing.Text`](https://docs.python.org/3/library/typing.html#typing.Text)
#[derive(ViolationMetadata)]
pub(crate) struct TypingTextStrAlias {
module: TypingModule,
}
pub(crate) struct TypingTextStrAlias;
impl Violation for TypingTextStrAlias {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("`{}.Text` is deprecated, use `str`", self.module)
"`typing.Text` is deprecated, use `str`".to_string()
}
fn fix_title(&self) -> Option<String> {
@@ -53,26 +47,16 @@ impl Violation for TypingTextStrAlias {
/// UP019
pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
if !checker
.semantic()
.seen_module(Modules::TYPING | Modules::TYPING_EXTENSIONS)
{
if !checker.semantic().seen_module(Modules::TYPING) {
return;
}
if let Some(qualified_name) = checker.semantic().resolve_qualified_name(expr) {
let segments = qualified_name.segments();
let module = match segments {
["typing", "Text"] => TypingModule::Typing,
["typing_extensions", "Text"]
if is_typing_extensions_str_alias_enabled(checker.settings()) =>
{
TypingModule::TypingExtensions
}
_ => return,
};
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias { module }, expr.range());
if checker
.semantic()
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["typing", "Text"]))
{
let mut diagnostic = checker.report_diagnostic(TypingTextStrAlias, expr.range());
diagnostic.add_primary_tag(ruff_db::diagnostic::DiagnosticTag::Deprecated);
diagnostic.try_set_fix(|| {
let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
@@ -87,18 +71,3 @@ pub(crate) fn typing_text_str_alias(checker: &Checker, expr: &Expr) {
});
}
}
#[derive(Copy, Clone, Debug)]
enum TypingModule {
Typing,
TypingExtensions,
}
impl Display for TypingModule {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
TypingModule::Typing => f.write_str("typing"),
TypingModule::TypingExtensions => f.write_str("typing_extensions"),
}
}
}

View File

@@ -66,5 +66,3 @@ help: Replace with `str`
- def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None:
20 | print(word)
21 |
22 |

View File

@@ -1,119 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:7:22
|
7 | def print_word(word: Text) -> None:
| ^^^^
8 | print(word)
|
help: Replace with `str`
4 | from typing import Text as Goodbye
5 |
6 |
- def print_word(word: Text) -> None:
7 + def print_word(word: str) -> None:
8 | print(word)
9 |
10 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:11:29
|
11 | def print_second_word(word: typing.Text) -> None:
| ^^^^^^^^^^^
12 | print(word)
|
help: Replace with `str`
8 | print(word)
9 |
10 |
- def print_second_word(word: typing.Text) -> None:
11 + def print_second_word(word: str) -> None:
12 | print(word)
13 |
14 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:15:28
|
15 | def print_third_word(word: Hello.Text) -> None:
| ^^^^^^^^^^
16 | print(word)
|
help: Replace with `str`
12 | print(word)
13 |
14 |
- def print_third_word(word: Hello.Text) -> None:
15 + def print_third_word(word: str) -> None:
16 | print(word)
17 |
18 |
UP019 [*] `typing.Text` is deprecated, use `str`
--> UP019.py:19:29
|
19 | def print_fourth_word(word: Goodbye) -> None:
| ^^^^^^^
20 | print(word)
|
help: Replace with `str`
16 | print(word)
17 |
18 |
- def print_fourth_word(word: Goodbye) -> None:
19 + def print_fourth_word(word: str) -> None:
20 | print(word)
21 |
22 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:28:28
|
28 | def print_fifth_word(word: typing_extensions.Text) -> None:
| ^^^^^^^^^^^^^^^^^^^^^^
29 | print(word)
|
help: Replace with `str`
25 | from typing_extensions import Text as TextAlias
26 |
27 |
- def print_fifth_word(word: typing_extensions.Text) -> None:
28 + def print_fifth_word(word: str) -> None:
29 | print(word)
30 |
31 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:32:28
|
32 | def print_sixth_word(word: TypingExt.Text) -> None:
| ^^^^^^^^^^^^^^
33 | print(word)
|
help: Replace with `str`
29 | print(word)
30 |
31 |
- def print_sixth_word(word: TypingExt.Text) -> None:
32 + def print_sixth_word(word: str) -> None:
33 | print(word)
34 |
35 |
UP019 [*] `typing_extensions.Text` is deprecated, use `str`
--> UP019.py:36:30
|
36 | def print_seventh_word(word: TextAlias) -> None:
| ^^^^^^^^^
37 | print(word)
|
help: Replace with `str`
33 | print(word)
34 |
35 |
- def print_seventh_word(word: TextAlias) -> None:
36 + def print_seventh_word(word: str) -> None:
37 | print(word)

View File

@@ -217,7 +217,7 @@ UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keywo
44 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45 |
46 | # `default` was added in Python 3.13
46 | # `default` should be skipped for now, added in Python 3.13
|
help: Use the `type` keyword
41 |
@@ -226,7 +226,7 @@ help: Use the `type` keyword
- x: typing.TypeAlias = list[T]
44 + type x = list[T]
45 |
46 | # `default` was added in Python 3.13
46 | # `default` should be skipped for now, added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
note: This is an unsafe fix and may change runtime behavior
@@ -355,26 +355,6 @@ help: Use the `type` keyword
87 | # OK: Other name
88 | T = TypeVar("T", bound=SupportGt)
UP040 [*] Type alias `AnyList` uses `TypeAliasType` assignment instead of the `type` keyword
--> UP040.py:95:1
|
93 | # `default` was added in Python 3.13
94 | T = typing.TypeVar("T", default=Any)
95 | AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
96 |
97 | # unsafe fix if comments within the fix
|
help: Use the `type` keyword
92 |
93 | # `default` was added in Python 3.13
94 | T = typing.TypeVar("T", default=Any)
- AnyList = TypeAliasType("AnyList", list[T], type_params=(T,))
95 + type AnyList[T = Any] = list[T]
96 |
97 | # unsafe fix if comments within the fix
98 | T = TypeVar("T")
UP040 [*] Type alias `PositiveList` uses `TypeAliasType` assignment instead of the `type` keyword
--> UP040.py:99:1
|
@@ -489,8 +469,6 @@ UP040 [*] Type alias `T` uses `TypeAlias` annotation instead of the `type` keywo
129 | | # comment7
130 | | ) # comment8
| |_^
131 |
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
|
help: Use the `type` keyword
119 | | str

View File

@@ -1,49 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 2
--- Added ---
UP040 [*] Type alias `x` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:48:1
|
46 | # `default` was added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
48 | x: typing.TypeAlias = list[T]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
49 |
50 | # OK
|
help: Use the `type` keyword
45 |
46 | # `default` was added in Python 3.13
47 | T = typing.TypeVar("T", default=Any)
- x: typing.TypeAlias = list[T]
48 + type x[T = Any] = list[T]
49 |
50 | # OK
51 | x: TypeAlias
note: This is an unsafe fix and may change runtime behavior
UP040 [*] Type alias `DefaultList` uses `TypeAlias` annotation instead of the `type` keyword
--> UP040.py:134:1
|
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
133 | T_default = TypeVar("T_default", default=int)
134 | DefaultList: TypeAlias = list[T_default]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
help: Use the `type` keyword
131 |
132 | # Test case for TypeVar with default - should be converted when preview mode is enabled
133 | T_default = TypeVar("T_default", default=int)
- DefaultList: TypeAlias = list[T_default]
134 + type DefaultList[T_default = int] = list[T_default]
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,10 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 0

View File

@@ -1,48 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 2
--- Added ---
UP046 [*] Generic class `DefaultTypeVar` uses `Generic` subclass instead of type parameters
--> UP046_0.py:129:22
|
129 | class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
| ^^^^^^^^^^
130 | var: V
|
help: Use type parameters
126 | V = TypeVar("V", default=Any, bound=str)
127 |
128 |
- class DefaultTypeVar(Generic[V]): # -> [V: str = Any]
129 + class DefaultTypeVar[V: str = Any]: # -> [V: str = Any]
130 | var: V
131 |
132 |
note: This is an unsafe fix and may change runtime behavior
UP046 [*] Generic class `DefaultOnlyTypeVar` uses `Generic` subclass instead of type parameters
--> UP046_0.py:137:26
|
137 | class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
| ^^^^^^^^^^
138 | var: W
|
help: Use type parameters
134 | W = TypeVar("W", default=int)
135 |
136 |
- class DefaultOnlyTypeVar(Generic[W]): # -> [W = int]
137 + class DefaultOnlyTypeVar[W = int]: # -> [W = int]
138 | var: W
139 |
140 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -1,10 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 0

View File

@@ -1,29 +0,0 @@
---
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
---
--- Linter settings ---
-linter.preview = disabled
+linter.preview = enabled
--- Summary ---
Removed: 0
Added: 1
--- Added ---
UP047 [*] Generic function `default_var` should use type parameters
--> UP047.py:51:5
|
51 | def default_var(v: V) -> V:
| ^^^^^^^^^^^^^^^^^
52 | return v
|
help: Use type parameters
48 | V = TypeVar("V", default=Any, bound=str)
49 |
50 |
- def default_var(v: V) -> V:
51 + def default_var[V: str = Any](v: V) -> V:
52 | return v
53 |
54 |
note: This is an unsafe fix and may change runtime behavior

View File

@@ -48,7 +48,7 @@ use crate::{AlwaysFixableViolation, Edit, Fix};
/// element. As such, any side effects that occur during iteration will be
/// delayed.
/// 2. Second, accessing members of a collection via square bracket notation
/// `[0]` or the `pop()` function will raise `IndexError` if the collection
/// `[0]` of the `pop()` function will raise `IndexError` if the collection
/// is empty, while `next(iter(...))` will raise `StopIteration`.
///
/// ## References

View File

@@ -26,7 +26,7 @@ use ruff_source_file::SourceFileBuilder;
use crate::codes::Rule;
use crate::fix::{FixResult, fix_file};
use crate::linter::check_path;
use crate::message::EmitterContext;
use crate::message::{EmitterContext, create_syntax_error_diagnostic};
use crate::package::PackageRoot;
use crate::packaging::detect_package_root;
use crate::settings::types::UnsafeFixes;
@@ -405,7 +405,7 @@ Either ensure you always emit a fix or change `Violation::FIX_AVAILABILITY` to e
diagnostic
})
.chain(parsed.errors().iter().map(|parse_error| {
Diagnostic::invalid_syntax(source_code.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_code.clone(), &parse_error.error, parse_error)
}))
.sorted_by(Diagnostic::ruff_start_ordering)
.collect();
@@ -419,7 +419,7 @@ fn print_syntax_errors(errors: &[ParseError], path: &Path, source: &SourceKind)
let messages: Vec<_> = errors
.iter()
.map(|parse_error| {
Diagnostic::invalid_syntax(source_file.clone(), &parse_error.error, parse_error)
create_syntax_error_diagnostic(source_file.clone(), &parse_error.error, parse_error)
})
.collect();

View File

@@ -12,7 +12,7 @@ use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::StatementVisitor;
use crate::visitor::Visitor;
use crate::{
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr, ExprNoneLiteral,
self as ast, Arguments, AtomicNodeIndex, CmpOp, DictItem, ExceptHandler, Expr,
InterpolatedStringElement, MatchCase, Operator, Pattern, Stmt, TypeParam,
};
use crate::{AnyNodeRef, ExprContext};
@@ -1219,8 +1219,6 @@ impl Truthiness {
F: Fn(&str) -> bool,
{
match expr {
Expr::Lambda(_) => Self::Truthy,
Expr::Generator(_) => Self::Truthy,
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if value.is_empty() {
Self::Falsey
@@ -1390,9 +1388,7 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
Expr::FString(f_string) => is_non_empty_f_string(f_string),
// These literals may or may not be empty.
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => !value.is_empty(),
// Confusingly, f"{b""}" renders as the string 'b""', which is non-empty.
// Therefore, any bytes interpolation is guaranteed non-empty when stringified.
Expr::BytesLiteral(_) => true,
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => !value.is_empty(),
}
}
@@ -1401,9 +1397,7 @@ fn is_non_empty_f_string(expr: &ast::ExprFString) -> bool {
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
InterpolatedStringElement::Literal(string_literal) => !string_literal.is_empty(),
InterpolatedStringElement::Interpolation(f_string) => {
f_string.debug_text.is_some() || inner(&f_string.expression)
}
InterpolatedStringElement::Interpolation(f_string) => inner(&f_string.expression),
})
}
})
@@ -1499,7 +1493,7 @@ pub fn pep_604_optional(expr: &Expr) -> Expr {
ast::ExprBinOp {
left: Box::new(expr.clone()),
op: Operator::BitOr,
right: Box::new(Expr::NoneLiteral(ExprNoneLiteral::default())),
right: Box::new(Expr::NoneLiteral(ast::ExprNoneLiteral::default())),
range: TextRange::default(),
node_index: AtomicNodeIndex::NONE,
}

View File

@@ -735,8 +735,6 @@ pub trait StringFlags: Copy {
fn prefix(self) -> AnyStringPrefix;
fn is_unclosed(self) -> bool;
/// Is the string triple-quoted, i.e.,
/// does it begin and end with three consecutive quote characters?
fn is_triple_quoted(self) -> bool {
@@ -781,7 +779,6 @@ pub trait StringFlags: Copy {
fn as_any_string_flags(self) -> AnyStringFlags {
AnyStringFlags::new(self.prefix(), self.quote_style(), self.triple_quotes())
.with_unclosed(self.is_unclosed())
}
fn display_contents(self, contents: &str) -> DisplayFlags<'_> {
@@ -832,10 +829,6 @@ bitflags! {
/// for why we track the casing of the `r` prefix,
/// but not for any other prefix
const R_PREFIX_UPPER = 1 << 3;
/// The f-string is unclosed, meaning it is missing a closing quote.
/// For example: `f"{bar`
const UNCLOSED = 1 << 4;
}
}
@@ -894,12 +887,6 @@ impl FStringFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(InterpolatedStringFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: FStringPrefix) -> Self {
match prefix {
@@ -997,12 +984,6 @@ impl TStringFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(InterpolatedStringFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: TStringPrefix) -> Self {
match prefix {
@@ -1070,10 +1051,6 @@ impl StringFlags for FStringFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Format(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(InterpolatedStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for FStringFlags {
@@ -1082,7 +1059,6 @@ impl fmt::Debug for FStringFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1114,10 +1090,6 @@ impl StringFlags for TStringFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Template(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(InterpolatedStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for TStringFlags {
@@ -1126,7 +1098,6 @@ impl fmt::Debug for TStringFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1456,9 +1427,6 @@ bitflags! {
/// The string was deemed invalid by the parser.
const INVALID = 1 << 5;
/// The string literal misses the matching closing quote(s).
const UNCLOSED = 1 << 6;
}
}
@@ -1511,12 +1479,6 @@ impl StringLiteralFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(StringLiteralFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(self, prefix: StringLiteralPrefix) -> Self {
let StringLiteralFlags(flags) = self;
@@ -1598,10 +1560,6 @@ impl StringFlags for StringLiteralFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Regular(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(StringLiteralFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for StringLiteralFlags {
@@ -1610,7 +1568,6 @@ impl fmt::Debug for StringLiteralFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -1889,9 +1846,6 @@ bitflags! {
/// The bytestring was deemed invalid by the parser.
const INVALID = 1 << 4;
/// The byte string misses the matching closing quote(s).
const UNCLOSED = 1 << 5;
}
}
@@ -1943,12 +1897,6 @@ impl BytesLiteralFlags {
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(BytesLiteralFlagsInner::UNCLOSED, unclosed);
self
}
#[must_use]
pub fn with_prefix(mut self, prefix: ByteStringPrefix) -> Self {
match prefix {
@@ -2011,10 +1959,6 @@ impl StringFlags for BytesLiteralFlags {
fn prefix(self) -> AnyStringPrefix {
AnyStringPrefix::Bytes(self.prefix())
}
fn is_unclosed(self) -> bool {
self.0.intersects(BytesLiteralFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for BytesLiteralFlags {
@@ -2023,7 +1967,6 @@ impl fmt::Debug for BytesLiteralFlags {
.field("quote_style", &self.quote_style())
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -2085,7 +2028,7 @@ bitflags! {
/// prefix flags is by calling the `as_flags()` method on the
/// `StringPrefix` enum.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
struct AnyStringFlagsInner: u16 {
struct AnyStringFlagsInner: u8 {
/// The string uses double quotes (`"`).
/// If this flag is not set, the string uses single quotes (`'`).
const DOUBLE = 1 << 0;
@@ -2128,9 +2071,6 @@ bitflags! {
/// for why we track the casing of the `r` prefix,
/// but not for any other prefix
const R_PREFIX_UPPER = 1 << 7;
/// String without matching closing quote(s).
const UNCLOSED = 1 << 8;
}
}
@@ -2226,12 +2166,6 @@ impl AnyStringFlags {
.set(AnyStringFlagsInner::TRIPLE_QUOTED, triple_quotes.is_yes());
self
}
#[must_use]
pub fn with_unclosed(mut self, unclosed: bool) -> Self {
self.0.set(AnyStringFlagsInner::UNCLOSED, unclosed);
self
}
}
impl StringFlags for AnyStringFlags {
@@ -2300,10 +2234,6 @@ impl StringFlags for AnyStringFlags {
}
AnyStringPrefix::Regular(StringLiteralPrefix::Empty)
}
fn is_unclosed(self) -> bool {
self.0.intersects(AnyStringFlagsInner::UNCLOSED)
}
}
impl fmt::Debug for AnyStringFlags {
@@ -2312,7 +2242,6 @@ impl fmt::Debug for AnyStringFlags {
.field("prefix", &self.prefix())
.field("triple_quoted", &self.is_triple_quoted())
.field("quote_style", &self.quote_style())
.field("unclosed", &self.is_unclosed())
.finish()
}
}
@@ -2329,7 +2258,6 @@ impl From<AnyStringFlags> for StringLiteralFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2351,7 +2279,6 @@ impl From<AnyStringFlags> for BytesLiteralFlags {
.with_quote_style(value.quote_style())
.with_prefix(bytestring_prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2373,7 +2300,6 @@ impl From<AnyStringFlags> for FStringFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}
@@ -2395,7 +2321,6 @@ impl From<AnyStringFlags> for TStringFlags {
.with_quote_style(value.quote_style())
.with_prefix(prefix)
.with_triple_quotes(value.triple_quotes())
.with_unclosed(value.is_unclosed())
}
}

View File

@@ -1,7 +0,0 @@
# regression test for #1765
class Foo:
def foo(self):
if True:
content_ids: Mapping[
str, Optional[ContentId]
] = self.publisher_content_store.store_config_contents(files)

View File

@@ -1,7 +0,0 @@
# regression test for #1765
class Foo:
def foo(self):
if True:
content_ids: Mapping[str, Optional[ContentId]] = (
self.publisher_content_store.store_config_contents(files)
)

View File

@@ -1 +1 @@
{"target_version": "3.10"}
{"target_version": "3.10"}

View File

@@ -1,35 +0,0 @@
# long variable name
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 0
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = 1 # with a comment
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = [
1, 2, 3
]
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function()
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
arg1, arg2, arg3
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
# long function name
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying()
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
arg1, arg2, arg3
)
normal_name = but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
string_variable_name = (
"a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa
)
for key in """
hostname
port
username
""".split():
if key in self.connect_kwargs:
raise ValueError(err.format(key))
concatenated_strings = "some strings that are " "concatenated implicitly, so if you put them on separate " "lines it will fit"
del concatenated_strings, string_variable_name, normal_function_name, normal_name, need_more_to_make_the_line_long_enough
del ([], name_1, name_2), [(), [], name_4, name_3], name_1[[name_2 for name_1 in name_0]]
del (),

View File

@@ -1,61 +0,0 @@
# long variable name
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
0
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
1 # with a comment
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = [
1,
2,
3,
]
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = (
function()
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
arg1, arg2, arg3
)
this_is_a_ridiculously_long_name_and_nobody_in_their_right_mind_would_use_one_like_it = function(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
# long function name
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying()
)
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
arg1, arg2, arg3
)
)
normal_name = (
but_the_function_name_is_now_ridiculously_long_and_it_is_still_super_annoying(
[1, 2, 3], arg1, [1, 2, 3], arg2, [1, 2, 3], arg3
)
)
string_variable_name = "a string that is waaaaaaaayyyyyyyy too long, even in parens, there's nothing you can do" # noqa
for key in """
hostname
port
username
""".split():
if key in self.connect_kwargs:
raise ValueError(err.format(key))
concatenated_strings = (
"some strings that are "
"concatenated implicitly, so if you put them on separate "
"lines it will fit"
)
del (
concatenated_strings,
string_variable_name,
normal_function_name,
normal_name,
need_more_to_make_the_line_long_enough,
)
del (
([], name_1, name_2),
[(), [], name_4, name_3],
name_1[[name_2 for name_1 in name_0]],
)
del ((),)

View File

@@ -1 +1 @@
{"target_version": "3.8"}
{"target_version": "3.8"}

View File

@@ -82,9 +82,3 @@ async def func():
argument1, argument2, argument3="some_value"
):
pass
# don't remove the brackets here, it changes the meaning of the code.
with (x, y) as z:
pass

View File

@@ -83,8 +83,3 @@ async def func():
some_other_function(argument1, argument2, argument3="some_value"),
):
pass
# don't remove the brackets here, it changes the meaning of the code.
with (x, y) as z:
pass

View File

@@ -1,3 +0,0 @@
"""
87 characters ............................................................................
"""

View File

@@ -1,3 +0,0 @@
"""
87 characters ............................................................................
"""

View File

@@ -1,8 +0,0 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip

View File

@@ -1,8 +0,0 @@
def foo(): return "mock" # fmt: skip
if True: print("yay") # fmt: skip
for i in range(10): print(i) # fmt: skip
j = 1 # fmt: skip
while j < 10: j += 1 # fmt: skip
b = [c for c in "A very long string that would normally generate some kind of collapse, since it is this long"] # fmt: skip

View File

@@ -1,6 +0,0 @@
def foo():
pass
# comment 1 # fmt: skip
# comment 2

View File

@@ -1,6 +0,0 @@
def foo():
pass
# comment 1 # fmt: skip
# comment 2

View File

@@ -1,15 +0,0 @@
x = "\x1F"
x = "\\x1B"
x = "\\\x1B"
x = "\U0001F60E"
x = "\u0001F60E"
x = r"\u0001F60E"
x = "don't format me"
x = "\xA3"
x = "\u2717"
x = "\uFaCe"
x = "\N{ox}\N{OX}"
x = "\N{lAtIn smaLL letteR x}"
x = "\N{CYRILLIC small LETTER BYELORUSSIAN-UKRAINIAN I}"
x = b"\x1Fdon't byte"
x = rb"\x1Fdon't format"

View File

@@ -1,15 +0,0 @@
x = "\x1f"
x = "\\x1B"
x = "\\\x1b"
x = "\U0001f60e"
x = "\u0001F60E"
x = r"\u0001F60E"
x = "don't format me"
x = "\xa3"
x = "\u2717"
x = "\uface"
x = "\N{OX}\N{OX}"
x = "\N{LATIN SMALL LETTER X}"
x = "\N{CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I}"
x = b"\x1fdon't byte"
x = rb"\x1Fdon't format"

View File

@@ -0,0 +1 @@
{"target_version": "3.12"}

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