Compare commits

..

1 Commits

Author SHA1 Message Date
Carl Meyer
89228c344c [WIP] add recursive visitor to more type methods 2025-07-27 22:38:11 -07:00
382 changed files with 4089 additions and 11858 deletions

View File

@@ -429,7 +429,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Install cargo-binstall"
uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
with:
tool: cargo-fuzz@0.11.2
- name: "Install cargo-fuzz"
@@ -451,7 +451,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
name: Download Ruff binary to test
id: download-cached-binary
@@ -652,7 +652,7 @@ jobs:
branch: ${{ github.event.pull_request.base.ref }}
workflow: "ci.yaml"
check_artifacts: true
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Fuzz
env:
FORCE_COLOR: 1
@@ -682,7 +682,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: cargo-bins/cargo-binstall@808dcb1b503398677d089d3216c51ac7cc11e7ab # v1.14.2
- uses: cargo-bins/cargo-binstall@8aac5aa2bf0dfaa2863eccad9f43c68fe40e5ec8 # v1.14.1
- run: cargo binstall --no-confirm cargo-shear
- run: cargo shear
@@ -722,7 +722,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
@@ -765,7 +765,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
run: uv pip install -r docs/requirements-insiders.txt --system
@@ -897,7 +897,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Rust toolchain"
run: rustup show
@@ -911,7 +911,7 @@ jobs:
run: cargo codspeed build --features "codspeed,instrumented" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}
@@ -930,7 +930,7 @@ jobs:
persist-credentials: false
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Rust toolchain"
run: rustup show
@@ -944,7 +944,7 @@ jobs:
run: cargo codspeed build --features "codspeed,walltime" --no-default-features -p ruff_benchmark
- name: "Run benchmarks"
uses: CodSpeedHQ/action@0b6e7a3d96c9d2a6057e7bcea6b45aaf2f7ce60b # v3.8.0
uses: CodSpeedHQ/action@c28fe9fbe7d57a3da1b7834ae3761c1d8217612d # v3.7.0
with:
run: cargo codspeed run
token: ${{ secrets.CODSPEED_TOKEN }}

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -38,7 +38,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
@@ -81,7 +81,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@9d47c6ad4b02e050fd481d890b2ea34778fd09d6 # v2.7.8
with:

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: wheels-*

View File

@@ -65,7 +65,7 @@ jobs:
run: |
git config --global user.name typeshedbot
git config --global user.email '<>'
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Sync typeshed stubs
run: |
rm -rf "ruff/${VENDORED_TYPESHED}"
@@ -117,7 +117,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Setup git
run: |
git config --global user.name typeshedbot
@@ -155,7 +155,7 @@ jobs:
with:
persist-credentials: true
ref: ${{ env.UPSTREAM_BRANCH}}
- uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- name: Setup git
run: |
git config --global user.name typeshedbot

View File

@@ -33,7 +33,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -29,7 +29,7 @@ jobs:
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
uses: astral-sh/setup-uv@bd01e18f51369d5a26f1651c3cb451d3417e3bba # v6.3.1
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:

View File

@@ -1,109 +0,0 @@
name: Run typing conformance
permissions: {}
on:
pull_request:
paths:
- "crates/ty*/**"
- "crates/ruff_db"
- "crates/ruff_python_ast"
- "crates/ruff_python_parser"
- ".github/workflows/typing_conformance.yaml"
- ".github/workflows/typing_conformance_comment.yaml"
- "Cargo.lock"
- "!**.md"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: 1
jobs:
typing_conformance:
name: Compute diagnostic diff
runs-on: depot-ubuntu-22.04-32
timeout-minutes: 10
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: ruff
fetch-depth: 0
persist-credentials: false
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
repository: python/typing
ref: d4f39b27a4a47aac8b6d4019e1b0b5b3156fabdc
path: typing
persist-credentials: false
- name: Install the latest version of uv
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
- uses: Swatinem/rust-cache@98c8021b550208e191a6a3145459bfc9fb29c4c0 # v2.8.0
with:
workspaces: "ruff"
- name: Install Rust toolchain
run: rustup show
- name: Compute diagnostic diff
shell: bash
run: |
RUFF_DIR="$GITHUB_WORKSPACE/ruff"
# Build the executable for the old and new commit
(
cd ruff
echo "new commit"
git checkout -b new_commit "${{ github.event.pull_request.head.sha }}"
git rev-list --format=%s --max-count=1 new_commit
cargo build --release --bin ty
mv target/release/ty ty-new
echo "old commit (merge base)"
MERGE_BASE="$(git merge-base "$GITHUB_SHA" "origin/$GITHUB_BASE_REF")"
git checkout -b old_commit "$MERGE_BASE"
git rev-list --format=%s --max-count=1 old_commit
cargo build --release --bin ty
mv target/release/ty ty-old
)
(
cd typing/conformance/tests
echo "Running ty on old commit (merge base)"
"$RUFF_DIR/ty-old" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/old-output.txt" 2>&1 || true
echo "Running ty on new commit"
"$RUFF_DIR/ty-new" check --color=never --output-format=concise . > "$GITHUB_WORKSPACE/new-output.txt" 2>&1 || true
)
if ! diff -u old-output.txt new-output.txt > typing_conformance_diagnostics.diff; then
echo "Differences found between base and PR"
else
echo "No differences found"
touch typing_conformance_diagnostics.diff
fi
echo ${{ github.event.number }} > pr-number
- name: Upload diff
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: typing_conformance_diagnostics_diff
path: typing_conformance_diagnostics.diff
- name: Upload pr-number
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pr-number
path: pr-number

View File

@@ -81,7 +81,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.5
rev: v0.12.4
hooks:
- id: ruff-format
- id: ruff-check

View File

@@ -1,35 +1,5 @@
# Changelog
## 0.12.7
This is a follow-up release to 0.12.6. Because of an issue in the package metadata, 0.12.6 failed to publish fully to PyPI and has been yanked. Similarly, there is no GitHub release or Git tag for 0.12.6. The contents of the 0.12.7 release are identical to 0.12.6, except for the updated metadata.
## 0.12.6
### Preview features
- \[`flake8-commas`\] Add support for trailing comma checks in type parameter lists (`COM812`, `COM819`) ([#19390](https://github.com/astral-sh/ruff/pull/19390))
- \[`pylint`\] Implement auto-fix for `missing-maxsplit-arg` (`PLC0207`) ([#19387](https://github.com/astral-sh/ruff/pull/19387))
- \[`ruff`\] Offer fixes for `RUF039` in more cases ([#19065](https://github.com/astral-sh/ruff/pull/19065))
### Bug fixes
- Support `.pyi` files in ruff analyze graph ([#19611](https://github.com/astral-sh/ruff/pull/19611))
- \[`flake8-pyi`\] Preserve inline comment in ellipsis removal (`PYI013`) ([#19399](https://github.com/astral-sh/ruff/pull/19399))
- \[`perflint`\] Ignore rule if target is `global` or `nonlocal` (`PERF401`) ([#19539](https://github.com/astral-sh/ruff/pull/19539))
- \[`pyupgrade`\] Fix `UP030` to avoid modifying double curly braces in format strings ([#19378](https://github.com/astral-sh/ruff/pull/19378))
- \[`refurb`\] Ignore decorated functions for `FURB118` ([#19339](https://github.com/astral-sh/ruff/pull/19339))
- \[`refurb`\] Mark `int` and `bool` cases for `Decimal.from_float` as safe fixes (`FURB164`) ([#19468](https://github.com/astral-sh/ruff/pull/19468))
- \[`ruff`\] Fix `RUF033` for named default expressions ([#19115](https://github.com/astral-sh/ruff/pull/19115))
### Rule changes
- \[`flake8-blind-except`\] Change `BLE001` to permit `logging.critical(..., exc_info=True)` ([#19520](https://github.com/astral-sh/ruff/pull/19520))
### Performance
- Add support for specifying minimum dots in detected string imports ([#19538](https://github.com/astral-sh/ruff/pull/19538))
## 0.12.5
### Preview features

358
Cargo.lock generated
View File

@@ -4,9 +4,9 @@ version = 4
[[package]]
name = "adler2"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aho-corasick"
@@ -77,52 +77,52 @@ checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
[[package]]
name = "anstyle-lossy"
version = "1.1.4"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04d3a5dc826f84d0ea11882bb8054ff7f3d482602e11bb181101303a279ea01f"
checksum = "934ff8719effd2023a48cf63e69536c1c3ced9d3895068f6f5cc9a4ff845e59b"
dependencies = [
"anstyle",
]
[[package]]
name = "anstyle-parse"
version = "0.2.7"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.3"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-svg"
version = "0.1.9"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a43964079ef399480603125d5afae2b219aceffb77478956e25f17b9bc3435c"
checksum = "d3607949e9f6de49ea4bafe12f5e4fd73613ebf24795e48587302a8cc0e4bb35"
dependencies = [
"anstream",
"anstyle",
"anstyle-lossy",
"anstyle-parse",
"html-escape",
"unicode-width 0.2.1",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.9"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell_polyfill",
"once_cell",
"windows-sys 0.59.0",
]
@@ -210,9 +210,9 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.5.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "base64"
@@ -301,9 +301,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.19.0"
version = "3.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
[[package]]
name = "byteorder"
@@ -337,18 +337,18 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "castaway"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
version = "1.2.30"
version = "1.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
dependencies = [
"jobserver",
"libc",
@@ -357,9 +357,9 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.1"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
@@ -408,9 +408,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.42"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882"
checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
dependencies = [
"clap_builder",
"clap_derive",
@@ -418,9 +418,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.42"
version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966"
checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
dependencies = [
"anstream",
"anstyle",
@@ -431,9 +431,9 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.55"
version = "4.5.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
checksum = "c91d3baa3bcd889d60e6ef28874126a0b384fd225ab83aa6d8a801c519194ce1"
dependencies = [
"clap",
]
@@ -451,9 +451,9 @@ dependencies = [
[[package]]
name = "clap_complete_nushell"
version = "4.5.8"
version = "4.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a0c951694691e65bf9d421d597d68416c22de9632e884c28412cb8cd8b73dce"
checksum = "c6a8b1593457dfc2fe539002b795710d022dc62a65bf15023f039f9760c7b18a"
dependencies = [
"clap",
"clap_complete",
@@ -473,9 +473,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.5"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "clearscreen"
@@ -492,9 +492,9 @@ dependencies = [
[[package]]
name = "codspeed"
version = "3.0.4"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29180405ab3b37bb020246ea66bf8ae233708766fd59581ae929feaef10ce91"
checksum = "922018102595f6668cdd09c03f4bff2d951ce2318c6dca4fe11bdcb24b65b2bf"
dependencies = [
"anyhow",
"bincode 1.3.3",
@@ -510,9 +510,9 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat"
version = "3.0.4"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2454d874ca820ffd71273565530ad318f413195bbc99dce6c958ca07db362c63"
checksum = "24d8ad82d2383cb74995f58993cbdd2914aed57b2f91f46580310dd81dc3d05a"
dependencies = [
"codspeed",
"codspeed-criterion-compat-walltime",
@@ -521,16 +521,16 @@ dependencies = [
[[package]]
name = "codspeed-criterion-compat-walltime"
version = "3.0.4"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "093a9383cdd1a5a0bd1a47cdafb49ae0c6dcd0793c8fb8f79768bab423128c9c"
checksum = "61badaa6c452d192a29f8387147888f0ab358553597c3fe9bf8a162ef7c2fa64"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"codspeed",
"criterion-plot 0.5.0",
"criterion-plot",
"is-terminal",
"itertools 0.10.5",
"num-traits",
@@ -546,9 +546,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat"
version = "3.0.4"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c73bce1e3f47738bf74a6b58b72a49b4f40c837ce420d8d65a270298592aac"
checksum = "3acf1d6fe367c2ff5ff136ca723f678490c3691d59d7f2b83d5e53b7b25ac91e"
dependencies = [
"codspeed",
"codspeed-divan-compat-macros",
@@ -557,9 +557,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat-macros"
version = "3.0.4"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea51dd8add7eba774cc24b4a98324252ac3ec092ccb5f07e52bbe1cb72a6d373"
checksum = "bcfa2013d7bee54a497d0e1410751d5de690fd67a3e9eb728ca049b6a3d16d0b"
dependencies = [
"divan-macros",
"itertools 0.14.0",
@@ -571,9 +571,9 @@ dependencies = [
[[package]]
name = "codspeed-divan-compat-walltime"
version = "3.0.4"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "417e9edfc4b0289d4b9b48e62f98c6168d5e30c0e612b2935e394b0dd930fe83"
checksum = "e513100fb0e7ba02fb3824546ecd2abfb8f334262f0972225b463aad07f99ff0"
dependencies = [
"cfg-if",
"clap",
@@ -586,15 +586,15 @@ dependencies = [
[[package]]
name = "collection_literals"
version = "1.0.2"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b3f65b8fb8e88ba339f7d23a390fe1b0896217da05e2a66c584c9b29a91df8"
checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271"
[[package]]
name = "colorchoice"
version = "1.0.4"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "colored"
@@ -704,24 +704,24 @@ dependencies = [
[[package]]
name = "crc32fast"
version = "1.5.0"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "criterion"
version = "0.7.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928"
checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679"
dependencies = [
"anes",
"cast",
"ciborium",
"clap",
"criterion-plot 0.6.0",
"criterion-plot",
"itertools 0.13.0",
"num-traits",
"oorandom",
@@ -742,16 +742,6 @@ dependencies = [
"itertools 0.10.5",
]
[[package]]
name = "criterion-plot"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338"
dependencies = [
"cast",
"itertools 0.13.0",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
@@ -810,9 +800,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
[[package]]
name = "crypto-common"
@@ -1000,9 +990,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dyn-clone"
version = "1.0.20"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005"
[[package]]
name = "either"
@@ -1030,12 +1020,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.13"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18"
dependencies = [
"libc",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
@@ -1096,9 +1086,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.2"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1161,9 +1151,9 @@ dependencies = [
[[package]]
name = "get-size-derive2"
version = "0.6.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca171f9f8ed2f416ac044de2dc4acde3e356662a14ac990345639653bdc7fc28"
checksum = "028f3cfad7c3e3b1d8d04ef0a1c03576f2d62800803fe1301a4cd262849f2dea"
dependencies = [
"attribute-derive",
"quote",
@@ -1172,9 +1162,9 @@ dependencies = [
[[package]]
name = "get-size2"
version = "0.6.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "965bc5c1c5fe05c5bbd398bb9b3f0f14d750261ebdd1af959f2c8a603fedb5ad"
checksum = "3a09c2043819a3def7bfbb4927e7df96aab0da4cfd8824484b22d0c94e84458e"
dependencies = [
"compact_str",
"get-size-derive2",
@@ -1184,11 +1174,11 @@ dependencies = [
[[package]]
name = "getopts"
version = "0.2.23"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1"
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
dependencies = [
"unicode-width 0.2.1",
"unicode-width 0.1.14",
]
[[package]]
@@ -1199,7 +1189,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if",
"libc",
"wasi 0.11.1+wasi-snapshot-preview1",
"wasi 0.11.0+wasi-snapshot-preview1",
]
[[package]]
@@ -1290,9 +1280,15 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hermit-abi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
[[package]]
name = "home"
@@ -1385,9 +1381,9 @@ checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
[[package]]
name = "icu_properties"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
checksum = "2549ca8c7241c82f59c80ba2a6f415d931c5b58d24fb8412caa1a1f02c49139a"
dependencies = [
"displaydoc",
"icu_collections",
@@ -1401,9 +1397,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "2.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
checksum = "8197e866e47b68f8f7d95249e172903bec06004b18b2937f1095d40a0c57de04"
[[package]]
name = "icu_provider"
@@ -1615,7 +1611,7 @@ version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"hermit-abi 0.5.1",
"libc",
"windows-sys 0.59.0",
]
@@ -1805,9 +1801,9 @@ dependencies = [
[[package]]
name = "libredox"
version = "0.1.8"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "360e552c93fa0e8152ab463bc4c4837fce76a225df11dfaeea66c313de5e61f7"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.9.1",
"libc",
@@ -1974,23 +1970,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.4"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"log",
"wasi 0.11.1+wasi-snapshot-preview1",
"windows-sys 0.59.0",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
[[package]]
@@ -2001,9 +1997,9 @@ checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
[[package]]
name = "newtype-uuid"
version = "1.2.4"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a17d82edb1c8a6c20c238747ae7aae9181133e766bc92cd2556fdd764407d0d1"
checksum = "ee3224f0e8be7c2a1ebc77ef9c3eecb90f55c6594399ee825de964526b3c9056"
dependencies = [
"uuid",
]
@@ -2093,11 +2089,11 @@ dependencies = [
[[package]]
name = "num_cpus"
version = "1.17.0"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"hermit-abi 0.3.9",
"libc",
]
@@ -2107,12 +2103,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "oorandom"
version = "11.1.5"
@@ -2137,9 +2127,9 @@ dependencies = [
[[package]]
name = "os_pipe"
version = "1.2.2"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db335f4760b14ead6290116f2427bf33a14d4f0617d49f78a246de10c1831224"
checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982"
dependencies = [
"libc",
"windows-sys 0.59.0",
@@ -2147,9 +2137,9 @@ dependencies = [
[[package]]
name = "os_str_bytes"
version = "7.1.1"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63eceb7b5d757011a87d08eb2123db15d87fb0c281f65d101ce30a1e96c3ad5c"
checksum = "c86e2db86dd008b4c88c77a9bb83d9286bf77204e255bb3fda3b2eebcae66b62"
dependencies = [
"memchr",
]
@@ -2162,9 +2152,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "parking_lot"
version = "0.12.4"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -2172,9 +2162,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.11"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
@@ -2289,9 +2279,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.8.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [
"memchr",
"thiserror 2.0.12",
@@ -2300,9 +2290,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.8.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
dependencies = [
"pest",
"pest_generator",
@@ -2310,9 +2300,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.8.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
dependencies = [
"pest",
"pest_meta",
@@ -2323,10 +2313,11 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.8.1"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
dependencies = [
"once_cell",
"pest",
"sha2",
]
@@ -2383,9 +2374,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.1"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "portable-atomic-util"
@@ -2571,9 +2562,9 @@ dependencies = [
[[package]]
name = "r-efi"
version = "5.3.0"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "radium"
@@ -2662,9 +2653,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.17"
version = "0.5.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af"
dependencies = [
"bitflags 2.9.1",
]
@@ -2743,7 +2734,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.12.7"
version = "0.12.5"
dependencies = [
"anyhow",
"argfile",
@@ -2795,7 +2786,7 @@ dependencies = [
"test-case",
"thiserror 2.0.12",
"tikv-jemallocator",
"toml 0.9.4",
"toml 0.9.2",
"tracing",
"walkdir",
"wild",
@@ -2811,7 +2802,7 @@ dependencies = [
"ruff_annotate_snippets",
"serde",
"snapbox",
"toml 0.9.4",
"toml 0.9.2",
"tryfn",
"unicode-width 0.2.1",
]
@@ -2927,7 +2918,7 @@ dependencies = [
"similar",
"strum",
"tempfile",
"toml 0.9.4",
"toml 0.9.2",
"tracing",
"tracing-indicatif",
"tracing-subscriber",
@@ -2995,7 +2986,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.12.7"
version = "0.12.5"
dependencies = [
"aho-corasick",
"anyhow",
@@ -3048,7 +3039,7 @@ dependencies = [
"tempfile",
"test-case",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.2",
"typed-arena",
"unicode-normalization",
"unicode-width 0.2.1",
@@ -3298,7 +3289,7 @@ dependencies = [
"serde_json",
"shellexpand",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.2",
"tracing",
"tracing-log",
"tracing-subscriber",
@@ -3327,7 +3318,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.12.7"
version = "0.12.5"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3388,7 +3379,7 @@ dependencies = [
"shellexpand",
"strum",
"tempfile",
"toml 0.9.4",
"toml 0.9.2",
]
[[package]]
@@ -3415,22 +3406,22 @@ checksum = "781442f29170c5c93b7185ad559492601acdc71d5bb0706f5868094f45cfcd08"
[[package]]
name = "rustix"
version = "1.0.8"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.60.2",
"windows-sys 0.59.0",
]
[[package]]
name = "rustversion"
version = "1.0.21"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
@@ -3441,7 +3432,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
dependencies = [
"boxcar",
"compact_str",
@@ -3466,12 +3457,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
[[package]]
name = "salsa-macros"
version = "0.23.0"
source = "git+https://github.com/salsa-rs/salsa?rev=f3dc2f30f9a250618161e35600a00de7fe744953#f3dc2f30f9a250618161e35600a00de7fe744953"
source = "git+https://github.com/salsa-rs/salsa?rev=dba66f1a37acca014c2402f231ed5b361bd7d8fe#dba66f1a37acca014c2402f231ed5b361bd7d8fe"
dependencies = [
"proc-macro2",
"quote",
@@ -3938,11 +3929,12 @@ dependencies = [
[[package]]
name = "thread_local"
version = "1.1.9"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
dependencies = [
"cfg-if",
"once_cell",
]
[[package]]
@@ -4023,9 +4015,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.9.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
dependencies = [
"indexmap",
"serde",
@@ -4096,9 +4088,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.30"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
@@ -4128,9 +4120,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.12"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1983afead46ff13a3c93581e0cec31d20b29efdd22cbdaa8b9f850eccf2c352"
checksum = "8c714cc8fc46db04fcfddbd274c6ef59bebb1b435155984e7c6e89c3ce66f200"
dependencies = [
"indicatif",
"tracing",
@@ -4203,7 +4195,7 @@ dependencies = [
"ruff_python_trivia",
"salsa",
"tempfile",
"toml 0.9.4",
"toml 0.9.2",
"tracing",
"tracing-flame",
"tracing-subscriber",
@@ -4264,7 +4256,7 @@ dependencies = [
"schemars",
"serde",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.2",
"tracing",
"ty_python_semantic",
"ty_vendored",
@@ -4387,7 +4379,7 @@ dependencies = [
"smallvec",
"tempfile",
"thiserror 2.0.12",
"toml 0.9.4",
"toml 0.9.2",
"tracing",
"ty_python_semantic",
"ty_static",
@@ -4711,9 +4703,9 @@ dependencies = [
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
@@ -4892,9 +4884,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40"
dependencies = [
"windows-implement",
"windows-interface",
@@ -4927,28 +4919,37 @@ dependencies = [
[[package]]
name = "windows-link"
version = "0.1.3"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-result"
version = "0.3.4"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
@@ -4964,7 +4965,7 @@ version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets 0.53.3",
"windows-targets 0.53.2",
]
[[package]]
@@ -4985,11 +4986,10 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.53.3"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91"
checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
@@ -5098,9 +5098,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.7.12"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
@@ -5167,18 +5167,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.8.26"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.26"
version = "0.8.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -73,7 +73,7 @@ console_error_panic_hook = { version = "0.1.7" }
console_log = { version = "1.0.0" }
countme = { version = "3.0.1" }
compact_str = "0.9.0"
criterion = { version = "0.7.0", default-features = false }
criterion = { version = "0.6.0", default-features = false }
crossbeam = { version = "0.8.4" }
dashmap = { version = "6.0.1" }
dir-test = { version = "0.4.0" }
@@ -83,7 +83,7 @@ etcetera = { version = "0.10.0" }
fern = { version = "0.7.0" }
filetime = { version = "0.2.23" }
getrandom = { version = "0.3.1" }
get-size2 = { version = "0.6.0", features = [
get-size2 = { version = "0.5.0", features = [
"derive",
"smallvec",
"hashbrown",
@@ -141,7 +141,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", rev = "f3dc2f30f9a250618161e35600a00de7fe744953" }
salsa = { git = "https://github.com/salsa-rs/salsa", rev = "dba66f1a37acca014c2402f231ed5b361bd7d8fe" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }

View File

@@ -148,8 +148,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.12.7/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.7/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.12.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.12.5/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.12.7
rev: v0.12.5
hooks:
# Run the linter.
- id: ruff-check

View File

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

View File

@@ -57,40 +57,33 @@ fn dependencies() -> Result<()> {
.write_str(indoc::indoc! {r#"
def f(): pass
"#})?;
root.child("ruff")
.child("e.pyi")
.write_str(indoc::indoc! {r#"
def f() -> None: ...
"#})?;
insta::with_settings!({
filters => INSTA_FILTERS.to_vec(),
}, {
assert_cmd_snapshot!(command().current_dir(&root), @r#"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": [
"ruff/d.py"
],
"ruff/d.py": [
"ruff/e.py",
"ruff/e.pyi"
],
"ruff/e.py": [],
"ruff/e.pyi": []
}
assert_cmd_snapshot!(command().current_dir(&root), @r###"
success: true
exit_code: 0
----- stdout -----
{
"ruff/__init__.py": [],
"ruff/a.py": [
"ruff/b.py"
],
"ruff/b.py": [
"ruff/c.py"
],
"ruff/c.py": [
"ruff/d.py"
],
"ruff/d.py": [
"ruff/e.py"
],
"ruff/e.py": []
}
----- stderr -----
"#);
----- stderr -----
"###);
});
Ok(())

View File

@@ -351,41 +351,6 @@ fn benchmark_many_tuple_assignments(criterion: &mut Criterion) {
});
}
fn benchmark_tuple_implicit_instance_attributes(criterion: &mut Criterion) {
setup_rayon();
criterion.bench_function("ty_micro[many_tuple_assignments]", |b| {
b.iter_batched_ref(
|| {
// This is a regression benchmark for a case that used to hang:
// https://github.com/astral-sh/ty/issues/765
setup_micro_case(
r#"
from typing import Any
class A:
foo: tuple[Any, ...]
class B(A):
def __init__(self, parent: "C", x: tuple[Any]):
self.foo = parent.foo + x
class C(A):
def __init__(self, parent: B, x: tuple[Any]):
self.foo = parent.foo + x
"#,
)
},
|case| {
let Case { db, .. } = case;
let result = db.check();
assert_eq!(result.len(), 0);
},
BatchSize::SmallInput,
);
});
}
fn benchmark_complex_constrained_attributes_1(criterion: &mut Criterion) {
setup_rayon();
@@ -665,7 +630,6 @@ criterion_group!(
micro,
benchmark_many_string_assignments,
benchmark_many_tuple_assignments,
benchmark_tuple_implicit_instance_attributes,
benchmark_complex_constrained_attributes_1,
benchmark_complex_constrained_attributes_2,
benchmark_many_enum_members,

View File

@@ -6,9 +6,7 @@ use ruff_source_file::{LineColumn, SourceCode, SourceFile};
use ruff_annotate_snippets::Level as AnnotateLevel;
use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{
DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input, ceil_char_boundary,
};
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
use crate::{Db, files::File};
mod render;
@@ -21,7 +19,7 @@ mod stylesheet;
/// characteristics in the inputs given to the tool. Typically, but not always,
/// a characteristic is a deficiency. An example of a characteristic that is
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub struct Diagnostic {
/// The actual diagnostic.
///
@@ -479,7 +477,7 @@ impl Diagnostic {
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
struct DiagnosticInner {
id: DiagnosticId,
severity: Severity,
@@ -555,7 +553,7 @@ impl Eq for RenderingSortKey<'_> {}
/// Currently, the order in which sub-diagnostics are rendered relative to one
/// another (for a single parent diagnostic) is the order in which they were
/// attached to the diagnostic.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub struct SubDiagnostic {
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
/// pointer-sized.
@@ -659,7 +657,7 @@ impl SubDiagnostic {
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
struct SubDiagnosticInner {
severity: SubDiagnosticSeverity,
message: DiagnosticMessage,
@@ -687,7 +685,7 @@ struct SubDiagnosticInner {
///
/// Messages attached to annotations should also be as brief and specific as
/// possible. Long messages could negative impact the quality of rendering.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub struct Annotation {
/// The span of this annotation, corresponding to some subsequence of the
/// user's input that we want to highlight.
@@ -807,7 +805,7 @@ impl Annotation {
///
/// These tags are used to provide additional information about the annotation.
/// and are passed through to the language server protocol.
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
pub enum DiagnosticTag {
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
Unnecessary,
@@ -1016,7 +1014,7 @@ impl std::fmt::Display for DiagnosticId {
///
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
/// emitting diagnostics from both ty and ruff.
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
pub enum UnifiedFile {
Ty(File),
Ruff(SourceFile),
@@ -1080,7 +1078,7 @@ impl DiagnosticSource {
/// It consists of a `File` and an optional range into that file. When the
/// range isn't present, it semantically implies that the diagnostic refers to
/// the entire file. For example, when the file should be executable but isn't.
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
pub struct Span {
file: UnifiedFile,
range: Option<TextRange>,
@@ -1158,7 +1156,7 @@ impl From<crate::files::FileRange> for Span {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
pub enum Severity {
Info,
Warning,
@@ -1193,7 +1191,7 @@ impl Severity {
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
/// deleted and the two combined again.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
pub enum SubDiagnosticSeverity {
Help,
Info,
@@ -1428,7 +1426,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
/// In most cases, callers shouldn't need to use this. Instead, there is
/// a blanket trait implementation for `IntoDiagnosticMessage` for
/// anything that implements `std::fmt::Display`.
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
pub struct DiagnosticMessage(Box<str>);
impl DiagnosticMessage {

View File

@@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::path::Path;
@@ -8,7 +7,7 @@ use ruff_annotate_snippets::{
};
use ruff_notebook::{Notebook, NotebookIndex};
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_text_size::{TextRange, TextSize};
use crate::diagnostic::stylesheet::DiagnosticStylesheet;
use crate::{
@@ -521,7 +520,7 @@ impl<'r> RenderableSnippets<'r> {
#[derive(Debug)]
struct RenderableSnippet<'r> {
/// The actual snippet text.
snippet: Cow<'r, str>,
snippet: &'r str,
/// The absolute line number corresponding to where this
/// snippet begins.
line_start: OneIndexed,
@@ -581,12 +580,6 @@ impl<'r> RenderableSnippet<'r> {
.iter()
.map(|ann| RenderableAnnotation::new(snippet_start, ann))
.collect();
let EscapedSourceCode {
text: snippet,
annotations,
} = replace_unprintable(snippet, annotations).fix_up_empty_spans_after_line_terminator();
RenderableSnippet {
snippet,
line_start,
@@ -597,7 +590,7 @@ impl<'r> RenderableSnippet<'r> {
/// Convert this to an "annotate" snippet.
fn to_annotate<'a>(&'a self, path: &'a str) -> AnnotateSnippet<'a> {
AnnotateSnippet::source(&self.snippet)
AnnotateSnippet::source(self.snippet)
.origin(path)
.line_start(self.line_start.get())
.annotations(
@@ -827,204 +820,6 @@ fn relativize_path<'p>(cwd: &SystemPath, path: &'p str) -> &'p str {
path
}
/// Given some source code and annotation ranges, this routine replaces
/// unprintable characters with printable representations of them.
///
/// The source code and annotations returned are updated to reflect changes made
/// to the source code (if any).
///
/// We don't need to normalize whitespace, such as converting tabs to spaces,
/// because `annotate-snippets` handles that internally. Similarly, it's safe to
/// modify the annotation ranges by inserting 3-byte Unicode replacements
/// because `annotate-snippets` will account for their actual width when
/// rendering and displaying the column to the user.
fn replace_unprintable<'r>(
source: &'r str,
mut annotations: Vec<RenderableAnnotation<'r>>,
) -> EscapedSourceCode<'r> {
// Updates the annotation ranges given by the caller whenever a single byte (at `index` in
// `source`) is replaced with `len` bytes.
//
// When the index occurs before the start of the range, the range is
// offset by `len`. When the range occurs after or at the start but before
// the end, then the end of the range only is offset by `len`.
let mut update_ranges = |index: usize, len: u32| {
for ann in &mut annotations {
if index < usize::from(ann.range.start()) {
ann.range += TextSize::new(len - 1);
} else if index < usize::from(ann.range.end()) {
ann.range = ann.range.add_end(TextSize::new(len - 1));
}
}
};
// If `c` is an unprintable character, then this returns a printable
// representation of it (using a fancier Unicode codepoint).
let unprintable_replacement = |c: char| -> Option<char> {
match c {
'\x07' => Some('␇'),
'\x08' => Some('␈'),
'\x1b' => Some('␛'),
'\x7f' => Some('␡'),
_ => None,
}
};
let mut last_end = 0;
let mut result = String::new();
for (index, c) in source.char_indices() {
if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
let len = printable.text_len().to_u32();
update_ranges(result.text_len().to_usize(), len);
result.push(printable);
last_end = index + 1;
}
}
// No tabs or unprintable chars
if result.is_empty() {
EscapedSourceCode {
annotations,
text: Cow::Borrowed(source),
}
} else {
result.push_str(&source[last_end..]);
EscapedSourceCode {
annotations,
text: Cow::Owned(result),
}
}
}
struct EscapedSourceCode<'r> {
text: Cow<'r, str>,
annotations: Vec<RenderableAnnotation<'r>>,
}
impl<'r> EscapedSourceCode<'r> {
// This attempts to "fix up" the spans on each annotation in the case where
// it's an empty span immediately following a line terminator.
//
// At present, `annotate-snippets` (both upstream and our vendored copy)
// will render annotations of such spans to point to the space immediately
// following the previous line. But ideally, this should point to the space
// immediately preceding the next line.
//
// After attempting to fix `annotate-snippets` and giving up after a couple
// hours, this routine takes a different tact: it adjusts the span to be
// non-empty and it will cover the first codepoint of the following line.
// This forces `annotate-snippets` to point to the right place.
//
// See also: <https://github.com/astral-sh/ruff/issues/15509> and
// `ruff_linter::message::text::SourceCode::fix_up_empty_spans_after_line_terminator`,
// from which this was adapted.
fn fix_up_empty_spans_after_line_terminator(mut self) -> EscapedSourceCode<'r> {
for ann in &mut self.annotations {
let range = ann.range;
if !range.is_empty()
|| range.start() == TextSize::from(0)
|| range.start() >= self.text.text_len()
{
continue;
}
if !matches!(
self.text.as_bytes()[range.start().to_usize() - 1],
b'\n' | b'\r'
) {
continue;
}
let start = range.start();
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
ann.range = TextRange::new(start, end);
}
self
}
}
/// Finds the closest [`TextSize`] not less than the offset given for which
/// `is_char_boundary` is `true`. Unless the offset given is greater than
/// the length of the underlying contents, in which case, the length of the
/// contents is returned.
///
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
///
/// # Examples
///
/// From `std`:
///
/// ```
/// use ruff_db::diagnostic::ceil_char_boundary;
/// use ruff_text_size::{Ranged, TextLen, TextSize};
///
/// let source = "❤️🧡💛💚💙💜";
/// assert_eq!(source.text_len(), TextSize::from(26));
/// assert!(!source.is_char_boundary(13));
///
/// let closest = ceil_char_boundary(source, TextSize::from(13));
/// assert_eq!(closest, TextSize::from(14));
/// assert_eq!(&source[..closest.to_usize()], "❤️🧡💛");
/// ```
///
/// Additional examples:
///
/// ```
/// use ruff_db::diagnostic::ceil_char_boundary;
/// use ruff_text_size::{Ranged, TextRange, TextSize};
///
/// let source = "Hello";
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(5)),
/// TextSize::from(5)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(6)),
/// TextSize::from(5)
/// );
///
/// let source = "α";
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(1)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(2)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// ceil_char_boundary(source, TextSize::from(3)),
/// TextSize::from(2)
/// );
/// ```
pub fn ceil_char_boundary(text: &str, offset: TextSize) -> TextSize {
let upper_bound = offset
.to_u32()
.saturating_add(4)
.min(text.text_len().to_u32());
(offset.to_u32()..upper_bound)
.map(TextSize::from)
.find(|offset| text.is_char_boundary(offset.to_usize()))
.unwrap_or_else(|| TextSize::from(upper_bound))
}
#[cfg(test)]
mod tests {
@@ -2564,7 +2359,7 @@ watermelon
}
/// Returns a builder for tersely constructing diagnostics.
pub(super) fn builder(
fn builder(
&mut self,
identifier: &'static str,
severity: Severity,
@@ -2631,7 +2426,7 @@ watermelon
///
/// See the docs on `TestEnvironment::span` for the meaning of
/// `path`, `line_offset_start` and `line_offset_end`.
pub(super) fn primary(
fn primary(
mut self,
path: &str,
line_offset_start: &str,

View File

@@ -1,8 +1,8 @@
#[cfg(test)]
mod tests {
use crate::diagnostic::{
DiagnosticFormat, Severity,
render::tests::{TestEnvironment, create_diagnostics, create_syntax_error_diagnostics},
DiagnosticFormat,
render::tests::{create_diagnostics, create_syntax_error_diagnostics},
};
#[test]
@@ -63,139 +63,4 @@ mod tests {
|
");
}
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit c9b99e4.
///
/// For example, without the fix, we get diagnostics like this:
///
/// ```
/// error[no-indented-block]: Expected an indented block
/// --> example.py:3:1
/// |
/// 2 | if False:
/// | ^
/// 3 | print()
/// |
/// ```
///
/// where the caret points to the end of the previous line instead of the start of the next.
#[test]
fn empty_span_after_line_terminator() {
let mut env = TestEnvironment::new();
env.add(
"example.py",
r#"
if False:
print()
"#,
);
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"no-indented-block",
Severity::Error,
"Expected an indented block",
)
.primary("example.py", "3:0", "3:0", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[no-indented-block]: Expected an indented block
--> example.py:3:1
|
2 | if False:
3 | print()
| ^
|
");
}
/// Check that the new `full` rendering code in `ruff_db` handles cases fixed by commit 2922490.
///
/// For example, without the fix, we get diagnostics like this:
///
/// ```
/// error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
/// --> example.py:1:25
/// |
/// 1 | nested_fstrings = f'␈{f'{f'␛'}'}'
/// | ^
/// |
/// ```
///
/// where the caret points to the `f` in the f-string instead of the start of the invalid
/// character (`^Z`).
#[test]
fn unprintable_characters() {
let mut env = TestEnvironment::new();
env.add("example.py", "nested_fstrings = f'{f'{f''}'}'");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
)
.primary("example.py", "1:24", "1:24", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
--> example.py:1:25
|
1 | nested_fstrings = f'␈{f'{f'␛'}'}'
| ^
|
"#);
}
#[test]
fn multiple_unprintable_characters() -> std::io::Result<()> {
let mut env = TestEnvironment::new();
env.add("example.py", "");
env.format(DiagnosticFormat::Full);
let diagnostic = env
.builder(
"invalid-character-sub",
Severity::Error,
r#"Invalid unescaped character SUB, use "\x1A" instead"#,
)
.primary("example.py", "1:1", "1:1", "")
.build();
insta::assert_snapshot!(env.render(&diagnostic), @r#"
error[invalid-character-sub]: Invalid unescaped character SUB, use "\x1A" instead
--> example.py:1:2
|
1 | ␈
| ^
|
"#);
Ok(())
}
/// Ensure that the header column matches the column in the user's input, even if we've replaced
/// tabs with spaces for rendering purposes.
#[test]
fn tab_replacement() {
let mut env = TestEnvironment::new();
env.add("example.py", "def foo():\n\treturn 1");
env.format(DiagnosticFormat::Full);
let diagnostic = env.err().primary("example.py", "2:1", "2:9", "").build();
insta::assert_snapshot!(env.render(&diagnostic), @r"
error[test-diagnostic]: main diagnostic message
--> example.py:2:2
|
1 | def foo():
2 | return 1
| ^^^^^^^^
|
");
}
}

View File

@@ -21,7 +21,7 @@ use crate::source::source_text;
/// reflected in the changed AST offsets.
/// The other reason is that Ruff's AST doesn't implement `Eq` which Salsa requires
/// for determining if a query result is unchanged.
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::heap_size)]
#[salsa::tracked(returns(ref), no_eq, heap_size=get_size2::GetSize::get_heap_size)]
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let _span = tracing::trace_span!("parsed_module", ?file).entered();

View File

@@ -9,7 +9,7 @@ use crate::Db;
use crate::files::{File, FilePath};
/// Reads the source text of a python text file (must be valid UTF8) or notebook.
#[salsa::tracked(heap_size=get_size2::heap_size)]
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
let path = file.path(db);
let _span = tracing::trace_span!("source_text", file = %path).entered();
@@ -69,21 +69,21 @@ impl SourceText {
pub fn as_str(&self) -> &str {
match &self.inner.kind {
SourceTextKind::Text(source) => source,
SourceTextKind::Notebook { notebook } => notebook.source_code(),
SourceTextKind::Notebook(notebook) => notebook.source_code(),
}
}
/// Returns the underlying notebook if this is a notebook file.
pub fn as_notebook(&self) -> Option<&Notebook> {
match &self.inner.kind {
SourceTextKind::Notebook { notebook } => Some(notebook),
SourceTextKind::Notebook(notebook) => Some(notebook),
SourceTextKind::Text(_) => None,
}
}
/// Returns `true` if this is a notebook source file.
pub fn is_notebook(&self) -> bool {
matches!(&self.inner.kind, SourceTextKind::Notebook { .. })
matches!(&self.inner.kind, SourceTextKind::Notebook(_))
}
/// Returns `true` if there was an error when reading the content of the file.
@@ -108,7 +108,7 @@ impl std::fmt::Debug for SourceText {
SourceTextKind::Text(text) => {
dbg.field(text);
}
SourceTextKind::Notebook { notebook } => {
SourceTextKind::Notebook(notebook) => {
dbg.field(notebook);
}
}
@@ -123,15 +123,23 @@ struct SourceTextInner {
read_error: Option<SourceTextError>,
}
#[derive(Eq, PartialEq, get_size2::GetSize)]
#[derive(Eq, PartialEq)]
enum SourceTextKind {
Text(String),
Notebook {
// Jupyter notebooks are not very relevant for memory profiling, and contain
// arbitrary JSON values that do not implement the `GetSize` trait.
#[get_size(ignore)]
notebook: Box<Notebook>,
},
Notebook(Box<Notebook>),
}
impl get_size2::GetSize for SourceTextKind {
fn get_heap_size(&self) -> usize {
match self {
SourceTextKind::Text(text) => text.get_heap_size(),
// TODO: The `get-size` derive does not support ignoring enum variants.
//
// Jupyter notebooks are not very relevant for memory profiling, and contain
// arbitrary JSON values that do not implement the `GetSize` trait.
SourceTextKind::Notebook(_) => 0,
}
}
}
impl From<String> for SourceTextKind {
@@ -142,9 +150,7 @@ impl From<String> for SourceTextKind {
impl From<Notebook> for SourceTextKind {
fn from(notebook: Notebook) -> Self {
SourceTextKind::Notebook {
notebook: Box::new(notebook),
}
SourceTextKind::Notebook(Box::new(notebook))
}
}
@@ -157,7 +163,7 @@ pub enum SourceTextError {
}
/// Computes the [`LineIndex`] for `file`.
#[salsa::tracked(heap_size=get_size2::heap_size)]
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
let _span = tracing::trace_span!("line_index", ?file).entered();

View File

@@ -43,7 +43,7 @@ pub enum IsolationLevel {
}
/// A collection of [`Edit`] elements to be applied to a source file.
#[derive(Debug, PartialEq, Eq, Clone, Hash, get_size2::GetSize)]
#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Fix {
/// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order.

View File

@@ -42,11 +42,13 @@ impl ModuleImports {
// Resolve the imports.
let mut resolved_imports = ModuleImports::default();
for import in imports {
for resolved in Resolver::new(db).resolve(import) {
if let Some(path) = resolved.as_system_path() {
resolved_imports.insert(path.to_path_buf());
}
}
let Some(resolved) = Resolver::new(db).resolve(import) else {
continue;
};
let Some(path) = resolved.as_system_path() else {
continue;
};
resolved_imports.insert(path.to_path_buf());
}
Ok(resolved_imports)

View File

@@ -1,5 +1,5 @@
use ruff_db::files::FilePath;
use ty_python_semantic::{ModuleName, resolve_module, resolve_real_module};
use ty_python_semantic::resolve_module;
use crate::ModuleDb;
use crate::collector::CollectedImport;
@@ -16,67 +16,24 @@ impl<'a> Resolver<'a> {
}
/// Resolve the [`CollectedImport`] into a [`FilePath`].
pub(crate) fn resolve(&self, import: CollectedImport) -> impl Iterator<Item = &'a FilePath> {
pub(crate) fn resolve(&self, import: CollectedImport) -> Option<&'a FilePath> {
match import {
CollectedImport::Import(import) => {
// Attempt to resolve the module (e.g., given `import foo`, look for `foo`).
let file = self.resolve_module(&import);
// If the file is a stub, look for the corresponding source file.
let source_file = file
.is_some_and(|file| file.extension() == Some("pyi"))
.then(|| self.resolve_real_module(&import))
.flatten();
std::iter::once(file)
.chain(std::iter::once(source_file))
.flatten()
let module = resolve_module(self.db, &import)?;
Some(module.file(self.db)?.path(self.db))
}
CollectedImport::ImportFrom(import) => {
// Attempt to resolve the member (e.g., given `from foo import bar`, look for `foo.bar`).
if let Some(file) = self.resolve_module(&import) {
// If the file is a stub, look for the corresponding source file.
let source_file = (file.extension() == Some("pyi"))
.then(|| self.resolve_real_module(&import))
.flatten();
return std::iter::once(Some(file))
.chain(std::iter::once(source_file))
.flatten();
}
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
let parent = import.parent();
let file = parent
.as_ref()
.and_then(|parent| self.resolve_module(parent));
// If the file is a stub, look for the corresponding source file.
let source_file = file
.is_some_and(|file| file.extension() == Some("pyi"))
.then(|| {
parent
.as_ref()
.and_then(|parent| self.resolve_real_module(parent))
})
.flatten();
let module = resolve_module(self.db, &import).or_else(|| {
// Attempt to resolve the module (e.g., given `from foo import bar`, look for `foo`).
std::iter::once(file)
.chain(std::iter::once(source_file))
.flatten()
resolve_module(self.db, &parent?)
})?;
Some(module.file(self.db)?.path(self.db))
}
}
}
/// Resolves a module name to a module.
fn resolve_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
/// Resolves a module name to a module (stubs not allowed).
fn resolve_real_module(&self, module_name: &ModuleName) -> Option<&'a FilePath> {
let module = resolve_real_module(self.db, module_name)?;
Some(module.file(self.db)?.path(self.db))
}
}

View File

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

View File

@@ -650,17 +650,3 @@ f"""This is a test. {
if True else
"Don't add a trailing comma here ->"
}"""
type X[
T
] = T
def f[
T
](): pass
class C[
T
]: pass
type X[T,] = T
def f[T,](): pass
class C[T,]: pass

View File

@@ -39,11 +39,6 @@ class NonEmptyWithInit:
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
class EmptyClass:
...

View File

@@ -38,10 +38,6 @@ class NonEmptyWithInit:
def __init__():
pass
class NonEmptyChildWithInlineComment:
value: int
... # preserve me
# Not violations
class EmptyClass: ...

View File

@@ -1,3 +0,0 @@
"""Hello, world!"""\
x = 1; y = 2

View File

@@ -289,19 +289,4 @@ def f():
i = "xyz"
result = []
for i in range(3):
result.append((x for x in [i]))
G_INDEX = None
def f():
global G_INDEX
result = []
for G_INDEX in range(3):
result.append(G_INDEX)
def f():
NL_INDEX = None
def x():
nonlocal NL_INDEX
result = []
for NL_INDEX in range(3):
result.append(NL_INDEX)
result.append((x for x in [i]))

View File

@@ -59,7 +59,3 @@ kwargs = {x: x for x in range(10)}
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)
r"\d{{1,2}} {0}".format(42)
"{{{0}}}".format(123)

View File

@@ -55,12 +55,3 @@ _ = Decimal(0.1)
_ = Decimal(-0.5)
_ = Decimal(5.0)
_ = decimal.Decimal(4.2)
# Cases with int and bool - should produce safe fixes
_ = Decimal.from_float(1)
_ = Decimal.from_float(True)
# Cases with non-finite floats - should produce safe fixes
_ = Decimal.from_float(float("-nan"))
_ = Decimal.from_float(float("\x2dnan"))
_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))

View File

@@ -52,7 +52,3 @@ f"{repr(lambda: 1)}"
f"{repr(x := 2)}"
f"{str(object=3)}"
f"{str(x for x in [])}"
f"{str((x for x in []))}"

View File

@@ -590,16 +590,6 @@ impl<'a> Checker<'a> {
member,
})
}
/// Return the [`LintContext`] for the current analysis.
///
/// Note that you should always prefer calling methods like `settings`, `report_diagnostic`, or
/// `is_rule_enabled` directly on [`Checker`] when possible. This method exists only for the
/// rare cases where rules or helper functions need to be accessed by both a `Checker` and a
/// `LintContext` in different analysis phases.
pub(crate) const fn context(&self) -> &'a LintContext<'a> {
self.context
}
}
pub(crate) struct TypingImporter<'a, 'b> {
@@ -3226,11 +3216,6 @@ impl<'a> LintContext<'a> {
pub(crate) fn iter(&mut self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics.get_mut().iter()
}
/// The [`LinterSettings`] for the current analysis, including the enabled rules.
pub(crate) const fn settings(&self) -> &LinterSettings {
self.settings
}
}
/// An abstraction for mutating a diagnostic.

View File

@@ -16,6 +16,7 @@ use crate::rules::{
eradicate, flake8_commas, flake8_executable, flake8_fixme, flake8_implicit_str_concat,
flake8_pyi, flake8_todos, pycodestyle, pygrep_hooks, pylint, pyupgrade, ruff,
};
use crate::settings::LinterSettings;
use super::ast::LintContext;
@@ -26,6 +27,7 @@ pub(crate) fn check_tokens(
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
settings: &LinterSettings,
source_type: PySourceType,
cell_offsets: Option<&CellOffsets>,
context: &mut LintContext,
@@ -40,8 +42,15 @@ pub(crate) fn check_tokens(
Rule::BlankLinesAfterFunctionOrClass,
Rule::BlankLinesBeforeNestedDefinition,
]) {
BlankLinesChecker::new(locator, stylist, source_type, cell_offsets, context)
.check_lines(tokens);
BlankLinesChecker::new(
locator,
stylist,
settings,
source_type,
cell_offsets,
context,
)
.check_lines(tokens);
}
if context.is_rule_enabled(Rule::BlanketTypeIgnore) {
@@ -54,12 +63,12 @@ pub(crate) fn check_tokens(
if context.is_rule_enabled(Rule::AmbiguousUnicodeCharacterComment) {
for range in comment_ranges {
ruff::rules::ambiguous_unicode_character_comment(context, locator, range);
ruff::rules::ambiguous_unicode_character_comment(context, locator, range, settings);
}
}
if context.is_rule_enabled(Rule::CommentedOutCode) {
eradicate::rules::commented_out_code(context, locator, comment_ranges);
eradicate::rules::commented_out_code(context, locator, comment_ranges, settings);
}
if context.is_rule_enabled(Rule::UTF8EncodingDeclaration) {
@@ -101,7 +110,7 @@ pub(crate) fn check_tokens(
Rule::SingleLineImplicitStringConcatenation,
Rule::MultiLineImplicitStringConcatenation,
]) {
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer);
flake8_implicit_str_concat::rules::implicit(context, tokens, locator, indexer, settings);
}
if context.any_rule_enabled(&[

View File

@@ -56,19 +56,13 @@ impl<'a> Insertion<'a> {
stylist: &Stylist,
) -> Insertion<'static> {
// Skip over any docstrings.
let mut location = if let Some(mut location) = match_docstring_end(body) {
let mut location = if let Some(location) = match_docstring_end(body) {
// If the first token after the docstring is a semicolon, insert after the semicolon as
// an inline statement.
if let Some(offset) = match_semicolon(locator.after(location)) {
return Insertion::inline(" ", location.add(offset).add(TextSize::of(';')), ";");
}
// If the first token after the docstring is a continuation character (i.e. "\"), advance
// an additional row to prevent inserting in the same logical line.
if match_continuation(locator.after(location)).is_some() {
location = locator.full_line_end(location);
}
// Otherwise, advance to the next row.
locator.full_line_end(location)
} else {
@@ -369,16 +363,6 @@ mod tests {
Insertion::own_line("", TextSize::from(20), "\n")
);
let contents = r#"
"""Hello, world!"""\
"#
.trim_start();
assert_eq!(
insert(contents)?,
Insertion::own_line("", TextSize::from(22), "\n")
);
let contents = r"
x = 1
"

View File

@@ -188,6 +188,7 @@ pub fn check_path(
locator,
indexer,
stylist,
settings,
source_type,
source_kind.as_ipy_notebook().map(Notebook::cell_offsets),
&mut context,

View File

@@ -118,6 +118,86 @@ impl<'a> Locator<'a> {
}
}
/// Finds the closest [`TextSize`] not less than the offset given for which
/// `is_char_boundary` is `true`. Unless the offset given is greater than
/// the length of the underlying contents, in which case, the length of the
/// contents is returned.
///
/// Can be replaced with `str::ceil_char_boundary` once it's stable.
///
/// # Examples
///
/// From `std`:
///
/// ```
/// use ruff_text_size::{Ranged, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("❤️🧡💛💚💙💜");
/// assert_eq!(locator.text_len(), TextSize::from(26));
/// assert!(!locator.contents().is_char_boundary(13));
///
/// let closest = locator.ceil_char_boundary(TextSize::from(13));
/// assert_eq!(closest, TextSize::from(14));
/// assert_eq!(&locator.contents()[..closest.to_usize()], "❤️🧡💛");
/// ```
///
/// Additional examples:
///
/// ```
/// use ruff_text_size::{Ranged, TextRange, TextSize};
/// use ruff_linter::Locator;
///
/// let locator = Locator::new("Hello");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(5)),
/// TextSize::from(5)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(6)),
/// TextSize::from(5)
/// );
///
/// let locator = Locator::new("α");
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(0)),
/// TextSize::from(0)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(1)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(2)),
/// TextSize::from(2)
/// );
///
/// assert_eq!(
/// locator.ceil_char_boundary(TextSize::from(3)),
/// TextSize::from(2)
/// );
/// ```
pub fn ceil_char_boundary(&self, offset: TextSize) -> TextSize {
let upper_bound = offset
.to_u32()
.saturating_add(4)
.min(self.text_len().to_u32());
(offset.to_u32()..upper_bound)
.map(TextSize::from)
.find(|offset| self.contents.is_char_boundary(offset.to_usize()))
.unwrap_or_else(|| TextSize::from(upper_bound))
}
/// Take the source code between the given [`TextRange`].
#[inline]
pub fn slice<T: Ranged>(&self, ranged: T) -> &'a str {

View File

@@ -6,13 +6,13 @@ use bitflags::bitflags;
use colored::Colorize;
use ruff_annotate_snippets::{Level, Renderer, Snippet};
use ruff_db::diagnostic::{
Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode, ceil_char_boundary,
};
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig, SecondaryCode};
use ruff_notebook::NotebookIndex;
use ruff_source_file::OneIndexed;
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::Locator;
use crate::line_width::{IndentWidth, LineWidthBuilder};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext};
use crate::settings::types::UnsafeFixes;
@@ -228,7 +228,7 @@ impl Display for MessageCodeFrame<'_> {
let start_offset = source_code.line_start(start_index);
let end_offset = source_code.line_end(end_index);
let source = replace_unprintable(
let source = replace_whitespace_and_unprintable(
source_code.slice(TextRange::new(start_offset, end_offset)),
self.message.expect_range() - start_offset,
)
@@ -271,20 +271,16 @@ impl Display for MessageCodeFrame<'_> {
}
/// Given some source code and an annotation range, this routine replaces
/// unprintable characters with printable representations of them.
/// tabs with ASCII whitespace, and unprintable characters with printable
/// representations of them.
///
/// The source code returned has an annotation that is updated to reflect
/// changes made to the source code (if any).
///
/// We don't need to normalize whitespace, such as converting tabs to spaces,
/// because `annotate-snippets` handles that internally. Similarly, it's safe to
/// modify the annotation ranges by inserting 3-byte Unicode replacements
/// because `annotate-snippets` will account for their actual width when
/// rendering and displaying the column to the user.
fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode {
fn replace_whitespace_and_unprintable(source: &str, annotation_range: TextRange) -> SourceCode {
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidthBuilder::new(IndentWidth::default());
// Updates the range given by the caller whenever a single byte (at
// `index` in `source`) is replaced with `len` bytes.
@@ -313,7 +309,19 @@ fn replace_unprintable(source: &str, annotation_range: TextRange) -> SourceCode
};
for (index, c) in source.char_indices() {
if let Some(printable) = unprintable_replacement(c) {
let old_width = line_width.get();
line_width = line_width.add_char(c);
if matches!(c, '\t') {
let tab_width = u32::try_from(line_width.get() - old_width)
.expect("small width because of tab size");
result.push_str(&source[last_end..index]);
for _ in 0..tab_width {
result.push(' ');
}
last_end = index + 1;
update_range(index, tab_width);
} else if let Some(printable) = unprintable_replacement(c) {
result.push_str(&source[last_end..index]);
result.push(printable);
last_end = index + 1;
@@ -368,8 +376,9 @@ impl<'a> SourceCode<'a> {
if self.text.as_bytes()[self.annotation_range.start().to_usize() - 1] != b'\n' {
return self;
}
let locator = Locator::new(&self.text);
let start = self.annotation_range.start();
let end = ceil_char_boundary(&self.text, start + TextSize::from(1));
let end = locator.ceil_char_boundary(start + TextSize::from(1));
SourceCode {
annotation_range: TextRange::new(start, end),
..self

View File

@@ -225,8 +225,3 @@ pub(crate) const fn is_assert_raises_exception_call_enabled(settings: &LinterSet
pub(crate) const fn is_add_future_annotations_imports_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}
// https://github.com/astral-sh/ruff/pull/19390
pub(crate) const fn is_trailing_comma_type_params_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

View File

@@ -5,6 +5,7 @@ use ruff_text_size::TextRange;
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::settings::LinterSettings;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::rules::eradicate::detection::comment_contains_code;
@@ -50,6 +51,7 @@ pub(crate) fn commented_out_code(
context: &LintContext,
locator: &Locator,
comment_ranges: &CommentRanges,
settings: &LinterSettings,
) {
let mut comments = comment_ranges.into_iter().peekable();
// Iterate over all comments in the document.
@@ -63,9 +65,7 @@ pub(crate) fn commented_out_code(
}
// Verify that the comment is on its own line, and that it contains code.
if is_own_line_comment(line)
&& comment_contains_code(line, &context.settings().task_tags[..])
{
if is_own_line_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
if let Some(mut diagnostic) =
context.report_diagnostic_if_enabled(CommentedOutCode, range)
{

View File

@@ -27,23 +27,4 @@ mod tests {
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("COM81.py"))]
#[test_case(Path::new("COM81_syntax_error.py"))]
fn preview_rules(path: &Path) -> Result<()> {
let snapshot = format!("preview__{}", path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_commas").join(path).as_path(),
&settings::LinterSettings {
preview: crate::settings::types::PreviewMode::Enabled,
..settings::LinterSettings::for_rules(vec![
Rule::MissingTrailingComma,
Rule::TrailingCommaOnBareTuple,
Rule::ProhibitedTrailingComma,
])
},
)?;
assert_diagnostics!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -5,8 +5,6 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::preview::is_trailing_comma_type_params_enabled;
use crate::settings::LinterSettings;
use crate::{AlwaysFixableViolation, Violation};
use crate::{Edit, Fix};
@@ -26,8 +24,6 @@ enum TokenType {
Def,
For,
Lambda,
Class,
Type,
Irrelevant,
}
@@ -73,8 +69,6 @@ impl From<(TokenKind, TextRange)> for SimpleToken {
TokenKind::Lbrace => TokenType::OpeningCurlyBracket,
TokenKind::Rbrace => TokenType::ClosingBracket,
TokenKind::Def => TokenType::Def,
TokenKind::Class => TokenType::Class,
TokenKind::Type => TokenType::Type,
TokenKind::For => TokenType::For,
TokenKind::Lambda => TokenType::Lambda,
// Import treated like a function.
@@ -104,8 +98,6 @@ enum ContextType {
Dict,
/// Lambda parameter list, e.g. `lambda a, b`.
LambdaParameters,
/// Type parameter list, e.g. `def foo[T, U](): ...`
TypeParameters,
}
/// Comma context - described a comma-delimited "situation".
@@ -298,7 +290,7 @@ pub(crate) fn trailing_commas(
}
// Update the comma context stack.
let context = update_context(token, prev, prev_prev, &mut stack, lint_context.settings());
let context = update_context(token, prev, prev_prev, &mut stack);
check_token(token, prev, prev_prev, context, locator, lint_context);
@@ -334,7 +326,6 @@ fn check_token(
ContextType::No => false,
ContextType::FunctionParameters => true,
ContextType::CallArguments => true,
ContextType::TypeParameters => true,
// `(1)` is not equivalent to `(1,)`.
ContextType::Tuple => context.num_commas != 0,
// `x[1]` is not equivalent to `x[1,]`.
@@ -417,7 +408,6 @@ fn update_context(
prev: SimpleToken,
prev_prev: SimpleToken,
stack: &mut Vec<Context>,
settings: &LinterSettings,
) -> Context {
let new_context = match token.ty {
TokenType::OpeningBracket => match (prev.ty, prev_prev.ty) {
@@ -427,17 +417,6 @@ fn update_context(
}
_ => Context::new(ContextType::Tuple),
},
TokenType::OpeningSquareBracket if is_trailing_comma_type_params_enabled(settings) => {
match (prev.ty, prev_prev.ty) {
(TokenType::Named, TokenType::Def | TokenType::Class | TokenType::Type) => {
Context::new(ContextType::TypeParameters)
}
(TokenType::ClosingBracket | TokenType::Named | TokenType::String, _) => {
Context::new(ContextType::Subscript)
}
_ => Context::new(ContextType::List),
}
}
TokenType::OpeningSquareBracket => match prev.ty {
TokenType::ClosingBracket | TokenType::Named | TokenType::String => {
Context::new(ContextType::Subscript)

View File

@@ -1,30 +0,0 @@
---
source: crates/ruff_linter/src/rules/flake8_commas/mod.rs
---
COM81_syntax_error.py:3:5: SyntaxError: Starred expression cannot be used here
|
1 | # Check for `flake8-commas` violation for a file containing syntax errors.
2 | (
3 | *args
| ^^^^^
4 | )
|
COM81_syntax_error.py:6:9: SyntaxError: Type parameter list cannot be empty
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^
7 | pass
|
COM81_syntax_error.py:6:38: COM819 Trailing comma prohibited
|
4 | )
5 |
6 | def foo[(param1='test', param2='test',):
| ^ COM819
7 | pass
|
= help: Remove trailing comma

View File

@@ -11,6 +11,7 @@ use ruff_text_size::{Ranged, TextRange};
use crate::Locator;
use crate::checkers::ast::LintContext;
use crate::settings::LinterSettings;
use crate::{Edit, Fix, FixAvailability, Violation};
/// ## What it does
@@ -107,15 +108,13 @@ pub(crate) fn implicit(
tokens: &Tokens,
locator: &Locator,
indexer: &Indexer,
settings: &LinterSettings,
) {
for (a_token, b_token) in tokens
.iter()
.filter(|token| {
token.kind() != TokenKind::Comment
&& (context
.settings()
.flake8_implicit_str_concat
.allow_multiline
&& (settings.flake8_implicit_str_concat.allow_multiline
|| token.kind() != TokenKind::NonLogicalNewline)
})
.tuple_windows()

View File

@@ -1,11 +1,10 @@
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::whitespace::trailing_comment_start_offset;
use ruff_python_ast::{Stmt, StmtExpr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix;
use crate::{Edit, Fix, FixAvailability, Violation};
use crate::{Fix, FixAvailability, Violation};
/// ## What it does
/// Removes ellipses (`...`) in otherwise non-empty class bodies.
@@ -51,21 +50,15 @@ pub(crate) fn ellipsis_in_non_empty_class_body(checker: &Checker, body: &[Stmt])
}
for stmt in body {
let Stmt::Expr(StmtExpr { value, .. }) = stmt else {
let Stmt::Expr(StmtExpr { value, .. }) = &stmt else {
continue;
};
if value.is_ellipsis_literal_expr() {
let mut diagnostic =
checker.report_diagnostic(EllipsisInNonEmptyClassBody, stmt.range());
// Try to preserve trailing comment if it exists
let edit = if let Some(index) = trailing_comment_start_offset(stmt, checker.source()) {
Edit::range_deletion(stmt.range().add_end(index))
} else {
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer())
};
let edit =
fix::edits::delete_stmt(stmt, Some(stmt), checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::safe_edit(edit).isolate(Checker::isolation(
checker.semantic().current_statement_id(),
)));

View File

@@ -145,22 +145,3 @@ PYI013.py:36:5: PYI013 [*] Non-empty class body must not contain `...`
37 36 |
38 37 | def __init__():
39 38 | pass
PYI013.py:44:5: PYI013 [*] Non-empty class body must not contain `...`
|
42 | class NonEmptyChildWithInlineComment:
43 | value: int
44 | ... # preserve me
| ^^^ PYI013
|
= help: Remove unnecessary `...`
Safe fix
41 41 |
42 42 | class NonEmptyChildWithInlineComment:
43 43 | value: int
44 |- ... # preserve me
44 |+ # preserve me
45 45 |
46 46 |
47 47 | class EmptyClass:

View File

@@ -17,10 +17,9 @@ PYI013.pyi:5:5: PYI013 [*] Non-empty class body must not contain `...`
3 3 | class OneAttributeClass:
4 4 | value: int
5 |- ... # Error
5 |+ # Error
6 6 |
7 7 | class OneAttributeClass2:
8 8 | ... # Error
6 5 |
7 6 | class OneAttributeClass2:
8 7 | ... # Error
PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -36,10 +35,9 @@ PYI013.pyi:8:5: PYI013 [*] Non-empty class body must not contain `...`
6 6 |
7 7 | class OneAttributeClass2:
8 |- ... # Error
8 |+ # Error
9 9 | value: int
10 10 |
11 11 | class MyClass:
9 8 | value: int
10 9 |
11 10 | class MyClass:
PYI013.pyi:12:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -93,10 +91,9 @@ PYI013.pyi:17:5: PYI013 [*] Non-empty class body must not contain `...`
15 15 | class TwoEllipsesClass:
16 16 | ...
17 |- ... # Error
17 |+ # Error
18 18 |
19 19 | class DocstringClass:
20 20 | """
18 17 |
19 18 | class DocstringClass:
20 19 | """
PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -114,10 +111,9 @@ PYI013.pyi:24:5: PYI013 [*] Non-empty class body must not contain `...`
22 22 | """
23 23 |
24 |- ... # Error
24 |+ # Error
25 25 |
26 26 | class NonEmptyChild(Exception):
27 27 | value: int
25 24 |
26 25 | class NonEmptyChild(Exception):
27 26 | value: int
PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -135,10 +131,9 @@ PYI013.pyi:28:5: PYI013 [*] Non-empty class body must not contain `...`
26 26 | class NonEmptyChild(Exception):
27 27 | value: int
28 |- ... # Error
28 |+ # Error
29 29 |
30 30 | class NonEmptyChild2(Exception):
31 31 | ... # Error
29 28 |
30 29 | class NonEmptyChild2(Exception):
31 30 | ... # Error
PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -154,10 +149,9 @@ PYI013.pyi:31:5: PYI013 [*] Non-empty class body must not contain `...`
29 29 |
30 30 | class NonEmptyChild2(Exception):
31 |- ... # Error
31 |+ # Error
32 32 | value: int
33 33 |
34 34 | class NonEmptyWithInit:
32 31 | value: int
33 32 |
34 33 | class NonEmptyWithInit:
PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
|
@@ -175,28 +169,6 @@ PYI013.pyi:36:5: PYI013 [*] Non-empty class body must not contain `...`
34 34 | class NonEmptyWithInit:
35 35 | value: int
36 |- ... # Error
36 |+ # Error
37 37 |
38 38 | def __init__():
39 39 | pass
PYI013.pyi:43:5: PYI013 [*] Non-empty class body must not contain `...`
|
41 | class NonEmptyChildWithInlineComment:
42 | value: int
43 | ... # preserve me
| ^^^ PYI013
44 |
45 | # Not violations
|
= help: Remove unnecessary `...`
Safe fix
40 40 |
41 41 | class NonEmptyChildWithInlineComment:
42 42 | value: int
43 |- ... # preserve me
43 |+ # preserve me
44 44 |
45 45 | # Not violations
46 46 |
37 36 |
38 37 | def __init__():
39 38 | pass

View File

@@ -794,7 +794,6 @@ mod tests {
#[test_case(Path::new("comments_and_newlines.py"))]
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring.pyi"))]
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("docstring_with_continuation.py"))]
#[test_case(Path::new("docstring_with_semicolon.py"))]
@@ -829,7 +828,6 @@ mod tests {
#[test_case(Path::new("comments_and_newlines.py"))]
#[test_case(Path::new("docstring.py"))]
#[test_case(Path::new("docstring.pyi"))]
#[test_case(Path::new("docstring_followed_by_continuation.py"))]
#[test_case(Path::new("docstring_only.py"))]
#[test_case(Path::new("docstring_with_continuation.py"))]
#[test_case(Path::new("docstring_with_semicolon.py"))]

View File

@@ -1,9 +0,0 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
docstring_followed_by_continuation.py:1:1: I002 [*] Missing required import: `from __future__ import annotations`
Safe fix
1 1 | """Hello, world!"""\
2 2 |
3 |+from __future__ import annotations
3 4 | x = 1; y = 2

View File

@@ -1,9 +0,0 @@
---
source: crates/ruff_linter/src/rules/isort/mod.rs
---
docstring_followed_by_continuation.py:1:1: I002 [*] Missing required import: `from __future__ import annotations as _annotations`
Safe fix
1 1 | """Hello, world!"""\
2 2 |
3 |+from __future__ import annotations as _annotations
3 4 | x = 1; y = 2

View File

@@ -249,11 +249,6 @@ pub(crate) fn manual_list_comprehension(checker: &Checker, for_stmt: &ast::StmtF
.iter()
.find(|binding| for_stmt.target.range() == binding.range)
.unwrap();
// If the target variable is global (e.g., `global INDEX`) or nonlocal (e.g., `nonlocal INDEX`),
// then it is intended to be used elsewhere outside the for loop.
if target_binding.is_global() || target_binding.is_nonlocal() {
return;
}
// If any references to the loop target variable are after the loop,
// then converting it into a comprehension would cause a NameError
if target_binding

View File

@@ -263,7 +263,5 @@ PERF401.py:292:9: PERF401 Use a list comprehension to create a transformed list
291 | for i in range(3):
292 | result.append((x for x in [i]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
293 |
294 | G_INDEX = None
|
= help: Replace for loop with list comprehension

View File

@@ -612,8 +612,6 @@ PERF401.py:292:9: PERF401 [*] Use a list comprehension to create a transformed l
291 | for i in range(3):
292 | result.append((x for x in [i]))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PERF401
293 |
294 | G_INDEX = None
|
= help: Replace for loop with list comprehension
@@ -625,6 +623,3 @@ PERF401.py:292:9: PERF401 [*] Use a list comprehension to create a transformed l
291 |- for i in range(3):
292 |- result.append((x for x in [i]))
290 |+ result = [(x for x in [i]) for i in range(3)]
293 291 |
294 292 | G_INDEX = None
295 293 | def f():

View File

@@ -21,6 +21,7 @@ use crate::checkers::ast::{DiagnosticGuard, LintContext};
use crate::checkers::logical_lines::expand_indent;
use crate::line_width::IndentWidth;
use crate::rules::pycodestyle::helpers::is_non_logical_token;
use crate::settings::LinterSettings;
use crate::{AlwaysFixableViolation, Edit, Fix, Locator, Violation};
/// Number of blank lines around top level classes and functions.
@@ -693,12 +694,14 @@ pub(crate) struct BlankLinesChecker<'a, 'b> {
source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>,
context: &'a LintContext<'b>,
settings: &'a LinterSettings,
}
impl<'a, 'b> BlankLinesChecker<'a, 'b> {
pub(crate) fn new(
locator: &'a Locator<'a>,
stylist: &'a Stylist<'a>,
settings: &'a LinterSettings,
source_type: PySourceType,
cell_offsets: Option<&'a CellOffsets>,
context: &'a LintContext<'b>,
@@ -709,6 +712,7 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
source_type,
cell_offsets,
context,
settings,
}
}
@@ -729,7 +733,7 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
let line_preprocessor = LinePreprocessor::new(
tokens,
self.locator,
self.context.settings().tab_size,
self.settings.tab_size,
self.cell_offsets,
);
@@ -875,8 +879,7 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
// `isort` defaults to 2 if before a class or function definition (except in stubs where it is one) and 1 otherwise.
// Defaulting to 2 (or 1 in stubs) here is correct because the variable is only used when testing the
// blank lines before a class or function definition.
u32::try_from(self.context.settings().isort.lines_after_imports)
.unwrap_or(max_lines_level)
u32::try_from(self.settings.isort.lines_after_imports).unwrap_or(max_lines_level)
} else {
max_lines_level
}
@@ -938,10 +941,8 @@ impl<'a, 'b> BlankLinesChecker<'a, 'b> {
(LogicalLineKind::Import, Follows::FromImport)
| (LogicalLineKind::FromImport, Follows::Import)
) {
max_lines_level.max(
u32::try_from(self.context.settings().isort.lines_between_types)
.unwrap_or(u32::MAX),
)
max_lines_level
.max(u32::try_from(self.settings.isort.lines_between_types).unwrap_or(u32::MAX))
} else {
expected_blank_lines_before_definition
};

View File

@@ -15,8 +15,8 @@ E101.py:15:1: E101 Indentation contains mixed spaces and tabs
|
13 | def func_mixed_start_with_space():
14 | # E101
15 | print("mixed starts with space")
| ^^^^^^^^^^^^^^^^^^^^ E101
15 | print("mixed starts with space")
| ^^^^^^^^^^^^^^^ E101
16 |
17 | def xyz():
|
@@ -25,6 +25,6 @@ E101.py:19:1: E101 Indentation contains mixed spaces and tabs
|
17 | def xyz():
18 | # E101
19 | print("xyz");
| ^^^^^^^ E101
19 | print("xyz");
| ^^^^ E101
|

View File

@@ -47,7 +47,7 @@ E20.py:6:15: E201 [*] Whitespace after '{'
6 | spam(ham[1], { eggs: 2})
| ^ E201
7 | #: E201:1:6
8 | spam( ham[1], {eggs: 2})
8 | spam( ham[1], {eggs: 2})
|
= help: Remove whitespace before '{'
@@ -65,10 +65,10 @@ E20.py:8:6: E201 [*] Whitespace after '('
|
6 | spam(ham[1], { eggs: 2})
7 | #: E201:1:6
8 | spam( ham[1], {eggs: 2})
| ^^^^ E201
8 | spam( ham[1], {eggs: 2})
| ^^^ E201
9 | #: E201:1:10
10 | spam(ham[ 1], {eggs: 2})
10 | spam(ham[ 1], {eggs: 2})
|
= help: Remove whitespace before '('
@@ -84,12 +84,12 @@ E20.py:8:6: E201 [*] Whitespace after '('
E20.py:10:10: E201 [*] Whitespace after '['
|
8 | spam( ham[1], {eggs: 2})
8 | spam( ham[1], {eggs: 2})
9 | #: E201:1:10
10 | spam(ham[ 1], {eggs: 2})
| ^^^^ E201
10 | spam(ham[ 1], {eggs: 2})
| ^^^ E201
11 | #: E201:1:15
12 | spam(ham[1], { eggs: 2})
12 | spam(ham[1], { eggs: 2})
|
= help: Remove whitespace before '['
@@ -105,10 +105,10 @@ E20.py:10:10: E201 [*] Whitespace after '['
E20.py:12:15: E201 [*] Whitespace after '{'
|
10 | spam(ham[ 1], {eggs: 2})
10 | spam(ham[ 1], {eggs: 2})
11 | #: E201:1:15
12 | spam(ham[1], { eggs: 2})
| ^^^^ E201
12 | spam(ham[1], { eggs: 2})
| ^^ E201
13 | #: Okay
14 | spam(ham[1], {eggs: 2})
|

View File

@@ -49,7 +49,7 @@ E20.py:23:11: E202 [*] Whitespace before ']'
23 | spam(ham[1 ], {eggs: 2})
| ^ E202
24 | #: E202:1:23
25 | spam(ham[1], {eggs: 2} )
25 | spam(ham[1], {eggs: 2} )
|
= help: Remove whitespace before ']'
@@ -67,10 +67,10 @@ E20.py:25:23: E202 [*] Whitespace before ')'
|
23 | spam(ham[1 ], {eggs: 2})
24 | #: E202:1:23
25 | spam(ham[1], {eggs: 2} )
| ^^^^ E202
25 | spam(ham[1], {eggs: 2} )
| ^^ E202
26 | #: E202:1:22
27 | spam(ham[1], {eggs: 2 })
27 | spam(ham[1], {eggs: 2 })
|
= help: Remove whitespace before ')'
@@ -86,12 +86,12 @@ E20.py:25:23: E202 [*] Whitespace before ')'
E20.py:27:22: E202 [*] Whitespace before '}'
|
25 | spam(ham[1], {eggs: 2} )
25 | spam(ham[1], {eggs: 2} )
26 | #: E202:1:22
27 | spam(ham[1], {eggs: 2 })
| ^^^^ E202
27 | spam(ham[1], {eggs: 2 })
| ^^^ E202
28 | #: E202:1:11
29 | spam(ham[1 ], {eggs: 2})
29 | spam(ham[1 ], {eggs: 2})
|
= help: Remove whitespace before '}'
@@ -107,10 +107,10 @@ E20.py:27:22: E202 [*] Whitespace before '}'
E20.py:29:11: E202 [*] Whitespace before ']'
|
27 | spam(ham[1], {eggs: 2 })
27 | spam(ham[1], {eggs: 2 })
28 | #: E202:1:11
29 | spam(ham[1 ], {eggs: 2})
| ^^^^ E202
29 | spam(ham[1 ], {eggs: 2})
| ^^ E202
30 | #: Okay
31 | spam(ham[1], {eggs: 2})
|

View File

@@ -25,8 +25,8 @@ E20.py:55:10: E203 [*] Whitespace before ':'
|
53 | x, y = y, x
54 | #: E203:1:10
55 | if x == 4 :
| ^^^^ E203
55 | if x == 4 :
| ^^^ E203
56 | print(x, y)
57 | x, y = y, x
|
@@ -67,8 +67,8 @@ E20.py:63:16: E203 [*] Whitespace before ';'
|
61 | #: E203:2:15 E702:2:16
62 | if x == 4:
63 | print(x, y) ; x, y = y, x
| ^^^^ E203
63 | print(x, y) ; x, y = y, x
| ^ E203
64 | #: E203:3:13
65 | if x == 4:
|

View File

@@ -1,12 +1,13 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
E22.py:43:2: E223 [*] Tab before operator
|
41 | #: E223
42 | foobart = 4
43 | a = 3 # aligned with tab
| ^^^^ E223
43 | a = 3 # aligned with tab
| ^^^ E223
44 | #:
|
= help: Replace with single space

View File

@@ -1,12 +1,13 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
E24.py:6:8: E242 [*] Tab after comma
|
4 | b = (1, 20)
5 | #: E242
6 | a = (1, 2) # tab before 2
| ^^^^ E242
6 | a = (1, 2) # tab before 2
| ^ E242
7 | #: Okay
8 | b = (1, 20) # space before 20
|

View File

@@ -66,7 +66,7 @@ E27.py:8:3: E271 [*] Multiple spaces after keyword
E27.py:15:6: E271 [*] Multiple spaces after keyword
|
13 | True and False
13 | True and False
14 | #: E271
15 | a and b
| ^^ E271

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
E27.py:21:2: E272 [*] Multiple spaces before keyword
|
@@ -50,7 +51,7 @@ E27.py:25:5: E272 [*] Multiple spaces before keyword
25 | this and False
| ^^ E272
26 | #: E273
27 | a and b
27 | a and b
|
= help: Replace with single space

View File

@@ -8,7 +8,7 @@ E27.py:11:9: E273 [*] Tab after keyword
11 | True and False
| ^^^^^^^^ E273
12 | #: E273 E274
13 | True and False
13 | True and False
|
= help: Replace with single space
@@ -26,7 +26,7 @@ E27.py:13:5: E273 [*] Tab after keyword
|
11 | True and False
12 | #: E273 E274
13 | True and False
13 | True and False
| ^^^^^^^^ E273
14 | #: E271
15 | a and b
@@ -47,8 +47,8 @@ E27.py:13:10: E273 [*] Tab after keyword
|
11 | True and False
12 | #: E273 E274
13 | True and False
| ^^^^ E273
13 | True and False
| ^ E273
14 | #: E271
15 | a and b
|
@@ -68,10 +68,10 @@ E27.py:27:6: E273 [*] Tab after keyword
|
25 | this and False
26 | #: E273
27 | a and b
| ^^^^ E273
27 | a and b
| ^^^ E273
28 | #: E274
29 | a and b
29 | a and b
|
= help: Replace with single space
@@ -87,10 +87,10 @@ E27.py:27:6: E273 [*] Tab after keyword
E27.py:31:10: E273 [*] Tab after keyword
|
29 | a and b
29 | a and b
30 | #: E273 E274
31 | this and False
| ^^^^ E273
31 | this and False
| ^ E273
32 | #: Okay
33 | from u import (a, b)
|

View File

@@ -1,14 +1,15 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
E27.py:29:2: E274 [*] Tab before keyword
|
27 | a and b
27 | a and b
28 | #: E274
29 | a and b
| ^^^^^^^^ E274
29 | a and b
| ^^^^^^^ E274
30 | #: E273 E274
31 | this and False
31 | this and False
|
= help: Replace with single space
@@ -24,9 +25,9 @@ E27.py:29:2: E274 [*] Tab before keyword
E27.py:31:5: E274 [*] Tab before keyword
|
29 | a and b
29 | a and b
30 | #: E273 E274
31 | this and False
31 | this and False
| ^^^^^^^^ E274
32 | #: Okay
33 | from u import (a, b)

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
snapshot_kind: text
---
W19.py:1:1: W191 Indentation contains tabs
|
@@ -370,7 +371,7 @@ W19.py:157:1: W191 Indentation contains tabs
156 | f"test{
157 | tab_indented_should_be_flagged
| ^^^^ W191
158 | } <- this tab is fine"
158 | } <- this tab is fine"
|
W19.py:161:1: W191 Indentation contains tabs
@@ -378,5 +379,5 @@ W19.py:161:1: W191 Indentation contains tabs
160 | f"""test{
161 | tab_indented_should_be_flagged
| ^^^^ W191
162 | } <- this tab is fine"""
162 | } <- this tab is fine"""
|

View File

@@ -6,9 +6,8 @@ use ruff_python_ast::{
use ruff_python_semantic::{SemanticModel, analyze::typing};
use ruff_text_size::Ranged;
use crate::Violation;
use crate::checkers::ast::Checker;
use crate::fix;
use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// ## What it does
/// Checks for access to the first or last element of `str.split()` or `str.rsplit()` without
@@ -36,14 +35,10 @@ use crate::{AlwaysFixableViolation, Applicability, Edit, Fix};
/// url = "www.example.com"
/// suffix = url.rsplit(".", maxsplit=1)[-1]
/// ```
///
/// ## Fix Safety
/// This rule's fix is marked as unsafe for `split()`/`rsplit()` calls that contain `**kwargs`, as
/// adding a `maxsplit` keyword to such a call may lead to a duplicate keyword argument error.
#[derive(ViolationMetadata)]
pub(crate) struct MissingMaxsplitArg {
index: SliceBoundary,
actual_split_type: String,
suggested_split_type: String,
}
/// Represents the index of the slice used for this rule (which can only be 0 or -1)
@@ -52,27 +47,25 @@ enum SliceBoundary {
Last,
}
impl AlwaysFixableViolation for MissingMaxsplitArg {
impl Violation for MissingMaxsplitArg {
#[derive_message_formats]
fn message(&self) -> String {
let MissingMaxsplitArg {
actual_split_type: _,
suggested_split_type,
} = self;
format!("Replace with `{suggested_split_type}(..., maxsplit=1)`.")
}
fn fix_title(&self) -> String {
let MissingMaxsplitArg {
index,
actual_split_type,
suggested_split_type,
} = self;
let suggested_split_type = match index {
SliceBoundary::First => "split",
SliceBoundary::Last => "rsplit",
};
if actual_split_type == suggested_split_type {
format!("Pass `maxsplit=1` into `str.{actual_split_type}()`")
} else {
format!("Use `str.{suggested_split_type}()` and pass `maxsplit=1`")
format!(
"Instead of `str.{actual_split_type}()`, call `str.{suggested_split_type}()` and pass `maxsplit=1`",
)
}
}
}
@@ -130,8 +123,8 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
};
// Check the function is "split" or "rsplit"
let actual_split_type = attr.as_str();
if !matches!(actual_split_type, "split" | "rsplit") {
let attr = attr.as_str();
if !matches!(attr, "split" | "rsplit") {
return;
}
@@ -168,48 +161,11 @@ pub(crate) fn missing_maxsplit_arg(checker: &Checker, value: &Expr, slice: &Expr
}
}
let suggested_split_type = match slice_boundary {
SliceBoundary::First => "split",
SliceBoundary::Last => "rsplit",
};
let maxsplit_argument_edit = fix::edits::add_argument(
"maxsplit=1",
arguments,
checker.comment_ranges(),
checker.locator().contents(),
);
// Only change `actual_split_type` if it doesn't match `suggested_split_type`
let split_type_edit: Option<Edit> = if actual_split_type == suggested_split_type {
None
} else {
Some(Edit::range_replacement(
suggested_split_type.to_string(),
attr.range(),
))
};
let mut diagnostic = checker.report_diagnostic(
checker.report_diagnostic(
MissingMaxsplitArg {
actual_split_type: actual_split_type.to_string(),
suggested_split_type: suggested_split_type.to_string(),
index: slice_boundary,
actual_split_type: attr.to_string(),
},
expr.range(),
);
diagnostic.set_fix(Fix::applicable_edits(
maxsplit_argument_edit,
split_type_edit,
// If keyword.arg is `None` (i.e. if the function call contains `**kwargs`), mark the fix as unsafe
if arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {
Applicability::Safe
},
));
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
missing_maxsplit_arg.py:14:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:14:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
12 | # Errors
13 | ## Test split called directly on string literal
@@ -10,19 +10,8 @@ missing_maxsplit_arg.py:14:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
11 11 |
12 12 | # Errors
13 13 | ## Test split called directly on string literal
14 |-"1,2,3".split(",")[0] # [missing-maxsplit-arg]
14 |+"1,2,3".split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
15 15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:15:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:15:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
13 | ## Test split called directly on string literal
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
@@ -31,19 +20,8 @@ missing_maxsplit_arg.py:15:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
Safe fix
12 12 | # Errors
13 13 | ## Test split called directly on string literal
14 14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 |-"1,2,3".split(",")[-1] # [missing-maxsplit-arg]
15 |+"1,2,3".rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
16 16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
18 18 |
missing_maxsplit_arg.py:16:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:16:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
@@ -51,19 +29,8 @@ missing_maxsplit_arg.py:16:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
| ^^^^^^^^^^^^^^^^^^^^^^ PLC0207
17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
13 13 | ## Test split called directly on string literal
14 14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 |-"1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
16 |+"1,2,3".split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
18 18 |
19 19 | ## Test split called on string variable
missing_maxsplit_arg.py:17:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:17:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
@@ -72,19 +39,8 @@ missing_maxsplit_arg.py:17:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
18 |
19 | ## Test split called on string variable
|
= help: Pass `maxsplit=1` into `str.rsplit()`
Safe fix
14 14 | "1,2,3".split(",")[0] # [missing-maxsplit-arg]
15 15 | "1,2,3".split(",")[-1] # [missing-maxsplit-arg]
16 16 | "1,2,3".rsplit(",")[0] # [missing-maxsplit-arg]
17 |-"1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
17 |+"1,2,3".rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
18 18 |
19 19 | ## Test split called on string variable
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:20:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:20:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -92,19 +48,8 @@ missing_maxsplit_arg.py:20:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
17 17 | "1,2,3".rsplit(",")[-1] # [missing-maxsplit-arg]
18 18 |
19 19 | ## Test split called on string variable
20 |-SEQ.split(",")[0] # [missing-maxsplit-arg]
20 |+SEQ.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
21 21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:21:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:21:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
19 | ## Test split called on string variable
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
@@ -113,19 +58,8 @@ missing_maxsplit_arg.py:21:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
Safe fix
18 18 |
19 19 | ## Test split called on string variable
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 |-SEQ.split(",")[-1] # [missing-maxsplit-arg]
21 |+SEQ.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
22 22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
24 24 |
missing_maxsplit_arg.py:22:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:22:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
@@ -133,19 +67,8 @@ missing_maxsplit_arg.py:22:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
| ^^^^^^^^^^^^^^^^^^ PLC0207
23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
19 19 | ## Test split called on string variable
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 |-SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
22 |+SEQ.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
24 24 |
25 25 | ## Test split called on class attribute
missing_maxsplit_arg.py:23:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:23:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -154,19 +77,8 @@ missing_maxsplit_arg.py:23:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
24 |
25 | ## Test split called on class attribute
|
= help: Pass `maxsplit=1` into `str.rsplit()`
Safe fix
20 20 | SEQ.split(",")[0] # [missing-maxsplit-arg]
21 21 | SEQ.split(",")[-1] # [missing-maxsplit-arg]
22 22 | SEQ.rsplit(",")[0] # [missing-maxsplit-arg]
23 |-SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
23 |+SEQ.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
24 24 |
25 25 | ## Test split called on class attribute
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:26:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:26:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -174,19 +86,8 @@ missing_maxsplit_arg.py:26:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
23 23 | SEQ.rsplit(",")[-1] # [missing-maxsplit-arg]
24 24 |
25 25 | ## Test split called on class attribute
26 |-Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
26 |+Foo.class_str.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
27 27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:27:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:27:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
25 | ## Test split called on class attribute
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
@@ -195,19 +96,8 @@ missing_maxsplit_arg.py:27:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
Safe fix
24 24 |
25 25 | ## Test split called on class attribute
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 |-Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
27 |+Foo.class_str.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
28 28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
30 30 |
missing_maxsplit_arg.py:28:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:28:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
@@ -215,19 +105,8 @@ missing_maxsplit_arg.py:28:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
25 25 | ## Test split called on class attribute
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 |-Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
28 |+Foo.class_str.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
30 30 |
31 31 | ## Test split called on sliced string
missing_maxsplit_arg.py:29:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:29:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -236,19 +115,8 @@ missing_maxsplit_arg.py:29:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
30 |
31 | ## Test split called on sliced string
|
= help: Pass `maxsplit=1` into `str.rsplit()`
Safe fix
26 26 | Foo.class_str.split(",")[0] # [missing-maxsplit-arg]
27 27 | Foo.class_str.split(",")[-1] # [missing-maxsplit-arg]
28 28 | Foo.class_str.rsplit(",")[0] # [missing-maxsplit-arg]
29 |-Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
29 |+Foo.class_str.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
30 30 |
31 31 | ## Test split called on sliced string
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:32:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:32:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -256,19 +124,8 @@ missing_maxsplit_arg.py:32:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
29 29 | Foo.class_str.rsplit(",")[-1] # [missing-maxsplit-arg]
30 30 |
31 31 | ## Test split called on sliced string
32 |-"1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
32 |+"1,2,3"[::-1].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:33:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:33:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
31 | ## Test split called on sliced string
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -277,19 +134,8 @@ missing_maxsplit_arg.py:33:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
30 30 |
31 31 | ## Test split called on sliced string
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 |-"1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
33 |+"1,2,3"[::-1][::-1].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:34:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:34:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
@@ -298,19 +144,8 @@ missing_maxsplit_arg.py:34:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
31 31 | ## Test split called on sliced string
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 |-SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
34 |+SEQ[:3].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:35:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:35:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
@@ -319,19 +154,8 @@ missing_maxsplit_arg.py:35:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
Safe fix
32 32 | "1,2,3"[::-1].split(",")[0] # [missing-maxsplit-arg]
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 |-Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
35 |+Foo.class_str[1:3].rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:36:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:36:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
@@ -340,19 +164,8 @@ missing_maxsplit_arg.py:36:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
33 33 | "1,2,3"[::-1][::-1].split(",")[0] # [missing-maxsplit-arg]
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 |-"1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
36 |+"1,2,3"[::-1].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
39 39 |
missing_maxsplit_arg.py:37:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:37:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -360,19 +173,8 @@ missing_maxsplit_arg.py:37:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
| ^^^^^^^^^^^^^^^^^^^^^^ PLC0207
38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
34 34 | SEQ[:3].split(",")[0] # [missing-maxsplit-arg]
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 |-SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
37 |+SEQ[:3].split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
39 39 |
40 40 | ## Test sep given as named argument
missing_maxsplit_arg.py:38:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:38:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
@@ -381,19 +183,8 @@ missing_maxsplit_arg.py:38:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
39 |
40 | ## Test sep given as named argument
|
= help: Pass `maxsplit=1` into `str.rsplit()`
Safe fix
35 35 | Foo.class_str[1:3].split(",")[-1] # [missing-maxsplit-arg]
36 36 | "1,2,3"[::-1].rsplit(",")[0] # [missing-maxsplit-arg]
37 37 | SEQ[:3].rsplit(",")[0] # [missing-maxsplit-arg]
38 |-Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
38 |+Foo.class_str[1:3].rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
39 39 |
40 40 | ## Test sep given as named argument
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:41:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:41:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -401,19 +192,8 @@ missing_maxsplit_arg.py:41:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
38 38 | Foo.class_str[1:3].rsplit(",")[-1] # [missing-maxsplit-arg]
39 39 |
40 40 | ## Test sep given as named argument
41 |-"1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
41 |+"1,2,3".split(maxsplit=1, sep=",")[0] # [missing-maxsplit-arg]
42 42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:42:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:42:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
40 | ## Test sep given as named argument
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
@@ -422,19 +202,8 @@ missing_maxsplit_arg.py:42:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
Safe fix
39 39 |
40 40 | ## Test sep given as named argument
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 |-"1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
42 |+"1,2,3".rsplit(maxsplit=1, sep=",")[-1] # [missing-maxsplit-arg]
43 43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
missing_maxsplit_arg.py:43:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:43:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
@@ -442,19 +211,8 @@ missing_maxsplit_arg.py:43:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
40 40 | ## Test sep given as named argument
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 |-"1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
43 |+"1,2,3".split(maxsplit=1, sep=",")[0] # [missing-maxsplit-arg]
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
46 46 | ## Special cases
missing_maxsplit_arg.py:44:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:44:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
@@ -463,19 +221,8 @@ missing_maxsplit_arg.py:44:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
45 |
46 | ## Special cases
|
= help: Pass `maxsplit=1` into `str.rsplit()`
Safe fix
41 41 | "1,2,3".split(sep=",")[0] # [missing-maxsplit-arg]
42 42 | "1,2,3".split(sep=",")[-1] # [missing-maxsplit-arg]
43 43 | "1,2,3".rsplit(sep=",")[0] # [missing-maxsplit-arg]
44 |-"1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
44 |+"1,2,3".rsplit(maxsplit=1, sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
46 46 | ## Special cases
47 47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:47:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:47:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -483,19 +230,8 @@ missing_maxsplit_arg.py:47:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
44 44 | "1,2,3".rsplit(sep=",")[-1] # [missing-maxsplit-arg]
45 45 |
46 46 | ## Special cases
47 |-"1,2,3".split("\n")[0] # [missing-maxsplit-arg]
47 |+"1,2,3".split("\n", maxsplit=1)[0] # [missing-maxsplit-arg]
48 48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
49 49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
50 50 |
missing_maxsplit_arg.py:48:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:48:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
46 | ## Special cases
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
@@ -503,19 +239,8 @@ missing_maxsplit_arg.py:48:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
Safe fix
45 45 |
46 46 | ## Special cases
47 47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 |-"1,2,3".split("split")[-1] # [missing-maxsplit-arg]
48 |+"1,2,3".rsplit("split", maxsplit=1)[-1] # [missing-maxsplit-arg]
49 49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
50 50 |
51 51 | ## Test class attribute named split
missing_maxsplit_arg.py:49:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:49:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
@@ -524,19 +249,8 @@ missing_maxsplit_arg.py:49:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
50 |
51 | ## Test class attribute named split
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
46 46 | ## Special cases
47 47 | "1,2,3".split("\n")[0] # [missing-maxsplit-arg]
48 48 | "1,2,3".split("split")[-1] # [missing-maxsplit-arg]
49 |-"1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
49 |+"1,2,3".split("rsplit", maxsplit=1)[0] # [missing-maxsplit-arg]
50 50 |
51 51 | ## Test class attribute named split
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:52:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:52:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -544,19 +258,8 @@ missing_maxsplit_arg.py:52:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
|
= help: Pass `maxsplit=1` into `str.split()`
Safe fix
49 49 | "1,2,3".rsplit("rsplit")[0] # [missing-maxsplit-arg]
50 50 |
51 51 | ## Test class attribute named split
52 |-Bar.split.split(",")[0] # [missing-maxsplit-arg]
52 |+Bar.split.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
53 53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:53:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:53:1: PLC0207 Instead of `str.split()`, call `str.rsplit()` and pass `maxsplit=1`
|
51 | ## Test class attribute named split
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
@@ -565,19 +268,8 @@ missing_maxsplit_arg.py:53:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.rsplit()` and pass `maxsplit=1`
Safe fix
50 50 |
51 51 | ## Test class attribute named split
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 |-Bar.split.split(",")[-1] # [missing-maxsplit-arg]
53 |+Bar.split.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
54 54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
56 56 |
missing_maxsplit_arg.py:54:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:54:1: PLC0207 Instead of `str.rsplit()`, call `str.split()` and pass `maxsplit=1`
|
52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
@@ -585,19 +277,8 @@ missing_maxsplit_arg.py:54:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
| ^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
|
= help: Use `str.split()` and pass `maxsplit=1`
Safe fix
51 51 | ## Test class attribute named split
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 |-Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
54 |+Bar.split.split(",", maxsplit=1)[0] # [missing-maxsplit-arg]
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
56 56 |
57 57 | ## Test unpacked dict literal kwargs
missing_maxsplit_arg.py:55:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`.
missing_maxsplit_arg.py:55:1: PLC0207 Pass `maxsplit=1` into `str.rsplit()`
|
53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
@@ -606,37 +287,15 @@ missing_maxsplit_arg.py:55:1: PLC0207 [*] Replace with `rsplit(..., maxsplit=1)`
56 |
57 | ## Test unpacked dict literal kwargs
|
= help: Pass `maxsplit=1` into `str.rsplit()`
Safe fix
52 52 | Bar.split.split(",")[0] # [missing-maxsplit-arg]
53 53 | Bar.split.split(",")[-1] # [missing-maxsplit-arg]
54 54 | Bar.split.rsplit(",")[0] # [missing-maxsplit-arg]
55 |-Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
55 |+Bar.split.rsplit(",", maxsplit=1)[-1] # [missing-maxsplit-arg]
56 56 |
57 57 | ## Test unpacked dict literal kwargs
58 58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
missing_maxsplit_arg.py:58:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:58:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
57 | ## Test unpacked dict literal kwargs
58 | "1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
= help: Pass `maxsplit=1` into `str.split()`
Unsafe fix
55 55 | Bar.split.rsplit(",")[-1] # [missing-maxsplit-arg]
56 56 |
57 57 | ## Test unpacked dict literal kwargs
58 |-"1,2,3".split(**{"sep": ","})[0] # [missing-maxsplit-arg]
58 |+"1,2,3".split(maxsplit=1, **{"sep": ","})[0] # [missing-maxsplit-arg]
59 59 |
60 60 |
61 61 | # OK
missing_maxsplit_arg.py:179:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:179:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
177 | # Errors
178 | kwargs_without_maxsplit = {"seq": ","}
@@ -645,19 +304,8 @@ missing_maxsplit_arg.py:179:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`
180 | # OK
181 | kwargs_with_maxsplit = {"maxsplit": 1}
|
= help: Pass `maxsplit=1` into `str.split()`
Unsafe fix
176 176 | ## TODO: These require the ability to resolve a dict variable name to a value
177 177 | # Errors
178 178 | kwargs_without_maxsplit = {"seq": ","}
179 |-"1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg]
179 |+"1,2,3".split(maxsplit=1, **kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg]
180 180 | # OK
181 181 | kwargs_with_maxsplit = {"maxsplit": 1}
182 182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
missing_maxsplit_arg.py:182:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:182:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
180 | # OK
181 | kwargs_with_maxsplit = {"maxsplit": 1}
@@ -666,29 +314,11 @@ missing_maxsplit_arg.py:182:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`
183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
|
= help: Pass `maxsplit=1` into `str.split()`
Unsafe fix
179 179 | "1,2,3".split(**kwargs_without_maxsplit)[0] # TODO: [missing-maxsplit-arg]
180 180 | # OK
181 181 | kwargs_with_maxsplit = {"maxsplit": 1}
182 |-"1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
182 |+"1,2,3".split(",", maxsplit=1, **kwargs_with_maxsplit)[0] # TODO: false positive
183 183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
missing_maxsplit_arg.py:184:1: PLC0207 [*] Replace with `split(..., maxsplit=1)`.
missing_maxsplit_arg.py:184:1: PLC0207 Pass `maxsplit=1` into `str.split()`
|
182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 | "1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLC0207
|
= help: Pass `maxsplit=1` into `str.split()`
Unsafe fix
181 181 | kwargs_with_maxsplit = {"maxsplit": 1}
182 182 | "1,2,3".split(",", **kwargs_with_maxsplit)[0] # TODO: false positive
183 183 | kwargs_with_maxsplit = {"sep": ",", "maxsplit": 1}
184 |-"1,2,3".split(**kwargs_with_maxsplit)[0] # TODO: false positive
184 |+"1,2,3".split(maxsplit=1, **kwargs_with_maxsplit)[0] # TODO: false positive

View File

@@ -124,20 +124,10 @@ fn is_sequential(indices: &[usize]) -> bool {
indices.iter().enumerate().all(|(idx, value)| idx == *value)
}
static FORMAT_SPECIFIER: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"(?x)
(?P<prefix>
^|[^{]|(?:\{{2})+ # preceded by nothing, a non-brace, or an even number of braces
)
\{ # opening curly brace
(?P<int>\d+) # followed by any integer
(?P<fmt>.*?) # followed by any text
} # followed by a closing brace
",
)
.unwrap()
});
// An opening curly brace, followed by any integer, followed by any text,
// followed by a closing brace.
static FORMAT_SPECIFIER: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\{(?P<int>\d+)(?P<fmt>.*?)}").unwrap());
/// Remove the explicit positional indices from a format string.
fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Arena<String>) {
@@ -145,7 +135,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
Expression::SimpleString(expr) => {
expr.value = arena.alloc(
FORMAT_SPECIFIER
.replace_all(expr.value, "$prefix{$fmt}")
.replace_all(expr.value, "{$fmt}")
.to_string(),
);
}
@@ -156,7 +146,7 @@ fn remove_specifiers<'a>(value: &mut Expression<'a>, arena: &'a typed_arena::Are
libcst_native::String::Simple(string) => {
string.value = arena.alloc(
FORMAT_SPECIFIER
.replace_all(string.value, "$prefix{$fmt}")
.replace_all(string.value, "{$fmt}")
.to_string(),
);
}

View File

@@ -481,7 +481,6 @@ UP030_0.py:59:1: UP030 [*] Use implicit references for positional format fields
59 |+"{}_{}".format(2, 1, )
60 60 |
61 61 | "{1}_{0}".format(1, 2)
62 62 |
UP030_0.py:61:1: UP030 [*] Use implicit references for positional format fields
|
@@ -489,8 +488,6 @@ UP030_0.py:61:1: UP030 [*] Use implicit references for positional format fields
60 |
61 | "{1}_{0}".format(1, 2)
| ^^^^^^^^^^^^^^^^^^^^^^ UP030
62 |
63 | r"\d{{1,2}} {0}".format(42)
|
= help: Remove explicit positional indices
@@ -500,42 +497,3 @@ UP030_0.py:61:1: UP030 [*] Use implicit references for positional format fields
60 60 |
61 |-"{1}_{0}".format(1, 2)
61 |+"{}_{}".format(2, 1)
62 62 |
63 63 | r"\d{{1,2}} {0}".format(42)
64 64 |
UP030_0.py:63:1: UP030 [*] Use implicit references for positional format fields
|
61 | "{1}_{0}".format(1, 2)
62 |
63 | r"\d{{1,2}} {0}".format(42)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP030
64 |
65 | "{{{0}}}".format(123)
|
= help: Remove explicit positional indices
Unsafe fix
60 60 |
61 61 | "{1}_{0}".format(1, 2)
62 62 |
63 |-r"\d{{1,2}} {0}".format(42)
63 |+r"\d{{1,2}} {}".format(42)
64 64 |
65 65 | "{{{0}}}".format(123)
UP030_0.py:65:1: UP030 [*] Use implicit references for positional format fields
|
63 | r"\d{{1,2}} {0}".format(42)
64 |
65 | "{{{0}}}".format(123)
| ^^^^^^^^^^^^^^^^^^^^^ UP030
|
= help: Remove explicit positional indices
Unsafe fix
62 62 |
63 63 | r"\d{{1,2}} {0}".format(42)
64 64 |
65 |-"{{{0}}}".format(123)
65 |+"{{{}}}".format(123)

View File

@@ -180,18 +180,18 @@ fn is_valid_argument_type(
typing::is_float(binding, semantic),
)
})
.unwrap_or((false, false))
.unwrap_or_default()
} else {
(false, false)
};
match (method_name, constructor) {
// Decimal.from_float: Only int or bool are safe (float is unsafe due to FloatOperation trap)
// Decimal.from_float accepts int, bool, float
(MethodName::FromFloat, Constructor::Decimal) => match resolved_type {
ResolvedPythonType::Atom(PythonType::Number(
NumberLike::Integer | NumberLike::Bool,
NumberLike::Integer | NumberLike::Bool | NumberLike::Float,
)) => true,
ResolvedPythonType::Unknown => is_int,
ResolvedPythonType::Unknown => is_int || is_float,
_ => false,
},
// Fraction.from_float accepts int, bool, float
@@ -286,8 +286,10 @@ fn handle_non_finite_float_special_case(
let [float_arg] = arguments.args.as_ref() else {
return None;
};
let normalized = as_non_finite_float_string_literal(float_arg)?;
let replacement_text = format!(r#"{constructor_name}("{normalized}")"#);
as_non_finite_float_string_literal(float_arg)?;
let replacement_arg = checker.locator().slice(float_arg).to_string();
let replacement_text = format!("{constructor_name}({replacement_arg})");
Some(Edit::range_replacement(replacement_text, call.range()))
}

View File

@@ -158,7 +158,7 @@ FURB164.py:13:27: FURB164 [*] Verbose method `from_float` in `Decimal` construct
|
= help: Replace with `Decimal` constructor
Unsafe fix
Safe fix
10 10 | _ = fractions.Fraction.from_float(4.2)
11 11 | _ = Fraction.from_decimal(Decimal("4.2"))
12 12 | _ = Fraction.from_decimal(Decimal("-4.2"))
@@ -179,7 +179,7 @@ FURB164.py:14:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Unsafe fix
Safe fix
11 11 | _ = Fraction.from_decimal(Decimal("4.2"))
12 12 | _ = Fraction.from_decimal(Decimal("-4.2"))
13 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
@@ -200,7 +200,7 @@ FURB164.py:15:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Unsafe fix
Safe fix
12 12 | _ = Fraction.from_decimal(Decimal("-4.2"))
13 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 14 | _ = Decimal.from_float(0.1)
@@ -221,7 +221,7 @@ FURB164.py:16:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Unsafe fix
Safe fix
13 13 | _ = Fraction.from_decimal(Decimal.from_float(4.2))
14 14 | _ = Decimal.from_float(0.1)
15 15 | _ = Decimal.from_float(-0.5)
@@ -242,7 +242,7 @@ FURB164.py:17:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
|
= help: Replace with `Decimal` constructor
Unsafe fix
Safe fix
14 14 | _ = Decimal.from_float(0.1)
15 15 | _ = Decimal.from_float(-0.5)
16 16 | _ = Decimal.from_float(5.0)
@@ -310,7 +310,7 @@ FURB164.py:20:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
18 18 | _ = Decimal.from_float(float("inf"))
19 19 | _ = Decimal.from_float(float("-inf"))
20 |-_ = Decimal.from_float(float("Infinity"))
20 |+_ = Decimal("infinity")
20 |+_ = Decimal("Infinity")
21 21 | _ = Decimal.from_float(float("-Infinity"))
22 22 | _ = Decimal.from_float(float("nan"))
23 23 | _ = Decimal.from_float(float("-NaN"))
@@ -331,7 +331,7 @@ FURB164.py:21:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
19 19 | _ = Decimal.from_float(float("-inf"))
20 20 | _ = Decimal.from_float(float("Infinity"))
21 |-_ = Decimal.from_float(float("-Infinity"))
21 |+_ = Decimal("-infinity")
21 |+_ = Decimal("-Infinity")
22 22 | _ = Decimal.from_float(float("nan"))
23 23 | _ = Decimal.from_float(float("-NaN"))
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
@@ -373,7 +373,7 @@ FURB164.py:23:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
21 21 | _ = Decimal.from_float(float("-Infinity"))
22 22 | _ = Decimal.from_float(float("nan"))
23 |-_ = Decimal.from_float(float("-NaN"))
23 |+_ = Decimal("-nan")
23 |+_ = Decimal("-NaN")
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
@@ -394,7 +394,7 @@ FURB164.py:24:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
22 22 | _ = Decimal.from_float(float("nan"))
23 23 | _ = Decimal.from_float(float("-NaN"))
24 |-_ = Decimal.from_float(float(" \n+nan \t"))
24 |+_ = Decimal("+nan")
24 |+_ = Decimal(" \n+nan \t")
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
@@ -415,7 +415,7 @@ FURB164.py:25:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
23 23 | _ = Decimal.from_float(float("-NaN"))
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 |-_ = Decimal.from_float(float(" iNf\n\t "))
25 |+_ = Decimal("inf")
25 |+_ = Decimal(" iNf\n\t ")
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
28 28 | _ = Decimal.from_float(float(" -InfinIty\n \t"))
@@ -436,7 +436,7 @@ FURB164.py:26:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
24 24 | _ = Decimal.from_float(float(" \n+nan \t"))
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 |-_ = Decimal.from_float(float(" -inF\n \t"))
26 |+_ = Decimal("-inf")
26 |+_ = Decimal(" -inF\n \t")
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
28 28 | _ = Decimal.from_float(float(" -InfinIty\n \t"))
29 29 |
@@ -456,7 +456,7 @@ FURB164.py:27:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
25 25 | _ = Decimal.from_float(float(" iNf\n\t "))
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 |-_ = Decimal.from_float(float(" InfinIty\n\t "))
27 |+_ = Decimal("infinity")
27 |+_ = Decimal(" InfinIty\n\t ")
28 28 | _ = Decimal.from_float(float(" -InfinIty\n \t"))
29 29 |
30 30 | # Cases with keyword arguments - should produce unsafe fixes
@@ -477,7 +477,7 @@ FURB164.py:28:5: FURB164 [*] Verbose method `from_float` in `Decimal` constructi
26 26 | _ = Decimal.from_float(float(" -inF\n \t"))
27 27 | _ = Decimal.from_float(float(" InfinIty\n\t "))
28 |-_ = Decimal.from_float(float(" -InfinIty\n \t"))
28 |+_ = Decimal("-infinity")
28 |+_ = Decimal(" -InfinIty\n \t")
29 29 |
30 30 | # Cases with keyword arguments - should produce unsafe fixes
31 31 | _ = Fraction.from_decimal(dec=Decimal("4.2"))
@@ -612,96 +612,3 @@ FURB164.py:45:5: FURB164 [*] Verbose method `from_float` in `Fraction` construct
46 46 |
47 47 | # OK - should not trigger the rule
48 48 | _ = Fraction(0.1)
FURB164.py:60:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
59 | # Cases with int and bool - should produce safe fixes
60 | _ = Decimal.from_float(1)
| ^^^^^^^^^^^^^^^^^^^^^ FURB164
61 | _ = Decimal.from_float(True)
|
= help: Replace with `Decimal` constructor
Safe fix
57 57 | _ = decimal.Decimal(4.2)
58 58 |
59 59 | # Cases with int and bool - should produce safe fixes
60 |-_ = Decimal.from_float(1)
60 |+_ = Decimal(1)
61 61 | _ = Decimal.from_float(True)
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
FURB164.py:61:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
59 | # Cases with int and bool - should produce safe fixes
60 | _ = Decimal.from_float(1)
61 | _ = Decimal.from_float(True)
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
62 |
63 | # Cases with non-finite floats - should produce safe fixes
|
= help: Replace with `Decimal` constructor
Safe fix
58 58 |
59 59 | # Cases with int and bool - should produce safe fixes
60 60 | _ = Decimal.from_float(1)
61 |-_ = Decimal.from_float(True)
61 |+_ = Decimal(True)
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
64 64 | _ = Decimal.from_float(float("-nan"))
FURB164.py:64:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
63 | # Cases with non-finite floats - should produce safe fixes
64 | _ = Decimal.from_float(float("-nan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
|
= help: Replace with `Decimal` constructor
Safe fix
61 61 | _ = Decimal.from_float(True)
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
64 |-_ = Decimal.from_float(float("-nan"))
64 |+_ = Decimal("-nan")
65 65 | _ = Decimal.from_float(float("\x2dnan"))
66 66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
FURB164.py:65:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
63 | # Cases with non-finite floats - should produce safe fixes
64 | _ = Decimal.from_float(float("-nan"))
65 | _ = Decimal.from_float(float("\x2dnan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
|
= help: Replace with `Decimal` constructor
Safe fix
62 62 |
63 63 | # Cases with non-finite floats - should produce safe fixes
64 64 | _ = Decimal.from_float(float("-nan"))
65 |-_ = Decimal.from_float(float("\x2dnan"))
65 |+_ = Decimal("-nan")
66 66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
FURB164.py:66:5: FURB164 [*] Verbose method `from_float` in `Decimal` construction
|
64 | _ = Decimal.from_float(float("-nan"))
65 | _ = Decimal.from_float(float("\x2dnan"))
66 | _ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB164
|
= help: Replace with `Decimal` constructor
Safe fix
63 63 | # Cases with non-finite floats - should produce safe fixes
64 64 | _ = Decimal.from_float(float("-nan"))
65 65 | _ = Decimal.from_float(float("\x2dnan"))
66 |-_ = Decimal.from_float(float("\N{HYPHEN-MINUS}nan"))
66 |+_ = Decimal("-nan")

View File

@@ -12,6 +12,7 @@ use crate::checkers::ast::{Checker, LintContext};
use crate::preview::is_unicode_to_unicode_confusables_enabled;
use crate::rules::ruff::rules::Context;
use crate::rules::ruff::rules::confusables::confusable;
use crate::settings::LinterSettings;
/// ## What it does
/// Checks for ambiguous Unicode characters in strings.
@@ -177,9 +178,12 @@ pub(crate) fn ambiguous_unicode_character_comment(
context: &LintContext,
locator: &Locator,
range: TextRange,
settings: &LinterSettings,
) {
let text = locator.slice(range);
ambiguous_unicode_character(text, range, Context::Comment, context);
for candidate in ambiguous_unicode_character(text, range, settings) {
candidate.into_diagnostic(Context::Comment, settings, context);
}
}
/// RUF001, RUF002
@@ -200,19 +204,22 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
match part {
ast::StringLikePart::String(string_literal) => {
let text = checker.locator().slice(string_literal);
ambiguous_unicode_character(
text,
string_literal.range(),
context,
checker.context(),
);
for candidate in
ambiguous_unicode_character(text, string_literal.range(), checker.settings())
{
candidate.report_diagnostic(checker, context);
}
}
ast::StringLikePart::Bytes(_) => {}
ast::StringLikePart::FString(FString { elements, .. })
| ast::StringLikePart::TString(TString { elements, .. }) => {
for literal in elements.literals() {
let text = checker.locator().slice(literal);
ambiguous_unicode_character(text, literal.range(), context, checker.context());
for candidate in
ambiguous_unicode_character(text, literal.range(), checker.settings())
{
candidate.report_diagnostic(checker, context);
}
}
}
}
@@ -222,12 +229,13 @@ pub(crate) fn ambiguous_unicode_character_string(checker: &Checker, string_like:
fn ambiguous_unicode_character(
text: &str,
range: TextRange,
context: Context,
lint_context: &LintContext,
) {
settings: &LinterSettings,
) -> Vec<Candidate> {
let mut candidates = Vec::new();
// Most of the time, we don't need to check for ambiguous unicode characters at all.
if text.is_ascii() {
return;
return candidates;
}
// Iterate over the "words" in the text.
@@ -239,7 +247,7 @@ fn ambiguous_unicode_character(
if !word_candidates.is_empty() {
if word_flags.is_candidate_word() {
for candidate in word_candidates.drain(..) {
candidate.into_diagnostic(context, lint_context);
candidates.push(candidate);
}
}
word_candidates.clear();
@@ -250,23 +258,21 @@ fn ambiguous_unicode_character(
// case, it's always included as a diagnostic.
if !current_char.is_ascii() {
if let Some(representant) = confusable(current_char as u32).filter(|representant| {
is_unicode_to_unicode_confusables_enabled(lint_context.settings())
|| representant.is_ascii()
is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii()
}) {
let candidate = Candidate::new(
TextSize::try_from(relative_offset).unwrap() + range.start(),
current_char,
representant,
);
candidate.into_diagnostic(context, lint_context);
candidates.push(candidate);
}
}
} else if current_char.is_ascii() {
// The current word contains at least one ASCII character.
word_flags |= WordFlags::ASCII;
} else if let Some(representant) = confusable(current_char as u32).filter(|representant| {
is_unicode_to_unicode_confusables_enabled(lint_context.settings())
|| representant.is_ascii()
is_unicode_to_unicode_confusables_enabled(settings) || representant.is_ascii()
}) {
// The current word contains an ambiguous unicode character.
word_candidates.push(Candidate::new(
@@ -284,11 +290,13 @@ fn ambiguous_unicode_character(
if !word_candidates.is_empty() {
if word_flags.is_candidate_word() {
for candidate in word_candidates.drain(..) {
candidate.into_diagnostic(context, lint_context);
candidates.push(candidate);
}
}
word_candidates.clear();
}
candidates
}
bitflags! {
@@ -334,12 +342,13 @@ impl Candidate {
}
}
fn into_diagnostic(self, context: Context, lint_context: &LintContext) {
if !lint_context
.settings()
.allowed_confusables
.contains(&self.confusable)
{
fn into_diagnostic(
self,
context: Context,
settings: &LinterSettings,
lint_context: &LintContext,
) {
if !settings.allowed_confusables.contains(&self.confusable) {
let char_range = TextRange::at(self.offset, self.confusable.text_len());
match context {
Context::String => lint_context.report_diagnostic_if_enabled(
@@ -366,6 +375,39 @@ impl Candidate {
};
}
}
fn report_diagnostic(self, checker: &Checker, context: Context) {
if !checker
.settings()
.allowed_confusables
.contains(&self.confusable)
{
let char_range = TextRange::at(self.offset, self.confusable.text_len());
match context {
Context::String => checker.report_diagnostic_if_enabled(
AmbiguousUnicodeCharacterString {
confusable: self.confusable,
representant: self.representant,
},
char_range,
),
Context::Docstring => checker.report_diagnostic_if_enabled(
AmbiguousUnicodeCharacterDocstring {
confusable: self.confusable,
representant: self.representant,
},
char_range,
),
Context::Comment => checker.report_diagnostic_if_enabled(
AmbiguousUnicodeCharacterComment {
confusable: self.confusable,
representant: self.representant,
},
char_range,
),
};
}
}
}
struct NamedUnicode(char);

View File

@@ -149,7 +149,8 @@ fn convert_call_to_conversion_flag(
formatted_string_expression.whitespace_before_expression = space();
}
formatted_string_expression.expression = if needs_paren_expr(arg) {
formatted_string_expression.expression = if needs_paren(OperatorPrecedence::from_expr(arg))
{
call.args[0]
.value
.clone()
@@ -177,16 +178,6 @@ fn needs_paren(precedence: OperatorPrecedence) -> bool {
precedence <= OperatorPrecedence::Lambda
}
fn needs_paren_expr(arg: &Expr) -> bool {
// Generator expressions need to be parenthesized in f-string expressions
if let Some(generator) = arg.as_generator_expr() {
return !generator.parenthesized;
}
// Check precedence for other expressions
needs_paren(OperatorPrecedence::from_expr(arg))
}
/// Represents the three built-in Python conversion functions that can be replaced
/// with f-string conversion flags.
#[derive(Copy, Clone)]

View File

@@ -359,7 +359,6 @@ RUF010.py:52:4: RUF010 [*] Use explicit conversion flag
52 |+f"{(x := 2)!r}"
53 53 |
54 54 | f"{str(object=3)}"
55 55 |
RUF010.py:54:4: RUF010 [*] Use explicit conversion flag
|
@@ -367,8 +366,6 @@ RUF010.py:54:4: RUF010 [*] Use explicit conversion flag
53 |
54 | f"{str(object=3)}"
| ^^^^^^^^^^^^^ RUF010
55 |
56 | f"{str(x for x in [])}"
|
= help: Replace with conversion flag
@@ -378,42 +375,3 @@ RUF010.py:54:4: RUF010 [*] Use explicit conversion flag
53 53 |
54 |-f"{str(object=3)}"
54 |+f"{3!s}"
55 55 |
56 56 | f"{str(x for x in [])}"
57 57 |
RUF010.py:56:4: RUF010 [*] Use explicit conversion flag
|
54 | f"{str(object=3)}"
55 |
56 | f"{str(x for x in [])}"
| ^^^^^^^^^^^^^^^^^^ RUF010
57 |
58 | f"{str((x for x in []))}"
|
= help: Replace with conversion flag
Safe fix
53 53 |
54 54 | f"{str(object=3)}"
55 55 |
56 |-f"{str(x for x in [])}"
56 |+f"{(x for x in [])!s}"
57 57 |
58 58 | f"{str((x for x in []))}"
RUF010.py:58:4: RUF010 [*] Use explicit conversion flag
|
56 | f"{str(x for x in [])}"
57 |
58 | f"{str((x for x in []))}"
| ^^^^^^^^^^^^^^^^^^^^ RUF010
|
= help: Replace with conversion flag
Safe fix
55 55 |
56 56 | f"{str(x for x in [])}"
57 57 |
58 |-f"{str((x for x in []))}"
58 |+f"{(x for x in [])!s}"

View File

@@ -22,7 +22,7 @@ RUF054.py:10:3: RUF054 Indented form feed
RUF054.py:13:2: RUF054 Indented form feed
|
12 | def _():
13 | pass
13 | pass
| ^ RUF054
14 |
15 | if False:

View File

@@ -1,6 +1,5 @@
use std::cmp::Ordering;
use std::fmt::{Debug, Display, Formatter};
use std::hash::Hash;
use std::sync::{Arc, OnceLock};
#[cfg(feature = "serde")]
@@ -163,7 +162,7 @@ impl SourceFileBuilder {
/// A source file that is identified by its name. Optionally stores the source code and [`LineIndex`].
///
/// Cloning a [`SourceFile`] is cheap, because it only requires bumping a reference count.
#[derive(Clone, Eq, PartialEq, Hash)]
#[derive(Clone, Eq, PartialEq)]
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
pub struct SourceFile {
inner: Arc<SourceFileInner>,
@@ -242,13 +241,6 @@ impl PartialEq for SourceFileInner {
impl Eq for SourceFileInner {}
impl Hash for SourceFileInner {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.code.hash(state);
}
}
/// The line and column of an offset in a source file.
///
/// See [`LineIndex::line_column`] for more information.

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_wasm"
version = "0.12.7"
version = "0.12.5"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -66,9 +66,9 @@ enum CompletionTargetTokens<'t> {
/// A token was found under the cursor, but it didn't
/// match any of our anticipated token patterns.
Generic { token: &'t Token },
/// No token was found. We generally treat this like
/// `Generic` (i.e., offer scope based completions).
Unknown,
/// No token was found, but we have the offset of the
/// cursor.
Unknown { offset: TextSize },
}
impl<'t> CompletionTargetTokens<'t> {
@@ -78,7 +78,7 @@ impl<'t> CompletionTargetTokens<'t> {
static OBJECT_DOT_NON_EMPTY: [TokenKind; 2] = [TokenKind::Dot, TokenKind::Name];
let offset = match parsed.tokens().at_offset(offset) {
TokenAt::None => return Some(CompletionTargetTokens::Unknown),
TokenAt::None => return Some(CompletionTargetTokens::Unknown { offset }),
TokenAt::Single(tok) => tok.end(),
TokenAt::Between(_, tok) => tok.start(),
};
@@ -122,7 +122,7 @@ impl<'t> CompletionTargetTokens<'t> {
return None;
} else {
let Some(last) = before.last() else {
return Some(CompletionTargetTokens::Unknown);
return Some(CompletionTargetTokens::Unknown { offset });
};
CompletionTargetTokens::Generic { token: last }
},
@@ -171,7 +171,7 @@ impl<'t> CompletionTargetTokens<'t> {
node: covering_node.node(),
})
}
CompletionTargetTokens::Unknown => {
CompletionTargetTokens::Unknown { offset } => {
let range = TextRange::empty(offset);
let covering_node = covering_node(parsed.syntax().into(), range);
Some(CompletionTargetAst::Scoped {

View File

@@ -38,7 +38,7 @@ mod tests {
impl CursorTest {
fn references(&self) -> String {
let Some(mut reference_results) =
let Some(reference_results) =
goto_references(&self.db, self.cursor.file, self.cursor.offset, true)
else {
return "No references found".to_string();
@@ -48,8 +48,6 @@ mod tests {
return "No references found".to_string();
}
reference_results.sort_by_key(ReferenceTarget::file);
self.render_diagnostics(reference_results.into_iter().enumerate().map(
|(i, ref_item)| -> ReferenceResult {
ReferenceResult {

View File

@@ -199,14 +199,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:13
@@ -228,14 +228,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:2:22
@@ -344,14 +344,14 @@ mod tests {
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:18
@@ -413,14 +413,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:2890:7
--> stdlib/builtins.pyi:2888:7
|
2888 | """See PEP 585"""
2889 |
2890 | class dict(MutableMapping[_KT, _VT]):
2886 | """See PEP 585"""
2887 |
2888 | class dict(MutableMapping[_KT, _VT]):
| ^^^^
2891 | """dict() -> new empty dictionary
2892 | dict(mapping) -> new dictionary initialized from a mapping object's
2889 | """dict() -> new empty dictionary
2890 | dict(mapping) -> new dictionary initialized from a mapping object's
|
info: Source
--> main.py:6:5
@@ -444,14 +444,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:17
@@ -537,14 +537,14 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:4:27
@@ -568,13 +568,13 @@ f(**kwargs<CURSOR>)
assert_snapshot!(test.goto_type_definition(), @r#"
info[goto-type-definition]: Type definition
--> stdlib/types.pyi:922:11
--> stdlib/types.pyi:921:11
|
920 | if sys.version_info >= (3, 10):
921 | @final
922 | class NoneType:
919 | if sys.version_info >= (3, 10):
920 | @final
921 | class NoneType:
| ^^^^^^^^
923 | """The type of the None singleton."""
922 | """The type of the None singleton."""
|
info: Source
--> main.py:3:17
@@ -585,14 +585,14 @@ f(**kwargs<CURSOR>)
|
info[goto-type-definition]: Type definition
--> stdlib/builtins.pyi:892:7
--> stdlib/builtins.pyi:890:7
|
890 | def __getitem__(self, key: int, /) -> str | int | None: ...
891 |
892 | class str(Sequence[str]):
888 | def __getitem__(self, key: int, /) -> str | int | None: ...
889 |
890 | class str(Sequence[str]):
| ^^^
893 | """str(object='') -> str
894 | str(bytes_or_buffer[, encoding[, errors]]) -> str
891 | """str(object='') -> str
892 | str(bytes_or_buffer[, encoding[, errors]]) -> str
|
info: Source
--> main.py:3:17

View File

@@ -6,20 +6,20 @@
//! types, and documentation. It supports multiple signatures for union types
//! and overloads.
use crate::{
Db, docstring::get_parameter_documentation, find_node::covering_node, stub_mapping::StubMapper,
};
use crate::{Db, docstring::get_parameter_documentation, find_node::covering_node};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ty_python_semantic::ResolvedDefinition;
use ty_python_semantic::semantic_index::definition::Definition;
use ty_python_semantic::types::{CallSignatureDetails, call_signature_details};
// TODO: We may want to add special-case handling for calls to constructors
// so the class docstring is used in place of (or inaddition to) any docstring
// associated with the __new__ or __init__ call.
// Limitations of the current implementation:
// TODO - If the target function is declared in a stub file but defined (implemented)
// in a source file, the documentation will not reflect the a docstring that appears
// only in the implementation. To do this, we'll need to map the function or
// method in the stub to the implementation and extract the docstring from there.
/// Information about a function parameter
#[derive(Debug, Clone)]
@@ -66,6 +66,10 @@ pub fn signature_help(db: &dyn Db, file: File, offset: TextSize) -> Option<Signa
// Get the call expression at the given position.
let (call_expr, current_arg_index) = get_call_expr(&parsed, offset)?;
if offset >= call_expr.end() {
return None;
}
// Get signature details from the semantic analyzer.
let signature_details: Vec<CallSignatureDetails<'_>> =
call_signature_details(db, file, call_expr);
@@ -97,19 +101,11 @@ fn get_call_expr(
parsed: &ruff_db::parsed::ParsedModuleRef,
offset: TextSize,
) -> Option<(&ast::ExprCall, usize)> {
let root_node: AnyNodeRef = parsed.syntax().into();
// Create a range from the offset for the covering_node function.
// Use length 1 if it fits within the root node, otherwise use zero-length range.
let one_char_range = TextRange::at(offset, TextSize::from(1));
let range = if root_node.range().contains_range(one_char_range) {
one_char_range
} else {
TextRange::at(offset, TextSize::from(0))
};
let range = TextRange::new(offset, offset);
// Find the covering node at the given position that is a function call.
let covering_node = covering_node(root_node, range)
let covering_node = covering_node(parsed.syntax().into(), range)
.find_first(|node| matches!(node, AnyNodeRef::ExprCall(_)))
.ok()?;
@@ -185,32 +181,11 @@ fn create_signature_details_from_call_signature_details(
/// Determine appropriate documentation for a callable type based on its original type.
fn get_callable_documentation(db: &dyn crate::Db, definition: Option<Definition>) -> String {
// TODO: If the definition is located within a stub file and no docstring
// is present, try to map the symbol to an implementation file and extract
// the docstring from that location.
if let Some(definition) = definition {
// First try to get the docstring from the original definition
let original_docstring = definition.docstring(db);
// If we got a docstring from the original definition, use it
if let Some(docstring) = original_docstring {
return docstring;
}
// If the definition is located within a stub file and no docstring
// is present, try to map the symbol to an implementation file and extract
// the docstring from that location.
let stub_mapper = StubMapper::new(db);
let resolved_definition = ResolvedDefinition::Definition(definition);
// Try to find the corresponding implementation definition
for mapped_definition in stub_mapper.map_definition(resolved_definition) {
if let ResolvedDefinition::Definition(impl_definition) = mapped_definition {
if let Some(impl_docstring) = impl_definition.docstring(db) {
return impl_docstring;
}
}
}
// Fall back to empty string if no docstring found anywhere
String::new()
definition.docstring(db).unwrap_or_default()
} else {
String::new()
}
@@ -708,95 +683,6 @@ mod tests {
);
}
#[test]
fn signature_help_after_closing_paren() {
let test = cursor_test(
r#"
def func1(v: str) -> str:
return v
r = func1("")<CURSOR>
print(r)
"#,
);
let result = test.signature_help();
assert!(
result.is_none(),
"Signature help should return None after closing paren"
);
}
#[test]
fn signature_help_after_nested_closing_paren() {
let test = cursor_test(
r#"
def inner_func(x: str) -> str:
return x.upper()
def outer_func(a: int, b: str) -> str:
return f"{a}: {b}"
result = outer_func(42, inner_func("hello")<CURSOR>
"#,
);
// Should return signature help for the outer function call
// even though cursor is after the closing paren of the inner call
let result = test
.signature_help()
.expect("Should have signature help for outer function");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert!(signature.label.contains("a: int") && signature.label.contains("b: str"));
// Should be on the second parameter (b: str) since we're after the inner call
assert_eq!(signature.active_parameter, Some(1));
assert_eq!(result.active_signature, Some(0));
}
#[test]
fn signature_help_stub_to_implementation_mapping() {
// Test that when a function is called from a stub file with no docstring,
// the signature help includes the docstring from the corresponding implementation file
let test = CursorTest::builder()
.source(
"main.py",
r#"
from lib import func
result = func(<CURSOR>
"#,
)
.source(
"lib.pyi",
r#"
def func() -> str: ...
"#,
)
.source(
"lib.py",
r#"
def func() -> str:
"""This function does something."""
return ""
"#,
)
.build();
let result = test.signature_help().expect("Should have signature help");
assert_eq!(result.signatures.len(), 1);
let signature = &result.signatures[0];
assert_eq!(signature.label, "() -> str");
let expected_docstring = "This function does something.";
assert_eq!(
signature.documentation,
Some(expected_docstring.to_string())
);
}
impl CursorTest {
fn signature_help(&self) -> Option<SignatureHelpInfo> {
crate::signature_help::signature_help(&self.db, self.cursor.file, self.cursor.offset)

View File

@@ -1,5 +1,5 @@
use std::fmt::Formatter;
use std::panic::RefUnwindSafe;
use std::panic::{AssertUnwindSafe, RefUnwindSafe};
use std::sync::Arc;
use std::{cmp, fmt};
@@ -87,7 +87,9 @@ impl ProjectDatabase {
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check(&self) -> Vec<Diagnostic> {
self.project().check(self, &mut DummyReporter)
let mut reporter = DummyReporter;
let reporter = AssertUnwindSafe(&mut reporter as &mut dyn ProgressReporter);
self.project().check(self, reporter)
}
/// Checks the files in the project and its dependencies, using the given reporter.
@@ -96,6 +98,7 @@ impl ProjectDatabase {
///
/// [`set_check_mode`]: ProjectDatabase::set_check_mode
pub fn check_with_reporter(&self, reporter: &mut dyn ProgressReporter) -> Vec<Diagnostic> {
let reporter = AssertUnwindSafe(reporter);
self.project().check(self, reporter)
}

View File

@@ -174,7 +174,7 @@ impl Project {
/// This is a salsa query to prevent re-computing queries if other, unrelated
/// settings change. For example, we don't want that changing the terminal settings
/// invalidates any type checking queries.
#[salsa::tracked(returns(deref), heap_size=get_size2::heap_size)]
#[salsa::tracked(returns(deref), heap_size=get_size2::GetSize::get_heap_size)]
pub fn rules(self, db: &dyn Db) -> Arc<RuleSelection> {
self.settings(db).to_rules()
}
@@ -228,7 +228,7 @@ impl Project {
pub(crate) fn check(
self,
db: &ProjectDatabase,
reporter: &mut dyn ProgressReporter,
mut reporter: AssertUnwindSafe<&mut dyn ProgressReporter>,
) -> Vec<Diagnostic> {
let project_span = tracing::debug_span!("Project::check");
let _span = project_span.enter();
@@ -511,7 +511,7 @@ impl Project {
}
}
#[salsa::tracked(returns(ref), heap_size=get_size2::heap_size)]
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn check_file_impl(db: &dyn Db, file: File) -> Result<Box<[Diagnostic]>, Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = Vec::new();

View File

@@ -96,7 +96,7 @@ impl Override {
}
/// Resolves the settings for a given file.
#[salsa::tracked(returns(ref), heap_size=get_size2::heap_size)]
#[salsa::tracked(returns(ref), heap_size=get_size2::GetSize::get_heap_size)]
pub(crate) fn file_settings(db: &dyn Db, file: File) -> FileSettings {
let settings = db.project().settings(db);
@@ -155,7 +155,7 @@ pub(crate) fn file_settings(db: &dyn Db, file: File) -> FileSettings {
/// This is to make Salsa happy because it requires that queries with only a single argument
/// take a salsa-struct as argument, which isn't the case here. The `()` enables salsa's
/// automatic interning for the arguments.
#[salsa::tracked(heap_size=get_size2::heap_size)]
#[salsa::tracked(heap_size=get_size2::GetSize::get_heap_size)]
fn merge_overrides(db: &dyn Db, overrides: Vec<Arc<InnerOverrideOptions>>, _: ()) -> FileSettings {
let mut overrides = overrides.into_iter().rev();
let mut merged = (*overrides.next().unwrap()).clone();

View File

@@ -13,7 +13,7 @@ reveal_type(x) # revealed: int | float
x = (1, 2)
x += (3, 4)
reveal_type(x) # revealed: tuple[Literal[1, 2, 3, 4], ...]
reveal_type(x) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
```
## Dunder methods

View File

@@ -1,123 +0,0 @@
# `async` / `await`
## Basic
```py
async def retrieve() -> int:
return 42
async def main():
result = await retrieve()
reveal_type(result) # revealed: int
```
## Generic `async` functions
```py
from typing import TypeVar
T = TypeVar("T")
async def persist(x: T) -> T:
return x
async def f(x: int):
result = await persist(x)
reveal_type(result) # revealed: int
```
## Use cases
### `Future`
```py
import asyncio
import concurrent.futures
def blocking_function() -> int:
return 42
async def main():
loop = asyncio.get_event_loop()
with concurrent.futures.ThreadPoolExecutor() as pool:
result = await loop.run_in_executor(pool, blocking_function)
# TODO: should be `int`
reveal_type(result) # revealed: Unknown
```
### `asyncio.Task`
```py
import asyncio
async def f() -> int:
return 1
async def main():
task = asyncio.create_task(f())
result = await task
# TODO: this should be `int`
reveal_type(result) # revealed: Unknown
```
### `asyncio.gather`
```py
import asyncio
async def task(name: str) -> int:
return len(name)
async def main():
(a, b) = await asyncio.gather(
task("A"),
task("B"),
)
# TODO: these should be `int`
reveal_type(a) # revealed: Unknown
reveal_type(b) # revealed: Unknown
```
## Under the hood
```toml
[environment]
python-version = "3.12" # Use 3.12 to be able to use PEP 695 generics
```
Let's look at the example from the beginning again:
```py
async def retrieve() -> int:
return 42
```
When we look at the signature of this function, we see that it actually returns a `CoroutineType`:
```py
reveal_type(retrieve) # revealed: def retrieve() -> CoroutineType[Any, Any, int]
```
The expression `await retrieve()` desugars into a call to the `__await__` dunder method on the
`CoroutineType` object, followed by a `yield from`. Let's first see the return type of `__await__`:
```py
reveal_type(retrieve().__await__()) # revealed: Generator[Any, None, int]
```
We can see that this returns a `Generator` that yields `Any`, and eventually returns `int`. For the
final type of the `await` expression, we retrieve that third argument of the `Generator` type:
```py
from typing import Generator
def _():
result = yield from retrieve().__await__()
reveal_type(result) # revealed: int
```

View File

@@ -0,0 +1,49 @@
# Static binary operations using `in`
## Basic functionality
This demonstrates type inference support for `<str-literal> in <tuple>`:
```py
from ty_extensions import static_assert
static_assert("foo" in ("quux", "foo", "baz"))
static_assert("foo" not in ("quux", "bar", "baz"))
```
## With variables
```py
from ty_extensions import static_assert
x = ("quux", "foo", "baz")
static_assert("foo" in x)
x = ("quux", "bar", "baz")
static_assert("foo" not in x)
```
## Statically unknown results in a `bool`
```py
def _(a: str, b: str):
reveal_type("foo" in (a, b)) # revealed: bool
```
## Values being unknown doesn't mean the result is unknown
For example, when the types are completely disjoint:
```py
from ty_extensions import static_assert
def _(a: int, b: int):
static_assert("foo" not in (a, b))
```
## Failure cases
```py
# We don't support byte strings.
reveal_type(b"foo" not in (b"quux", b"foo", b"baz")) # revealed: bool
```

View File

@@ -3,14 +3,14 @@
## Concatenation for heterogeneous tuples
```py
reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1, 2, 3, 4], ...]
reveal_type(() + (1, 2)) # revealed: tuple[Literal[1, 2], ...]
reveal_type((1, 2) + ()) # revealed: tuple[Literal[1, 2], ...]
reveal_type((1, 2) + (3, 4)) # revealed: tuple[Literal[1], Literal[2], Literal[3], Literal[4]]
reveal_type(() + (1, 2)) # revealed: tuple[Literal[1], Literal[2]]
reveal_type((1, 2) + ()) # revealed: tuple[Literal[1], Literal[2]]
reveal_type(() + ()) # revealed: tuple[()]
def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
reveal_type(x + y) # revealed: tuple[int | str | None | tuple[int], ...]
reveal_type(y + x) # revealed: tuple[None | tuple[int] | int | str, ...]
reveal_type(x + y) # revealed: tuple[int, str, None, tuple[int]]
reveal_type(y + x) # revealed: tuple[None, tuple[int], int, str]
```
## Concatenation for homogeneous tuples
@@ -19,10 +19,10 @@ def _(x: tuple[int, str], y: tuple[None, tuple[int]]):
def _(x: tuple[int, ...], y: tuple[str, ...]):
reveal_type(x + x) # revealed: tuple[int, ...]
reveal_type(x + y) # revealed: tuple[int | str, ...]
reveal_type((1, 2) + x) # revealed: tuple[int, ...]
reveal_type(x + (3, 4)) # revealed: tuple[int, ...]
reveal_type((1, 2) + x + (3, 4)) # revealed: tuple[int, ...]
reveal_type((1, 2) + y + (3, 4) + x) # revealed: tuple[int | str, ...]
reveal_type((1, 2) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]]
reveal_type(x + (3, 4)) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]]
reveal_type((1, 2) + x + (3, 4)) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]]
reveal_type((1, 2) + y + (3, 4) + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]]
```
We get the same results even when we use a legacy type alias, even though this involves first
@@ -41,8 +41,8 @@ StrTuple = tuple[str, ...]
def _(one_two: OneTwo, x: IntTuple, y: StrTuple, three_four: ThreeFour):
reveal_type(x + x) # revealed: tuple[int, ...]
reveal_type(x + y) # revealed: tuple[int | str, ...]
reveal_type(one_two + x) # revealed: tuple[int, ...]
reveal_type(x + three_four) # revealed: tuple[int, ...]
reveal_type(one_two + x + three_four) # revealed: tuple[int, ...]
reveal_type(one_two + y + three_four + x) # revealed: tuple[int | str, ...]
reveal_type(one_two + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...]]
reveal_type(x + three_four) # revealed: tuple[*tuple[int, ...], Literal[3], Literal[4]]
reveal_type(one_two + x + three_four) # revealed: tuple[Literal[1], Literal[2], *tuple[int, ...], Literal[3], Literal[4]]
reveal_type(one_two + y + three_four + x) # revealed: tuple[Literal[1], Literal[2], *tuple[int | str, ...]]
```

View File

@@ -1,12 +1,25 @@
# Call expression
## A
## Simple
```py
reveal_type(1)
def get_int() -> int:
return 42
reveal_type(get_int()) # revealed: int
```
## B
## Async
```py
async def get_int_async() -> int:
return 42
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
```
## Generic
```toml
[environment]
@@ -14,5 +27,618 @@ python-version = "3.12"
```
```py
reveal_type(1)
def get_int[T]() -> int:
return 42
reveal_type(get_int()) # revealed: int
```
## Decorated
```py
from typing import Callable
def foo() -> int:
return 42
def decorator(func) -> Callable[[], int]:
return foo
@decorator
def bar() -> str:
return "bar"
reveal_type(bar()) # revealed: int
```
## Invalid callable
```py
nonsense = 123
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
```
## Potentially unbound function
```py
def _(flag: bool):
if flag:
def foo() -> int:
return 42
# error: [possibly-unresolved-reference]
reveal_type(foo()) # revealed: int
```
## Splatted arguments
### Unknown argument length
```py
def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
# Test all of the above with a number of different splatted argument types
def _(args: list[int]) -> None:
takes_zero(*args)
takes_one(*args)
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: tuple[int, ...]) -> None:
takes_zero(*args)
takes_one(*args)
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
```
### Fixed-length tuple argument
```py
def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
# Test all of the above with a number of different splatted argument types
def _(args: tuple[int]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args) # error: [missing-argument]
takes_two_positional_only(*args) # error: [missing-argument]
takes_two_different(*args) # error: [missing-argument]
takes_two_different_positional_only(*args) # error: [missing-argument]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args) # error: [missing-argument]
takes_at_least_two_positional_only(*args) # error: [missing-argument]
def _(args: tuple[int, int]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: tuple[int, str]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args) # error: [invalid-argument-type]
takes_two_positional_only(*args) # error: [invalid-argument-type]
takes_two_different(*args)
takes_two_different_positional_only(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args) # error: [invalid-argument-type]
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
```
### Mixed tuple argument
```toml
[environment]
python-version = "3.11"
```
```py
def takes_zero() -> None: ...
def takes_one(x: int) -> None: ...
def takes_two(x: int, y: int) -> None: ...
def takes_two_positional_only(x: int, y: int, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: int, *args) -> None: ...
def takes_at_least_two(x: int, y: int, *args) -> None: ...
def takes_at_least_two_positional_only(x: int, y: int, /, *args) -> None: ...
# Test all of the above with a number of different splatted argument types
def _(args: tuple[int, *tuple[int, ...]]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: tuple[int, *tuple[str, ...]]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args) # error: [invalid-argument-type]
takes_two_positional_only(*args) # error: [invalid-argument-type]
takes_two_different(*args)
takes_two_different_positional_only(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args) # error: [invalid-argument-type]
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
def _(args: tuple[int, int, *tuple[int, ...]]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: tuple[int, int, *tuple[str, ...]]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: tuple[int, *tuple[int, ...], int]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: tuple[int, *tuple[str, ...], int]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args) # error: [invalid-argument-type]
takes_two_positional_only(*args) # error: [invalid-argument-type]
takes_two_different(*args)
takes_two_different_positional_only(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args) # error: [invalid-argument-type]
takes_at_least_two_positional_only(*args) # error: [invalid-argument-type]
```
### String argument
```py
from typing import Literal
def takes_zero() -> None: ...
def takes_one(x: str) -> None: ...
def takes_two(x: str, y: str) -> None: ...
def takes_two_positional_only(x: str, y: str, /) -> None: ...
def takes_two_different(x: int, y: str) -> None: ...
def takes_two_different_positional_only(x: int, y: str, /) -> None: ...
def takes_at_least_zero(*args) -> None: ...
def takes_at_least_one(x: str, *args) -> None: ...
def takes_at_least_two(x: str, y: str, *args) -> None: ...
def takes_at_least_two_positional_only(x: str, y: str, /, *args) -> None: ...
# Test all of the above with a number of different splatted argument types
def _(args: Literal["a"]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args)
takes_two(*args) # error: [missing-argument]
takes_two_positional_only(*args) # error: [missing-argument]
# error: [invalid-argument-type]
# error: [missing-argument]
takes_two_different(*args)
# error: [invalid-argument-type]
# error: [missing-argument]
takes_two_different_positional_only(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args) # error: [missing-argument]
takes_at_least_two_positional_only(*args) # error: [missing-argument]
def _(args: Literal["ab"]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: Literal["abc"]) -> None:
takes_zero(*args) # error: [too-many-positional-arguments]
takes_one(*args) # error: [too-many-positional-arguments]
takes_two(*args) # error: [too-many-positional-arguments]
takes_two_positional_only(*args) # error: [too-many-positional-arguments]
# error: [invalid-argument-type]
# error: [too-many-positional-arguments]
takes_two_different(*args)
# error: [invalid-argument-type]
# error: [too-many-positional-arguments]
takes_two_different_positional_only(*args)
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
def _(args: str) -> None:
takes_zero(*args)
takes_one(*args)
takes_two(*args)
takes_two_positional_only(*args)
takes_two_different(*args) # error: [invalid-argument-type]
takes_two_different_positional_only(*args) # error: [invalid-argument-type]
takes_at_least_zero(*args)
takes_at_least_one(*args)
takes_at_least_two(*args)
takes_at_least_two_positional_only(*args)
```
### Argument expansion regression
This is a regression that was highlighted by the ecosystem check, which shows that we might need to
rethink how we perform argument expansion during overload resolution. In particular, we might need
to retry both `match_parameters` _and_ `check_types` for each expansion. Currently we only retry
`check_types`.
The issue is that argument expansion might produce a splatted value with a different arity than what
we originally inferred for the unexpanded value, and that in turn can affect which parameters the
splatted value is matched with.
The first example correctly produces an error. The `tuple[int, str]` union element has a precise
arity of two, and so parameter matching chooses the first overload. The second element of the tuple
does not match the second parameter type, which yielding an `invalid-argument-type` error.
The third example should produce the same error. However, because we have a union, we do not see the
precise arity of each union element during parameter matching. Instead, we infer an arity of "zero
or more" for the union as a whole, and use that less precise arity when matching parameters. We
therefore consider the second overload to still be a potential candidate for the `tuple[int, str]`
union element. During type checking, we have to force the arity of each union element to match the
inferred arity of the union as a whole (turning `tuple[int, str]` into `tuple[int | str, ...]`).
That less precise tuple type-checks successfully against the second overload, making us incorrectly
think that `tuple[int, str]` is a valid splatted call.
If we update argument expansion to retry parameter matching with the precise arity of each union
element, we will correctly rule out the second overload for `tuple[int, str]`, just like we do when
splatting that tuple directly (instead of as part of a union).
```py
from typing import overload
@overload
def f(x: int, y: int) -> None: ...
@overload
def f(x: int, y: str, z: int) -> None: ...
def f(*args): ...
# Test all of the above with a number of different splatted argument types
def _(t: tuple[int, str]) -> None:
f(*t) # error: [invalid-argument-type]
def _(t: tuple[int, str, int]) -> None:
f(*t)
def _(t: tuple[int, str] | tuple[int, str, int]) -> None:
# TODO: error: [invalid-argument-type]
f(*t)
```
## Wrong argument type
### Positional argument, positional-or-keyword parameter
```py
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, positional-only parameter
```py
def f(x: int, /) -> int:
return 1
# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo")) # revealed: int
```
### Positional argument, variadic parameter
```py
def f(*args: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f("foo")) # revealed: int
```
### Keyword argument, positional-or-keyword parameter
```py
def f(x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keyword-only parameter
```py
def f(*, x: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo")) # revealed: int
```
### Keyword argument, keywords parameter
```py
def f(**kwargs: int) -> int:
return 1
# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["foo"]`"
reveal_type(f(x="foo")) # revealed: int
```
### Correctly match keyword out-of-order
```py
def f(x: int = 1, y: str = "foo") -> int:
return 1
# error: 15 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `str`, found `Literal[2]`"
# error: 20 [invalid-argument-type] "Argument to function `f` is incorrect: Expected `int`, found `Literal["bar"]`"
reveal_type(f(y=2, x="bar")) # revealed: int
```
## Too many positional arguments
### One too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 1"
reveal_type(f("foo")) # revealed: int
```
### Two too many
```py
def f() -> int:
return 1
# error: 15 [too-many-positional-arguments] "Too many positional arguments to function `f`: expected 0, got 2"
reveal_type(f("foo", "bar")) # revealed: int
```
### No too-many-positional if variadic is taken
```py
def f(*args: int) -> int:
return 1
reveal_type(f(1, 2, 3)) # revealed: int
```
### Multiple keyword arguments map to keyword variadic parameter
```py
def f(**kwargs: int) -> int:
return 1
reveal_type(f(foo=1, bar=2)) # revealed: int
```
## Missing arguments
### No defaults or variadic
```py
def f(x: int) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### With default
```py
def f(x: int, y: str = "foo") -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Defaulted argument is not required
```py
def f(x: int = 1) -> int:
return 1
reveal_type(f()) # revealed: int
```
### With variadic
```py
def f(x: int, *y: str) -> int:
return 1
# error: 13 [missing-argument] "No argument provided for required parameter `x` of function `f`"
reveal_type(f()) # revealed: int
```
### Variadic argument is not required
```py
def f(*args: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Keywords argument is not required
```py
def f(**kwargs: int) -> int:
return 1
reveal_type(f()) # revealed: int
```
### Multiple
```py
def f(x: int, y: int) -> int:
return 1
# error: 13 [missing-argument] "No arguments provided for required parameters `x`, `y` of function `f`"
reveal_type(f()) # revealed: int
```
## Unknown argument
```py
def f(x: int) -> int:
return 1
# error: 20 [unknown-argument] "Argument `y` does not match any known parameter of function `f`"
reveal_type(f(x=1, y=2)) # revealed: int
```
## Parameter already assigned
```py
def f(x: int) -> int:
return 1
# error: 18 [parameter-already-assigned] "Multiple values provided for parameter `x` of function `f`"
reveal_type(f(1, x=2)) # revealed: int
```
## Special functions
Some functions require special handling in type inference. Here, we make sure that we still emit
proper diagnostics in case of missing or superfluous arguments.
### `reveal_type`
```py
from typing_extensions import reveal_type
# error: [missing-argument] "No argument provided for required parameter `obj` of function `reveal_type`"
reveal_type()
# error: [too-many-positional-arguments] "Too many positional arguments to function `reveal_type`: expected 1, got 2"
reveal_type(1, 2)
```
### `static_assert`
```py
from ty_extensions import static_assert
# error: [missing-argument] "No argument provided for required parameter `condition` of function `static_assert`"
static_assert()
# error: [too-many-positional-arguments] "Too many positional arguments to function `static_assert`: expected 2, got 3"
static_assert(True, 2, 3)
```
### `len`
```py
# error: [missing-argument] "No argument provided for required parameter `obj` of function `len`"
len()
# error: [too-many-positional-arguments] "Too many positional arguments to function `len`: expected 1, got 2"
len([], 1)
```
### Type property predicates
```py
from ty_extensions import is_subtype_of
# error: [missing-argument]
is_subtype_of()
# error: [missing-argument]
is_subtype_of(int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int)
# error: [too-many-positional-arguments]
is_subtype_of(int, int, int, int)
```

View File

@@ -1,70 +0,0 @@
# `replace`
The `replace` function and the `replace` protocol were added in Python 3.13:
<https://docs.python.org/3/whatsnew/3.13.html#copy>
```toml
[environment]
python-version = "3.13"
```
## Basic
```py
from copy import replace
from datetime import time
t = time(12, 0, 0)
t = replace(t, minute=30)
reveal_type(t) # revealed: time
```
## The `__replace__` protocol
### Dataclasses
Dataclasses support the `__replace__` protocol:
```py
from dataclasses import dataclass
from copy import replace
@dataclass
class Point:
x: int
y: int
reveal_type(Point.__replace__) # revealed: (self: Point, *, x: int = int, y: int = int) -> Point
```
The `__replace__` method can either be called directly or through the `replace` function:
```py
a = Point(1, 2)
b = a.__replace__(x=3, y=4)
reveal_type(b) # revealed: Point
b = replace(a, x=3, y=4)
reveal_type(b) # revealed: Point
```
A call to `replace` does not require all keyword arguments:
```py
c = a.__replace__(y=4)
reveal_type(c) # revealed: Point
d = replace(a, y=4)
reveal_type(d) # revealed: Point
```
Invalid calls to `__replace__` or `replace` will raise an error:
```py
e = a.__replace__(x="wrong") # error: [invalid-argument-type]
# TODO: this should ideally also be emit an error
e = replace(a, x="wrong")
```

View File

@@ -128,7 +128,7 @@ class AsyncIterable:
return AsyncIterator()
async def _():
# revealed: int
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in AsyncIterable()]
```
@@ -147,7 +147,6 @@ class Iterable:
return Iterator()
async def _():
# error: [not-iterable] "Object of type `Iterable` is not async-iterable"
# revealed: Unknown
# revealed: @Todo(async iterables/iterators)
[reveal_type(x) async for x in Iterable()]
```

View File

@@ -27,7 +27,6 @@ If all of the comprehensions are `async`, on the other hand, the code was still
```py
async def test():
# error: [not-iterable] "Object of type `range` is not async-iterable"
return [[x async for x in elements(n)] async for n in range(3)]
```

View File

@@ -1,130 +0,0 @@
# `yield` and `yield from`
## Basic `yield` and `yield from`
The type of a `yield` expression is the "send" type of the generator function. The type of a
`yield from` expression is the return type of the inner generator:
```py
from typing import Generator
def inner_generator() -> Generator[int, bytes, str]:
yield 1
yield 2
x = yield 3
# TODO: this should be `bytes`
reveal_type(x) # revealed: @Todo(yield expressions)
return "done"
def outer_generator():
result = yield from inner_generator()
reveal_type(result) # revealed: str
```
## `yield from` with a custom iterable
`yield from` can also be used with custom iterable types. In that case, the type of the `yield from`
expression can not be determined
```py
from typing import Generator, TypeVar, Generic
T = TypeVar("T")
class OnceIterator(Generic[T]):
def __init__(self, value: T):
self.value = value
self.returned = False
def __next__(self) -> T:
if self.returned:
raise StopIteration(42)
self.returned = True
return self.value
class Once(Generic[T]):
def __init__(self, value: T):
self.value = value
def __iter__(self) -> OnceIterator[T]:
return OnceIterator(self.value)
for x in Once("a"):
reveal_type(x) # revealed: str
def generator() -> Generator:
result = yield from Once("a")
# At runtime, the value of `result` will be the `.value` attribute of the `StopIteration`
# error raised by `OnceIterator` to signal to the interpreter that the iterator has been
# exhausted. Here that will always be 42, but this information cannot be captured in the
# signature of `OnceIterator.__next__`, since exceptions lie outside the type signature.
# We therefore just infer `Unknown` here.
#
# If the `StopIteration` error in `OnceIterator.__next__` had been simply `raise StopIteration`
# (the more common case), then the `.value` attribute of the `StopIteration` instance
# would default to `None`.
reveal_type(result) # revealed: Unknown
```
## `yield from` with a generator that return `types.GeneratorType`
`types.GeneratorType` is a nominal type that implements the `typing.Generator` protocol:
```py
from types import GeneratorType
def inner_generator() -> GeneratorType[int, bytes, str]:
yield 1
yield 2
x = yield 3
# TODO: this should be `bytes`
reveal_type(x) # revealed: @Todo(yield expressions)
return "done"
def outer_generator():
result = yield from inner_generator()
reveal_type(result) # revealed: str
```
## Error cases
### Non-iterable type
```py
from typing import Generator
def generator() -> Generator:
yield from 42 # error: [not-iterable] "Object of type `Literal[42]` is not iterable"
```
### Invalid `yield` type
```py
from typing import Generator
# TODO: This should be an error. Claims to yield `int`, but yields `str`.
def invalid_generator() -> Generator[int, None, None]:
yield "not an int" # This should be an `int`
```
### Invalid return type
```py
from typing import Generator
# TODO: should emit an error (does not return `str`)
def invalid_generator1() -> Generator[int, None, str]:
yield 1
# TODO: should emit an error (does not return `int`)
def invalid_generator2() -> Generator[int, None, None]:
yield 1
return "done"
```

View File

@@ -433,8 +433,6 @@ def f(cond: bool) -> str:
<!-- snapshot-diagnostics -->
### Synchronous
A function with a `yield` or `yield from` expression anywhere in its body is a
[generator function](https://docs.python.org/3/glossary.html#term-generator). A generator function
implicitly returns an instance of `types.GeneratorType` even if it does not contain any `return`
@@ -463,8 +461,6 @@ def j() -> str: # error: [invalid-return-type]
yield 42
```
### Asynchronous
If it is an `async` function with a `yield` statement in its body, it is an
[asynchronous generator function](https://docs.python.org/3/glossary.html#term-asynchronous-generator).
An asynchronous generator function implicitly returns an instance of `types.AsyncGeneratorType` even

View File

@@ -145,34 +145,27 @@ T = TypeVar("T")
def takes_mixed_tuple_suffix(x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
return x[-2]
# TODO: revealed: Literal[True]
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_mixed_tuple_prefix(x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
return x[1]
def _(x: tuple[int, bytes, *tuple[str, ...], bool, int]):
reveal_type(takes_mixed_tuple_suffix(x)) # revealed: bool
reveal_type(takes_mixed_tuple_prefix(x)) # revealed: bytes
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[True]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[b"foo"]
# TODO: revealed: Literal[b"foo"]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_fixed_tuple(x: tuple[T, int]) -> T:
return x[0]
def _(x: tuple[str, int]):
reveal_type(takes_fixed_tuple(x)) # revealed: str
reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]
def takes_homogeneous_tuple(x: tuple[T, ...]) -> T:
return x[0]
def _(x: tuple[str, int], y: tuple[bool, ...], z: tuple[int, str, *tuple[range, ...], bytes]):
reveal_type(takes_homogeneous_tuple(x)) # revealed: str | int
reveal_type(takes_homogeneous_tuple(y)) # revealed: bool
reveal_type(takes_homogeneous_tuple(z)) # revealed: int | str | range | bytes
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Literal[42, 43]
# TODO: revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
# TODO: revealed: Literal[42, 43]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
```
## Inferring a bound typevar

View File

@@ -131,34 +131,27 @@ reveal_type(takes_in_protocol(ExplicitGenericSub[str]())) # revealed: str
def takes_mixed_tuple_suffix[T](x: tuple[int, bytes, *tuple[str, ...], T, int]) -> T:
return x[-2]
# TODO: revealed: Literal[True]
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_mixed_tuple_prefix[T](x: tuple[int, T, *tuple[str, ...], bool, int]) -> T:
return x[1]
def _(x: tuple[int, bytes, *tuple[str, ...], bool, int]):
reveal_type(takes_mixed_tuple_suffix(x)) # revealed: bool
reveal_type(takes_mixed_tuple_prefix(x)) # revealed: bytes
reveal_type(takes_mixed_tuple_suffix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[True]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Literal[b"foo"]
# TODO: revealed: Literal[b"foo"]
reveal_type(takes_mixed_tuple_prefix((1, b"foo", "bar", "baz", True, 42))) # revealed: Unknown
def takes_fixed_tuple[T](x: tuple[T, int]) -> T:
return x[0]
def _(x: tuple[str, int]):
reveal_type(takes_fixed_tuple(x)) # revealed: str
reveal_type(takes_fixed_tuple((True, 42))) # revealed: Literal[True]
def takes_homogeneous_tuple[T](x: tuple[T, ...]) -> T:
return x[0]
def _(x: tuple[str, int], y: tuple[bool, ...], z: tuple[int, str, *tuple[range, ...], bytes]):
reveal_type(takes_homogeneous_tuple(x)) # revealed: str | int
reveal_type(takes_homogeneous_tuple(y)) # revealed: bool
reveal_type(takes_homogeneous_tuple(z)) # revealed: int | str | range | bytes
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Literal[42, 43]
# TODO: revealed: Literal[42]
reveal_type(takes_homogeneous_tuple((42,))) # revealed: Unknown
# TODO: revealed: Literal[42, 43]
reveal_type(takes_homogeneous_tuple((42, 43))) # revealed: Unknown
```
## Inferring a bound typevar

View File

@@ -1,65 +1,60 @@
# List all members
This test suite acts as a set of unit tests for our `ide_support::all_members` routine, which lists
all members available on a given type. This routine is used for autocomplete suggestions.
## Basic functionality
<!-- snapshot-diagnostics -->
The `ty_extensions.all_members` and `ty_extensions.has_member` functions expose a Python-level API
that can be used to query which attributes `ide_support::all_members` understands as being available
on a given object. For example, all member functions of `str` are available on `"a"`. The Python API
`all_members` returns a tuple of all available members; `has_member` returns `Literal[True]` if a
given member is present in that tuple, and `Literal[False]` if not:
The `ty_extensions.all_members` function allows access to a tuple of accessible members/attributes
on a given object. For example, all member functions of `str` are available on `"a"`:
```py
from ty_extensions import static_assert, has_member
from ty_extensions import all_members, static_assert
static_assert(has_member("a", "replace"))
static_assert(has_member("a", "startswith"))
static_assert(has_member("a", "isupper"))
members_of_str = all_members("a")
static_assert("replace" in members_of_str)
static_assert("startswith" in members_of_str)
static_assert("isupper" in members_of_str)
```
Similarly, special members such as `__add__` are also available:
```py
static_assert(has_member("a", "__add__"))
static_assert(has_member("a", "__gt__"))
static_assert("__add__" in members_of_str)
static_assert("__gt__" in members_of_str)
```
Members of base classes are also included (these dunder methods are defined on `object`):
```py
static_assert(has_member("a", "__doc__"))
static_assert(has_member("a", "__repr__"))
static_assert("__doc__" in members_of_str)
static_assert("__repr__" in members_of_str)
```
Non-existent members are not included:
```py
static_assert(not has_member("a", "non_existent"))
static_assert("non_existent" not in members_of_str)
```
The full list of all members is relatively long, but `reveal_type` can be used in combination with
`all_members` to see them all:
Note: The full list of all members is relatively long, but `reveal_type` can theoretically be used
to see them all:
```py
from typing_extensions import reveal_type
from ty_extensions import all_members
reveal_type(all_members("a")) # error: [revealed-type]
reveal_type(members_of_str) # error: [revealed-type]
```
## Kinds of types
### Class instances
For instances of classes, class members and implicit instance members of all superclasses are
understood as being available:
For instances of classes, `all_members` returns class members and implicit instance members of all
classes in the MRO:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
class Base:
base_class_attr: int = 1
@@ -91,23 +86,25 @@ class C(Intermediate):
def static_method() -> int:
return 1
static_assert(has_member(C(), "base_class_attr"))
static_assert(has_member(C(), "intermediate_attr"))
static_assert(has_member(C(), "class_attr"))
members_of_instance = all_members(C())
static_assert(has_member(C(), "base_instance_attr"))
static_assert(has_member(C(), "intermediate_instance_attr"))
static_assert(has_member(C(), "instance_attr"))
static_assert("base_class_attr" in members_of_instance)
static_assert("intermediate_attr" in members_of_instance)
static_assert("class_attr" in members_of_instance)
static_assert(has_member(C(), "f_base"))
static_assert(has_member(C(), "f_intermediate"))
static_assert(has_member(C(), "f_c"))
static_assert("base_instance_attr" in members_of_instance)
static_assert("intermediate_instance_attr" in members_of_instance)
static_assert("instance_attr" in members_of_instance)
static_assert(has_member(C(), "property_attr"))
static_assert(has_member(C(), "class_method"))
static_assert(has_member(C(), "static_method"))
static_assert("f_base" in members_of_instance)
static_assert("f_intermediate" in members_of_instance)
static_assert("f_c" in members_of_instance)
static_assert(not has_member(C(), "non_existent"))
static_assert("property_attr" in members_of_instance)
static_assert("class_method" in members_of_instance)
static_assert("static_method" in members_of_instance)
static_assert("non_existent" not in members_of_instance)
```
### Class objects
@@ -115,7 +112,7 @@ static_assert(not has_member(C(), "non_existent"))
Class-level attributes can also be accessed through the class itself:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
class Base:
base_attr: int = 1
@@ -126,16 +123,18 @@ class C(Base):
def f(self):
self.instance_attr = True
static_assert(has_member(C, "class_attr"))
static_assert(has_member(C, "base_attr"))
members_of_class = all_members(C)
static_assert(not has_member(C, "non_existent"))
static_assert("class_attr" in members_of_class)
static_assert("base_attr" in members_of_class)
static_assert("non_existent" not in members_of_class)
```
But instance attributes can not be accessed this way:
```py
static_assert(not has_member(C, "instance_attr"))
static_assert("instance_attr" not in members_of_class)
```
When a class has a metaclass, members of that metaclass (and bases of that metaclass) are also
@@ -151,16 +150,16 @@ class Meta(MetaBase):
class D(Base, metaclass=Meta):
class_attr = 3
static_assert(has_member(D, "meta_base_attr"))
static_assert(has_member(D, "meta_attr"))
static_assert(has_member(D, "base_attr"))
static_assert(has_member(D, "class_attr"))
static_assert("meta_base_attr" in all_members(D))
static_assert("meta_attr" in all_members(D))
static_assert("base_attr" in all_members(D))
static_assert("class_attr" in all_members(D))
```
### Generic classes
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
from typing import Generic, TypeVar
T = TypeVar("T")
@@ -168,53 +167,52 @@ T = TypeVar("T")
class C(Generic[T]):
base_attr: T
static_assert(has_member(C[int], "base_attr"))
static_assert(has_member(C[int](), "base_attr"))
static_assert("base_attr" in all_members(C[int]))
static_assert("base_attr" in all_members(C[int]()))
```
### Other instance-like types
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
from typing_extensions import LiteralString
static_assert(has_member(True, "__xor__"))
static_assert(has_member(1, "bit_length"))
static_assert(has_member("a", "startswith"))
static_assert(has_member(b"a", "__buffer__"))
static_assert(has_member(3.14, "is_integer"))
static_assert("__xor__" in all_members(True))
static_assert("bit_length" in all_members(1))
static_assert("startswith" in all_members("a"))
static_assert("__buffer__" in all_members(b"a"))
static_assert("is_integer" in all_members(3.14))
def _(literal_string: LiteralString):
static_assert(has_member(literal_string, "startswith"))
static_assert("startswith" in all_members(literal_string))
static_assert(has_member(("some", "tuple", 1, 2), "count"))
static_assert("count" in all_members(("some", "tuple", 1, 2)))
static_assert(has_member(len, "__doc__"))
static_assert(has_member("a".startswith, "__doc__"))
static_assert("__doc__" in all_members(len))
static_assert("__doc__" in all_members("a".startswith))
```
### Enums
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
from enum import Enum
class Answer(Enum):
NO = 0
YES = 1
static_assert(has_member(Answer, "NO"))
static_assert(has_member(Answer, "YES"))
static_assert(has_member(Answer, "__members__"))
static_assert("NO" in all_members(Answer))
static_assert("YES" in all_members(Answer))
static_assert("__members__" in all_members(Answer))
```
### Unions
For unions, `ide_support::all_members` only returns members that are available on all elements of
the union.
For unions, `all_members` will only return members that are available on all elements of the union.
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
class A:
on_both: int = 1
@@ -225,20 +223,20 @@ class B:
only_on_b: str = "b"
def f(union: A | B):
static_assert(has_member(union, "on_both"))
static_assert(not has_member(union, "only_on_a"))
static_assert(not has_member(union, "only_on_b"))
static_assert("on_both" in all_members(union))
static_assert("only_on_a" not in all_members(union))
static_assert("only_on_b" not in all_members(union))
```
### Intersections
#### Only positive types
Conversely, for intersections, `ide_support::all_members` lists members that are available on any of
the elements:
Conversely, for intersections, `all_members` will list members that are available on any of the
elements:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
class A:
on_both: int = 1
@@ -251,9 +249,9 @@ class B:
def f(intersection: object):
if isinstance(intersection, A):
if isinstance(intersection, B):
static_assert(has_member(intersection, "on_both"))
static_assert(has_member(intersection, "only_on_a"))
static_assert(has_member(intersection, "only_on_b"))
static_assert("on_both" in all_members(intersection))
static_assert("only_on_a" in all_members(intersection))
static_assert("only_on_b" in all_members(intersection))
```
#### With negative types
@@ -261,7 +259,7 @@ def f(intersection: object):
It also works when negative types are introduced:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
class A:
on_all: int = 1
@@ -286,27 +284,27 @@ def f(intersection: object):
if isinstance(intersection, B):
if not isinstance(intersection, C):
reveal_type(intersection) # revealed: A & B & ~C
static_assert(has_member(intersection, "on_all"))
static_assert(has_member(intersection, "only_on_a"))
static_assert(has_member(intersection, "only_on_b"))
static_assert(not has_member(intersection, "only_on_c"))
static_assert(has_member(intersection, "only_on_ab"))
static_assert(has_member(intersection, "only_on_ac"))
static_assert(has_member(intersection, "only_on_bc"))
static_assert("on_all" in all_members(intersection))
static_assert("only_on_a" in all_members(intersection))
static_assert("only_on_b" in all_members(intersection))
static_assert("only_on_c" not in all_members(intersection))
static_assert("only_on_ab" in all_members(intersection))
static_assert("only_on_ac" in all_members(intersection))
static_assert("only_on_bc" in all_members(intersection))
```
## Modules
### Basic support with sub-modules
`ide_support::all_members` can also list attributes on modules:
`all_members` can also list attributes on modules:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
import math
static_assert(has_member(math, "pi"))
static_assert(has_member(math, "cos"))
static_assert("pi" in all_members(math))
static_assert("cos" in all_members(math))
```
This also works for submodules:
@@ -314,18 +312,18 @@ This also works for submodules:
```py
import os
static_assert(has_member(os, "path"))
static_assert("path" in all_members(os))
import os.path
static_assert(has_member(os.path, "join"))
static_assert("join" in all_members(os.path))
```
Special members available on all modules are also included:
```py
static_assert(has_member(math, "__name__"))
static_assert(has_member(math, "__doc__"))
static_assert("__name__" in all_members(math))
static_assert("__doc__" in all_members(math))
```
### `__all__` is not respected for direct module access
@@ -333,12 +331,12 @@ static_assert(has_member(math, "__doc__"))
`foo.py`:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
import bar
static_assert(has_member(bar, "lion"))
static_assert(has_member(bar, "tiger"))
static_assert("lion" in all_members(bar))
static_assert("tiger" in all_members(bar))
```
`bar.py`:
@@ -350,17 +348,17 @@ lion = 1
tiger = 1
```
### `__all__` is respected for `*` imports
### `__all__` is respected for glob imports
`foo.py`:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
import bar
static_assert(has_member(bar, "lion"))
static_assert(not has_member(bar, "tiger"))
static_assert("lion" in all_members(bar))
static_assert("tiger" not in all_members(bar))
```
`bar.py`:
@@ -402,12 +400,12 @@ def evaluate(x: Optional[int] = None) -> int: ...
`play.py`:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
import module
static_assert(has_member(module, "evaluate"))
static_assert(not has_member(module, "Optional"))
static_assert("evaluate" in all_members(module))
static_assert("Optional" not in all_members(module))
```
## Conditionally available members
@@ -423,9 +421,9 @@ python-version = "3.9"
```
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
static_assert(not has_member(42, "bit_count"))
static_assert("bit_count" not in all_members(42))
```
### 3.10
@@ -436,19 +434,19 @@ python-version = "3.10"
```
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
static_assert(has_member(42, "bit_count"))
static_assert("bit_count" in all_members(42))
```
## Failure cases
## Failures cases
### Dynamically added members
Dynamically added members cannot be accessed:
Dynamically added members can not be accessed:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
class C:
static_attr = 1
@@ -462,8 +460,8 @@ class C:
c = C()
c.dynamic_attr = "a"
static_assert(has_member(c, "static_attr"))
static_assert(not has_member(c, "dynamic_attr"))
static_assert("static_attr" in all_members(c))
static_assert("dynamic_attr" not in all_members(c))
```
### Dataclasses
@@ -471,24 +469,24 @@ static_assert(not has_member(c, "dynamic_attr"))
So far, we do not include synthetic members of dataclasses.
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
from dataclasses import dataclass
@dataclass(order=True)
class Person:
age: int
name: str
age: int
static_assert(has_member(Person, "name"))
static_assert(has_member(Person, "age"))
static_assert("name" in all_members(Person))
static_assert("age" in all_members(Person))
# These are always available, since they are also defined on `object`:
static_assert(has_member(Person, "__init__"))
static_assert(has_member(Person, "__repr__"))
static_assert(has_member(Person, "__eq__"))
static_assert("__init__" in all_members(Person))
static_assert("__repr__" in all_members(Person))
static_assert("__eq__" in all_members(Person))
# TODO: this should ideally be available:
static_assert(has_member(Person, "__lt__")) # error: [static-assert-error]
static_assert("__lt__" in all_members(Person)) # error: [static-assert-error]
```
### Attributes not available at runtime
@@ -498,8 +496,8 @@ example, `__annotations__` does not exist on `int` at runtime, but it is availab
on `object` in typeshed:
```py
from ty_extensions import has_member, static_assert
from ty_extensions import all_members, static_assert
# TODO: this should ideally not be available:
static_assert(not has_member(3, "__annotations__")) # error: [static-assert-error]
static_assert("__annotations__" not in all_members(3)) # error: [static-assert-error]
```

View File

@@ -122,14 +122,15 @@ class A:
__slots__ = ()
__slots__ += ("a", "b")
reveal_type(A.__slots__) # revealed: tuple[Literal["a", "b"], ...]
reveal_type(A.__slots__) # revealed: tuple[Literal["a"], Literal["b"]]
class B:
__slots__ = ("c", "d")
# TODO: ideally this would trigger `[instance-layout-conflict]`
# (but it's also not high-priority)
class C(A, B): ...
class C( # error: [instance-layout-conflict]
A,
B,
): ...
```
## Explicitly annotated `__slots__`

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