Compare commits
46 Commits
gankra/nam
...
alex/union
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c872fe4c08 | ||
|
|
f497167798 | ||
|
|
046c5a46d8 | ||
|
|
cfed34334c | ||
|
|
11cc324449 | ||
|
|
c88e1a0663 | ||
|
|
2c7ac17b1e | ||
|
|
a0f2cd0ded | ||
|
|
dc61104726 | ||
|
|
f40c578ffb | ||
|
|
baaf6966f6 | ||
|
|
a6df4a3be7 | ||
|
|
1094009790 | ||
|
|
10eb3d52d5 | ||
|
|
91d24ebb92 | ||
|
|
e68fba20a9 | ||
|
|
64117c1146 | ||
|
|
2196ef3a33 | ||
|
|
c36397031b | ||
|
|
eef34958f9 | ||
|
|
f1bd5f1941 | ||
|
|
56862f8241 | ||
|
|
e9cc2f6f42 | ||
|
|
8dd56d4264 | ||
|
|
d4c1b0ccc7 | ||
|
|
e61657ff3c | ||
|
|
ba5dd5837c | ||
|
|
c5f6a74da5 | ||
|
|
b3cde98cd1 | ||
|
|
f9f7a6901b | ||
|
|
c920cf8cdb | ||
|
|
bb757b5a79 | ||
|
|
1f49e8ef51 | ||
|
|
701f5134ab | ||
|
|
eea9ad8352 | ||
|
|
eeac2bd3ee | ||
|
|
7319c37f4e | ||
|
|
805503c19a | ||
|
|
68a2f6c57d | ||
|
|
abaa735e1d | ||
|
|
c02d164357 | ||
|
|
88aa3f82f0 | ||
|
|
30902497db | ||
|
|
3ad99fb1f4 | ||
|
|
d0ff59cfe5 | ||
|
|
952193e0c6 |
1
.github/mypy-primer-ty.toml
vendored
1
.github/mypy-primer-ty.toml
vendored
@@ -5,5 +5,4 @@
|
||||
[rules]
|
||||
possibly-unresolved-reference = "warn"
|
||||
possibly-missing-import = "warn"
|
||||
unused-ignore-comment = "warn"
|
||||
division-by-zero = "warn"
|
||||
|
||||
8
.github/workflows/build-binaries.yml
vendored
8
.github/workflows/build-binaries.yml
vendored
@@ -51,6 +51,7 @@ jobs:
|
||||
- name: "Build sdist"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
command: sdist
|
||||
args: --out dist
|
||||
- name: "Test sdist"
|
||||
@@ -81,6 +82,7 @@ jobs:
|
||||
- name: "Build wheels - x86_64"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
target: x86_64
|
||||
args: --release --locked --out dist
|
||||
- name: "Upload wheels"
|
||||
@@ -123,6 +125,7 @@ jobs:
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel - aarch64"
|
||||
@@ -179,6 +182,7 @@ jobs:
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
target: ${{ matrix.platform.target }}
|
||||
args: --release --locked --out dist
|
||||
env:
|
||||
@@ -232,6 +236,7 @@ jobs:
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
args: --release --locked --out dist
|
||||
@@ -308,6 +313,7 @@ jobs:
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: auto
|
||||
docker-options: ${{ matrix.platform.maturin_docker_options }}
|
||||
@@ -374,6 +380,7 @@ jobs:
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: musllinux_1_2
|
||||
args: --release --locked --out dist
|
||||
@@ -439,6 +446,7 @@ jobs:
|
||||
- name: "Build wheels"
|
||||
uses: PyO3/maturin-action@86b9d133d34bc1b40018696f782949dac11bd380 # v1.49.4
|
||||
with:
|
||||
maturin-version: v1.9.6
|
||||
target: ${{ matrix.platform.target }}
|
||||
manylinux: musllinux_1_2
|
||||
args: --release --locked --out dist
|
||||
|
||||
58
.github/workflows/build-wasm.yml
vendored
Normal file
58
.github/workflows/build-wasm.yml
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
# Build ruff_wasm for npm.
|
||||
#
|
||||
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a local
|
||||
# artifacts job within `cargo-dist`.
|
||||
name: "Build wasm"
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: true
|
||||
type: string
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-wasm.yml
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-build') }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
target: [web, bundler, nodejs]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Run wasm-pack build"
|
||||
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm
|
||||
- name: "Rename generated package"
|
||||
run: | # Replace the package name w/ jq
|
||||
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
|
||||
mv /tmp/package.json crates/ruff_wasm/pkg
|
||||
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
|
||||
- name: "Upload wasm artifact"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: artifacts-wasm-${{ matrix.target }}
|
||||
path: crates/ruff_wasm/pkg
|
||||
35
.github/workflows/publish-wasm.yml
vendored
35
.github/workflows/publish-wasm.yml
vendored
@@ -1,25 +1,18 @@
|
||||
# Build and publish ruff-api for wasm.
|
||||
# Publish ruff_wasm to npm.
|
||||
#
|
||||
# Assumed to run as a subworkflow of .github/workflows/release.yml; specifically, as a publish
|
||||
# job within `cargo-dist`.
|
||||
name: "Build and publish wasm"
|
||||
name: "Publish wasm"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
CARGO_NET_RETRY: 10
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
|
||||
jobs:
|
||||
ruff_wasm:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -29,31 +22,19 @@ jobs:
|
||||
target: [web, bundler, nodejs]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: jetli/wasm-pack-action@0d096b08b4e5a7de8c28de67e11e945404e9eefa # v0.4.0
|
||||
with:
|
||||
version: v0.13.1
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Run wasm-pack build"
|
||||
run: wasm-pack build --target ${{ matrix.target }} crates/ruff_wasm
|
||||
- name: "Rename generated package"
|
||||
run: | # Replace the package name w/ jq
|
||||
jq '.name="@astral-sh/ruff-wasm-${{ matrix.target }}"' crates/ruff_wasm/pkg/package.json > /tmp/package.json
|
||||
mv /tmp/package.json crates/ruff_wasm/pkg
|
||||
- run: cp LICENSE crates/ruff_wasm/pkg # wasm-pack does not put the LICENSE file in the pkg
|
||||
name: artifacts-wasm-${{ matrix.target }}
|
||||
path: pkg
|
||||
- uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
|
||||
with:
|
||||
node-version: 24
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
- name: "Publish (dry-run)"
|
||||
if: ${{ inputs.plan == '' || fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
run: npm publish --dry-run crates/ruff_wasm/pkg
|
||||
run: npm publish --dry-run pkg
|
||||
- name: "Publish"
|
||||
if: ${{ inputs.plan != '' && !fromJson(inputs.plan).announcement_tag_is_implicit }}
|
||||
run: npm publish --provenance --access public crates/ruff_wasm/pkg
|
||||
run: npm publish --provenance --access public pkg
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -112,12 +112,22 @@ jobs:
|
||||
"contents": "read"
|
||||
"packages": "write"
|
||||
|
||||
custom-build-wasm:
|
||||
needs:
|
||||
- plan
|
||||
if: ${{ needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload' || inputs.tag == 'dry-run' }}
|
||||
uses: ./.github/workflows/build-wasm.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
|
||||
# Build and package all the platform-agnostic(ish) things
|
||||
build-global-artifacts:
|
||||
needs:
|
||||
- plan
|
||||
- custom-build-binaries
|
||||
- custom-build-docker
|
||||
- custom-build-wasm
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -165,9 +175,10 @@ jobs:
|
||||
- plan
|
||||
- custom-build-binaries
|
||||
- custom-build-docker
|
||||
- custom-build-wasm
|
||||
- build-global-artifacts
|
||||
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') }}
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-build-binaries.result == 'skipped' || needs.custom-build-binaries.result == 'success') && (needs.custom-build-docker.result == 'skipped' || needs.custom-build-docker.result == 'success') && (needs.custom-build-wasm.result == 'skipped' || needs.custom-build-wasm.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "depot-ubuntu-latest-4"
|
||||
|
||||
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
4
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -40,12 +40,12 @@ jobs:
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
enable-cache: true
|
||||
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
lookup-only: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
run: rustup show
|
||||
|
||||
@@ -125,6 +125,7 @@ repos:
|
||||
args:
|
||||
- "-ignore=SC2129" # ignorable stylistic lint from shellcheck
|
||||
- "-ignore=SC2016" # another shellcheck lint: seems to have false positives?
|
||||
language: golang # means renovate will also update `additional_dependencies`
|
||||
additional_dependencies:
|
||||
# actionlint has a shellcheck integration which extracts shell scripts in `run:` steps from GitHub Actions
|
||||
# and checks these with shellcheck. This is arguably its most useful feature,
|
||||
|
||||
58
CHANGELOG.md
58
CHANGELOG.md
@@ -1,5 +1,63 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.11
|
||||
|
||||
Released on 2026-01-08.
|
||||
|
||||
### Preview features
|
||||
|
||||
- Consolidate diagnostics for matched disable/enable suppression comments ([#22099](https://github.com/astral-sh/ruff/pull/22099))
|
||||
- Report diagnostics for invalid/unmatched range suppression comments ([#21908](https://github.com/astral-sh/ruff/pull/21908))
|
||||
- \[`airflow`\] Passing positional argument into `airflow.lineage.hook.HookLineageCollector.create_asset` is not allowed (`AIR303`) ([#22046](https://github.com/astral-sh/ruff/pull/22046))
|
||||
- \[`refurb`\] Mark `FURB192` fix as always unsafe ([#22210](https://github.com/astral-sh/ruff/pull/22210))
|
||||
- \[`ruff`\] Add `non-empty-init-module` (`RUF067`) ([#22143](https://github.com/astral-sh/ruff/pull/22143))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Fix GitHub format for multi-line diagnostics ([#22108](https://github.com/astral-sh/ruff/pull/22108))
|
||||
- \[`flake8-unused-arguments`\] Mark `**kwargs` in `TypeVar` as used (`ARG001`) ([#22214](https://github.com/astral-sh/ruff/pull/22214))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Add `help:` subdiagnostics for several Ruff rules that can sometimes appear to disagree with `ty` ([#22331](https://github.com/astral-sh/ruff/pull/22331))
|
||||
- \[`pylint`\] Demote `PLW1510` fix to display-only ([#22318](https://github.com/astral-sh/ruff/pull/22318))
|
||||
- \[`pylint`\] Ignore identical members (`PLR1714`) ([#22220](https://github.com/astral-sh/ruff/pull/22220))
|
||||
- \[`pylint`\] Improve diagnostic range for `PLC0206` ([#22312](https://github.com/astral-sh/ruff/pull/22312))
|
||||
- \[`ruff`\] Improve fix title for `RUF102` invalid rule code ([#22100](https://github.com/astral-sh/ruff/pull/22100))
|
||||
- \[`flake8-simplify`\]: Avoid unnecessary builtins import for `SIM105` ([#22358](https://github.com/astral-sh/ruff/pull/22358))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Allow Python 3.15 as valid `target-version` value in preview ([#22419](https://github.com/astral-sh/ruff/pull/22419))
|
||||
- Check `required-version` before parsing rules ([#22410](https://github.com/astral-sh/ruff/pull/22410))
|
||||
- Include configured `src` directories when resolving graphs ([#22451](https://github.com/astral-sh/ruff/pull/22451))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update `T201` suggestion to not use root logger to satisfy `LOG015` ([#22059](https://github.com/astral-sh/ruff/pull/22059))
|
||||
- Fix `iter` example in unsafe fixes doc ([#22118](https://github.com/astral-sh/ruff/pull/22118))
|
||||
- \[`flake8_print`\] better suggestion for `basicConfig` in `T201` docs ([#22101](https://github.com/astral-sh/ruff/pull/22101))
|
||||
- \[`pylint`\] Restore the fix safety docs for `PLW0133` ([#22211](https://github.com/astral-sh/ruff/pull/22211))
|
||||
- Fix Jupyter notebook discovery info for editors ([#22447](https://github.com/astral-sh/ruff/pull/22447))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@charliermarsh](https://github.com/charliermarsh)
|
||||
- [@ntBre](https://github.com/ntBre)
|
||||
- [@cenviity](https://github.com/cenviity)
|
||||
- [@njhearp](https://github.com/njhearp)
|
||||
- [@cbachhuber](https://github.com/cbachhuber)
|
||||
- [@jelle-openai](https://github.com/jelle-openai)
|
||||
- [@AlexWaygood](https://github.com/AlexWaygood)
|
||||
- [@ValdonVitija](https://github.com/ValdonVitija)
|
||||
- [@BurntSushi](https://github.com/BurntSushi)
|
||||
- [@Jkhall81](https://github.com/Jkhall81)
|
||||
- [@PeterJCLaw](https://github.com/PeterJCLaw)
|
||||
- [@harupy](https://github.com/harupy)
|
||||
- [@amyreese](https://github.com/amyreese)
|
||||
- [@sjyangkevin](https://github.com/sjyangkevin)
|
||||
- [@woodruffw](https://github.com/woodruffw)
|
||||
|
||||
## 0.14.10
|
||||
|
||||
Released on 2025-12-18.
|
||||
|
||||
14
Cargo.lock
generated
14
Cargo.lock
generated
@@ -2912,7 +2912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2928,6 +2928,7 @@ dependencies = [
|
||||
"filetime",
|
||||
"globwalk",
|
||||
"ignore",
|
||||
"indexmap",
|
||||
"indoc",
|
||||
"insta",
|
||||
"insta-cmd",
|
||||
@@ -3171,7 +3172,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3529,7 +3530,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3645,7 +3646,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.25.2"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3670,12 +3671,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.25.2"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.25.2"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=309c249088fdeef0129606fa34ec2eefc74736ff#309c249088fdeef0129606fa34ec2eefc74736ff"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=9860ff6ca0f1f8f3a8d6b832020002790b501254#9860ff6ca0f1f8f3a8d6b832020002790b501254"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4443,6 +4444,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"camino",
|
||||
"compact_str",
|
||||
"get-size2",
|
||||
"insta",
|
||||
"itertools 0.14.0",
|
||||
|
||||
@@ -150,7 +150,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "309c249088fdeef0129606fa34ec2eefc74736ff", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "9860ff6ca0f1f8f3a8d6b832020002790b501254", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
||||
@@ -150,8 +150,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.14.10/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.10/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.14.11/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.11/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -184,7 +184,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.10
|
||||
rev: v0.14.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -48,6 +48,7 @@ colored = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
globwalk = { workspace = true }
|
||||
ignore = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
is-macro = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
jiff = { workspace = true }
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::args::{AnalyzeGraphArgs, ConfigArguments};
|
||||
use crate::resolve::resolve;
|
||||
use crate::{ExitStatus, resolve_default_files};
|
||||
use anyhow::Result;
|
||||
use indexmap::IndexSet;
|
||||
use log::{debug, warn};
|
||||
use path_absolutize::CWD;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
@@ -11,7 +12,7 @@ use ruff_linter::source_kind::SourceKind;
|
||||
use ruff_linter::{warn_user, warn_user_once};
|
||||
use ruff_python_ast::{PySourceType, SourceType};
|
||||
use ruff_workspace::resolver::{ResolvedFile, match_exclusion, python_files_in_path};
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex};
|
||||
@@ -59,17 +60,34 @@ pub(crate) fn analyze_graph(
|
||||
})
|
||||
.collect::<FxHashMap<_, _>>();
|
||||
|
||||
// Create a database from the source roots.
|
||||
let src_roots = package_roots
|
||||
.values()
|
||||
.filter_map(|package| package.as_deref())
|
||||
.filter_map(|package| package.parent())
|
||||
.map(Path::to_path_buf)
|
||||
.filter_map(|path| SystemPathBuf::from_path_buf(path).ok())
|
||||
.collect();
|
||||
// Create a database from the source roots, combining configured `src` paths with detected
|
||||
// package roots. Configured paths are added first so they take precedence, and duplicates
|
||||
// are removed.
|
||||
let mut src_roots: IndexSet<SystemPathBuf, FxBuildHasher> = IndexSet::default();
|
||||
|
||||
// Add configured `src` paths first (for precedence), filtering to only include existing
|
||||
// directories.
|
||||
src_roots.extend(
|
||||
pyproject_config
|
||||
.settings
|
||||
.linter
|
||||
.src
|
||||
.iter()
|
||||
.filter(|path| path.is_dir())
|
||||
.filter_map(|path| SystemPathBuf::from_path_buf(path.clone()).ok()),
|
||||
);
|
||||
|
||||
// Add detected package roots.
|
||||
src_roots.extend(
|
||||
package_roots
|
||||
.values()
|
||||
.filter_map(|package| package.as_deref())
|
||||
.filter_map(|path| path.parent())
|
||||
.filter_map(|path| SystemPathBuf::from_path_buf(path.to_path_buf()).ok()),
|
||||
);
|
||||
|
||||
let db = ModuleDb::from_src_roots(
|
||||
src_roots,
|
||||
src_roots.into_iter().collect(),
|
||||
pyproject_config
|
||||
.settings
|
||||
.analyze
|
||||
|
||||
@@ -29,10 +29,10 @@ pub(crate) fn show_settings(
|
||||
bail!("No files found under the given path");
|
||||
};
|
||||
|
||||
let settings = resolver.resolve(&path);
|
||||
let (settings, config_path) = resolver.resolve_with_path(&path);
|
||||
|
||||
writeln!(writer, "Resolved settings for: \"{}\"", path.display())?;
|
||||
if let Some(settings_path) = pyproject_config.path.as_ref() {
|
||||
if let Some(settings_path) = config_path {
|
||||
writeln!(writer, "Settings path: \"{}\"", settings_path.display())?;
|
||||
}
|
||||
write!(writer, "{settings}")?;
|
||||
|
||||
@@ -714,6 +714,121 @@ fn notebook_basic() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that the `src` configuration option is respected.
|
||||
///
|
||||
/// This is useful for monorepos where there are multiple source directories that need to be
|
||||
/// included in the module resolution search path.
|
||||
#[test]
|
||||
fn src_option() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
// Create a lib directory with a package.
|
||||
root.child("lib")
|
||||
.child("mylib")
|
||||
.child("__init__.py")
|
||||
.write_str("def helper(): pass")?;
|
||||
|
||||
// Create an app directory with a file that imports from mylib.
|
||||
root.child("app").child("__init__.py").write_str("")?;
|
||||
root.child("app")
|
||||
.child("main.py")
|
||||
.write_str("from mylib import helper")?;
|
||||
|
||||
// Without src configured, the import from mylib won't resolve.
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().arg("app").current_dir(&root), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"app/__init__.py": [],
|
||||
"app/main.py": []
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
});
|
||||
|
||||
// With src = ["lib"], the import should resolve.
|
||||
root.child("ruff.toml").write_str(indoc::indoc! {r#"
|
||||
src = ["lib"]
|
||||
"#})?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().arg("app").current_dir(&root), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"app/__init__.py": [],
|
||||
"app/main.py": [
|
||||
"lib/mylib/__init__.py"
|
||||
]
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test that glob patterns in `src` are expanded.
|
||||
#[test]
|
||||
fn src_glob_expansion() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let root = ChildPath::new(tempdir.path());
|
||||
|
||||
// Create multiple lib directories with packages.
|
||||
root.child("libs")
|
||||
.child("lib_a")
|
||||
.child("pkg_a")
|
||||
.child("__init__.py")
|
||||
.write_str("def func_a(): pass")?;
|
||||
root.child("libs")
|
||||
.child("lib_b")
|
||||
.child("pkg_b")
|
||||
.child("__init__.py")
|
||||
.write_str("def func_b(): pass")?;
|
||||
|
||||
// Create an app that imports from both packages.
|
||||
root.child("app").child("__init__.py").write_str("")?;
|
||||
root.child("app")
|
||||
.child("main.py")
|
||||
.write_str("from pkg_a import func_a\nfrom pkg_b import func_b")?;
|
||||
|
||||
// Use a glob pattern to include all lib directories.
|
||||
root.child("ruff.toml").write_str(indoc::indoc! {r#"
|
||||
src = ["libs/*"]
|
||||
"#})?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => INSTA_FILTERS.to_vec(),
|
||||
}, {
|
||||
assert_cmd_snapshot!(command().arg("app").current_dir(&root), @r#"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
{
|
||||
"app/__init__.py": [],
|
||||
"app/main.py": [
|
||||
"libs/lib_a/pkg_a/__init__.py",
|
||||
"libs/lib_b/pkg_b/__init__.py"
|
||||
]
|
||||
}
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn notebook_with_magic() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
@@ -1126,6 +1126,35 @@ import os
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_version_fails_to_parse() -> Result<()> {
|
||||
let fixture = CliTest::with_file(
|
||||
"ruff.toml",
|
||||
r#"
|
||||
required-version = "pikachu"
|
||||
"#,
|
||||
)?;
|
||||
assert_cmd_snapshot!(fixture
|
||||
.check_command(), @r#"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Failed to load configuration `[TMP]/ruff.toml`
|
||||
Cause: Failed to parse [TMP]/ruff.toml
|
||||
Cause: TOML parse error at line 2, column 20
|
||||
|
|
||||
2 | required-version = "pikachu"
|
||||
| ^^^^^^^^^
|
||||
Failed to parse version: Unexpected end of version specifier, expected operator:
|
||||
pikachu
|
||||
^^^^^^^
|
||||
"#);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_version_exact_mismatch() -> Result<()> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
@@ -1137,10 +1166,10 @@ required-version = "0.1.0"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(version, "[VERSION]")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(fixture
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(version, "[VERSION]");
|
||||
settings.bind(|| {
|
||||
assert_cmd_snapshot!(fixture
|
||||
.check_command()
|
||||
.arg("--config")
|
||||
.arg("ruff.toml")
|
||||
@@ -1154,6 +1183,7 @@ import os
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Failed to load configuration `[TMP]/ruff.toml`
|
||||
Cause: Required version `==0.1.0` does not match the running version `[VERSION]`
|
||||
");
|
||||
});
|
||||
@@ -1212,10 +1242,10 @@ required-version = ">{version}"
|
||||
),
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(version, "[VERSION]")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(fixture
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(version, "[VERSION]");
|
||||
settings.bind(|| {
|
||||
assert_cmd_snapshot!(fixture
|
||||
.check_command()
|
||||
.arg("--config")
|
||||
.arg("ruff.toml")
|
||||
@@ -1229,6 +1259,48 @@ import os
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Failed to load configuration `[TMP]/ruff.toml`
|
||||
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_version_precedes_rule_validation() -> Result<()> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
let fixture = CliTest::with_file(
|
||||
"ruff.toml",
|
||||
&format!(
|
||||
r#"
|
||||
required-version = ">{version}"
|
||||
|
||||
[lint]
|
||||
select = ["RUF999"]
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(version, "[VERSION]");
|
||||
settings.bind(|| {
|
||||
assert_cmd_snapshot!(fixture
|
||||
.check_command()
|
||||
.arg("--config")
|
||||
.arg("ruff.toml")
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
import os
|
||||
"#), @"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Failed to load configuration `[TMP]/ruff.toml`
|
||||
Cause: Required version `>[VERSION]` does not match the running version `[VERSION]`
|
||||
");
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Resolved settings for: "[TMP]/foo/test.py"
|
||||
Settings path: "[TMP]/foo/pyproject.toml"
|
||||
|
||||
# General Settings
|
||||
cache_dir = "[TMP]/foo/.ruff_cache"
|
||||
|
||||
@@ -50,6 +50,56 @@ ignore = [
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_settings_from_nested_directory() -> anyhow::Result<()> {
|
||||
let tempdir = TempDir::new().context("Failed to create temp directory.")?;
|
||||
|
||||
// Tempdir path's on macos are symlinks, which doesn't play nicely with
|
||||
// our snapshot filtering.
|
||||
let project_dir =
|
||||
dunce::canonicalize(tempdir.path()).context("Failed to canonical tempdir path.")?;
|
||||
|
||||
// Root pyproject.toml.
|
||||
std::fs::write(
|
||||
project_dir.join("pyproject.toml"),
|
||||
r#"
|
||||
[tool.ruff]
|
||||
line-length = 100
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
// Create a subdirectory with its own pyproject.toml.
|
||||
let subdir = project_dir.join("subdir");
|
||||
std::fs::create_dir(&subdir)?;
|
||||
|
||||
std::fs::write(
|
||||
subdir.join("pyproject.toml"),
|
||||
r#"
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = ["E", "F", "I"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
std::fs::write(subdir.join("test.py"), r#"import os"#).context("Failed to write test.py.")?;
|
||||
|
||||
insta::with_settings!({filters => vec![
|
||||
(&*tempdir_filter(&project_dir), "<temp_dir>/"),
|
||||
(r#"\\(\w\w|\s|\.|")"#, "/$1"),
|
||||
]}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(["check", "--show-settings", "subdir/test.py"])
|
||||
.current_dir(&project_dir));
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tempdir_filter(project_dir: &Path) -> String {
|
||||
format!(r#"{}\\?/?"#, regex::escape(project_dir.to_str().unwrap()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
---
|
||||
source: crates/ruff/tests/show_settings.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--show-settings"
|
||||
- subdir/test.py
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Resolved settings for: "<temp_dir>/subdir/test.py"
|
||||
Settings path: "<temp_dir>/subdir/pyproject.toml"
|
||||
|
||||
# General Settings
|
||||
cache_dir = "<temp_dir>/subdir/.ruff_cache"
|
||||
fix = false
|
||||
fix_only = false
|
||||
output_format = full
|
||||
show_fixes = false
|
||||
unsafe_fixes = hint
|
||||
|
||||
# File Resolver Settings
|
||||
file_resolver.exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
]
|
||||
file_resolver.extend_exclude = []
|
||||
file_resolver.force_exclude = false
|
||||
file_resolver.include = [
|
||||
"*.py",
|
||||
"*.pyi",
|
||||
"*.ipynb",
|
||||
"**/pyproject.toml",
|
||||
]
|
||||
file_resolver.extend_include = []
|
||||
file_resolver.respect_gitignore = true
|
||||
file_resolver.project_root = "<temp_dir>/subdir"
|
||||
|
||||
# Linter Settings
|
||||
linter.exclude = []
|
||||
linter.project_root = "<temp_dir>/subdir"
|
||||
linter.rules.enabled = [
|
||||
unsorted-imports (I001),
|
||||
missing-required-import (I002),
|
||||
mixed-spaces-and-tabs (E101),
|
||||
multiple-imports-on-one-line (E401),
|
||||
module-import-not-at-top-of-file (E402),
|
||||
line-too-long (E501),
|
||||
multiple-statements-on-one-line-colon (E701),
|
||||
multiple-statements-on-one-line-semicolon (E702),
|
||||
useless-semicolon (E703),
|
||||
none-comparison (E711),
|
||||
true-false-comparison (E712),
|
||||
not-in-test (E713),
|
||||
not-is-test (E714),
|
||||
type-comparison (E721),
|
||||
bare-except (E722),
|
||||
lambda-assignment (E731),
|
||||
ambiguous-variable-name (E741),
|
||||
ambiguous-class-name (E742),
|
||||
ambiguous-function-name (E743),
|
||||
io-error (E902),
|
||||
unused-import (F401),
|
||||
import-shadowed-by-loop-var (F402),
|
||||
undefined-local-with-import-star (F403),
|
||||
late-future-import (F404),
|
||||
undefined-local-with-import-star-usage (F405),
|
||||
undefined-local-with-nested-import-star-usage (F406),
|
||||
future-feature-not-defined (F407),
|
||||
percent-format-invalid-format (F501),
|
||||
percent-format-expected-mapping (F502),
|
||||
percent-format-expected-sequence (F503),
|
||||
percent-format-extra-named-arguments (F504),
|
||||
percent-format-missing-argument (F505),
|
||||
percent-format-mixed-positional-and-named (F506),
|
||||
percent-format-positional-count-mismatch (F507),
|
||||
percent-format-star-requires-sequence (F508),
|
||||
percent-format-unsupported-format-character (F509),
|
||||
string-dot-format-invalid-format (F521),
|
||||
string-dot-format-extra-named-arguments (F522),
|
||||
string-dot-format-extra-positional-arguments (F523),
|
||||
string-dot-format-missing-arguments (F524),
|
||||
string-dot-format-mixing-automatic (F525),
|
||||
f-string-missing-placeholders (F541),
|
||||
multi-value-repeated-key-literal (F601),
|
||||
multi-value-repeated-key-variable (F602),
|
||||
expressions-in-star-assignment (F621),
|
||||
multiple-starred-expressions (F622),
|
||||
assert-tuple (F631),
|
||||
is-literal (F632),
|
||||
invalid-print-syntax (F633),
|
||||
if-tuple (F634),
|
||||
break-outside-loop (F701),
|
||||
continue-outside-loop (F702),
|
||||
yield-outside-function (F704),
|
||||
return-outside-function (F706),
|
||||
default-except-not-last (F707),
|
||||
forward-annotation-syntax-error (F722),
|
||||
redefined-while-unused (F811),
|
||||
undefined-name (F821),
|
||||
undefined-export (F822),
|
||||
undefined-local (F823),
|
||||
unused-variable (F841),
|
||||
unused-annotation (F842),
|
||||
raise-not-implemented (F901),
|
||||
]
|
||||
linter.rules.should_fix = [
|
||||
unsorted-imports (I001),
|
||||
missing-required-import (I002),
|
||||
mixed-spaces-and-tabs (E101),
|
||||
multiple-imports-on-one-line (E401),
|
||||
module-import-not-at-top-of-file (E402),
|
||||
line-too-long (E501),
|
||||
multiple-statements-on-one-line-colon (E701),
|
||||
multiple-statements-on-one-line-semicolon (E702),
|
||||
useless-semicolon (E703),
|
||||
none-comparison (E711),
|
||||
true-false-comparison (E712),
|
||||
not-in-test (E713),
|
||||
not-is-test (E714),
|
||||
type-comparison (E721),
|
||||
bare-except (E722),
|
||||
lambda-assignment (E731),
|
||||
ambiguous-variable-name (E741),
|
||||
ambiguous-class-name (E742),
|
||||
ambiguous-function-name (E743),
|
||||
io-error (E902),
|
||||
unused-import (F401),
|
||||
import-shadowed-by-loop-var (F402),
|
||||
undefined-local-with-import-star (F403),
|
||||
late-future-import (F404),
|
||||
undefined-local-with-import-star-usage (F405),
|
||||
undefined-local-with-nested-import-star-usage (F406),
|
||||
future-feature-not-defined (F407),
|
||||
percent-format-invalid-format (F501),
|
||||
percent-format-expected-mapping (F502),
|
||||
percent-format-expected-sequence (F503),
|
||||
percent-format-extra-named-arguments (F504),
|
||||
percent-format-missing-argument (F505),
|
||||
percent-format-mixed-positional-and-named (F506),
|
||||
percent-format-positional-count-mismatch (F507),
|
||||
percent-format-star-requires-sequence (F508),
|
||||
percent-format-unsupported-format-character (F509),
|
||||
string-dot-format-invalid-format (F521),
|
||||
string-dot-format-extra-named-arguments (F522),
|
||||
string-dot-format-extra-positional-arguments (F523),
|
||||
string-dot-format-missing-arguments (F524),
|
||||
string-dot-format-mixing-automatic (F525),
|
||||
f-string-missing-placeholders (F541),
|
||||
multi-value-repeated-key-literal (F601),
|
||||
multi-value-repeated-key-variable (F602),
|
||||
expressions-in-star-assignment (F621),
|
||||
multiple-starred-expressions (F622),
|
||||
assert-tuple (F631),
|
||||
is-literal (F632),
|
||||
invalid-print-syntax (F633),
|
||||
if-tuple (F634),
|
||||
break-outside-loop (F701),
|
||||
continue-outside-loop (F702),
|
||||
yield-outside-function (F704),
|
||||
return-outside-function (F706),
|
||||
default-except-not-last (F707),
|
||||
forward-annotation-syntax-error (F722),
|
||||
redefined-while-unused (F811),
|
||||
undefined-name (F821),
|
||||
undefined-export (F822),
|
||||
undefined-local (F823),
|
||||
unused-variable (F841),
|
||||
unused-annotation (F842),
|
||||
raise-not-implemented (F901),
|
||||
]
|
||||
linter.per_file_ignores = {}
|
||||
linter.safety_table.forced_safe = []
|
||||
linter.safety_table.forced_unsafe = []
|
||||
linter.unresolved_target_version = none
|
||||
linter.per_file_target_version = {}
|
||||
linter.preview = disabled
|
||||
linter.explicit_preview_rules = false
|
||||
linter.extension = ExtensionMapping({})
|
||||
linter.allowed_confusables = []
|
||||
linter.builtins = []
|
||||
linter.dummy_variable_rgx = ^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$
|
||||
linter.external = []
|
||||
linter.ignore_init_module_imports = true
|
||||
linter.logger_objects = []
|
||||
linter.namespace_packages = []
|
||||
linter.src = [
|
||||
"<temp_dir>/subdir",
|
||||
"<temp_dir>/subdir/src",
|
||||
]
|
||||
linter.tab_size = 4
|
||||
linter.line_length = 120
|
||||
linter.task_tags = [
|
||||
TODO,
|
||||
FIXME,
|
||||
XXX,
|
||||
]
|
||||
linter.typing_modules = []
|
||||
linter.typing_extensions = true
|
||||
|
||||
# Linter Plugins
|
||||
linter.flake8_annotations.mypy_init_return = false
|
||||
linter.flake8_annotations.suppress_dummy_args = false
|
||||
linter.flake8_annotations.suppress_none_returning = false
|
||||
linter.flake8_annotations.allow_star_arg_any = false
|
||||
linter.flake8_annotations.ignore_fully_untyped = false
|
||||
linter.flake8_bandit.hardcoded_tmp_directory = [
|
||||
/tmp,
|
||||
/var/tmp,
|
||||
/dev/shm,
|
||||
]
|
||||
linter.flake8_bandit.check_typed_exception = false
|
||||
linter.flake8_bandit.extend_markup_names = []
|
||||
linter.flake8_bandit.allowed_markup_calls = []
|
||||
linter.flake8_bugbear.extend_immutable_calls = []
|
||||
linter.flake8_builtins.allowed_modules = []
|
||||
linter.flake8_builtins.ignorelist = []
|
||||
linter.flake8_builtins.strict_checking = false
|
||||
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
|
||||
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
|
||||
linter.flake8_copyright.author = none
|
||||
linter.flake8_copyright.min_file_size = 0
|
||||
linter.flake8_errmsg.max_string_length = 0
|
||||
linter.flake8_gettext.function_names = [
|
||||
_,
|
||||
gettext,
|
||||
ngettext,
|
||||
]
|
||||
linter.flake8_implicit_str_concat.allow_multiline = true
|
||||
linter.flake8_import_conventions.aliases = {
|
||||
altair = alt,
|
||||
holoviews = hv,
|
||||
matplotlib = mpl,
|
||||
matplotlib.pyplot = plt,
|
||||
networkx = nx,
|
||||
numpy = np,
|
||||
numpy.typing = npt,
|
||||
pandas = pd,
|
||||
panel = pn,
|
||||
plotly.express = px,
|
||||
polars = pl,
|
||||
pyarrow = pa,
|
||||
seaborn = sns,
|
||||
tensorflow = tf,
|
||||
tkinter = tk,
|
||||
xml.etree.ElementTree = ET,
|
||||
}
|
||||
linter.flake8_import_conventions.banned_aliases = {}
|
||||
linter.flake8_import_conventions.banned_from = []
|
||||
linter.flake8_pytest_style.fixture_parentheses = false
|
||||
linter.flake8_pytest_style.parametrize_names_type = tuple
|
||||
linter.flake8_pytest_style.parametrize_values_type = list
|
||||
linter.flake8_pytest_style.parametrize_values_row_type = tuple
|
||||
linter.flake8_pytest_style.raises_require_match_for = [
|
||||
BaseException,
|
||||
Exception,
|
||||
ValueError,
|
||||
OSError,
|
||||
IOError,
|
||||
EnvironmentError,
|
||||
socket.error,
|
||||
]
|
||||
linter.flake8_pytest_style.raises_extend_require_match_for = []
|
||||
linter.flake8_pytest_style.mark_parentheses = false
|
||||
linter.flake8_quotes.inline_quotes = double
|
||||
linter.flake8_quotes.multiline_quotes = double
|
||||
linter.flake8_quotes.docstring_quotes = double
|
||||
linter.flake8_quotes.avoid_escape = true
|
||||
linter.flake8_self.ignore_names = [
|
||||
_make,
|
||||
_asdict,
|
||||
_replace,
|
||||
_fields,
|
||||
_field_defaults,
|
||||
_name_,
|
||||
_value_,
|
||||
]
|
||||
linter.flake8_tidy_imports.ban_relative_imports = "parents"
|
||||
linter.flake8_tidy_imports.banned_api = {}
|
||||
linter.flake8_tidy_imports.banned_module_level_imports = []
|
||||
linter.flake8_type_checking.strict = false
|
||||
linter.flake8_type_checking.exempt_modules = [
|
||||
typing,
|
||||
typing_extensions,
|
||||
]
|
||||
linter.flake8_type_checking.runtime_required_base_classes = []
|
||||
linter.flake8_type_checking.runtime_required_decorators = []
|
||||
linter.flake8_type_checking.quote_annotations = false
|
||||
linter.flake8_unused_arguments.ignore_variadic_names = false
|
||||
linter.isort.required_imports = []
|
||||
linter.isort.combine_as_imports = false
|
||||
linter.isort.force_single_line = false
|
||||
linter.isort.force_sort_within_sections = false
|
||||
linter.isort.detect_same_package = true
|
||||
linter.isort.case_sensitive = false
|
||||
linter.isort.force_wrap_aliases = false
|
||||
linter.isort.force_to_top = []
|
||||
linter.isort.known_modules = {}
|
||||
linter.isort.order_by_type = true
|
||||
linter.isort.relative_imports_order = furthest_to_closest
|
||||
linter.isort.single_line_exclusions = []
|
||||
linter.isort.split_on_trailing_comma = true
|
||||
linter.isort.classes = []
|
||||
linter.isort.constants = []
|
||||
linter.isort.variables = []
|
||||
linter.isort.no_lines_before = []
|
||||
linter.isort.lines_after_imports = -1
|
||||
linter.isort.lines_between_types = 0
|
||||
linter.isort.forced_separate = []
|
||||
linter.isort.section_order = [
|
||||
known { type = future },
|
||||
known { type = standard_library },
|
||||
known { type = third_party },
|
||||
known { type = first_party },
|
||||
known { type = local_folder },
|
||||
]
|
||||
linter.isort.default_section = known { type = third_party }
|
||||
linter.isort.no_sections = false
|
||||
linter.isort.from_first = false
|
||||
linter.isort.length_sort = false
|
||||
linter.isort.length_sort_straight = false
|
||||
linter.mccabe.max_complexity = 10
|
||||
linter.pep8_naming.ignore_names = [
|
||||
setUp,
|
||||
tearDown,
|
||||
setUpClass,
|
||||
tearDownClass,
|
||||
setUpModule,
|
||||
tearDownModule,
|
||||
asyncSetUp,
|
||||
asyncTearDown,
|
||||
setUpTestData,
|
||||
failureException,
|
||||
longMessage,
|
||||
maxDiff,
|
||||
]
|
||||
linter.pep8_naming.classmethod_decorators = []
|
||||
linter.pep8_naming.staticmethod_decorators = []
|
||||
linter.pycodestyle.max_line_length = 120
|
||||
linter.pycodestyle.max_doc_length = none
|
||||
linter.pycodestyle.ignore_overlong_task_comments = false
|
||||
linter.pyflakes.extend_generics = []
|
||||
linter.pyflakes.allowed_unused_imports = []
|
||||
linter.pylint.allow_magic_value_types = [
|
||||
str,
|
||||
bytes,
|
||||
]
|
||||
linter.pylint.allow_dunder_method_names = []
|
||||
linter.pylint.max_args = 5
|
||||
linter.pylint.max_positional_args = 5
|
||||
linter.pylint.max_returns = 6
|
||||
linter.pylint.max_bool_expr = 5
|
||||
linter.pylint.max_branches = 12
|
||||
linter.pylint.max_statements = 50
|
||||
linter.pylint.max_public_methods = 20
|
||||
linter.pylint.max_locals = 15
|
||||
linter.pylint.max_nested_blocks = 5
|
||||
linter.pyupgrade.keep_runtime_typing = false
|
||||
linter.ruff.parenthesize_tuple_in_subscript = false
|
||||
linter.ruff.strictly_empty_init_modules = false
|
||||
|
||||
# Formatter Settings
|
||||
formatter.exclude = []
|
||||
formatter.unresolved_target_version = 3.10
|
||||
formatter.per_file_target_version = {}
|
||||
formatter.preview = disabled
|
||||
formatter.line_width = 120
|
||||
formatter.line_ending = auto
|
||||
formatter.indent_style = space
|
||||
formatter.indent_width = 4
|
||||
formatter.quote_style = double
|
||||
formatter.magic_trailing_comma = respect
|
||||
formatter.docstring_code_format = disabled
|
||||
formatter.docstring_code_line_width = dynamic
|
||||
|
||||
# Analyze Settings
|
||||
analyze.exclude = []
|
||||
analyze.preview = disabled
|
||||
analyze.target_version = 3.10
|
||||
analyze.string_imports = disabled
|
||||
analyze.extension = ExtensionMapping({})
|
||||
analyze.include_dependencies = {}
|
||||
analyze.type_checking_imports = true
|
||||
|
||||
----- stderr -----
|
||||
@@ -15,7 +15,7 @@ use ruff_db::files::{File, system_path_to_file};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{InMemorySystem, MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem};
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use ty_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use ty_project::metadata::options::{AnalysisOptions, EnvironmentOptions, Options};
|
||||
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
|
||||
use ty_project::watch::{ChangeEvent, ChangedKind};
|
||||
use ty_project::{CheckMode, Db, ProjectDatabase, ProjectMetadata};
|
||||
@@ -67,6 +67,7 @@ fn tomllib_path(file: &TestFile) -> SystemPathBuf {
|
||||
SystemPathBuf::from("src").join(file.name())
|
||||
}
|
||||
|
||||
#[expect(clippy::needless_update)]
|
||||
fn setup_tomllib_case() -> Case {
|
||||
let system = TestSystem::default();
|
||||
let fs = system.memory_file_system().clone();
|
||||
@@ -85,6 +86,10 @@ fn setup_tomllib_case() -> Case {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY312)),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
analysis: Some(AnalysisOptions {
|
||||
respect_type_ignore_comments: Some(false),
|
||||
..AnalysisOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
|
||||
@@ -755,7 +760,7 @@ fn datetype(criterion: &mut Criterion) {
|
||||
max_dep_date: "2025-07-04",
|
||||
python_version: PythonVersion::PY313,
|
||||
},
|
||||
2,
|
||||
4,
|
||||
);
|
||||
|
||||
bench_project(&benchmark, criterion);
|
||||
|
||||
@@ -71,6 +71,8 @@ impl Display for Benchmark<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[expect(clippy::cast_precision_loss)]
|
||||
fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usize) {
|
||||
let result = db.check();
|
||||
let diagnostics = result.len();
|
||||
@@ -79,6 +81,12 @@ fn check_project(db: &ProjectDatabase, project_name: &str, max_diagnostics: usiz
|
||||
diagnostics > 1 && diagnostics <= max_diagnostics,
|
||||
"Expected between 1 and {max_diagnostics} diagnostics on project '{project_name}' but got {diagnostics}",
|
||||
);
|
||||
|
||||
if (max_diagnostics - diagnostics) as f64 / max_diagnostics as f64 > 0.10 {
|
||||
tracing::warn!(
|
||||
"The expected diagnostics for project `{project_name}` can be reduced: expected {max_diagnostics} but got {diagnostics}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static ALTAIR: Benchmark = Benchmark::new(
|
||||
@@ -101,7 +109,7 @@ static ALTAIR: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
1000,
|
||||
850,
|
||||
);
|
||||
|
||||
static COLOUR_SCIENCE: Benchmark = Benchmark::new(
|
||||
@@ -120,7 +128,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY310,
|
||||
},
|
||||
1070,
|
||||
350,
|
||||
);
|
||||
|
||||
static FREQTRADE: Benchmark = Benchmark::new(
|
||||
@@ -163,7 +171,7 @@ static PANDAS: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
4000,
|
||||
3800,
|
||||
);
|
||||
|
||||
static PYDANTIC: Benchmark = Benchmark::new(
|
||||
@@ -181,7 +189,7 @@ static PYDANTIC: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY39,
|
||||
},
|
||||
7000,
|
||||
3200,
|
||||
);
|
||||
|
||||
static SYMPY: Benchmark = Benchmark::new(
|
||||
@@ -194,7 +202,7 @@ static SYMPY: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
13116,
|
||||
13400,
|
||||
);
|
||||
|
||||
static TANJUN: Benchmark = Benchmark::new(
|
||||
@@ -207,7 +215,7 @@ static TANJUN: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY312,
|
||||
},
|
||||
320,
|
||||
110,
|
||||
);
|
||||
|
||||
static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
@@ -223,7 +231,7 @@ static STATIC_FRAME: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-08-09",
|
||||
python_version: PythonVersion::PY311,
|
||||
},
|
||||
1100,
|
||||
1700,
|
||||
);
|
||||
|
||||
#[track_caller]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -52,6 +52,7 @@ impl InvalidRuleCodeKind {
|
||||
pub(crate) struct InvalidRuleCode {
|
||||
pub(crate) rule_code: String,
|
||||
pub(crate) kind: InvalidRuleCodeKind,
|
||||
pub(crate) whole_comment: bool,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for InvalidRuleCode {
|
||||
@@ -65,7 +66,11 @@ impl AlwaysFixableViolation for InvalidRuleCode {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove the rule code".to_string()
|
||||
if self.whole_comment {
|
||||
format!("Remove the {} comment", self.kind.as_str())
|
||||
} else {
|
||||
format!("Remove the rule code `{}`", self.rule_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +127,7 @@ fn all_codes_invalid_diagnostic(
|
||||
.collect::<Vec<_>>()
|
||||
.join(", "),
|
||||
kind: InvalidRuleCodeKind::Noqa,
|
||||
whole_comment: true,
|
||||
},
|
||||
directive.range(),
|
||||
)
|
||||
@@ -139,6 +145,7 @@ fn some_codes_are_invalid_diagnostic(
|
||||
InvalidRuleCode {
|
||||
rule_code: invalid_code.to_string(),
|
||||
kind: InvalidRuleCodeKind::Noqa,
|
||||
whole_comment: false,
|
||||
},
|
||||
invalid_code.range(),
|
||||
)
|
||||
|
||||
@@ -52,6 +52,25 @@ impl UnusedNOQAKind {
|
||||
/// foo.bar()
|
||||
/// ```
|
||||
///
|
||||
/// ## Conflict with other linters
|
||||
/// When using `RUF100` with the `--fix` option, Ruff may remove trailing comments
|
||||
/// that follow a `# noqa` directive on the same line, as it interprets the
|
||||
/// remainder of the line as a description for the suppression.
|
||||
///
|
||||
/// To prevent Ruff from removing suppressions for other tools (like `pylint`
|
||||
/// or `mypy`), separate them with a second `#` character:
|
||||
///
|
||||
/// ```python
|
||||
/// # Bad: Ruff --fix will remove the pylint comment
|
||||
/// def visit_ImportFrom(self, node): # noqa: N802, pylint: disable=invalid-name
|
||||
/// pass
|
||||
///
|
||||
///
|
||||
/// # Good: Ruff will preserve the pylint comment
|
||||
/// def visit_ImportFrom(self, node): # noqa: N802 # pylint: disable=invalid-name
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.external`
|
||||
///
|
||||
|
||||
@@ -10,7 +10,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
|
||||
3 | # External code
|
||||
4 | import re # noqa: V123
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the `# noqa` comment
|
||||
1 | # Invalid code
|
||||
- import os # noqa: INVALID123
|
||||
2 + import os
|
||||
@@ -28,7 +28,7 @@ RUF102 [*] Invalid rule code in `# noqa`: V123
|
||||
5 | # Valid noqa
|
||||
6 | import sys # noqa: E402
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the `# noqa` comment
|
||||
1 | # Invalid code
|
||||
2 | import os # noqa: INVALID123
|
||||
3 | # External code
|
||||
@@ -48,7 +48,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID456
|
||||
8 | from itertools import product # Preceeding comment # noqa: INVALID789
|
||||
9 | # Succeeding comment
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID456`
|
||||
4 | import re # noqa: V123
|
||||
5 | # Valid noqa
|
||||
6 | import sys # noqa: E402
|
||||
@@ -68,7 +68,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID789
|
||||
9 | # Succeeding comment
|
||||
10 | import math # noqa: INVALID000 # Succeeding comment
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the `# noqa` comment
|
||||
5 | # Valid noqa
|
||||
6 | import sys # noqa: E402
|
||||
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
|
||||
@@ -88,7 +88,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID000
|
||||
11 | # Mixed valid and invalid
|
||||
12 | from typing import List # noqa: F401, INVALID123
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the `# noqa` comment
|
||||
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
|
||||
8 | from itertools import product # Preceeding comment # noqa: INVALID789
|
||||
9 | # Succeeding comment
|
||||
@@ -108,7 +108,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
|
||||
13 | # Test for multiple invalid
|
||||
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID123`
|
||||
9 | # Succeeding comment
|
||||
10 | import math # noqa: INVALID000 # Succeeding comment
|
||||
11 | # Mixed valid and invalid
|
||||
@@ -128,7 +128,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID100
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID100`
|
||||
11 | # Mixed valid and invalid
|
||||
12 | from typing import List # noqa: F401, INVALID123
|
||||
13 | # Test for multiple invalid
|
||||
@@ -148,7 +148,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID200
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID200`
|
||||
11 | # Mixed valid and invalid
|
||||
12 | from typing import List # noqa: F401, INVALID123
|
||||
13 | # Test for multiple invalid
|
||||
@@ -168,7 +168,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID300
|
||||
17 | # Test for mixed code types
|
||||
18 | import json # noqa: E402, INVALID400, V100
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID300`
|
||||
13 | # Test for multiple invalid
|
||||
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
@@ -188,7 +188,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID400
|
||||
19 | # Test for rule redirects
|
||||
20 | import pandas as pd # noqa: TCH002
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID400`
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
||||
17 | # Test for mixed code types
|
||||
@@ -207,7 +207,7 @@ RUF102 [*] Invalid rule code in `# noqa`: V100
|
||||
19 | # Test for rule redirects
|
||||
20 | import pandas as pd # noqa: TCH002
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `V100`
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
||||
17 | # Test for mixed code types
|
||||
|
||||
@@ -10,7 +10,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
|
||||
3 | # External code
|
||||
4 | import re # noqa: V123
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the `# noqa` comment
|
||||
1 | # Invalid code
|
||||
- import os # noqa: INVALID123
|
||||
2 + import os
|
||||
@@ -28,7 +28,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID456
|
||||
8 | from itertools import product # Preceeding comment # noqa: INVALID789
|
||||
9 | # Succeeding comment
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID456`
|
||||
4 | import re # noqa: V123
|
||||
5 | # Valid noqa
|
||||
6 | import sys # noqa: E402
|
||||
@@ -48,7 +48,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID789
|
||||
9 | # Succeeding comment
|
||||
10 | import math # noqa: INVALID000 # Succeeding comment
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the `# noqa` comment
|
||||
5 | # Valid noqa
|
||||
6 | import sys # noqa: E402
|
||||
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
|
||||
@@ -68,7 +68,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID000
|
||||
11 | # Mixed valid and invalid
|
||||
12 | from typing import List # noqa: F401, INVALID123
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the `# noqa` comment
|
||||
7 | from functools import cache # Preceeding comment # noqa: F401, INVALID456
|
||||
8 | from itertools import product # Preceeding comment # noqa: INVALID789
|
||||
9 | # Succeeding comment
|
||||
@@ -88,7 +88,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID123
|
||||
13 | # Test for multiple invalid
|
||||
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID123`
|
||||
9 | # Succeeding comment
|
||||
10 | import math # noqa: INVALID000 # Succeeding comment
|
||||
11 | # Mixed valid and invalid
|
||||
@@ -108,7 +108,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID100
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID100`
|
||||
11 | # Mixed valid and invalid
|
||||
12 | from typing import List # noqa: F401, INVALID123
|
||||
13 | # Test for multiple invalid
|
||||
@@ -128,7 +128,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID200
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID200`
|
||||
11 | # Mixed valid and invalid
|
||||
12 | from typing import List # noqa: F401, INVALID123
|
||||
13 | # Test for multiple invalid
|
||||
@@ -148,7 +148,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID300
|
||||
17 | # Test for mixed code types
|
||||
18 | import json # noqa: E402, INVALID400, V100
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID300`
|
||||
13 | # Test for multiple invalid
|
||||
14 | from collections import defaultdict # noqa: INVALID100, INVALID200, F401
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
@@ -168,7 +168,7 @@ RUF102 [*] Invalid rule code in `# noqa`: INVALID400
|
||||
19 | # Test for rule redirects
|
||||
20 | import pandas as pd # noqa: TCH002
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `INVALID400`
|
||||
15 | # Test for preserving valid codes when fixing
|
||||
16 | from itertools import chain # noqa: E402, INVALID300, F401
|
||||
17 | # Test for mixed code types
|
||||
|
||||
@@ -552,7 +552,7 @@ RUF102 [*] Invalid rule code in suppression: YF829
|
||||
97 | # ruff: enable[YF829]
|
||||
| -----
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the suppression comment
|
||||
90 |
|
||||
91 | def f():
|
||||
92 | # Unknown rule codes
|
||||
@@ -578,7 +578,7 @@ RUF102 [*] Invalid rule code in suppression: RQW320
|
||||
| ------
|
||||
97 | # ruff: enable[YF829]
|
||||
|
|
||||
help: Remove the rule code
|
||||
help: Remove the rule code `RQW320`
|
||||
91 | def f():
|
||||
92 | # Unknown rule codes
|
||||
93 | # ruff: disable[YF829]
|
||||
|
||||
@@ -607,13 +607,21 @@ impl TryFrom<String> for RequiredVersion {
|
||||
type Error = pep440_rs::VersionSpecifiersParseError;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||
value.parse()
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RequiredVersion {
|
||||
type Err = pep440_rs::VersionSpecifiersParseError;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
// Treat `0.3.1` as `==0.3.1`, for backwards compatibility.
|
||||
if let Ok(version) = pep440_rs::Version::from_str(&value) {
|
||||
if let Ok(version) = pep440_rs::Version::from_str(value) {
|
||||
Ok(Self(VersionSpecifiers::from(
|
||||
VersionSpecifier::equals_version(version),
|
||||
)))
|
||||
} else {
|
||||
Ok(Self(VersionSpecifiers::from_str(&value)?))
|
||||
Ok(Self(VersionSpecifiers::from_str(value)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,12 @@ pub(crate) struct Suppression {
|
||||
comments: DisableEnableComments,
|
||||
}
|
||||
|
||||
impl Suppression {
|
||||
fn codes(&self) -> &[TextRange] {
|
||||
&self.comments.disable_comment().codes
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum DisableEnableComments {
|
||||
/// An implicitly closed disable comment without a matching enable comment.
|
||||
@@ -201,6 +207,7 @@ impl Suppressions {
|
||||
InvalidRuleCode {
|
||||
rule_code: suppression.code.to_string(),
|
||||
kind: InvalidRuleCodeKind::Suppression,
|
||||
whole_comment: suppression.codes().len() == 1,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.10"
|
||||
version = "0.14.11"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -7,7 +7,6 @@ use std::collections::BTreeMap;
|
||||
use std::env::VarError;
|
||||
use std::num::{NonZeroU8, NonZeroU16};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use glob::{GlobError, Paths, PatternError, glob};
|
||||
@@ -36,8 +35,7 @@ use ruff_linter::settings::{
|
||||
DEFAULT_SELECTORS, DUMMY_VARIABLE_RGX, LinterSettings, TASK_TAGS, TargetVersion,
|
||||
};
|
||||
use ruff_linter::{
|
||||
RUFF_PKG_VERSION, RuleSelector, fs, warn_user_once, warn_user_once_by_id,
|
||||
warn_user_once_by_message,
|
||||
RuleSelector, fs, warn_user_once, warn_user_once_by_id, warn_user_once_by_message,
|
||||
};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_formatter::{
|
||||
@@ -53,6 +51,7 @@ use crate::options::{
|
||||
Flake8UnusedArgumentsOptions, FormatOptions, IsortOptions, LintCommonOptions, LintOptions,
|
||||
McCabeOptions, Options, Pep8NamingOptions, PyUpgradeOptions, PycodestyleOptions,
|
||||
PydoclintOptions, PydocstyleOptions, PyflakesOptions, PylintOptions, RuffOptions,
|
||||
validate_required_version,
|
||||
};
|
||||
use crate::settings::{
|
||||
EXCLUDE, FileResolverSettings, FormatterSettings, INCLUDE, INCLUDE_PREVIEW, LineEnding,
|
||||
@@ -155,13 +154,7 @@ pub struct Configuration {
|
||||
impl Configuration {
|
||||
pub fn into_settings(self, project_root: &Path) -> Result<Settings> {
|
||||
if let Some(required_version) = &self.required_version {
|
||||
let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION)
|
||||
.expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
|
||||
if !required_version.contains(&ruff_pkg_version) {
|
||||
return Err(anyhow!(
|
||||
"Required version `{required_version}` does not match the running version `{RUFF_PKG_VERSION}`"
|
||||
));
|
||||
}
|
||||
validate_required_version(required_version)?;
|
||||
}
|
||||
|
||||
let linter_target_version = TargetVersion(self.target_version);
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
use anyhow::Result;
|
||||
use regex::Regex;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
use serde::de::{self};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use strum::IntoEnumIterator;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
|
||||
use crate::settings::LineEnding;
|
||||
use ruff_formatter::IndentStyle;
|
||||
use ruff_graph::Direction;
|
||||
use ruff_linter::RUFF_PKG_VERSION;
|
||||
|
||||
use ruff_linter::line_width::{IndentWidth, LineLength};
|
||||
use ruff_linter::rules::flake8_import_conventions::settings::BannedAliases;
|
||||
use ruff_linter::rules::flake8_pytest_style::settings::SettingsError;
|
||||
@@ -556,6 +560,17 @@ pub struct LintOptions {
|
||||
pub future_annotations: Option<bool>,
|
||||
}
|
||||
|
||||
pub fn validate_required_version(required_version: &RequiredVersion) -> anyhow::Result<()> {
|
||||
let ruff_pkg_version = pep440_rs::Version::from_str(RUFF_PKG_VERSION)
|
||||
.expect("RUFF_PKG_VERSION is not a valid PEP 440 version specifier");
|
||||
if !required_version.contains(&ruff_pkg_version) {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Required version `{required_version}` does not match the running version `{RUFF_PKG_VERSION}`"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Newtype wrapper for [`LintCommonOptions`] that allows customizing the JSON schema and omitting the fields from the [`OptionsMetadata`].
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
#[serde(transparent)]
|
||||
|
||||
@@ -5,12 +5,13 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::{Context, Result};
|
||||
use log::debug;
|
||||
use pep440_rs::{Operator, Version, VersionSpecifiers};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use ruff_linter::settings::types::PythonVersion;
|
||||
use ruff_linter::settings::types::{PythonVersion, RequiredVersion};
|
||||
|
||||
use crate::options::Options;
|
||||
use crate::options::{Options, validate_required_version};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
struct Tools {
|
||||
@@ -40,20 +41,38 @@ impl Pyproject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a `ruff.toml` file.
|
||||
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||
fn parse_toml<P: AsRef<Path>, T: DeserializeOwned>(path: P, table_path: &[&str]) -> Result<T> {
|
||||
let path = path.as_ref();
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
|
||||
|
||||
// Parse the TOML document once into a spanned representation so we can:
|
||||
// - Inspect `required-version` without triggering strict deserialization errors.
|
||||
// - Deserialize with precise spans (line/column and excerpt) on errors.
|
||||
let root = toml::de::DeTable::parse(&contents)
|
||||
.with_context(|| format!("Failed to parse {}", path.display()))?;
|
||||
|
||||
check_required_version(root.get_ref(), table_path)?;
|
||||
|
||||
let deserializer = toml::de::Deserializer::from(root);
|
||||
T::deserialize(deserializer)
|
||||
.map_err(|mut err| {
|
||||
// `Deserializer::from` doesn't have access to the original input, but we do.
|
||||
// Attach it so TOML errors include line/column and a source excerpt.
|
||||
err.set_input(Some(&contents));
|
||||
err
|
||||
})
|
||||
.with_context(|| format!("Failed to parse {}", path.display()))
|
||||
}
|
||||
|
||||
/// Parse a `ruff.toml` file.
|
||||
fn parse_ruff_toml<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||
parse_toml(path, &[])
|
||||
}
|
||||
|
||||
/// Parse a `pyproject.toml` file.
|
||||
fn parse_pyproject_toml<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
|
||||
let path = path.as_ref();
|
||||
let contents = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("Failed to read {}", path.display()))?;
|
||||
toml::from_str(&contents).with_context(|| format!("Failed to parse {}", path.display()))
|
||||
parse_toml(path, &["tool", "ruff"])
|
||||
}
|
||||
|
||||
/// Return `true` if a `pyproject.toml` contains a `[tool.ruff]` section.
|
||||
@@ -98,6 +117,33 @@ pub fn find_settings_toml<P: AsRef<Path>>(path: P) -> Result<Option<PathBuf>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn check_required_version(value: &toml::de::DeTable, table_path: &[&str]) -> Result<()> {
|
||||
let mut current = value;
|
||||
for key in table_path {
|
||||
let Some(next) = current.get(*key) else {
|
||||
return Ok(());
|
||||
};
|
||||
let toml::de::DeValue::Table(next) = next.get_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
current = next;
|
||||
}
|
||||
|
||||
let required_version = current
|
||||
.get("required-version")
|
||||
.and_then(|value| value.get_ref().as_str());
|
||||
|
||||
let Some(required_version) = required_version else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// If it doesn't parse, we just fall through to normal parsing; it will give a nicer error message.
|
||||
if let Ok(required_version) = required_version.parse::<RequiredVersion>() {
|
||||
validate_required_version(&required_version)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Derive target version from `required-version` in `pyproject.toml`, if
|
||||
/// such a file exists in an ancestor directory.
|
||||
pub fn find_fallback_target_version<P: AsRef<Path>>(path: P) -> Option<PythonVersion> {
|
||||
|
||||
@@ -102,8 +102,8 @@ impl Relativity {
|
||||
#[derive(Debug)]
|
||||
pub struct Resolver<'a> {
|
||||
pyproject_config: &'a PyprojectConfig,
|
||||
/// All [`Settings`] that have been added to the resolver.
|
||||
settings: Vec<Settings>,
|
||||
/// All [`Settings`] that have been added to the resolver, along with their config file paths.
|
||||
settings: Vec<(Settings, PathBuf)>,
|
||||
/// A router from path to index into the `settings` vector.
|
||||
router: Router<usize>,
|
||||
}
|
||||
@@ -146,8 +146,8 @@ impl<'a> Resolver<'a> {
|
||||
}
|
||||
|
||||
/// Add a resolved [`Settings`] under a given [`PathBuf`] scope.
|
||||
fn add(&mut self, path: &Path, settings: Settings) {
|
||||
self.settings.push(settings);
|
||||
fn add(&mut self, path: &Path, settings: Settings, config_path: PathBuf) {
|
||||
self.settings.push((settings, config_path));
|
||||
|
||||
// Normalize the path to use `/` separators and escape the '{' and '}' characters,
|
||||
// which matchit uses for routing parameters.
|
||||
@@ -172,13 +172,27 @@ impl<'a> Resolver<'a> {
|
||||
|
||||
/// Return the appropriate [`Settings`] for a given [`Path`].
|
||||
pub fn resolve(&self, path: &Path) -> &Settings {
|
||||
self.resolve_with_path(path).0
|
||||
}
|
||||
|
||||
/// Return the appropriate [`Settings`] and config file path for a given [`Path`].
|
||||
pub fn resolve_with_path(&self, path: &Path) -> (&Settings, Option<&Path>) {
|
||||
match self.pyproject_config.strategy {
|
||||
PyprojectDiscoveryStrategy::Fixed => &self.pyproject_config.settings,
|
||||
PyprojectDiscoveryStrategy::Fixed => (
|
||||
&self.pyproject_config.settings,
|
||||
self.pyproject_config.path.as_deref(),
|
||||
),
|
||||
PyprojectDiscoveryStrategy::Hierarchical => self
|
||||
.router
|
||||
.at(path.to_slash_lossy().as_ref())
|
||||
.map(|Match { value, .. }| &self.settings[*value])
|
||||
.unwrap_or(&self.pyproject_config.settings),
|
||||
.map(|Match { value, .. }| {
|
||||
let (settings, config_path) = &self.settings[*value];
|
||||
(settings, Some(config_path.as_path()))
|
||||
})
|
||||
.unwrap_or((
|
||||
&self.pyproject_config.settings,
|
||||
self.pyproject_config.path.as_deref(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +269,8 @@ impl<'a> Resolver<'a> {
|
||||
|
||||
/// Return an iterator over the resolved [`Settings`] in this [`Resolver`].
|
||||
pub fn settings(&self) -> impl Iterator<Item = &Settings> {
|
||||
std::iter::once(&self.pyproject_config.settings).chain(&self.settings)
|
||||
std::iter::once(&self.pyproject_config.settings)
|
||||
.chain(self.settings.iter().map(|(settings, _)| settings))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,17 +394,17 @@ pub fn resolve_configuration(
|
||||
|
||||
/// Extract the project root (scope) and [`Settings`] from a given
|
||||
/// `pyproject.toml`.
|
||||
fn resolve_scoped_settings<'a>(
|
||||
pyproject: &'a Path,
|
||||
fn resolve_scoped_settings(
|
||||
pyproject: &Path,
|
||||
transformer: &dyn ConfigurationTransformer,
|
||||
origin: ConfigurationOrigin,
|
||||
) -> Result<(&'a Path, Settings)> {
|
||||
) -> Result<(PathBuf, Settings)> {
|
||||
let relativity = Relativity::from(origin);
|
||||
|
||||
let configuration = resolve_configuration(pyproject, transformer, origin)?;
|
||||
let project_root = relativity.resolve(pyproject);
|
||||
let settings = configuration.into_settings(project_root)?;
|
||||
Ok((project_root, settings))
|
||||
Ok((project_root.to_path_buf(), settings))
|
||||
}
|
||||
|
||||
/// Extract the [`Settings`] from a given `pyproject.toml` and process the
|
||||
@@ -455,7 +470,7 @@ pub fn python_files_in_path<'a>(
|
||||
transformer,
|
||||
ConfigurationOrigin::Ancestor,
|
||||
)?;
|
||||
resolver.add(root, settings);
|
||||
resolver.add(&root, settings, pyproject);
|
||||
// We found the closest configuration.
|
||||
break;
|
||||
}
|
||||
@@ -647,7 +662,11 @@ impl ParallelVisitor for PythonFilesVisitor<'_, '_> {
|
||||
ConfigurationOrigin::Ancestor,
|
||||
) {
|
||||
Ok((root, settings)) => {
|
||||
self.global.resolver.write().unwrap().add(root, settings);
|
||||
self.global
|
||||
.resolver
|
||||
.write()
|
||||
.unwrap()
|
||||
.add(&root, settings, pyproject);
|
||||
}
|
||||
Err(err) => {
|
||||
self.local_error = Err(err);
|
||||
@@ -767,7 +786,7 @@ pub fn python_file_at_path(
|
||||
if let Some(pyproject) = settings_toml(ancestor)? {
|
||||
let (root, settings) =
|
||||
resolve_scoped_settings(&pyproject, transformer, ConfigurationOrigin::Unknown)?;
|
||||
resolver.add(root, settings);
|
||||
resolver.add(&root, settings, pyproject);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
15
crates/ty/docs/rules.md
generated
15
crates/ty/docs/rules.md
generated
@@ -467,7 +467,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ignore-comment-unknown-rule" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L50" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L54" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1047,7 +1047,7 @@ class D(Generic[U, T]): ...
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-ignore-comment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L75" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L79" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2895,7 +2895,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
## `unused-ignore-comment`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Default level: <a href="../../rules#rule-levels" title="This lint has a default level of 'warn'."><code>warn</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.0.1-alpha.1</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unused-ignore-comment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Fsuppression.rs#L25" target="_blank">View source</a>
|
||||
@@ -2904,11 +2904,11 @@ Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.1">0.
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable.
|
||||
Checks for `ty: ignore` or `type: ignore` directives that are no longer applicable.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
A `type: ignore` directive that no longer matches any diagnostic violations is likely
|
||||
A `ty: ignore` directive that no longer matches any diagnostic violations is likely
|
||||
included by mistake, and should be removed to avoid confusion.
|
||||
|
||||
**Examples**
|
||||
@@ -2923,6 +2923,11 @@ Use instead:
|
||||
a = 20 / 2
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
Set [`analysis.respect-type-ignore-comments`](https://docs.astral.sh/ty/reference/configuration/#respect-type-ignore-comments)
|
||||
to `false` to prevent this rule from reporting unused `type: ignore` comments.
|
||||
|
||||
## `useless-overload-body`
|
||||
|
||||
<small>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
name,file,index,rank
|
||||
auto-import-includes-modules,main.py,0,1
|
||||
auto-import-includes-modules,main.py,1,7
|
||||
auto-import-includes-modules,main.py,1,2
|
||||
auto-import-includes-modules,main.py,2,1
|
||||
auto-import-skips-current-module,main.py,0,1
|
||||
class-arg-completion,main.py,0,1
|
||||
exact-over-fuzzy,main.py,0,1
|
||||
fstring-completions,main.py,0,1
|
||||
higher-level-symbols-preferred,main.py,0,
|
||||
higher-level-symbols-preferred,main.py,1,1
|
||||
@@ -16,8 +17,10 @@ import-deprioritizes-type_check_only,main.py,3,2
|
||||
import-deprioritizes-type_check_only,main.py,4,3
|
||||
import-keyword-completion,main.py,0,1
|
||||
internal-typeshed-hidden,main.py,0,2
|
||||
local-over-auto-import,main.py,0,1
|
||||
modules-over-other-symbols,main.py,0,1
|
||||
none-completion,main.py,0,1
|
||||
numpy-array,main.py,0,159
|
||||
numpy-array,main.py,0,57
|
||||
numpy-array,main.py,1,1
|
||||
object-attr-instance-methods,main.py,0,1
|
||||
object-attr-instance-methods,main.py,1,1
|
||||
@@ -26,7 +29,12 @@ raise-uses-base-exception,main.py,0,1
|
||||
scope-existing-over-new-import,main.py,0,1
|
||||
scope-prioritize-closer,main.py,0,2
|
||||
scope-simple-long-identifier,main.py,0,1
|
||||
third-party-over-stdlib,main.py,0,1
|
||||
tighter-over-looser-scope,main.py,0,3
|
||||
tstring-completions,main.py,0,1
|
||||
ty-extensions-lower-stdlib,main.py,0,9
|
||||
type-var-typing-over-ast,main.py,0,3
|
||||
type-var-typing-over-ast,main.py,1,253
|
||||
ty-extensions-lower-stdlib,main.py,0,1
|
||||
typing-gets-priority,main.py,0,1
|
||||
typing-gets-priority,main.py,1,1
|
||||
typing-gets-priority,main.py,2,1
|
||||
typing-gets-priority,main.py,3,1
|
||||
typing-gets-priority,main.py,4,1
|
||||
|
||||
|
@@ -540,16 +540,17 @@ fn copy_project(src_dir: &SystemPath, dst_dir: &SystemPath) -> anyhow::Result<Ve
|
||||
std::fs::create_dir_all(dst_dir).with_context(|| dst_dir.to_string())?;
|
||||
|
||||
let mut cursors = vec![];
|
||||
for result in walkdir::WalkDir::new(src_dir.as_std_path()) {
|
||||
let it = walkdir::WalkDir::new(src_dir.as_std_path())
|
||||
.into_iter()
|
||||
.filter_entry(|dent| {
|
||||
!dent
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_some_and(|name| name.starts_with('.'))
|
||||
});
|
||||
for result in it {
|
||||
let dent =
|
||||
result.with_context(|| format!("failed to get directory entry from {src_dir}"))?;
|
||||
if dent
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_some_and(|name| name.starts_with('.'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let src = SystemPath::from_std_path(dent.path()).ok_or_else(|| {
|
||||
anyhow::anyhow!("path `{}` is not valid UTF-8", dent.path().display())
|
||||
|
||||
3
crates/ty_completion_eval/truth/exact-over-fuzzy/main.py
Normal file
3
crates/ty_completion_eval/truth/exact-over-fuzzy/main.py
Normal file
@@ -0,0 +1,3 @@
|
||||
pattern = 1
|
||||
pttn = 1
|
||||
pttn<CURSOR: pttn>
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = true
|
||||
@@ -0,0 +1,11 @@
|
||||
def foo(x):
|
||||
# We specifically want the local `x` to be
|
||||
# suggested first here, and NOT an `x` or an
|
||||
# `X` from some other module (via auto-import).
|
||||
# We'd also like this to come before `except`,
|
||||
# which is a keyword that contains `x`, but is
|
||||
# not an exact match (where as `x` is). `except`
|
||||
# also isn't legal in this context, although
|
||||
# that sort of context sensitivity is a bit
|
||||
# trickier.
|
||||
return x<CURSOR: x>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/local-over-auto-import/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/local-over-auto-import/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = true
|
||||
@@ -0,0 +1 @@
|
||||
os<CURSOR: os>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
@@ -0,0 +1 @@
|
||||
os = 1
|
||||
78
crates/ty_completion_eval/truth/modules-over-other-symbols/uv.lock
generated
Normal file
78
crates/ty_completion_eval/truth/modules-over-other-symbols/uv.lock
generated
Normal file
@@ -0,0 +1,78 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2025.11.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "regex" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "regex", specifier = ">=2025.11.3" }]
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = true
|
||||
@@ -0,0 +1,12 @@
|
||||
# This test was originally written to
|
||||
# check that a third party dependency
|
||||
# gets priority over stdlib. But it was
|
||||
# not clearly the right choice[1].
|
||||
#
|
||||
# 2026-01-09: We stuck with it for now,
|
||||
# but it seems likely that we'll want
|
||||
# to regress this task in favor of
|
||||
# another.
|
||||
#
|
||||
# [1]: https://github.com/astral-sh/ruff/pull/22460#discussion_r2676343225
|
||||
fullma<CURSOR: regex.fullmatch>
|
||||
@@ -0,0 +1,7 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"regex>=2025.11.3",
|
||||
]
|
||||
78
crates/ty_completion_eval/truth/third-party-over-stdlib/uv.lock
generated
Normal file
78
crates/ty_completion_eval/truth/third-party-over-stdlib/uv.lock
generated
Normal file
@@ -0,0 +1,78 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "2025.11.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "regex" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "regex", specifier = ">=2025.11.3" }]
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = true
|
||||
@@ -0,0 +1,10 @@
|
||||
scope1 = 1
|
||||
def x():
|
||||
scope2 = 1
|
||||
def xx():
|
||||
scope3 = 1
|
||||
def xxx():
|
||||
# We specifically want `scope3` to be
|
||||
# suggested first here, since that's in
|
||||
# the "tighter" scope.
|
||||
scope<CURSOR: scope3>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
8
crates/ty_completion_eval/truth/tighter-over-looser-scope/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/tighter-over-looser-scope/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -1,12 +0,0 @@
|
||||
# This one demands that `TypeVa` complete to `typing.TypeVar`
|
||||
# even though there is also an `ast.TypeVar`. Getting this one
|
||||
# right seems tricky, and probably requires module-specific
|
||||
# heuristics.
|
||||
#
|
||||
# ref: https://github.com/astral-sh/ty/issues/1274#issuecomment-3345884227
|
||||
TypeVa<CURSOR: typing.TypeVar>
|
||||
|
||||
# This is a similar case of `ctypes.cast` being preferred over
|
||||
# `typing.cast`. Maybe `typing` should just get a slightly higher
|
||||
# weight than most other stdlib modules?
|
||||
cas<CURSOR: typing.cast>
|
||||
@@ -0,0 +1,2 @@
|
||||
[settings]
|
||||
auto-import = true
|
||||
19
crates/ty_completion_eval/truth/typing-gets-priority/main.py
Normal file
19
crates/ty_completion_eval/truth/typing-gets-priority/main.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Many of these came from discussion in:
|
||||
# <https://github.com/astral-sh/ty/issues/1274>
|
||||
|
||||
# We should prefer `typing` over `asyncio` here.
|
||||
class Foo(Protoco<CURSOR: typing.Protocol>): ...
|
||||
|
||||
# We should prefer `typing` over `ty_extensions`
|
||||
# or `typing_extensions`.
|
||||
reveal_<CURSOR: typing.reveal_type>
|
||||
|
||||
# We should prefer `typing` over `ast`.
|
||||
TypeVa<CURSOR: typing.TypeVar>
|
||||
|
||||
# We should prefer `typing` over `ctypes`.
|
||||
cast<CURSOR: typing.cast>
|
||||
|
||||
# We should prefer a non-stdlib project import
|
||||
# over a stdlib `typing` import.
|
||||
NoRetur<CURSOR: sub1.NoReturn>
|
||||
@@ -0,0 +1,5 @@
|
||||
[project]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = []
|
||||
@@ -0,0 +1 @@
|
||||
NoReturn = 1
|
||||
8
crates/ty_completion_eval/truth/typing-gets-priority/uv.lock
generated
Normal file
8
crates/ty_completion_eval/truth/typing-gets-priority/uv.lock
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.13"
|
||||
|
||||
[[package]]
|
||||
name = "test"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
@@ -27,6 +27,7 @@ ty_project = { workspace = true, features = ["testing"] }
|
||||
ty_python_semantic = { workspace = true }
|
||||
ty_vendored = { workspace = true }
|
||||
|
||||
compact_str = { workspace = true }
|
||||
get-size2 = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
|
||||
@@ -64,7 +64,11 @@ pub fn all_symbols<'db>(
|
||||
continue;
|
||||
}
|
||||
s.spawn(move |_| {
|
||||
let symbols_for_file_span = tracing::debug_span!(parent: all_symbols_span, "symbols_for_file_global_only", ?file);
|
||||
let symbols_for_file_span = tracing::debug_span!(
|
||||
parent: all_symbols_span,
|
||||
"symbols_for_file_global_only",
|
||||
path = %file.path(&*db),
|
||||
);
|
||||
let _entered = symbols_for_file_span.entered();
|
||||
|
||||
if query.is_match_symbol_name(module.name(&*db)) {
|
||||
@@ -94,11 +98,13 @@ pub fn all_symbols<'db>(
|
||||
let key1 = (
|
||||
s1.name_in_file()
|
||||
.unwrap_or_else(|| s1.module().name(db).as_str()),
|
||||
s1.module().name(db).as_str(),
|
||||
s1.file.path(db).as_str(),
|
||||
);
|
||||
let key2 = (
|
||||
s2.name_in_file()
|
||||
.unwrap_or_else(|| s2.module().name(db).as_str()),
|
||||
s2.module().name(db).as_str(),
|
||||
s2.file.path(db).as_str(),
|
||||
);
|
||||
key1.cmp(&key2)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,8 +34,8 @@ pub struct ParameterDetails<'db> {
|
||||
pub name: String,
|
||||
/// The parameter label in the signature (e.g., "param1: str")
|
||||
pub label: String,
|
||||
/// The annotated type of the parameter, if any
|
||||
pub ty: Option<Type<'db>>,
|
||||
/// The annotated type of the parameter. If no annotation was provided, this is `Unknown`.
|
||||
pub ty: Type<'db>,
|
||||
/// Documentation specific to the parameter, typically extracted from the
|
||||
/// function's docstring
|
||||
pub documentation: Option<String>,
|
||||
@@ -237,7 +237,7 @@ fn create_parameters_from_offsets<'db>(
|
||||
docstring: Option<&Docstring>,
|
||||
parameter_names: &[String],
|
||||
parameter_kinds: &[ParameterKind],
|
||||
parameter_types: &[Option<Type<'db>>],
|
||||
parameter_types: &[Type<'db>],
|
||||
) -> Vec<ParameterDetails<'db>> {
|
||||
// Extract parameter documentation from the function's docstring if available.
|
||||
let param_docs = if let Some(docstring) = docstring {
|
||||
@@ -264,12 +264,11 @@ fn create_parameters_from_offsets<'db>(
|
||||
parameter_kinds.get(i),
|
||||
Some(ParameterKind::PositionalOnly { .. })
|
||||
);
|
||||
let ty = parameter_types.get(i).copied().flatten();
|
||||
|
||||
ParameterDetails {
|
||||
name: param_name.to_string(),
|
||||
label,
|
||||
ty,
|
||||
ty: parameter_types[i],
|
||||
documentation: param_docs.get(param_name).cloned(),
|
||||
is_positional_only,
|
||||
}
|
||||
|
||||
@@ -554,10 +554,17 @@ impl SearchPath {
|
||||
)
|
||||
}
|
||||
|
||||
/// Is this search path in "first party" code? i.e., The
|
||||
/// end user's project code.
|
||||
pub fn is_first_party(&self) -> bool {
|
||||
matches!(&*self.0, SearchPathInner::FirstParty(_))
|
||||
}
|
||||
|
||||
/// Is the module in a site-packages directory?
|
||||
pub fn is_site_packages(&self) -> bool {
|
||||
matches!(&*self.0, SearchPathInner::SitePackages(_))
|
||||
}
|
||||
|
||||
fn is_valid_extension(&self, extension: &str) -> bool {
|
||||
if self.is_standard_library() {
|
||||
extension == "pyi"
|
||||
|
||||
@@ -40,25 +40,22 @@ impl ExcludeFilter {
|
||||
}
|
||||
|
||||
fn matches(&self, path: &SystemPath, mode: GlobFilterCheckMode, directory: bool) -> bool {
|
||||
// If the path is excluded, return `ignore`
|
||||
if self.ignore.matched(path, directory).is_ignore() {
|
||||
return true;
|
||||
}
|
||||
|
||||
match mode {
|
||||
GlobFilterCheckMode::TopDown => {
|
||||
match self.ignore.matched(path, directory) {
|
||||
// No hit or an allow hit means the file or directory is not excluded.
|
||||
Match::None | Match::Allow => false,
|
||||
Match::Ignore => true,
|
||||
}
|
||||
// No hit or an allow hit means the file or directory is not excluded.
|
||||
false
|
||||
}
|
||||
GlobFilterCheckMode::Adhoc => {
|
||||
for ancestor in path.ancestors() {
|
||||
match self.ignore.matched(ancestor, directory) {
|
||||
// If the path is allowlisted or there's no hit, try the parent to ensure we don't return false
|
||||
// for a folder where there's an exclude for a parent.
|
||||
Match::None | Match::Allow => {}
|
||||
Match::Ignore => return true,
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
// If the path is allowlisted or there's no hit, try the parent to ensure we don't return false
|
||||
// for a folder where there's an exclude for a parent.
|
||||
path.ancestors()
|
||||
.skip(1)
|
||||
.any(|ancestor| self.ignore.matched(ancestor, true).is_ignore())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +121,7 @@ struct Gitignore {
|
||||
set: GlobSet,
|
||||
globs: Vec<IgnoreGlob>,
|
||||
#[get_size(ignore)]
|
||||
matches: Option<Arc<Pool<Vec<usize>>>>,
|
||||
matches: Arc<Pool<Vec<usize>>>,
|
||||
}
|
||||
|
||||
impl Gitignore {
|
||||
@@ -140,7 +137,7 @@ impl Gitignore {
|
||||
return Match::None;
|
||||
}
|
||||
|
||||
let mut matches = self.matches.as_ref().unwrap().get();
|
||||
let mut matches = self.matches.get();
|
||||
let candidate = Candidate::new(path);
|
||||
self.set.matches_candidate_into(&candidate, &mut matches);
|
||||
for &i in matches.iter().rev() {
|
||||
@@ -187,6 +184,12 @@ enum Match {
|
||||
Allow,
|
||||
}
|
||||
|
||||
impl Match {
|
||||
const fn is_ignore(self) -> bool {
|
||||
matches!(self, Match::Ignore)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
struct IgnoreGlob {
|
||||
/// The pattern that was originally parsed.
|
||||
@@ -232,7 +235,7 @@ impl GitignoreBuilder {
|
||||
Ok(Gitignore {
|
||||
set,
|
||||
globs: self.globs.clone(),
|
||||
matches: Some(Arc::new(Pool::new(Vec::new))),
|
||||
matches: Arc::new(Pool::new(Vec::new)),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use globset::{Glob, GlobBuilder, GlobSet, GlobSetBuilder};
|
||||
use regex_automata::dfa;
|
||||
use regex_automata::dfa::Automaton;
|
||||
use regex_automata::util::pool::Pool;
|
||||
use ruff_db::system::SystemPath;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR};
|
||||
use std::sync::Arc;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::glob::portable::AbsolutePortableGlobPattern;
|
||||
@@ -37,6 +39,8 @@ pub(crate) struct IncludeFilter {
|
||||
literal_pattern_indices: Box<[usize]>,
|
||||
#[get_size(size_fn = dfa_memory_usage)]
|
||||
dfa: Option<dfa::dense::DFA<Vec<u32>>>,
|
||||
#[get_size(ignore)]
|
||||
matches: Arc<Pool<Vec<usize>>>,
|
||||
}
|
||||
|
||||
#[allow(clippy::ref_option)]
|
||||
@@ -57,13 +61,14 @@ impl IncludeFilter {
|
||||
};
|
||||
}
|
||||
|
||||
let matches = self.glob_set.matches(path);
|
||||
let mut matches = self.matches.get();
|
||||
self.glob_set.matches_into(path, &mut matches);
|
||||
|
||||
if matches.is_empty() {
|
||||
MatchFile::No
|
||||
} else {
|
||||
for match_index in matches {
|
||||
if self.literal_pattern_indices.contains(&match_index) {
|
||||
for match_index in matches.iter() {
|
||||
if self.literal_pattern_indices.contains(match_index) {
|
||||
return MatchFile::Literal;
|
||||
}
|
||||
}
|
||||
@@ -291,6 +296,7 @@ impl IncludeFilterBuilder {
|
||||
dfa,
|
||||
literal_pattern_indices: self.literal_pattern_indices.into(),
|
||||
original_patterns: self.original_patterns.into(),
|
||||
matches: Arc::new(Pool::new(Vec::new)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1292,7 +1292,7 @@ pub struct AnalysisOptions {
|
||||
respect-type-ignore-comments = false
|
||||
"#
|
||||
)]
|
||||
respect_type_ignore_comments: Option<bool>,
|
||||
pub respect_type_ignore_comments: Option<bool>,
|
||||
}
|
||||
|
||||
impl AnalysisOptions {
|
||||
|
||||
@@ -359,6 +359,51 @@ reveal_type(GenericCircle[int].bar()) # revealed: GenericCircle[int]
|
||||
reveal_type(GenericCircle.baz(1)) # revealed: GenericShape[Literal[1]]
|
||||
```
|
||||
|
||||
### Calling `super()` in overridden methods with `Self` return type
|
||||
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/2122>.
|
||||
|
||||
When a child class overrides a parent method with a `Self` return type and calls `super().method()`,
|
||||
the return type should be the child's `Self` type variable, not the concrete child class type.
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class Parent:
|
||||
def copy(self) -> Self:
|
||||
return self
|
||||
|
||||
class Child(Parent):
|
||||
def copy(self) -> Self:
|
||||
result = super().copy()
|
||||
reveal_type(result) # revealed: Self@copy
|
||||
return result
|
||||
|
||||
# When called on concrete types, Self is substituted correctly.
|
||||
reveal_type(Child().copy()) # revealed: Child
|
||||
```
|
||||
|
||||
The same applies to classmethods with `Self` return types:
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class Parent:
|
||||
@classmethod
|
||||
def create(cls) -> Self:
|
||||
return cls()
|
||||
|
||||
class Child(Parent):
|
||||
@classmethod
|
||||
def create(cls) -> Self:
|
||||
result = super().create()
|
||||
reveal_type(result) # revealed: Self@create
|
||||
return result
|
||||
|
||||
# When called on concrete types, Self is substituted correctly.
|
||||
reveal_type(Child.create()) # revealed: Child
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
TODO: The use of `Self` to annotate the `next_node` attribute should be
|
||||
|
||||
@@ -117,3 +117,15 @@ def _():
|
||||
result = yield from retrieve().__await__()
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Un-annotated async functions
|
||||
|
||||
An `async def` with no annotated return type is still known to return `CoroutineType` of `Unknown`,
|
||||
not just `Unknown`:
|
||||
|
||||
```py
|
||||
async def f():
|
||||
pass
|
||||
|
||||
reveal_type(f()) # revealed: CoroutineType[Any, Any, Unknown]
|
||||
```
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -168,13 +168,13 @@ class A:
|
||||
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, B>
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Self@__init__>
|
||||
reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
|
||||
super().__init__(a)
|
||||
|
||||
@classmethod
|
||||
def f(cls):
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, <class 'B'>>
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, type[Self@f]>
|
||||
super().f()
|
||||
|
||||
super(B, B(42)).__init__(42)
|
||||
@@ -229,16 +229,16 @@ class Foo[T]:
|
||||
reveal_type(super())
|
||||
|
||||
def method4(self: Self):
|
||||
# revealed: <super: <class 'Foo'>, Foo[T@Foo]>
|
||||
# revealed: <super: <class 'Foo'>, Self@method4>
|
||||
reveal_type(super())
|
||||
|
||||
def method5[S: Foo[int]](self: S, other: S) -> S:
|
||||
# revealed: <super: <class 'Foo'>, Foo[int]>
|
||||
# revealed: <super: <class 'Foo'>, S@method5>
|
||||
reveal_type(super())
|
||||
return self
|
||||
|
||||
def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
|
||||
# revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
|
||||
# revealed: <super: <class 'Foo'>, S@method6> | <super: <class 'Foo'>, S@method6>
|
||||
reveal_type(super())
|
||||
return self
|
||||
|
||||
@@ -265,6 +265,19 @@ class Foo[T]:
|
||||
# revealed: Unknown
|
||||
reveal_type(super())
|
||||
return self
|
||||
# TypeVar bounded by `type[Foo]` rather than `Foo`
|
||||
# TODO: Should error on signature - `self` is annotated as a class type, not an instance type
|
||||
def method11[S: type[Foo[int]]](self: S, other: S) -> S:
|
||||
# Delegates to the bound to resolve the super type
|
||||
reveal_type(super()) # revealed: <super: <class 'Foo'>, <class 'Foo[int]'>>
|
||||
return self
|
||||
# TypeVar bounded by `type[Foo]`, used in `type[T]` position
|
||||
# TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass
|
||||
# Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed
|
||||
@classmethod
|
||||
def method12[S: type[Foo[int]]](cls: type[S]) -> S:
|
||||
reveal_type(super()) # revealed: <super: <class 'Foo'>, Unknown>
|
||||
raise NotImplementedError
|
||||
|
||||
type Alias = Bar
|
||||
|
||||
@@ -359,15 +372,15 @@ from __future__ import annotations
|
||||
|
||||
class A:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: <class 'A'>, A>
|
||||
reveal_type(super()) # revealed: <super: <class 'A'>, Self@test>
|
||||
|
||||
class B:
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, B>
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, Self@test>
|
||||
|
||||
class C(A.B):
|
||||
def test(self):
|
||||
reveal_type(super()) # revealed: <super: <class 'C'>, C>
|
||||
reveal_type(super()) # revealed: <super: <class 'C'>, Self@test>
|
||||
|
||||
def inner(t: C):
|
||||
reveal_type(super()) # revealed: <super: <class 'B'>, C>
|
||||
@@ -645,7 +658,7 @@ class A:
|
||||
class B(A):
|
||||
def __init__(self, a: int):
|
||||
super().__init__(a)
|
||||
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
|
||||
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, Self@__init__>` has no attribute `a`"
|
||||
super().a
|
||||
|
||||
# error: [unresolved-attribute] "Object of type `<super: <class 'B'>, B>` has no attribute `a`"
|
||||
@@ -670,3 +683,58 @@ reveal_type(super(B, B()).__getitem__) # revealed: bound method B.__getitem__(k
|
||||
# error: [not-subscriptable] "Cannot subscript object of type `<super: <class 'B'>, B>` with no `__getitem__` method"
|
||||
super(B, B())[0]
|
||||
```
|
||||
|
||||
## Subclass Using Concrete Type Instead of `Self`
|
||||
|
||||
When a parent class uses `Self` in a parameter type and a subclass overrides it with a concrete
|
||||
type, passing that parameter to `super().__init__()` is a type error. This is because `Self` in the
|
||||
parent could represent a further subclass. The fix is to use `Self` consistently in the subclass.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
from typing import Self
|
||||
|
||||
class Parent:
|
||||
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
|
||||
self.children = children
|
||||
|
||||
class Child(Parent):
|
||||
def __init__(self, children: Mapping[str, Child] | None = None) -> None:
|
||||
# error: [invalid-argument-type] "Argument to bound method `__init__` is incorrect: Expected `Mapping[str, Self@__init__] | None`, found `Mapping[str, Child] | None`"
|
||||
super().__init__(children)
|
||||
|
||||
# The fix is to use `Self` consistently in the subclass:
|
||||
|
||||
class Parent2:
|
||||
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
|
||||
self.children = children
|
||||
|
||||
class Child2(Parent2):
|
||||
def __init__(self, children: Mapping[str, Self] | None = None) -> None:
|
||||
super().__init__(children) # OK
|
||||
```
|
||||
|
||||
## Super in Protocol Classes
|
||||
|
||||
Using `super()` in a class that inherits from `typing.Protocol` (similar to beartype's caching
|
||||
Protocol):
|
||||
|
||||
```py
|
||||
from typing import Protocol, Generic, TypeVar
|
||||
|
||||
_T_co = TypeVar("_T_co", covariant=True)
|
||||
|
||||
class MyProtocol(Protocol, Generic[_T_co]):
|
||||
def __class_getitem__(cls, item):
|
||||
# Accessing parent's __class_getitem__ through super()
|
||||
reveal_type(super()) # revealed: <super: <class 'MyProtocol'>, type[Self@__class_getitem__]>
|
||||
parent_method = super().__class_getitem__
|
||||
reveal_type(parent_method) # revealed: @Todo(super in generic class)
|
||||
return parent_method(item)
|
||||
```
|
||||
|
||||
@@ -1032,4 +1032,125 @@ reveal_type(asdict(p)) # revealed: dict[str, Any]
|
||||
reveal_type(replace(p, name="Bob")) # revealed: Person
|
||||
```
|
||||
|
||||
## Calling decorator function directly with a class argument
|
||||
|
||||
When a function decorated with `@dataclass_transform()` is called directly with a class argument
|
||||
(not used as a decorator), it should return the class with the dataclass transformation applied.
|
||||
|
||||
### Basic case
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
B = my_dataclass(A)
|
||||
|
||||
reveal_type(B) # revealed: <class 'A'>
|
||||
|
||||
B(1)
|
||||
```
|
||||
|
||||
### Function with additional parameters
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T], *, order: bool = False) -> type[T]:
|
||||
return cls
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
B = my_dataclass(A, order=True)
|
||||
|
||||
reveal_type(B) # revealed: <class 'A'>
|
||||
|
||||
reveal_type(B(1) < B(2)) # revealed: bool
|
||||
```
|
||||
|
||||
### Overloaded decorator function
|
||||
|
||||
When the decorator function has overloads (one for direct class application, one for returning a
|
||||
decorator), calling it with a class should return the class type.
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform, Callable, overload
|
||||
|
||||
@overload
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T]) -> type[T]: ...
|
||||
@overload
|
||||
def my_dataclass[T]() -> Callable[[type[T]], type[T]]: ...
|
||||
def my_dataclass[T](cls: type[T] | None = None) -> type[T] | Callable[[type[T]], type[T]]:
|
||||
raise NotImplementedError
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
B = my_dataclass(A)
|
||||
|
||||
reveal_type(B) # revealed: <class 'A'>
|
||||
|
||||
B(1)
|
||||
```
|
||||
|
||||
### Passing a specialized generic class
|
||||
|
||||
When calling a `@dataclass_transform()` decorated function with a specialized generic class, the
|
||||
specialization should be preserved.
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def my_dataclass[T](cls: type[T]) -> type[T]:
|
||||
return cls
|
||||
|
||||
class A[T]:
|
||||
x: T
|
||||
|
||||
B = my_dataclass(A[int])
|
||||
|
||||
reveal_type(B) # revealed: <class 'A[int]'>
|
||||
|
||||
B(1)
|
||||
```
|
||||
|
||||
### Decorator factory with class parameter
|
||||
|
||||
When a `@dataclass_transform()` decorated function takes a class as a parameter but is used as a
|
||||
decorator factory (returns a decorator), the dataclass behavior should be applied to the decorated
|
||||
class, not to the parameter class.
|
||||
|
||||
```py
|
||||
from typing_extensions import dataclass_transform
|
||||
|
||||
@dataclass_transform()
|
||||
def hydrated_dataclass[T](target: type[T], *, frozen: bool = False):
|
||||
def decorator[U](cls: type[U]) -> type[U]:
|
||||
return cls
|
||||
return decorator
|
||||
|
||||
class Target:
|
||||
pass
|
||||
|
||||
decorator = hydrated_dataclass(Target)
|
||||
reveal_type(decorator) # revealed: <decorator produced by dataclass-like function>
|
||||
|
||||
@hydrated_dataclass(Target)
|
||||
class Model:
|
||||
x: int
|
||||
|
||||
# Model should be a dataclass-like class with x as a field
|
||||
Model(x=1)
|
||||
reveal_type(Model.__init__) # revealed: (self: Model, x: int) -> None
|
||||
```
|
||||
|
||||
[`typing.dataclass_transform`]: https://docs.python.org/3/library/typing.html#typing.dataclass_transform
|
||||
|
||||
@@ -1682,9 +1682,7 @@ def sequence4(cls: type) -> type:
|
||||
class Foo: ...
|
||||
|
||||
ordered_foo = dataclass(order=True)(Foo)
|
||||
reveal_type(ordered_foo) # revealed: type[Foo] & Any
|
||||
# TODO: should be `Foo & Any`
|
||||
reveal_type(ordered_foo()) # revealed: @Todo(Type::Intersection.call)
|
||||
# TODO: should be `Any`
|
||||
reveal_type(ordered_foo() < ordered_foo()) # revealed: @Todo(Type::Intersection.call)
|
||||
reveal_type(ordered_foo) # revealed: <class 'Foo'>
|
||||
reveal_type(ordered_foo()) # revealed: Foo
|
||||
reveal_type(ordered_foo() < ordered_foo()) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -244,3 +244,36 @@ reveal_type(n1 > n2) # revealed: bool
|
||||
n1 <= n2 # error: [unsupported-operator]
|
||||
n1 >= n2 # error: [unsupported-operator]
|
||||
```
|
||||
|
||||
## Function call form
|
||||
|
||||
When `total_ordering` is called as a function (not as a decorator), the same validation is
|
||||
performed:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
class NoOrderingMethod:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
# error: [invalid-total-ordering]
|
||||
InvalidOrderedClass = total_ordering(NoOrderingMethod)
|
||||
```
|
||||
|
||||
When the class does define an ordering method, no error is emitted:
|
||||
|
||||
```py
|
||||
from functools import total_ordering
|
||||
|
||||
class HasOrderingMethod:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
return True
|
||||
|
||||
def __lt__(self, other: "HasOrderingMethod") -> bool:
|
||||
return True
|
||||
|
||||
# No error (class defines `__lt__`).
|
||||
ValidOrderedClass = total_ordering(HasOrderingMethod)
|
||||
reveal_type(ValidOrderedClass) # revealed: type[HasOrderingMethod]
|
||||
```
|
||||
|
||||
@@ -949,3 +949,29 @@ class Builder(Generic[TMsg]):
|
||||
def _handler(self, stream: Stream[Msg]) -> Stream[Msg]:
|
||||
return stream
|
||||
```
|
||||
|
||||
## Regressions
|
||||
|
||||
### Only consider fully static types as pivots for transitivity
|
||||
|
||||
This is a regression test for [ty#2371]. When working with constraint sets, we track transitive
|
||||
relationships between the constraints in the set. For instance, in `S ≤ int ∧ int ≤ T`, we can infer
|
||||
that `S ≤ T`. However, we should only consider fully static types when looking for a "pivot" for
|
||||
this kind of transitive relationship. The same pattern does not hold for `S ≤ Any ∧ Any ≤ T`;
|
||||
because the two `Any`s can materialize to different types, we cannot infer that `S ≤ T`.
|
||||
|
||||
We have lower level tests of this in [`type_properties/implies_subtype_of.md`][implies_subtype_of].
|
||||
`functools.reduce` has a signature that exercises this behavior, as well, so we also include this
|
||||
regression test.
|
||||
|
||||
```py
|
||||
from functools import reduce
|
||||
|
||||
def _(keys: list[str]):
|
||||
# TODO: revealed: int
|
||||
# revealed: Unknown | Literal[0]
|
||||
reveal_type(reduce(lambda total, k: total + len(k), keys, 0))
|
||||
```
|
||||
|
||||
[implies_subtype_of]: ../../type_properties/implies_subtype_of.md
|
||||
[ty#2371]: https://github.com/astral-sh/ty/issues/2371
|
||||
|
||||
@@ -783,3 +783,20 @@ class Container[**P]:
|
||||
# error: [invalid-argument-type] "Argument to bound method `method` is incorrect: Expected `(**P@Container) -> None`, found `(**Q@try_assign) -> None`"
|
||||
return self.method(f)
|
||||
```
|
||||
|
||||
## `ParamSpec` inference with un-annotated return type
|
||||
|
||||
Regression test for an issue where `ParamSpec` inference failed when the callable we were inferring
|
||||
from did not have an annotated return type.
|
||||
|
||||
```py
|
||||
from typing import Callable
|
||||
|
||||
def infer_paramspec[**P](func: Callable[P, None]) -> Callable[P, None]:
|
||||
return func
|
||||
|
||||
def f(x: int, y: str):
|
||||
pass
|
||||
|
||||
reveal_type(infer_paramspec(f)) # revealed: (x: int, y: str) -> None
|
||||
```
|
||||
|
||||
@@ -361,6 +361,52 @@ def f(union: A | B):
|
||||
static_assert(not has_member(union, "only_on_b"))
|
||||
```
|
||||
|
||||
Unless one of the elements of the union is `Any`, thus making it dynamic. In which case, we consider
|
||||
items on the intersection of the non-`Any` elements:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import has_member, static_assert
|
||||
|
||||
class A:
|
||||
on_both: int = 1
|
||||
only_on_a: str = "a"
|
||||
|
||||
class B:
|
||||
on_both: int = 2
|
||||
only_on_b: str = "b"
|
||||
|
||||
def f(union: Any | A):
|
||||
static_assert(has_member(union, "on_both"))
|
||||
static_assert(has_member(union, "only_on_a"))
|
||||
|
||||
def g(union: Any | 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"))
|
||||
```
|
||||
|
||||
Similarly, unioning with an intersection involving `Any` is treated the same as if it was just
|
||||
unioned with `Any`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import Intersection, has_member, static_assert
|
||||
|
||||
class A:
|
||||
on_both: int = 1
|
||||
only_on_a: str = "a"
|
||||
|
||||
class B:
|
||||
on_both: int = 2
|
||||
only_on_b: str = "b"
|
||||
|
||||
def f(x: Intersection[Any, A] | B):
|
||||
static_assert(has_member(x, "on_both"))
|
||||
static_assert(not has_member(x, "only_on_a"))
|
||||
static_assert(has_member(x, "only_on_b"))
|
||||
```
|
||||
|
||||
### Intersections
|
||||
|
||||
#### Only positive types
|
||||
|
||||
@@ -140,7 +140,11 @@ If a child class's method definition is Liskov-compatible with the method defini
|
||||
class, Liskov compatibility must also nonetheless be checked with respect to the method definition
|
||||
on its grandparent class. This is because type checkers will treat the child class as a subtype of
|
||||
the grandparent class just as much as they treat it as a subtype of the parent class, so
|
||||
substitutability with respect to the grandparent class is just as important:
|
||||
substitutability with respect to the grandparent class is just as important.
|
||||
|
||||
However, if the parent class itself already has an LSP violation with an ancestor, we do not report
|
||||
the same violation for the child class. This is because the child class cannot fix the violation
|
||||
without introducing a new, worse violation against its immediate parent's contract.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
@@ -156,13 +160,31 @@ class Parent(Grandparent):
|
||||
def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class Child(Parent):
|
||||
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
|
||||
def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
# compatible with the signature of `Parent.method`, but not with `Grandparent.method`.
|
||||
# However, since `Parent.method` already violates LSP with `Grandparent.method`,
|
||||
# we don't report the same violation for `Child` -- it's inherited from `Parent`.
|
||||
def method(self, x: str) -> None: ...
|
||||
|
||||
class OtherChild(Parent):
|
||||
# compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
|
||||
def method(self, x: int) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class ChildWithNewViolation(Parent):
|
||||
# incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
|
||||
# We report the violation against the immediate parent (`Parent`), not the grandparent.
|
||||
def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
|
||||
|
||||
class GrandparentWithReturnType:
|
||||
def method(self) -> int: ...
|
||||
|
||||
class ParentWithReturnType(GrandparentWithReturnType):
|
||||
def method(self) -> str: ... # error: [invalid-method-override]
|
||||
|
||||
class ChildWithReturnType(ParentWithReturnType):
|
||||
# Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
|
||||
# but not with `ParentWithReturnType.method`. We report against the immediate parent.
|
||||
def method(self) -> int: ... # error: [invalid-method-override]
|
||||
|
||||
class GradualParent(Grandparent):
|
||||
def method(self, x: Any) -> None: ...
|
||||
|
||||
@@ -190,8 +212,9 @@ class C(B):
|
||||
foo = get
|
||||
|
||||
class D(C):
|
||||
# compatible with `C.get` and `B.get`, but not with `A.get`
|
||||
def get(self, my_default): ... # error: [invalid-method-override]
|
||||
# compatible with `C.get` and `B.get`, but not with `A.get`.
|
||||
# Since `B.get` already violates LSP with `A.get`, we don't report for `D`.
|
||||
def get(self, my_default): ...
|
||||
```
|
||||
|
||||
## Non-generic methods on generic classes work as expected
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Tuple pair is assignable to their union
|
||||
# Disjointness of two types implies that neither is a subtype of the other
|
||||
|
||||
Regression test for <https://github.com/astral-sh/ty/issues/2236>.
|
||||
This is a regression test for <https://github.com/astral-sh/ty/issues/2236>.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
|
||||
@@ -22,21 +22,39 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
8 |
|
||||
9 | class Child(Parent):
|
||||
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
|
||||
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
12 |
|
||||
13 | class OtherChild(Parent):
|
||||
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
|
||||
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
|
||||
16 |
|
||||
17 | class GradualParent(Grandparent):
|
||||
18 | def method(self, x: Any) -> None: ...
|
||||
19 |
|
||||
20 | class ThirdChild(GradualParent):
|
||||
21 | # `GradualParent.method` is compatible with the signature of `Grandparent.method`,
|
||||
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
|
||||
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
|
||||
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`.
|
||||
11 | # However, since `Parent.method` already violates LSP with `Grandparent.method`,
|
||||
12 | # we don't report the same violation for `Child` -- it's inherited from `Parent`.
|
||||
13 | def method(self, x: str) -> None: ...
|
||||
14 |
|
||||
15 | class OtherChild(Parent):
|
||||
16 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
|
||||
17 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
|
||||
18 |
|
||||
19 | class ChildWithNewViolation(Parent):
|
||||
20 | # incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
|
||||
21 | # We report the violation against the immediate parent (`Parent`), not the grandparent.
|
||||
22 | def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
|
||||
23 |
|
||||
24 | class GrandparentWithReturnType:
|
||||
25 | def method(self) -> int: ...
|
||||
26 |
|
||||
27 | class ParentWithReturnType(GrandparentWithReturnType):
|
||||
28 | def method(self) -> str: ... # error: [invalid-method-override]
|
||||
29 |
|
||||
30 | class ChildWithReturnType(ParentWithReturnType):
|
||||
31 | # Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
|
||||
32 | # but not with `ParentWithReturnType.method`. We report against the immediate parent.
|
||||
33 | def method(self) -> int: ... # error: [invalid-method-override]
|
||||
34 |
|
||||
35 | class GradualParent(Grandparent):
|
||||
36 | def method(self, x: Any) -> None: ...
|
||||
37 |
|
||||
38 | class ThirdChild(GradualParent):
|
||||
39 | # `GradualParent.method` is compatible with the signature of `Grandparent.method`,
|
||||
40 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
|
||||
41 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
|
||||
42 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
```
|
||||
|
||||
## other_stub.pyi
|
||||
@@ -56,8 +74,9 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/liskov.md
|
||||
12 | foo = get
|
||||
13 |
|
||||
14 | class D(C):
|
||||
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
|
||||
16 | def get(self, my_default): ... # error: [invalid-method-override]
|
||||
15 | # compatible with `C.get` and `B.get`, but not with `A.get`.
|
||||
16 | # Since `B.get` already violates LSP with `A.get`, we don't report for `D`.
|
||||
17 | def get(self, my_default): ...
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
@@ -83,38 +102,14 @@ info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/stub.pyi:11:9
|
||||
--> src/stub.pyi:17:9
|
||||
|
|
||||
9 | class Child(Parent):
|
||||
10 | # compatible with the signature of `Parent.method`, but not with `Grandparent.method`:
|
||||
11 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
|
||||
12 |
|
||||
13 | class OtherChild(Parent):
|
||||
|
|
||||
::: src/stub.pyi:4:9
|
||||
|
|
||||
3 | class Grandparent:
|
||||
4 | def method(self, x: int) -> None: ...
|
||||
| ---------------------------- `Grandparent.method` defined here
|
||||
5 |
|
||||
6 | class Parent(Grandparent):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/stub.pyi:15:9
|
||||
|
|
||||
13 | class OtherChild(Parent):
|
||||
14 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
|
||||
15 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
|
||||
15 | class OtherChild(Parent):
|
||||
16 | # compatible with the signature of `Grandparent.method`, but not with `Parent.method`:
|
||||
17 | def method(self, x: int) -> None: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method`
|
||||
16 |
|
||||
17 | class GradualParent(Grandparent):
|
||||
18 |
|
||||
19 | class ChildWithNewViolation(Parent):
|
||||
|
|
||||
::: src/stub.pyi:7:9
|
||||
|
|
||||
@@ -131,11 +126,75 @@ info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/stub.pyi:24:9
|
||||
--> src/stub.pyi:22:9
|
||||
|
|
||||
22 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
|
||||
23 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
|
||||
24 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
20 | # incompatible with BOTH `Parent.method` (str) and `Grandparent.method` (int).
|
||||
21 | # We report the violation against the immediate parent (`Parent`), not the grandparent.
|
||||
22 | def method(self, x: bytes) -> None: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Parent.method`
|
||||
23 |
|
||||
24 | class GrandparentWithReturnType:
|
||||
|
|
||||
::: src/stub.pyi:7:9
|
||||
|
|
||||
6 | class Parent(Grandparent):
|
||||
7 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
| ---------------------------- `Parent.method` defined here
|
||||
8 |
|
||||
9 | class Child(Parent):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/stub.pyi:25:9
|
||||
|
|
||||
24 | class GrandparentWithReturnType:
|
||||
25 | def method(self) -> int: ...
|
||||
| ------------------- `GrandparentWithReturnType.method` defined here
|
||||
26 |
|
||||
27 | class ParentWithReturnType(GrandparentWithReturnType):
|
||||
28 | def method(self) -> str: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `GrandparentWithReturnType.method`
|
||||
29 |
|
||||
30 | class ChildWithReturnType(ParentWithReturnType):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/stub.pyi:28:9
|
||||
|
|
||||
27 | class ParentWithReturnType(GrandparentWithReturnType):
|
||||
28 | def method(self) -> str: ... # error: [invalid-method-override]
|
||||
| ------------------- `ParentWithReturnType.method` defined here
|
||||
29 |
|
||||
30 | class ChildWithReturnType(ParentWithReturnType):
|
||||
31 | # Returns `int` again -- compatible with `GrandparentWithReturnType.method`,
|
||||
32 | # but not with `ParentWithReturnType.method`. We report against the immediate parent.
|
||||
33 | def method(self) -> int: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `ParentWithReturnType.method`
|
||||
34 |
|
||||
35 | class GradualParent(Grandparent):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `method`
|
||||
--> src/stub.pyi:42:9
|
||||
|
|
||||
40 | # and `ThirdChild.method` is compatible with the signature of `GradualParent.method`,
|
||||
41 | # but `ThirdChild.method` is not compatible with the signature of `Grandparent.method`
|
||||
42 | def method(self, x: str) -> None: ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `Grandparent.method`
|
||||
|
|
||||
::: src/stub.pyi:4:9
|
||||
@@ -169,25 +228,3 @@ info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-method-override]: Invalid override of method `get`
|
||||
--> src/other_stub.pyi:16:9
|
||||
|
|
||||
14 | class D(C):
|
||||
15 | # compatible with `C.get` and `B.get`, but not with `A.get`
|
||||
16 | def get(self, my_default): ... # error: [invalid-method-override]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Definition is incompatible with `A.get`
|
||||
|
|
||||
::: src/other_stub.pyi:2:9
|
||||
|
|
||||
1 | class A:
|
||||
2 | def get(self, default): ...
|
||||
| ------------------ `A.get` defined here
|
||||
3 |
|
||||
4 | class B(A):
|
||||
|
|
||||
info: This violates the Liskov Substitution Principle
|
||||
info: rule `invalid-method-override` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
@@ -304,7 +304,7 @@ info: rule `duplicate-base` is enabled by default
|
||||
```
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused blanket `type: ignore` directive
|
||||
warning[unused-ignore-comment]: Unused blanket `type: ignore` directive
|
||||
--> src/mdtest_snippet.py:72:9
|
||||
|
|
||||
70 | A,
|
||||
@@ -356,7 +356,7 @@ info: rule `duplicate-base` is enabled by default
|
||||
```
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused blanket `type: ignore` directive
|
||||
warning[unused-ignore-comment]: Unused blanket `type: ignore` directive
|
||||
--> src/mdtest_snippet.py:81:13
|
||||
|
|
||||
79 | ):
|
||||
|
||||
@@ -22,13 +22,13 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
|
||||
7 |
|
||||
8 | class B(A):
|
||||
9 | def __init__(self, a: int):
|
||||
10 | reveal_type(super()) # revealed: <super: <class 'B'>, B>
|
||||
10 | reveal_type(super()) # revealed: <super: <class 'B'>, Self@__init__>
|
||||
11 | reveal_type(super(object, super())) # revealed: <super: <class 'object'>, super>
|
||||
12 | super().__init__(a)
|
||||
13 |
|
||||
14 | @classmethod
|
||||
15 | def f(cls):
|
||||
16 | reveal_type(super()) # revealed: <super: <class 'B'>, <class 'B'>>
|
||||
16 | reveal_type(super()) # revealed: <super: <class 'B'>, type[Self@f]>
|
||||
17 | super().f()
|
||||
18 |
|
||||
19 | super(B, B(42)).__init__(42)
|
||||
@@ -78,16 +78,16 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
|
||||
63 | reveal_type(super())
|
||||
64 |
|
||||
65 | def method4(self: Self):
|
||||
66 | # revealed: <super: <class 'Foo'>, Foo[T@Foo]>
|
||||
66 | # revealed: <super: <class 'Foo'>, Self@method4>
|
||||
67 | reveal_type(super())
|
||||
68 |
|
||||
69 | def method5[S: Foo[int]](self: S, other: S) -> S:
|
||||
70 | # revealed: <super: <class 'Foo'>, Foo[int]>
|
||||
70 | # revealed: <super: <class 'Foo'>, S@method5>
|
||||
71 | reveal_type(super())
|
||||
72 | return self
|
||||
73 |
|
||||
74 | def method6[S: (Foo[int], Foo[str])](self: S, other: S) -> S:
|
||||
75 | # revealed: <super: <class 'Foo'>, Foo[int]> | <super: <class 'Foo'>, Foo[str]>
|
||||
75 | # revealed: <super: <class 'Foo'>, S@method6> | <super: <class 'Foo'>, S@method6>
|
||||
76 | reveal_type(super())
|
||||
77 | return self
|
||||
78 |
|
||||
@@ -114,35 +114,48 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/class/super.md
|
||||
99 | # revealed: Unknown
|
||||
100 | reveal_type(super())
|
||||
101 | return self
|
||||
102 |
|
||||
103 | type Alias = Bar
|
||||
104 |
|
||||
105 | class Bar:
|
||||
106 | def method(self: Alias):
|
||||
107 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
108 | reveal_type(super())
|
||||
109 |
|
||||
110 | def pls_dont_call_me(self: Never):
|
||||
111 | # revealed: <super: <class 'Bar'>, Unknown>
|
||||
112 | reveal_type(super())
|
||||
113 |
|
||||
114 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
|
||||
115 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
116 | reveal_type(super())
|
||||
102 | # TypeVar bounded by `type[Foo]` rather than `Foo`
|
||||
103 | # TODO: Should error on signature - `self` is annotated as a class type, not an instance type
|
||||
104 | def method11[S: type[Foo[int]]](self: S, other: S) -> S:
|
||||
105 | # Delegates to the bound to resolve the super type
|
||||
106 | reveal_type(super()) # revealed: <super: <class 'Foo'>, <class 'Foo[int]'>>
|
||||
107 | return self
|
||||
108 | # TypeVar bounded by `type[Foo]`, used in `type[T]` position
|
||||
109 | # TODO: Should error on signature - `cls` would be `type[type[Foo[int]]]`, a metaclass
|
||||
110 | # Delegates to `type[Unknown]` since `type[type[Foo[int]]]` can't be constructed
|
||||
111 | @classmethod
|
||||
112 | def method12[S: type[Foo[int]]](cls: type[S]) -> S:
|
||||
113 | reveal_type(super()) # revealed: <super: <class 'Foo'>, Unknown>
|
||||
114 | raise NotImplementedError
|
||||
115 |
|
||||
116 | type Alias = Bar
|
||||
117 |
|
||||
118 | class P(Protocol):
|
||||
119 | def method(self: P):
|
||||
120 | # revealed: <super: <class 'P'>, P>
|
||||
118 | class Bar:
|
||||
119 | def method(self: Alias):
|
||||
120 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
121 | reveal_type(super())
|
||||
122 |
|
||||
123 | class E(enum.Enum):
|
||||
124 | X = 1
|
||||
125 |
|
||||
126 | def method(self: E):
|
||||
127 | match self:
|
||||
128 | case E.X:
|
||||
129 | # revealed: <super: <class 'E'>, E>
|
||||
130 | reveal_type(super())
|
||||
123 | def pls_dont_call_me(self: Never):
|
||||
124 | # revealed: <super: <class 'Bar'>, Unknown>
|
||||
125 | reveal_type(super())
|
||||
126 |
|
||||
127 | def only_call_me_on_callable_subclasses(self: Intersection[Bar, Callable[..., object]]):
|
||||
128 | # revealed: <super: <class 'Bar'>, Bar>
|
||||
129 | reveal_type(super())
|
||||
130 |
|
||||
131 | class P(Protocol):
|
||||
132 | def method(self: P):
|
||||
133 | # revealed: <super: <class 'P'>, P>
|
||||
134 | reveal_type(super())
|
||||
135 |
|
||||
136 | class E(enum.Enum):
|
||||
137 | X = 1
|
||||
138 |
|
||||
139 | def method(self: E):
|
||||
140 | match self:
|
||||
141 | case E.X:
|
||||
142 | # revealed: <super: <class 'E'>, E>
|
||||
143 | reveal_type(super())
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
@@ -205,6 +218,7 @@ error[invalid-super-argument]: `S@method10` is a type variable with an abstract/
|
||||
100 | reveal_type(super())
|
||||
| ^^^^^^^
|
||||
101 | return self
|
||||
102 | # TypeVar bounded by `type[Foo]` rather than `Foo`
|
||||
|
|
||||
info: Type variable `S` has upper bound `(...) -> str`
|
||||
info: rule `invalid-super-argument` is enabled by default
|
||||
|
||||
@@ -27,7 +27,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/ty_ignore.m
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||
--> src/mdtest_snippet.py:2:13
|
||||
|
|
||||
1 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive"
|
||||
@@ -47,7 +47,7 @@ help: Remove the unused suppression comment
|
||||
```
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment'
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment'
|
||||
--> src/mdtest_snippet.py:6:26
|
||||
|
|
||||
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||
@@ -70,7 +70,7 @@ help: Remove the unused suppression code
|
||||
```
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-reference'
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'unresolved-reference'
|
||||
--> src/mdtest_snippet.py:6:64
|
||||
|
|
||||
4 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment'"
|
||||
@@ -93,7 +93,7 @@ help: Remove the unused suppression code
|
||||
```
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'
|
||||
--> src/mdtest_snippet.py:9:26
|
||||
|
|
||||
8 | # error: [unused-ignore-comment] "Unused `ty: ignore` directive: 'invalid-assignment', 'unresolved-reference'"
|
||||
|
||||
@@ -32,7 +32,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/suppressions/type_ignore
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||
--> src/mdtest_snippet.py:10:9
|
||||
|
|
||||
8 | a = (3
|
||||
@@ -55,7 +55,7 @@ help: Remove the unused suppression comment
|
||||
```
|
||||
|
||||
```
|
||||
info[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||
warning[unused-ignore-comment]: Unused `ty: ignore` directive
|
||||
--> src/mdtest_snippet.py:14:21
|
||||
|
|
||||
12 | a = (3
|
||||
|
||||
@@ -537,4 +537,43 @@ def identity2[T](t: T) -> T:
|
||||
return t
|
||||
```
|
||||
|
||||
## Transitivity
|
||||
|
||||
### Transitivity can propagate across typevars
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
def concrete_pivot[T, U]():
|
||||
# If [int ≤ T ∧ T ≤ U], then [int ≤ U] must be true as well.
|
||||
constraints = ConstraintSet.range(int, T, object) & ConstraintSet.range(T, U, object)
|
||||
static_assert(constraints.implies_subtype_of(int, U))
|
||||
```
|
||||
|
||||
### Transitivity can propagate across fully static concrete types
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
def concrete_pivot[T, U]():
|
||||
# If [T ≤ int ∧ int ≤ U], then [T ≤ U] must be true as well.
|
||||
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(int, U, object)
|
||||
static_assert(constraints.implies_subtype_of(T, U))
|
||||
```
|
||||
|
||||
### Transitivity cannot propagate across non-fully-static concrete types
|
||||
|
||||
```py
|
||||
from typing import Any, Never
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
def concrete_pivot[T, U]():
|
||||
# If [T ≤ Any ∧ Any ≤ U], then the two `Any`s might materialize to different types. That means
|
||||
# [T ≤ U] is NOT necessarily true.
|
||||
constraints = ConstraintSet.range(Never, T, Any) & ConstraintSet.range(Any, U, object)
|
||||
static_assert(not constraints.implies_subtype_of(T, U))
|
||||
```
|
||||
|
||||
[subtyping]: https://typing.python.org/en/latest/spec/concepts.html#subtype-supertype-and-type-equivalence
|
||||
|
||||
@@ -736,6 +736,29 @@ static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal[""]]], No
|
||||
static_assert(is_assignable_to(Intersection[LiteralString, Not[Literal["", "a"]]], Not[AlwaysFalsy]))
|
||||
```
|
||||
|
||||
## Callable types with Unknown/missing return type
|
||||
|
||||
See <https://github.com/astral-sh/ty/issues/2363>, a property test failure involving
|
||||
`~type & ~((...) -> Unknown)` not being assignable to `~type`. Since `~type & ~Callable` is a subset
|
||||
of `~type`, the intersection should be assignable to `~type`.
|
||||
|
||||
The root cause was that we failed to properly materialize a `Callable[..., Unknown]` type when the
|
||||
`Unknown` return type originated from a missing annotation.
|
||||
|
||||
```py
|
||||
from ty_extensions import static_assert, is_assignable_to, Intersection, Not, Unknown, CallableTypeOf
|
||||
from typing import Callable
|
||||
|
||||
# `Callable[..., Unknown]` has explicit Unknown return type
|
||||
static_assert(is_assignable_to(Intersection[Not[type], Not[Callable[..., Unknown]]], Not[type]))
|
||||
|
||||
# Function with no return annotation (has implicit Unknown return type internally)
|
||||
def no_return_annotation(*args, **kwargs): ...
|
||||
|
||||
# `CallableTypeOf[no_return_annotation]` has `returns: None` internally (no annotation)
|
||||
static_assert(is_assignable_to(Intersection[Not[type], Not[CallableTypeOf[no_return_annotation]]], Not[type]))
|
||||
```
|
||||
|
||||
## Intersections with non-fully-static negated elements
|
||||
|
||||
A type can be _assignable_ to an intersection containing negated elements only if the _bottom_
|
||||
|
||||
@@ -36,6 +36,7 @@ com2ann
|
||||
comtypes
|
||||
core
|
||||
cpython
|
||||
cryptography
|
||||
cwltool
|
||||
dacite
|
||||
dd-trace-py
|
||||
@@ -79,6 +80,7 @@ mypy-protobuf
|
||||
mypy_primer
|
||||
nionutils
|
||||
nox
|
||||
numpy-stl
|
||||
openlibrary
|
||||
operator
|
||||
optuna
|
||||
|
||||
@@ -156,9 +156,10 @@ impl<'db> DefinedPlace<'db> {
|
||||
/// bound_or_declared: Place::Defined(DefinedPlace { ty: Literal[1], origin: TypeOrigin::Inferred, definedness: Definedness::PossiblyUndefined, .. }),
|
||||
/// non_existent: Place::Undefined,
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub(crate) enum Place<'db> {
|
||||
Defined(DefinedPlace<'db>),
|
||||
#[default]
|
||||
Undefined,
|
||||
}
|
||||
|
||||
@@ -592,6 +593,7 @@ type DeclaredTypeAndConflictingTypes<'db> = (
|
||||
);
|
||||
|
||||
/// The result of looking up a declared type from declarations; see [`place_from_declarations`].
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct PlaceFromDeclarationsResult<'db> {
|
||||
place_and_quals: PlaceAndQualifiers<'db>,
|
||||
conflicting_types: Option<Box<indexmap::set::Slice<Type<'db>>>>,
|
||||
@@ -641,21 +643,12 @@ impl<'db> PlaceFromDeclarationsResult<'db> {
|
||||
/// that this comes with a [`CLASS_VAR`] type qualifier.
|
||||
///
|
||||
/// [`CLASS_VAR`]: crate::types::TypeQualifiers::CLASS_VAR
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
||||
pub(crate) struct PlaceAndQualifiers<'db> {
|
||||
pub(crate) place: Place<'db>,
|
||||
pub(crate) qualifiers: TypeQualifiers,
|
||||
}
|
||||
|
||||
impl Default for PlaceAndQualifiers<'_> {
|
||||
fn default() -> Self {
|
||||
PlaceAndQualifiers {
|
||||
place: Place::Undefined,
|
||||
qualifiers: TypeQualifiers::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> PlaceAndQualifiers<'db> {
|
||||
/// Constructor that creates a [`PlaceAndQualifiers`] instance with a [`TodoType`] type
|
||||
/// and no qualifiers.
|
||||
@@ -669,10 +662,7 @@ impl<'db> PlaceAndQualifiers<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn unbound() -> Self {
|
||||
PlaceAndQualifiers {
|
||||
place: Place::Undefined,
|
||||
qualifiers: TypeQualifiers::empty(),
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn is_undefined(&self) -> bool {
|
||||
@@ -1567,11 +1557,7 @@ fn place_from_declarations_impl<'db>(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
PlaceFromDeclarationsResult {
|
||||
place_and_quals: Place::Undefined.into(),
|
||||
conflicting_types: None,
|
||||
first_declaration: None,
|
||||
}
|
||||
PlaceFromDeclarationsResult::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1671,7 +1657,7 @@ mod implicit_globals {
|
||||
Place::Defined(
|
||||
DefinedPlace::new(KnownClass::Dict.to_specialized_instance(
|
||||
db,
|
||||
[Type::any(), KnownClass::Int.to_instance(db)],
|
||||
&[Type::any(), KnownClass::Int.to_instance(db)],
|
||||
))
|
||||
.with_definedness(Definedness::PossiblyUndefined),
|
||||
)
|
||||
@@ -1687,10 +1673,10 @@ mod implicit_globals {
|
||||
[Parameter::positional_only(Some(Name::new_static("format")))
|
||||
.with_annotated_type(KnownClass::Int.to_instance(db))],
|
||||
),
|
||||
Some(KnownClass::Dict.to_specialized_instance(
|
||||
KnownClass::Dict.to_specialized_instance(
|
||||
db,
|
||||
[KnownClass::Str.to_instance(db), Type::any()],
|
||||
)),
|
||||
&[KnownClass::Str.to_instance(db), Type::any()],
|
||||
),
|
||||
);
|
||||
Place::Defined(
|
||||
DefinedPlace::new(Type::function_like_callable(db, signature))
|
||||
@@ -1893,7 +1879,7 @@ mod tests {
|
||||
let ty1 = Type::IntLiteral(1);
|
||||
let ty2 = Type::IntLiteral(2);
|
||||
|
||||
let unbound = || Place::Undefined.with_qualifiers(TypeQualifiers::empty());
|
||||
let unbound = || PlaceAndQualifiers::default();
|
||||
|
||||
let possibly_unbound_ty1 = || {
|
||||
Place::Defined(DefinedPlace {
|
||||
|
||||
@@ -781,6 +781,13 @@ mod tests {
|
||||
.find_map(|constrained_binding| constrained_binding.binding.definition())
|
||||
}
|
||||
|
||||
fn first_public_declaration(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
|
||||
self.end_of_scope_symbol_declarations(symbol)
|
||||
.find_map(|declaration_with_constraint| {
|
||||
declaration_with_constraint.declaration.definition()
|
||||
})
|
||||
}
|
||||
|
||||
fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option<Definition<'_>> {
|
||||
self.bindings_at_use(use_id)
|
||||
.find_map(|constrained_binding| constrained_binding.binding.definition())
|
||||
@@ -833,10 +840,19 @@ mod tests {
|
||||
#[test]
|
||||
fn annotation_only() {
|
||||
let TestCase { db, file } = test_case("x: int");
|
||||
let global_table = place_table(&db, global_scope(&db, file));
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = place_table(&db, scope);
|
||||
|
||||
assert_eq!(names(global_table), vec!["int", "x"]);
|
||||
// TODO record definition
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let declaration = use_def
|
||||
.first_public_declaration(global_table.symbol_id("x").expect("symbol to exist"))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
declaration.kind(&db),
|
||||
DefinitionKind::AnnotatedAssignment(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -899,9 +899,7 @@ impl ReachabilityConstraints {
|
||||
let (no_overloads_return_never, all_overloads_return_never) = overloads_iterator
|
||||
.fold((true, true), |(none, all), overload| {
|
||||
let overload_returns_never =
|
||||
overload.return_ty.is_some_and(|return_type| {
|
||||
return_type.is_equivalent_to(db, Type::Never)
|
||||
});
|
||||
overload.return_ty.is_equivalent_to(db, Type::Never);
|
||||
|
||||
(
|
||||
none && !overload_returns_never,
|
||||
|
||||
@@ -427,12 +427,6 @@ pub struct Completion<'db> {
|
||||
pub builtin: bool,
|
||||
}
|
||||
|
||||
impl<'db> Completion<'db> {
|
||||
pub fn is_type_check_only(&self, db: &'db dyn Db) -> bool {
|
||||
self.ty.is_some_and(|ty| ty.is_type_check_only(db))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait HasType {
|
||||
/// Returns the inferred type of `self`.
|
||||
///
|
||||
|
||||
@@ -24,10 +24,10 @@ use crate::{Db, declare_lint, lint::LintId};
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for `type: ignore` or `ty: ignore` directives that are no longer applicable.
|
||||
/// Checks for `ty: ignore` or `type: ignore` directives that are no longer applicable.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// A `type: ignore` directive that no longer matches any diagnostic violations is likely
|
||||
/// A `ty: ignore` directive that no longer matches any diagnostic violations is likely
|
||||
/// included by mistake, and should be removed to avoid confusion.
|
||||
///
|
||||
/// ## Examples
|
||||
@@ -40,10 +40,14 @@ declare_lint! {
|
||||
/// ```py
|
||||
/// a = 20 / 2
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// Set [`analysis.respect-type-ignore-comments`](https://docs.astral.sh/ty/reference/configuration/#respect-type-ignore-comments)
|
||||
/// to `false` to prevent this rule from reporting unused `type: ignore` comments.
|
||||
pub static UNUSED_IGNORE_COMMENT = {
|
||||
summary: "detects unused `type: ignore` comments",
|
||||
summary: "detects unused `ty: ignore` and `type: ignore` comments",
|
||||
status: LintStatus::stable("0.0.1-alpha.1"),
|
||||
default_level: Level::Ignore,
|
||||
default_level: Level::Warn,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1064,7 +1064,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_dynamic(&self) -> bool {
|
||||
pub(crate) const fn is_dynamic(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(_))
|
||||
}
|
||||
|
||||
@@ -1073,7 +1073,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
/// Is a value of this type only usable in typing contexts?
|
||||
pub(crate) fn is_type_check_only(&self, db: &'db dyn Db) -> bool {
|
||||
pub fn is_type_check_only(&self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::ClassLiteral(class_literal) => class_literal.type_check_only(db),
|
||||
Type::FunctionLiteral(f) => {
|
||||
@@ -1833,7 +1833,7 @@ impl<'db> Type<'db> {
|
||||
///
|
||||
/// This method may have false negatives, but it should not have false positives. It should be
|
||||
/// a cheap shallow check, not an exhaustive recursive check.
|
||||
fn subtyping_is_always_reflexive(self) -> bool {
|
||||
const fn subtyping_is_always_reflexive(self) -> bool {
|
||||
match self {
|
||||
Type::Never
|
||||
| Type::FunctionLiteral(..)
|
||||
@@ -1854,6 +1854,7 @@ impl<'db> Type<'db> {
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::PropertyInstance(_)
|
||||
| Type::TypeVar(_)
|
||||
// might inherit `Any`, but subtyping is still reflexive
|
||||
| Type::ClassLiteral(_)
|
||||
=> true,
|
||||
@@ -1865,7 +1866,6 @@ impl<'db> Type<'db> {
|
||||
| Type::Union(_)
|
||||
| Type::Intersection(_)
|
||||
| Type::Callable(_)
|
||||
| Type::TypeVar(_)
|
||||
| Type::BoundSuper(_)
|
||||
| Type::TypeIs(_)
|
||||
| Type::TypeGuard(_)
|
||||
@@ -1925,7 +1925,7 @@ impl<'db> Type<'db> {
|
||||
SubclassOfInner::Dynamic(_) | SubclassOfInner::TypeVar(_) => {
|
||||
Some(CallableTypes::one(CallableType::single(
|
||||
db,
|
||||
Signature::new(Parameters::unknown(), Some(Type::from(subclass_of_ty))),
|
||||
Signature::new(Parameters::unknown(), Type::from(subclass_of_ty)),
|
||||
)))
|
||||
}
|
||||
},
|
||||
@@ -1968,7 +1968,7 @@ impl<'db> Type<'db> {
|
||||
[Parameter::positional_only(None)
|
||||
.with_annotated_type(newtype.base(db).instance_type(db))],
|
||||
),
|
||||
Some(Type::NewTypeInstance(newtype)),
|
||||
Type::NewTypeInstance(newtype),
|
||||
),
|
||||
)))
|
||||
}
|
||||
@@ -3811,7 +3811,7 @@ impl<'db> Type<'db> {
|
||||
[Parameter::positional_only(Some(Name::new_static("func")))
|
||||
.with_annotated_type(Type::object())],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
@@ -3836,7 +3836,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
KnownClass::ConstraintSet.to_instance(db),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
@@ -3851,7 +3851,7 @@ impl<'db> Type<'db> {
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any())],
|
||||
),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
KnownClass::Bool.to_instance(db),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -3878,7 +3878,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
Some(Type::TypeVar(val_ty)),
|
||||
Type::TypeVar(val_ty),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -3897,7 +3897,7 @@ impl<'db> Type<'db> {
|
||||
// errors instead of `type-assertion-failure` errors.
|
||||
.with_annotated_type(Type::any())],
|
||||
),
|
||||
Some(Type::none(db)),
|
||||
Type::none(db),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -3916,7 +3916,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
Some(Type::any()),
|
||||
Type::any(),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
@@ -3932,7 +3932,7 @@ impl<'db> Type<'db> {
|
||||
[Parameter::positional_only(Some(Name::new_static("cls")))
|
||||
.with_annotated_type(Type::none(db))],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
// def dataclass(cls: type[_T], /) -> type[_T]: ...
|
||||
Signature::new(
|
||||
@@ -3941,7 +3941,7 @@ impl<'db> Type<'db> {
|
||||
[Parameter::positional_only(Some(Name::new_static("cls")))
|
||||
.with_annotated_type(KnownClass::Type.to_instance(db))],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
// TODO: make this overload Python-version-dependent
|
||||
|
||||
@@ -3994,7 +3994,7 @@ impl<'db> Type<'db> {
|
||||
.with_default_type(Type::BooleanLiteral(false)),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -4029,7 +4029,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::any())
|
||||
.with_default_type(Type::BooleanLiteral(false))],
|
||||
),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
KnownClass::Bool.to_instance(db),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -4053,7 +4053,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::object())
|
||||
.with_default_type(Type::string_literal(db, ""))],
|
||||
),
|
||||
Some(KnownClass::Str.to_instance(db)),
|
||||
KnownClass::Str.to_instance(db),
|
||||
),
|
||||
Signature::new(
|
||||
Parameters::new(
|
||||
@@ -4083,7 +4083,7 @@ impl<'db> Type<'db> {
|
||||
.with_default_type(Type::string_literal(db, "strict")),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::Str.to_instance(db)),
|
||||
KnownClass::Str.to_instance(db),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -4110,7 +4110,7 @@ impl<'db> Type<'db> {
|
||||
[Parameter::positional_only(Some(Name::new_static("o")))
|
||||
.with_annotated_type(Type::any())],
|
||||
),
|
||||
Some(type_instance),
|
||||
type_instance,
|
||||
),
|
||||
Signature::new(
|
||||
Parameters::new(
|
||||
@@ -4127,12 +4127,12 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(
|
||||
KnownClass::Dict.to_specialized_instance(
|
||||
db,
|
||||
[str_instance, Type::any()],
|
||||
&[str_instance, Type::any()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Some(type_instance),
|
||||
type_instance,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -4145,11 +4145,8 @@ impl<'db> Type<'db> {
|
||||
// def __init__(self) -> None: ...
|
||||
// def __new__(cls) -> Self: ...
|
||||
// ```
|
||||
Binding::single(
|
||||
self,
|
||||
Signature::new(Parameters::empty(), Some(Type::object())),
|
||||
)
|
||||
.into()
|
||||
Binding::single(self, Signature::new(Parameters::empty(), Type::object()))
|
||||
.into()
|
||||
}
|
||||
|
||||
Some(KnownClass::Enum) => {
|
||||
@@ -4179,7 +4176,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::Super.to_instance(db)),
|
||||
KnownClass::Super.to_instance(db),
|
||||
),
|
||||
Signature::new(
|
||||
Parameters::new(
|
||||
@@ -4187,12 +4184,9 @@ impl<'db> Type<'db> {
|
||||
[Parameter::positional_only(Some(Name::new_static("t")))
|
||||
.with_annotated_type(Type::any())],
|
||||
),
|
||||
Some(KnownClass::Super.to_instance(db)),
|
||||
),
|
||||
Signature::new(
|
||||
Parameters::empty(),
|
||||
Some(KnownClass::Super.to_instance(db)),
|
||||
KnownClass::Super.to_instance(db),
|
||||
),
|
||||
Signature::new(Parameters::empty(), KnownClass::Super.to_instance(db)),
|
||||
],
|
||||
)
|
||||
.into()
|
||||
@@ -4234,7 +4228,7 @@ impl<'db> Type<'db> {
|
||||
.with_default_type(Type::IntLiteral(1)),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::Deprecated.to_instance(db)),
|
||||
KnownClass::Deprecated.to_instance(db),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -4276,7 +4270,7 @@ impl<'db> Type<'db> {
|
||||
.with_default_type(Type::empty_tuple(db)),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -4288,7 +4282,7 @@ impl<'db> Type<'db> {
|
||||
db,
|
||||
[Parameter::positional_only(None).with_annotated_type(Type::any())],
|
||||
),
|
||||
Some(Type::any()),
|
||||
Type::any(),
|
||||
);
|
||||
let setter_signature = Signature::new(
|
||||
Parameters::new(
|
||||
@@ -4298,14 +4292,14 @@ impl<'db> Type<'db> {
|
||||
Parameter::positional_only(None).with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
Some(Type::none(db)),
|
||||
Type::none(db),
|
||||
);
|
||||
let deleter_signature = Signature::new(
|
||||
Parameters::new(
|
||||
db,
|
||||
[Parameter::positional_only(None).with_annotated_type(Type::any())],
|
||||
),
|
||||
Some(Type::any()),
|
||||
Type::any(),
|
||||
);
|
||||
|
||||
Binding::single(
|
||||
@@ -4349,7 +4343,7 @@ impl<'db> Type<'db> {
|
||||
.with_default_type(Type::none(db)),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -4372,7 +4366,7 @@ impl<'db> Type<'db> {
|
||||
CallableBinding::from_overloads(
|
||||
self,
|
||||
[
|
||||
Signature::new(Parameters::empty(), Some(Type::empty_tuple(db))),
|
||||
Signature::new(Parameters::empty(), Type::empty_tuple(db)),
|
||||
Signature::new_generic(
|
||||
Some(GenericContext::from_typevar_instances(db, [element_ty])),
|
||||
Parameters::new(
|
||||
@@ -4383,11 +4377,11 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(
|
||||
KnownClass::Iterable.to_specialized_instance(
|
||||
db,
|
||||
[Type::TypeVar(element_ty)],
|
||||
&[Type::TypeVar(element_ty)],
|
||||
),
|
||||
)],
|
||||
),
|
||||
Some(Type::homogeneous_tuple(db, Type::TypeVar(element_ty))),
|
||||
Type::homogeneous_tuple(db, Type::TypeVar(element_ty)),
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -4404,7 +4398,7 @@ impl<'db> Type<'db> {
|
||||
Signature::new_generic(
|
||||
class.generic_context(db),
|
||||
Parameters::gradual_form(),
|
||||
self.to_instance(db),
|
||||
self.to_instance(db).unwrap_or(Type::unknown()),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
@@ -4430,7 +4424,7 @@ impl<'db> Type<'db> {
|
||||
.with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
@@ -4445,7 +4439,10 @@ impl<'db> Type<'db> {
|
||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
||||
Binding::single(
|
||||
self,
|
||||
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
|
||||
Signature::new(
|
||||
Parameters::gradual_form(),
|
||||
self.to_instance(db).unwrap_or(Type::unknown()),
|
||||
),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
@@ -4463,7 +4460,10 @@ impl<'db> Type<'db> {
|
||||
// TODO check call vs signatures of `__new__` and/or `__init__`
|
||||
SubclassOfInner::TypeVar(_) => Binding::single(
|
||||
self,
|
||||
Signature::new(Parameters::gradual_form(), self.to_instance(db)),
|
||||
Signature::new(
|
||||
Parameters::gradual_form(),
|
||||
self.to_instance(db).unwrap_or(Type::unknown()),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
},
|
||||
@@ -4530,11 +4530,8 @@ impl<'db> Type<'db> {
|
||||
// Intersect with `Any` for the return type to reflect the fact that the `dataclass()`
|
||||
// decorator adds methods to the class
|
||||
let returns = IntersectionType::from_elements(db, [typevar_meta, Type::any()]);
|
||||
let signature = Signature::new_generic(
|
||||
Some(context),
|
||||
Parameters::new(db, parameters),
|
||||
Some(returns),
|
||||
);
|
||||
let signature =
|
||||
Signature::new_generic(Some(context), Parameters::new(db, parameters), returns);
|
||||
Binding::single(self, signature).into()
|
||||
}
|
||||
|
||||
@@ -4551,7 +4548,7 @@ impl<'db> Type<'db> {
|
||||
[Parameter::positional_only(None)
|
||||
.with_annotated_type(newtype.base(db).instance_type(db))],
|
||||
),
|
||||
Some(Type::NewTypeInstance(newtype)),
|
||||
Type::NewTypeInstance(newtype),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
@@ -5843,7 +5840,7 @@ impl<'db> Type<'db> {
|
||||
pub(crate) fn dunder_class(self, db: &'db dyn Db) -> Type<'db> {
|
||||
if self.is_typed_dict() {
|
||||
return KnownClass::Dict
|
||||
.to_specialized_class_type(db, [KnownClass::Str.to_instance(db), Type::object()])
|
||||
.to_specialized_class_type(db, &[KnownClass::Str.to_instance(db), Type::object()])
|
||||
.map(Type::from)
|
||||
// Guard against user-customized typesheds with a broken `dict` class
|
||||
.unwrap_or_else(Type::unknown);
|
||||
@@ -8373,7 +8370,7 @@ impl<'db> BoundTypeVarInstance<'db> {
|
||||
let upper_bound = TypeVarBoundOrConstraints::UpperBound(match kind {
|
||||
ParamSpecAttrKind::Args => Type::homogeneous_tuple(db, Type::object()),
|
||||
ParamSpecAttrKind::Kwargs => KnownClass::Dict
|
||||
.to_specialized_instance(db, [KnownClass::Str.to_instance(db), Type::any()])
|
||||
.to_specialized_instance(db, &[KnownClass::Str.to_instance(db), Type::any()])
|
||||
.top_materialization(db),
|
||||
});
|
||||
|
||||
@@ -10387,7 +10384,7 @@ impl<'db> CallableType<'db> {
|
||||
) -> CallableType<'db> {
|
||||
CallableType::new(
|
||||
db,
|
||||
CallableSignature::single(Signature::new(parameters, None)),
|
||||
CallableSignature::single(Signature::new(parameters, Type::unknown())),
|
||||
CallableTypeKind::ParamSpecValue,
|
||||
)
|
||||
}
|
||||
@@ -10990,7 +10987,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
.with_annotated_type(KnownClass::Type.to_instance(db)),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
Signature::new(
|
||||
Parameters::new(
|
||||
@@ -11006,7 +11003,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
.with_default_type(Type::none(db)),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
]
|
||||
.into_iter(),
|
||||
@@ -11025,7 +11022,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
.with_annotated_type(Type::object()),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
)))
|
||||
}
|
||||
KnownBoundMethodType::StrStartswith(_) => {
|
||||
@@ -11058,7 +11055,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
.with_default_type(Type::none(db)),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
KnownClass::Bool.to_instance(db),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -11078,7 +11075,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
.with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
KnownClass::ConstraintSet.to_instance(db),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -11086,7 +11083,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
| KnownBoundMethodType::ConstraintSetNever => {
|
||||
Either::Right(std::iter::once(Signature::new(
|
||||
Parameters::empty(),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
KnownClass::ConstraintSet.to_instance(db),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -11103,7 +11100,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
.with_annotated_type(Type::any()),
|
||||
],
|
||||
),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
KnownClass::ConstraintSet.to_instance(db),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -11114,7 +11111,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
[Parameter::positional_only(Some(Name::new_static("other")))
|
||||
.with_annotated_type(KnownClass::ConstraintSet.to_instance(db))],
|
||||
),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
KnownClass::ConstraintSet.to_instance(db),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -11130,7 +11127,7 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
))
|
||||
.with_default_type(Type::none(db))],
|
||||
),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
KnownClass::Bool.to_instance(db),
|
||||
)))
|
||||
}
|
||||
|
||||
@@ -11143,10 +11140,10 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
.with_annotated_type(KnownClass::ConstraintSet.to_instance(db)),
|
||||
],
|
||||
),
|
||||
Some(UnionType::from_elements(
|
||||
UnionType::from_elements(
|
||||
db,
|
||||
[KnownClass::Specialization.to_instance(db), Type::none(db)],
|
||||
)),
|
||||
),
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -11193,7 +11190,7 @@ impl WrapperDescriptorKind {
|
||||
.with_annotated_type(type_instance),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
Signature::new(
|
||||
Parameters::new(
|
||||
@@ -11211,7 +11208,7 @@ impl WrapperDescriptorKind {
|
||||
.with_default_type(none),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
),
|
||||
]
|
||||
}
|
||||
@@ -11237,7 +11234,7 @@ impl WrapperDescriptorKind {
|
||||
.with_annotated_type(object),
|
||||
],
|
||||
),
|
||||
None,
|
||||
Type::unknown(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -11727,8 +11724,8 @@ pub(super) struct MetaclassCandidate<'db> {
|
||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
pub struct UnionType<'db> {
|
||||
/// The union type includes values in any of these types.
|
||||
#[returns(deref)]
|
||||
pub elements: Box<[Type<'db>]>,
|
||||
#[returns(ref)]
|
||||
pub elements: FxOrderSet<Type<'db>>,
|
||||
/// Whether the value pointed to by this type is recursively defined.
|
||||
/// If `Yes`, union literal widening is performed early.
|
||||
recursively_defined: RecursivelyDefined,
|
||||
@@ -13082,11 +13079,11 @@ pub(crate) mod tests {
|
||||
let recursive = UnionType::from_elements(
|
||||
&db,
|
||||
[
|
||||
KnownClass::List.to_specialized_instance(&db, [div]),
|
||||
KnownClass::List.to_specialized_instance(&db, &[div]),
|
||||
Type::none(&db),
|
||||
],
|
||||
);
|
||||
let nested_rec = KnownClass::List.to_specialized_instance(&db, [recursive]);
|
||||
let nested_rec = KnownClass::List.to_specialized_instance(&db, &[recursive]);
|
||||
assert_eq!(
|
||||
nested_rec.display(&db).to_string(),
|
||||
"list[list[Divergent] | None]"
|
||||
|
||||
@@ -8,9 +8,10 @@ use crate::{
|
||||
Db, DisplaySettings,
|
||||
place::{Place, PlaceAndQualifiers},
|
||||
types::{
|
||||
ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass, MemberLookupPolicy,
|
||||
NominalInstanceType, NormalizedVisitor, SpecialFormType, SubclassOfInner, Type,
|
||||
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder,
|
||||
BoundTypeVarInstance, ClassBase, ClassType, DynamicType, IntersectionBuilder, KnownClass,
|
||||
MemberLookupPolicy, NominalInstanceType, NormalizedVisitor, SpecialFormType,
|
||||
SubclassOfInner, SubclassOfType, Type, TypeVarBoundOrConstraints, TypeVarConstraints,
|
||||
TypeVarInstance, UnionBuilder,
|
||||
context::InferContext,
|
||||
diagnostic::{INVALID_SUPER_ARGUMENT, UNAVAILABLE_IMPLICIT_SUPER_ARGUMENTS},
|
||||
todo_type, visitor,
|
||||
@@ -185,6 +186,12 @@ pub enum SuperOwnerKind<'db> {
|
||||
Dynamic(DynamicType<'db>),
|
||||
Class(ClassType<'db>),
|
||||
Instance(NominalInstanceType<'db>),
|
||||
/// An instance-like type variable owner (e.g., `self: Self` in an instance method).
|
||||
/// The second element is the class extracted from the `TypeVar` bound for MRO lookup.
|
||||
InstanceTypeVar(BoundTypeVarInstance<'db>, ClassType<'db>),
|
||||
/// A class-like type variable owner (e.g., `cls: type[Self]` in a classmethod).
|
||||
/// The second element is the class extracted from the `TypeVar` bound for MRO lookup.
|
||||
ClassTypeVar(BoundTypeVarInstance<'db>, ClassType<'db>),
|
||||
}
|
||||
|
||||
impl<'db> SuperOwnerKind<'db> {
|
||||
@@ -199,6 +206,16 @@ impl<'db> SuperOwnerKind<'db> {
|
||||
.as_nominal_instance()
|
||||
.map(Self::Instance)
|
||||
.unwrap_or(Self::Dynamic(DynamicType::Any)),
|
||||
SuperOwnerKind::InstanceTypeVar(bound_typevar, class) => {
|
||||
SuperOwnerKind::InstanceTypeVar(
|
||||
bound_typevar.normalized_impl(db, visitor),
|
||||
class.normalized_impl(db, visitor),
|
||||
)
|
||||
}
|
||||
SuperOwnerKind::ClassTypeVar(bound_typevar, class) => SuperOwnerKind::ClassTypeVar(
|
||||
bound_typevar.normalized_impl(db, visitor),
|
||||
class.normalized_impl(db, visitor),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,6 +235,10 @@ impl<'db> SuperOwnerKind<'db> {
|
||||
SuperOwnerKind::Instance(instance) => Some(SuperOwnerKind::Instance(
|
||||
instance.recursive_type_normalized_impl(db, div, nested)?,
|
||||
)),
|
||||
SuperOwnerKind::InstanceTypeVar(_, _) | SuperOwnerKind::ClassTypeVar(_, _) => {
|
||||
// TODO: we might need to normalize the nested class here?
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +249,9 @@ impl<'db> SuperOwnerKind<'db> {
|
||||
}
|
||||
SuperOwnerKind::Class(class) => Either::Right(class.iter_mro(db)),
|
||||
SuperOwnerKind::Instance(instance) => Either::Right(instance.class(db).iter_mro(db)),
|
||||
SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => {
|
||||
Either::Right(class.iter_mro(db))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,16 +260,31 @@ impl<'db> SuperOwnerKind<'db> {
|
||||
SuperOwnerKind::Dynamic(_) => None,
|
||||
SuperOwnerKind::Class(class) => Some(class),
|
||||
SuperOwnerKind::Instance(instance) => Some(instance.class(db)),
|
||||
SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => {
|
||||
Some(class)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<SuperOwnerKind<'db>> for Type<'db> {
|
||||
fn from(owner: SuperOwnerKind<'db>) -> Self {
|
||||
match owner {
|
||||
/// Returns the `TypeVar` instance if this owner is a `TypeVar` variant.
|
||||
fn typevar(self, db: &'db dyn Db) -> Option<TypeVarInstance<'db>> {
|
||||
match self {
|
||||
SuperOwnerKind::InstanceTypeVar(bound_typevar, _)
|
||||
| SuperOwnerKind::ClassTypeVar(bound_typevar, _) => Some(bound_typevar.typevar(db)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the type representation of this owner.
|
||||
pub(super) fn owner_type(self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
SuperOwnerKind::Dynamic(dynamic) => Type::Dynamic(dynamic),
|
||||
SuperOwnerKind::Class(class) => class.into(),
|
||||
SuperOwnerKind::Instance(instance) => instance.into(),
|
||||
SuperOwnerKind::InstanceTypeVar(bound_typevar, _) => Type::TypeVar(bound_typevar),
|
||||
SuperOwnerKind::ClassTypeVar(bound_typevar, _) => {
|
||||
SubclassOfType::from(db, SubclassOfInner::TypeVar(bound_typevar))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -266,7 +305,7 @@ pub(super) fn walk_bound_super_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
visitor: &V,
|
||||
) {
|
||||
visitor.visit_type(db, Type::from(bound_super.pivot_class(db)));
|
||||
visitor.visit_type(db, Type::from(bound_super.owner(db)));
|
||||
visitor.visit_type(db, bound_super.owner(db).owner_type(db));
|
||||
}
|
||||
|
||||
impl<'db> BoundSuperType<'db> {
|
||||
@@ -285,6 +324,7 @@ impl<'db> BoundSuperType<'db> {
|
||||
let delegate_to =
|
||||
|type_to_delegate_to| BoundSuperType::build(db, pivot_class_type, type_to_delegate_to);
|
||||
|
||||
// Delegate but rewrite errors to preserve TypeVar context.
|
||||
let delegate_with_error_mapped =
|
||||
|type_to_delegate_to, error_context: Option<TypeVarInstance<'db>>| {
|
||||
delegate_to(type_to_delegate_to).map_err(|err| match err {
|
||||
@@ -315,19 +355,128 @@ impl<'db> BoundSuperType<'db> {
|
||||
})
|
||||
};
|
||||
|
||||
// We don't use `ClassBase::try_from_type` here because:
|
||||
// - There are objects that may validly be present in a class's bases list
|
||||
// but are not valid as pivot classes, e.g. `typing.ChainMap`
|
||||
// - There are objects that are not valid in a class's bases list
|
||||
// but are valid as pivot classes, e.g. unsubscripted `typing.Generic`
|
||||
let pivot_class = match pivot_class_type {
|
||||
Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)),
|
||||
Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
|
||||
SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
|
||||
_ => match subclass_of.subclass_of().into_class(db) {
|
||||
Some(class) => ClassBase::Class(class),
|
||||
None => {
|
||||
return Err(BoundSuperError::InvalidPivotClassType {
|
||||
pivot_class: pivot_class_type,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol,
|
||||
Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic,
|
||||
Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict,
|
||||
Type::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
|
||||
_ => {
|
||||
return Err(BoundSuperError::InvalidPivotClassType {
|
||||
pivot_class: pivot_class_type,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Helper to build a union of bound-super instances for constrained TypeVars.
|
||||
// Each constraint must be a subclass of the pivot class.
|
||||
let build_constrained_union =
|
||||
|constraints: TypeVarConstraints<'db>,
|
||||
bound_typevar: BoundTypeVarInstance<'db>,
|
||||
typevar: TypeVarInstance<'db>,
|
||||
make_owner: fn(BoundTypeVarInstance<'db>, ClassType<'db>) -> SuperOwnerKind<'db>|
|
||||
-> Result<Type<'db>, BoundSuperError<'db>> {
|
||||
let pivot_class_literal = pivot_class.into_class().map(|c| c.class_literal(db).0);
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for constraint in constraints.elements(db) {
|
||||
let class = match constraint {
|
||||
Type::NominalInstance(instance) => Some(instance.class(db)),
|
||||
_ => constraint.to_class_type(db),
|
||||
};
|
||||
match class {
|
||||
Some(class) => {
|
||||
// Validate constraint is a subclass of pivot class.
|
||||
if let Some(pivot) = pivot_class_literal {
|
||||
if !class.iter_mro(db).any(|superclass| match superclass {
|
||||
ClassBase::Dynamic(_) => true,
|
||||
ClassBase::Generic
|
||||
| ClassBase::Protocol
|
||||
| ClassBase::TypedDict => false,
|
||||
ClassBase::Class(superclass) => {
|
||||
superclass.class_literal(db).0 == pivot
|
||||
}
|
||||
}) {
|
||||
return Err(BoundSuperError::FailingConditionCheck {
|
||||
pivot_class: pivot_class_type,
|
||||
owner: owner_type,
|
||||
typevar_context: Some(typevar),
|
||||
});
|
||||
}
|
||||
}
|
||||
let owner = make_owner(bound_typevar, class);
|
||||
builder = builder.add(Type::BoundSuper(BoundSuperType::new(
|
||||
db,
|
||||
pivot_class,
|
||||
owner,
|
||||
)));
|
||||
}
|
||||
None => {
|
||||
// Delegate to the constraint to get better error messages
|
||||
// if the constraint is incompatible with the pivot class.
|
||||
builder = builder.add(delegate_to(*constraint)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(builder.build())
|
||||
};
|
||||
|
||||
let owner = match owner_type {
|
||||
Type::Never => SuperOwnerKind::Dynamic(DynamicType::Unknown),
|
||||
Type::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic),
|
||||
Type::ClassLiteral(class) => SuperOwnerKind::Class(ClassType::NonGeneric(class)),
|
||||
Type::SubclassOf(subclass_of_type) => {
|
||||
match subclass_of_type.subclass_of().with_transposed_type_var(db) {
|
||||
SubclassOfInner::Class(class) => SuperOwnerKind::Class(class),
|
||||
SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic),
|
||||
SubclassOfInner::TypeVar(bound_typevar) => {
|
||||
return delegate_to(Type::TypeVar(bound_typevar));
|
||||
Type::SubclassOf(subclass_of_type) => match subclass_of_type.subclass_of() {
|
||||
SubclassOfInner::Class(class) => SuperOwnerKind::Class(class),
|
||||
SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic),
|
||||
SubclassOfInner::TypeVar(bound_typevar) => {
|
||||
let typevar = bound_typevar.typevar(db);
|
||||
match typevar.bound_or_constraints(db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
let class = match bound {
|
||||
Type::NominalInstance(instance) => Some(instance.class(db)),
|
||||
Type::ProtocolInstance(protocol) => protocol
|
||||
.to_nominal_instance()
|
||||
.map(|instance| instance.class(db)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(class) = class {
|
||||
SuperOwnerKind::ClassTypeVar(bound_typevar, class)
|
||||
} else {
|
||||
let subclass_of = SubclassOfType::try_from_instance(db, bound)
|
||||
.unwrap_or_else(SubclassOfType::subclass_of_unknown);
|
||||
return delegate_with_error_mapped(subclass_of, Some(typevar));
|
||||
}
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
return build_constrained_union(
|
||||
constraints,
|
||||
bound_typevar,
|
||||
typevar,
|
||||
SuperOwnerKind::ClassTypeVar,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
// No bound means the implicit upper bound is `object`.
|
||||
SuperOwnerKind::ClassTypeVar(bound_typevar, ClassType::object(db))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Type::NominalInstance(instance) => SuperOwnerKind::Instance(instance),
|
||||
|
||||
Type::ProtocolInstance(protocol) => {
|
||||
@@ -375,19 +524,38 @@ impl<'db> BoundSuperType<'db> {
|
||||
return Ok(builder.build());
|
||||
}
|
||||
Type::TypeAlias(alias) => {
|
||||
return delegate_with_error_mapped(alias.value_type(db), None);
|
||||
return delegate_to(alias.value_type(db));
|
||||
}
|
||||
Type::TypeVar(type_var) => {
|
||||
let type_var = type_var.typevar(db);
|
||||
return match type_var.bound_or_constraints(db) {
|
||||
Type::TypeVar(bound_typevar) => {
|
||||
let typevar = bound_typevar.typevar(db);
|
||||
match typevar.bound_or_constraints(db) {
|
||||
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
|
||||
delegate_with_error_mapped(bound, Some(type_var))
|
||||
let class = match bound {
|
||||
Type::NominalInstance(instance) => Some(instance.class(db)),
|
||||
Type::ProtocolInstance(protocol) => protocol
|
||||
.to_nominal_instance()
|
||||
.map(|instance| instance.class(db)),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(class) = class {
|
||||
SuperOwnerKind::InstanceTypeVar(bound_typevar, class)
|
||||
} else {
|
||||
return delegate_with_error_mapped(bound, Some(typevar));
|
||||
}
|
||||
}
|
||||
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
|
||||
delegate_with_error_mapped(constraints.as_type(db), Some(type_var))
|
||||
return build_constrained_union(
|
||||
constraints,
|
||||
bound_typevar,
|
||||
typevar,
|
||||
SuperOwnerKind::InstanceTypeVar,
|
||||
);
|
||||
}
|
||||
None => delegate_with_error_mapped(Type::object(), Some(type_var)),
|
||||
};
|
||||
None => {
|
||||
// No bound means the implicit upper bound is `object`.
|
||||
SuperOwnerKind::InstanceTypeVar(bound_typevar, ClassType::object(db))
|
||||
}
|
||||
}
|
||||
}
|
||||
Type::BooleanLiteral(_) | Type::TypeIs(_) | Type::TypeGuard(_) => {
|
||||
return delegate_to(KnownClass::Bool.to_instance(db));
|
||||
@@ -435,7 +603,7 @@ impl<'db> BoundSuperType<'db> {
|
||||
}
|
||||
return delegate_to(
|
||||
KnownClass::Dict
|
||||
.to_specialized_instance(db, [key_builder.build(), value_builder.build()]),
|
||||
.to_specialized_instance(db, &[key_builder.build(), value_builder.build()]),
|
||||
);
|
||||
}
|
||||
Type::NewTypeInstance(newtype) => {
|
||||
@@ -456,35 +624,6 @@ impl<'db> BoundSuperType<'db> {
|
||||
}
|
||||
};
|
||||
|
||||
// We don't use `Classbase::try_from_type` here because:
|
||||
// - There are objects that may validly be present in a class's bases list
|
||||
// but are not valid as pivot classes, e.g. `typing.ChainMap`
|
||||
// - There are objects that are not valid in a class's bases list
|
||||
// but are valid as pivot classes, e.g. unsubscripted `typing.Generic`
|
||||
let pivot_class = match pivot_class_type {
|
||||
Type::ClassLiteral(class) => ClassBase::Class(ClassType::NonGeneric(class)),
|
||||
Type::SubclassOf(subclass_of) => match subclass_of.subclass_of() {
|
||||
SubclassOfInner::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
|
||||
_ => match subclass_of.subclass_of().into_class(db) {
|
||||
Some(class) => ClassBase::Class(class),
|
||||
None => {
|
||||
return Err(BoundSuperError::InvalidPivotClassType {
|
||||
pivot_class: pivot_class_type,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
Type::SpecialForm(SpecialFormType::Protocol) => ClassBase::Protocol,
|
||||
Type::SpecialForm(SpecialFormType::Generic) => ClassBase::Generic,
|
||||
Type::SpecialForm(SpecialFormType::TypedDict) => ClassBase::TypedDict,
|
||||
Type::Dynamic(dynamic) => ClassBase::Dynamic(dynamic),
|
||||
_ => {
|
||||
return Err(BoundSuperError::InvalidPivotClassType {
|
||||
pivot_class: pivot_class_type,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(pivot_class) = pivot_class.into_class()
|
||||
&& let Some(owner_class) = owner.into_class(db)
|
||||
{
|
||||
@@ -497,7 +636,7 @@ impl<'db> BoundSuperType<'db> {
|
||||
return Err(BoundSuperError::FailingConditionCheck {
|
||||
pivot_class: pivot_class_type,
|
||||
owner: owner_type,
|
||||
typevar_context: None,
|
||||
typevar_context: owner.typevar(db),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -556,18 +695,30 @@ impl<'db> BoundSuperType<'db> {
|
||||
db,
|
||||
attribute,
|
||||
Type::none(db),
|
||||
Type::from(owner),
|
||||
owner.owner_type(db),
|
||||
)
|
||||
.0,
|
||||
),
|
||||
SuperOwnerKind::Instance(_) => {
|
||||
let owner = Type::from(owner);
|
||||
SuperOwnerKind::Instance(_) | SuperOwnerKind::InstanceTypeVar(..) => {
|
||||
let owner_type = owner.owner_type(db);
|
||||
Some(
|
||||
Type::try_call_dunder_get_on_attribute(
|
||||
db,
|
||||
attribute,
|
||||
owner,
|
||||
owner.to_meta_type(db),
|
||||
owner_type,
|
||||
owner_type.to_meta_type(db),
|
||||
)
|
||||
.0,
|
||||
)
|
||||
}
|
||||
SuperOwnerKind::ClassTypeVar(..) => {
|
||||
let owner_type = owner.owner_type(db);
|
||||
Some(
|
||||
Type::try_call_dunder_get_on_attribute(
|
||||
db,
|
||||
attribute,
|
||||
Type::none(db),
|
||||
owner_type,
|
||||
)
|
||||
.0,
|
||||
)
|
||||
@@ -592,6 +743,9 @@ impl<'db> BoundSuperType<'db> {
|
||||
}
|
||||
SuperOwnerKind::Class(class) => class,
|
||||
SuperOwnerKind::Instance(instance) => instance.class(db),
|
||||
SuperOwnerKind::InstanceTypeVar(_, class) | SuperOwnerKind::ClassTypeVar(_, class) => {
|
||||
class
|
||||
}
|
||||
};
|
||||
|
||||
let (class_literal, _) = class.class_literal(db);
|
||||
|
||||
@@ -28,40 +28,55 @@
|
||||
//!
|
||||
//! In practice, there are two kinds of unions found in the wild: relatively-small unions made up
|
||||
//! of normal user types (classes, etc), and large unions made up of literals, which can occur via
|
||||
//! large enums (not yet implemented) or from string/integer/bytes literals, which can grow due to
|
||||
//! literal arithmetic or operations on literal strings/bytes. For normal unions, it's most
|
||||
//! efficient to just store the member types in a vector, and do O(n^2) `is_subtype_of` checks to
|
||||
//! maintain the union in simplified form. But literal unions can grow to a size where this becomes
|
||||
//! a performance problem. For this reason, we group literal types in `UnionBuilder`. Since every
|
||||
//! different string literal type shares exactly the same possible super-types, and none of them
|
||||
//! are subtypes of each other (unless exactly the same literal type), we can avoid many
|
||||
//! unnecessary `is_subtype_of` checks.
|
||||
//! large enums or from string/integer/bytes literals, which can grow due to literal arithmetic or
|
||||
//! operations on literal strings/bytes. For normal unions, it's most efficient to just store the
|
||||
//! member types in a vector, and do O(n^2) redundancy checks to maintain the union in simplified
|
||||
//! form. But literal unions can grow to a size where this becomes a performance problem. For this
|
||||
//! reason, we group literal types in `UnionBuilder`. Since every different string literal type
|
||||
//! shares exactly the same possible super-types, and none of them are subtypes of each other
|
||||
//! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks.
|
||||
|
||||
use std::hash::BuildHasherDefault;
|
||||
|
||||
use crate::types::enums::{enum_member_literals, enum_metadata};
|
||||
use crate::types::type_ordering::union_or_intersection_elements_ordering;
|
||||
use crate::types::{
|
||||
BytesLiteralType, IntersectionType, KnownClass, NegativeIntersectionElements,
|
||||
StringLiteralType, Type, TypeVarBoundOrConstraints, UnionType,
|
||||
BytesLiteralType, ClassLiteral, EnumLiteralType, IntersectionType, KnownClass,
|
||||
NegativeIntersectionElements, StringLiteralType, Type, TypeVarBoundOrConstraints, UnionType,
|
||||
};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use rustc_hash::FxHashSet;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum LiteralKind {
|
||||
enum LiteralKind<'db> {
|
||||
Int,
|
||||
String,
|
||||
Bytes,
|
||||
Enum { enum_class: ClassLiteral<'db> },
|
||||
}
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
/// Return `true` if this type can be a supertype of some literals of `kind` and not others.
|
||||
fn splits_literals(self, db: &'db dyn Db, kind: LiteralKind) -> bool {
|
||||
match (self, kind) {
|
||||
// Note that as of 2026-01-04, `AlwaysFalsy` and `AlwaysTruthy` never split
|
||||
// enum literals, but that could change in the future. `Literal[Foo.X]` could
|
||||
// plausibly be understood by ty as a subtype of `AlwaysFalsy` in the following
|
||||
// snippet, because `Foo` is an IntEnum that does not override `__bool__` and
|
||||
// `Foo.X` has a falsy value whereas `Foo.Y` does not:
|
||||
//
|
||||
// ```py
|
||||
// class Foo(enum.IntEnum):
|
||||
// X = 0
|
||||
// Y = 1
|
||||
// ```
|
||||
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => true,
|
||||
(Type::StringLiteral(_), LiteralKind::String) => true,
|
||||
(Type::BytesLiteral(_), LiteralKind::Bytes) => true,
|
||||
(Type::IntLiteral(_), LiteralKind::Int) => true,
|
||||
(Type::EnumLiteral(enum_literal), LiteralKind::Enum { enum_class }) => {
|
||||
enum_literal.enum_class(db) == enum_class
|
||||
}
|
||||
(Type::Intersection(intersection), _) => {
|
||||
intersection
|
||||
.positive(db)
|
||||
@@ -86,17 +101,14 @@ enum UnionElement<'db> {
|
||||
IntLiterals(FxOrderSet<i64>),
|
||||
StringLiterals(FxOrderSet<StringLiteralType<'db>>),
|
||||
BytesLiterals(FxOrderSet<BytesLiteralType<'db>>),
|
||||
EnumLiterals {
|
||||
enum_class: ClassLiteral<'db>,
|
||||
literals: FxOrderSet<EnumLiteralType<'db>>,
|
||||
},
|
||||
Type(Type<'db>),
|
||||
}
|
||||
|
||||
impl<'db> UnionElement<'db> {
|
||||
const fn to_type_element(&self) -> Option<Type<'db>> {
|
||||
match self {
|
||||
UnionElement::Type(ty) => Some(*ty),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try reducing this `UnionElement` given the presence in the same union of `other_type`.
|
||||
fn try_reduce(&mut self, db: &'db dyn Db, other_type: Type<'db>) -> ReduceResult<'db> {
|
||||
let mut other_type_negated_cache = None;
|
||||
@@ -110,22 +122,22 @@ impl<'db> UnionElement<'db> {
|
||||
// to determine whether the element should be retained in the set.
|
||||
//
|
||||
// If `ignore` or `collapse` is `true` for any element in the set,
|
||||
// we no longer need to do any expensive subtyping checks for any
|
||||
// we no longer need to do any expensive redundancy checks for any
|
||||
// further elements in the set:
|
||||
//
|
||||
// - if `ignore` is `true`, this indicates that `other_type` is a
|
||||
// subtype of one of the literals in this set. Given this fact,
|
||||
// - if `ignore` is `true`, this indicates that `other_type` is
|
||||
// redundant with one of the literals in this set. Given this fact,
|
||||
// it cannot be possible for any other literals in this set to be
|
||||
// a subtype of `other_type`.
|
||||
// redundant with `other_type`.
|
||||
// - if `collapse` is `true`, all literals of this kind will be
|
||||
// removed from the union, so it's irrelevant to answer the
|
||||
// question of which literals should remain in this set.
|
||||
//
|
||||
// We therefore only ask if `ty` is a subtype of `other_type` if
|
||||
// We therefore only ask if `ty` is redundant with `other_type` if
|
||||
// both `ignore` and `collapse` are `false`. If either is `true`,
|
||||
// we skip the expensive subtype check and return `true`.
|
||||
// we skip the expensive redundancy check and return `true`.
|
||||
let mut should_retain_type = |ty| {
|
||||
if ignore || other_type.is_subtype_of(db, ty) {
|
||||
if ignore || other_type.is_redundant_with(db, ty) {
|
||||
ignore = true;
|
||||
return true;
|
||||
}
|
||||
@@ -133,7 +145,7 @@ impl<'db> UnionElement<'db> {
|
||||
collapse = true;
|
||||
return true;
|
||||
}
|
||||
!ty.is_subtype_of(db, other_type)
|
||||
!ty.is_redundant_with(db, other_type)
|
||||
};
|
||||
|
||||
let should_keep = match self {
|
||||
@@ -142,7 +154,7 @@ impl<'db> UnionElement<'db> {
|
||||
literals.retain(|literal| should_retain_type(Type::IntLiteral(*literal)));
|
||||
!literals.is_empty()
|
||||
} else {
|
||||
!Type::IntLiteral(literals[0]).is_subtype_of(db, other_type)
|
||||
!Type::IntLiteral(literals[0]).is_redundant_with(db, other_type)
|
||||
}
|
||||
}
|
||||
UnionElement::StringLiterals(literals) => {
|
||||
@@ -150,7 +162,7 @@ impl<'db> UnionElement<'db> {
|
||||
literals.retain(|literal| should_retain_type(Type::StringLiteral(*literal)));
|
||||
!literals.is_empty()
|
||||
} else {
|
||||
!Type::StringLiteral(literals[0]).is_subtype_of(db, other_type)
|
||||
!Type::StringLiteral(literals[0]).is_redundant_with(db, other_type)
|
||||
}
|
||||
}
|
||||
UnionElement::BytesLiterals(literals) => {
|
||||
@@ -158,7 +170,21 @@ impl<'db> UnionElement<'db> {
|
||||
literals.retain(|literal| should_retain_type(Type::BytesLiteral(*literal)));
|
||||
!literals.is_empty()
|
||||
} else {
|
||||
!Type::BytesLiteral(literals[0]).is_subtype_of(db, other_type)
|
||||
!Type::BytesLiteral(literals[0]).is_redundant_with(db, other_type)
|
||||
}
|
||||
}
|
||||
UnionElement::EnumLiterals {
|
||||
enum_class,
|
||||
literals,
|
||||
} => {
|
||||
let literal_kind = LiteralKind::Enum {
|
||||
enum_class: *enum_class,
|
||||
};
|
||||
if other_type.splits_literals(db, literal_kind) {
|
||||
literals.retain(|literal| should_retain_type(Type::EnumLiteral(*literal)));
|
||||
!literals.is_empty()
|
||||
} else {
|
||||
!Type::EnumLiteral(literals[0]).is_redundant_with(db, other_type)
|
||||
}
|
||||
}
|
||||
UnionElement::Type(existing) => return ReduceResult::Type(*existing),
|
||||
@@ -206,19 +232,26 @@ impl RecursivelyDefined {
|
||||
}
|
||||
}
|
||||
|
||||
/// If the value is defined recursively, widening is performed from fewer literal elements, resulting in faster convergence of the fixed-point iteration.
|
||||
/// If the value is defined recursively, widening is performed from fewer literal elements,
|
||||
/// resulting in faster convergence of the fixed-point iteration.
|
||||
const MAX_RECURSIVE_UNION_LITERALS: usize = 10;
|
||||
/// If the value is defined non-recursively, the fixed-point iteration will converge in one go,
|
||||
/// so in principle we can have as many literal elements as we want, but to avoid unintended huge computational loads, we limit it to 256.
|
||||
/// so in principle we can have as many literal elements as we want,
|
||||
/// but to avoid unintended huge computational loads, we limit it to 256.
|
||||
const MAX_NON_RECURSIVE_UNION_LITERALS: usize = 256;
|
||||
/// However, we set a much larger limit for enum literals than for other kinds of literals.
|
||||
/// Huge enums are not uncommon (especially in generated code), and it's annoying
|
||||
/// if reachability analysis etc. fails when analysing these enums.
|
||||
const MAX_NON_RECURSIVE_UNION_ENUM_LITERALS: usize = 8192;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<UnionElement<'db>>,
|
||||
db: &'db dyn Db,
|
||||
unpack_aliases: bool,
|
||||
order_elements: bool,
|
||||
// This is enabled when joining types in a `cycle_recovery` function.
|
||||
// Since a cycle cannot be created within a `cycle_recovery` function, execution of `is_redundant_with` is skipped.
|
||||
/// This is enabled when joining types in a `cycle_recovery` function.
|
||||
/// Since a cycle cannot be created within a `cycle_recovery` function,
|
||||
/// execution of `is_redundant_with` is skipped.
|
||||
cycle_recovery: bool,
|
||||
recursively_defined: RecursivelyDefined,
|
||||
}
|
||||
@@ -281,6 +314,9 @@ impl<'db> UnionBuilder<'db> {
|
||||
UnionElement::BytesLiterals(_) => {
|
||||
replace_with.push(KnownClass::Bytes.to_instance(self.db));
|
||||
}
|
||||
UnionElement::EnumLiterals { literals, .. } => {
|
||||
replace_with.push(literals[0].enum_class_instance(self.db));
|
||||
}
|
||||
UnionElement::Type(_) => {}
|
||||
}
|
||||
}
|
||||
@@ -328,6 +364,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
UnionElement::IntLiterals(literals) => acc + literals.len(),
|
||||
UnionElement::StringLiterals(literals) => acc + literals.len(),
|
||||
UnionElement::BytesLiterals(literals) => acc + literals.len(),
|
||||
UnionElement::EnumLiterals { literals, .. } => acc + literals.len(),
|
||||
UnionElement::Type(_) => acc,
|
||||
});
|
||||
if should_widen(literals, self.recursively_defined) {
|
||||
@@ -367,10 +404,10 @@ impl<'db> UnionBuilder<'db> {
|
||||
UnionElement::Type(existing) => {
|
||||
// e.g. `existing` could be `Literal[""] & Any`,
|
||||
// and `ty` could be `Literal[""]`
|
||||
if ty.is_subtype_of(self.db, *existing) {
|
||||
if ty.is_redundant_with(self.db, *existing) {
|
||||
return;
|
||||
}
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
if existing.is_redundant_with(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
continue;
|
||||
}
|
||||
@@ -412,12 +449,12 @@ impl<'db> UnionBuilder<'db> {
|
||||
continue;
|
||||
}
|
||||
UnionElement::Type(existing) => {
|
||||
if ty.is_subtype_of(self.db, *existing) {
|
||||
if ty.is_redundant_with(self.db, *existing) {
|
||||
return;
|
||||
}
|
||||
// e.g. `existing` could be `Literal[b""] & Any`,
|
||||
// and `ty` could be `Literal[b""]`
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
if existing.is_redundant_with(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
continue;
|
||||
}
|
||||
@@ -459,12 +496,12 @@ impl<'db> UnionBuilder<'db> {
|
||||
continue;
|
||||
}
|
||||
UnionElement::Type(existing) => {
|
||||
if ty.is_subtype_of(self.db, *existing) {
|
||||
if ty.is_redundant_with(self.db, *existing) {
|
||||
return;
|
||||
}
|
||||
// e.g. `existing` could be `Literal[1] & Any`,
|
||||
// and `ty` could be `Literal[1]`
|
||||
if existing.is_subtype_of(self.db, ty) {
|
||||
if existing.is_redundant_with(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
continue;
|
||||
}
|
||||
@@ -493,32 +530,78 @@ impl<'db> UnionBuilder<'db> {
|
||||
let metadata =
|
||||
enum_metadata(self.db, enum_class).expect("Class of enum literal is an enum");
|
||||
|
||||
let enum_members_in_union = self
|
||||
.elements
|
||||
.iter()
|
||||
.filter_map(UnionElement::to_type_element)
|
||||
.filter_map(Type::as_enum_literal)
|
||||
.map(|literal| literal.name(self.db))
|
||||
.chain(std::iter::once(enum_member_to_add.name(self.db)))
|
||||
.collect::<FxHashSet<_>>();
|
||||
|
||||
let all_members_are_in_union = metadata
|
||||
.members
|
||||
.keys()
|
||||
.all(|name| enum_members_in_union.contains(name));
|
||||
|
||||
if all_members_are_in_union {
|
||||
if metadata.members.len() == 1 {
|
||||
self.add_in_place_impl(
|
||||
enum_member_to_add.enum_class_instance(self.db),
|
||||
seen_aliases,
|
||||
);
|
||||
} else if !self
|
||||
.elements
|
||||
.iter()
|
||||
.filter_map(UnionElement::to_type_element)
|
||||
.any(|ty| Type::EnumLiteral(enum_member_to_add).is_subtype_of(self.db, ty))
|
||||
{
|
||||
self.push_type(Type::EnumLiteral(enum_member_to_add), seen_aliases);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
for (index, element) in self.elements.iter_mut().enumerate() {
|
||||
match element {
|
||||
UnionElement::EnumLiterals {
|
||||
enum_class: existing_enum_class,
|
||||
literals,
|
||||
} => {
|
||||
if *existing_enum_class != enum_class {
|
||||
continue;
|
||||
}
|
||||
// See the doc-comment above `MAX_NON_RECURSIVE_UNION_ENUM_LITERALS`
|
||||
// for why we avoid using the `should_widen` closure here.
|
||||
let enum_literals_limit =
|
||||
if self.recursively_defined.is_yes() && cycle_recovery {
|
||||
MAX_RECURSIVE_UNION_LITERALS
|
||||
} else {
|
||||
MAX_NON_RECURSIVE_UNION_ENUM_LITERALS
|
||||
};
|
||||
if literals.len() >= enum_literals_limit {
|
||||
let replace_with = literals[0].enum_class_instance(self.db);
|
||||
self.add_in_place_impl(replace_with, seen_aliases);
|
||||
return;
|
||||
}
|
||||
found = Some(literals);
|
||||
continue;
|
||||
}
|
||||
UnionElement::Type(existing) => {
|
||||
if ty.is_redundant_with(self.db, *existing) {
|
||||
return;
|
||||
}
|
||||
// e.g. `existing` could be `Literal[Foo.X] & Any`,
|
||||
// and `ty` could be `Literal[Foo.X]`
|
||||
if existing.is_redundant_with(self.db, ty) {
|
||||
to_remove = Some(index);
|
||||
continue;
|
||||
}
|
||||
if ty_negated().is_subtype_of(self.db, *existing) {
|
||||
// The type that includes both this new element, and its negation
|
||||
// (or a supertype of its negation), must be simply `object`.
|
||||
self.collapse_to_object();
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if let Some(found) = found {
|
||||
let newly_added = found.insert(enum_member_to_add);
|
||||
if newly_added && found.len() == metadata.members.len() {
|
||||
self.add_in_place_impl(
|
||||
enum_member_to_add.enum_class_instance(self.db),
|
||||
seen_aliases,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
self.elements.push(UnionElement::EnumLiterals {
|
||||
enum_class,
|
||||
literals: FxOrderSet::from_iter([enum_member_to_add]),
|
||||
});
|
||||
}
|
||||
if let Some(index) = to_remove {
|
||||
self.elements.swap_remove(index);
|
||||
}
|
||||
}
|
||||
// Adding `object` to a union results in `object`.
|
||||
@@ -625,7 +708,10 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
|
||||
pub(crate) fn try_build(self) -> Option<Type<'db>> {
|
||||
let mut types = vec![];
|
||||
let mut types: FxOrderSet<Type<'db>> = FxOrderSet::with_capacity_and_hasher(
|
||||
self.elements.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
for element in self.elements {
|
||||
match element {
|
||||
UnionElement::IntLiterals(literals) => {
|
||||
@@ -637,7 +723,12 @@ impl<'db> UnionBuilder<'db> {
|
||||
UnionElement::BytesLiterals(literals) => {
|
||||
types.extend(literals.into_iter().map(Type::BytesLiteral));
|
||||
}
|
||||
UnionElement::Type(ty) => types.push(ty),
|
||||
UnionElement::EnumLiterals { literals, .. } => {
|
||||
types.extend(literals.into_iter().map(Type::EnumLiteral));
|
||||
}
|
||||
UnionElement::Type(ty) => {
|
||||
types.insert(ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.order_elements {
|
||||
@@ -646,11 +737,14 @@ impl<'db> UnionBuilder<'db> {
|
||||
match types.len() {
|
||||
0 => None,
|
||||
1 => Some(types[0]),
|
||||
_ => Some(Type::Union(UnionType::new(
|
||||
self.db,
|
||||
types.into_boxed_slice(),
|
||||
self.recursively_defined,
|
||||
))),
|
||||
_ => {
|
||||
types.shrink_to_fit();
|
||||
Some(Type::Union(UnionType::new(
|
||||
self.db,
|
||||
types,
|
||||
self.recursively_defined,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -763,7 +857,7 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
db,
|
||||
enum_member_literals(db, instance.class_literal(db), None)
|
||||
.expect("Calling `enum_member_literals` on an enum class")
|
||||
.collect::<Box<[_]>>(),
|
||||
.collect::<FxOrderSet<_>>(),
|
||||
RecursivelyDefined::No,
|
||||
)),
|
||||
seen_aliases,
|
||||
@@ -1328,6 +1422,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, Type, UnionBuilder, UnionType};
|
||||
|
||||
use crate::FxOrderSet;
|
||||
use crate::db::tests::setup_db;
|
||||
use crate::place::known_module_symbol;
|
||||
use crate::types::enums::enum_member_literals;
|
||||
@@ -1361,7 +1456,7 @@ mod tests {
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
|
||||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
assert_eq!(union.elements(&db), &FxOrderSet::from_iter([t0, t1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user