Compare commits
42 Commits
0.14.7
...
gankra/stm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0c31d0c59 | ||
|
|
7ca384d0de | ||
|
|
05d053376b | ||
|
|
ac2552b11b | ||
|
|
644096ea8a | ||
|
|
015ab9e576 | ||
|
|
cf4196466c | ||
|
|
2182c750db | ||
|
|
72304b01eb | ||
|
|
ec854c7199 | ||
|
|
edc6ed5077 | ||
|
|
f052bd644c | ||
|
|
bc44dc2afb | ||
|
|
52f59c5c39 | ||
|
|
53299cbff4 | ||
|
|
3738ab1c46 | ||
|
|
b4f618e180 | ||
|
|
a561e6659d | ||
|
|
0e651b50b7 | ||
|
|
116fd7c7af | ||
|
|
5358ddae88 | ||
|
|
3a11e714c6 | ||
|
|
a2096ee2cb | ||
|
|
2e229aa8cb | ||
|
|
c2773b4c6f | ||
|
|
bc6517a807 | ||
|
|
4686c36079 | ||
|
|
a6cbc138d2 | ||
|
|
846df40a6e | ||
|
|
c61e885527 | ||
|
|
13af584428 | ||
|
|
984480a586 | ||
|
|
aef056954b | ||
|
|
5265af4eee | ||
|
|
5b32908920 | ||
|
|
d8d1464d96 | ||
|
|
e7beb7e1f4 | ||
|
|
b02e8212c9 | ||
|
|
69ace00210 | ||
|
|
d40590c8f9 | ||
|
|
b2387f4eab | ||
|
|
8795d9f0cb |
@@ -7,10 +7,6 @@ serial = { max-threads = 1 }
|
||||
filter = 'binary(file_watching)'
|
||||
test-group = 'serial'
|
||||
|
||||
[[profile.default.overrides]]
|
||||
filter = 'binary(e2e)'
|
||||
test-group = 'serial'
|
||||
|
||||
[profile.ci]
|
||||
# Print out output for failing tests as soon as they fail, and also at the end
|
||||
# of the run (for easy scrollability).
|
||||
|
||||
16
.github/workflows/build-binaries.yml
vendored
16
.github/workflows/build-binaries.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
@@ -72,7 +72,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -114,7 +114,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: arm64
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: ${{ matrix.platform.arch }}
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -300,7 +300,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
@@ -365,7 +365,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
@@ -431,7 +431,7 @@ jobs:
|
||||
with:
|
||||
submodules: recursive
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
- name: "Prep README.md"
|
||||
|
||||
88
.github/workflows/ci.yaml
vendored
88
.github/workflows/ci.yaml
vendored
@@ -230,7 +230,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -252,7 +252,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -261,11 +261,11 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: "Install uv"
|
||||
@@ -315,7 +315,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -323,7 +323,7 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
@@ -350,13 +350,13 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install uv"
|
||||
@@ -378,7 +378,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -415,7 +415,7 @@ jobs:
|
||||
with:
|
||||
file: "Cargo.toml"
|
||||
field: "workspace.package.rust-version"
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -439,7 +439,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "fuzz -> target"
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
@@ -448,7 +448,7 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo-binstall"
|
||||
uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
|
||||
uses: cargo-bins/cargo-binstall@3fc81674af4165a753833a94cae9f91d8849049f # v1.16.2
|
||||
- name: "Install cargo-fuzz"
|
||||
# Download the latest version from quick install and not the github releases because github releases only has MUSL targets.
|
||||
run: cargo binstall cargo-fuzz --force --disable-strategies crate-meta-data --no-confirm
|
||||
@@ -467,7 +467,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: false
|
||||
@@ -498,7 +498,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
@@ -547,7 +547,7 @@ jobs:
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: false
|
||||
@@ -643,7 +643,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -688,7 +688,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: cargo-bins/cargo-binstall@ae04fb5e853ae6cd3ad7de4a1d554a8b646d12aa # v1.15.11
|
||||
- uses: cargo-bins/cargo-binstall@3fc81674af4165a753833a94cae9f91d8849049f # v1.16.2
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo shear
|
||||
|
||||
@@ -702,7 +702,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -723,11 +723,11 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Prep README.md"
|
||||
@@ -753,7 +753,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
@@ -785,7 +785,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Add SSH key"
|
||||
@@ -829,7 +829,7 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: "Install Rust toolchain"
|
||||
@@ -857,7 +857,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
shared-key: ruff-linux-debug
|
||||
save-if: false
|
||||
@@ -875,7 +875,7 @@ jobs:
|
||||
repository: "astral-sh/ruff-lsp"
|
||||
path: ruff-lsp
|
||||
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
# installation fails on 3.13 and newer
|
||||
python-version: "3.12"
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
@@ -918,7 +918,7 @@ jobs:
|
||||
cache-dependency-path: playground/package-lock.json
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Install Node dependencies"
|
||||
run: npm ci
|
||||
run: npm ci --ignore-scripts
|
||||
working-directory: playground
|
||||
- name: "Build playgrounds"
|
||||
run: npm run dev:wasm
|
||||
@@ -942,13 +942,16 @@ jobs:
|
||||
needs.determine_changes.outputs.linter == 'true'
|
||||
)
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read # required for actions/checkout
|
||||
id-token: write # required for OIDC authentication with CodSpeed
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
@@ -957,7 +960,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -965,11 +968,10 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench formatter --bench lexer --bench linter --bench parser
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
|
||||
with:
|
||||
mode: instrumentation
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
benchmarks-instrumented-ty:
|
||||
name: "benchmarks instrumented (ty)"
|
||||
@@ -982,13 +984,16 @@ jobs:
|
||||
needs.determine_changes.outputs.ty == 'true'
|
||||
)
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read # required for actions/checkout
|
||||
id-token: write # required for OIDC authentication with CodSpeed
|
||||
steps:
|
||||
- name: "Checkout Branch"
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
@@ -997,7 +1002,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1005,11 +1010,10 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,instrumented" --profile profiling --no-default-features -p ruff_benchmark --bench ty
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
|
||||
with:
|
||||
mode: instrumentation
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
benchmarks-walltime:
|
||||
name: "benchmarks walltime (${{ matrix.benchmarks }})"
|
||||
@@ -1017,6 +1021,9 @@ jobs:
|
||||
needs: determine_changes
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.ty == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
permissions:
|
||||
contents: read # required for actions/checkout
|
||||
id-token: write # required for OIDC authentication with CodSpeed
|
||||
strategy:
|
||||
matrix:
|
||||
benchmarks:
|
||||
@@ -1028,7 +1035,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
@@ -1037,7 +1044,7 @@ jobs:
|
||||
run: rustup show
|
||||
|
||||
- name: "Install codspeed"
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-codspeed
|
||||
|
||||
@@ -1045,7 +1052,7 @@ jobs:
|
||||
run: cargo codspeed build --features "codspeed,walltime" --profile profiling --no-default-features -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@6a8e2b874c338bf81cc5e8be715ada75908d3871 # v4.3.4
|
||||
uses: CodSpeedHQ/action@346a2d8a8d9d38909abd0bc3d23f773110f076ad # v4.4.1
|
||||
env:
|
||||
# enabling walltime flamegraphs adds ~6 minutes to the CI time, and they don't
|
||||
# appear to provide much useful insight for our walltime benchmarks right now
|
||||
@@ -1054,4 +1061,3 @@ jobs:
|
||||
with:
|
||||
mode: walltime
|
||||
run: cargo codspeed run --bench ty_walltime "${{ matrix.benchmarks }}"
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
- name: Build ruff
|
||||
# A debug build means the script runs slower once it gets started,
|
||||
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
|
||||
|
||||
4
.github/workflows/mypy_primer.yaml
vendored
4
.github/workflows/mypy_primer.yaml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: true
|
||||
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
- uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
|
||||
2
.github/workflows/publish-playground.yml
vendored
2
.github/workflows/publish-playground.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
package-manager-cache: false
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Install Node dependencies"
|
||||
run: npm ci
|
||||
run: npm ci --ignore-scripts
|
||||
working-directory: playground
|
||||
- name: "Run TypeScript checks"
|
||||
run: npm run check
|
||||
|
||||
6
.github/workflows/publish-pypi.yml
vendored
6
.github/workflows/publish-pypi.yml
vendored
@@ -18,8 +18,7 @@ jobs:
|
||||
environment:
|
||||
name: release
|
||||
permissions:
|
||||
# For PyPI's trusted publishing.
|
||||
id-token: write
|
||||
id-token: write # For PyPI's trusted publishing + PEP 740 attestations
|
||||
steps:
|
||||
- name: "Install uv"
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
@@ -28,5 +27,8 @@ jobs:
|
||||
pattern: wheels-*
|
||||
path: wheels
|
||||
merge-multiple: true
|
||||
- uses: astral-sh/attest-action@2c727738cea36d6c97dd85eb133ea0e0e8fe754b # v0.0.4
|
||||
with:
|
||||
paths: wheels/*
|
||||
- name: Publish to PyPi
|
||||
run: uv publish -v wheels/*
|
||||
|
||||
2
.github/workflows/publish-ty-playground.yml
vendored
2
.github/workflows/publish-ty-playground.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
package-manager-cache: false
|
||||
- uses: jetli/wasm-bindgen-action@20b33e20595891ab1a0ed73145d8a21fc96e7c29 # v0.2.0
|
||||
- name: "Install Node dependencies"
|
||||
run: npm ci
|
||||
run: npm ci --ignore-scripts
|
||||
working-directory: playground
|
||||
- name: "Run TypeScript checks"
|
||||
run: npm run check
|
||||
|
||||
6
.github/workflows/sync_typeshed.yaml
vendored
6
.github/workflows/sync_typeshed.yaml
vendored
@@ -198,7 +198,7 @@ jobs:
|
||||
run: |
|
||||
rm "${VENDORED_TYPESHED}/pyproject.toml"
|
||||
git commit -am "Remove pyproject.toml file"
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
- name: "Install Rust toolchain"
|
||||
if: ${{ success() }}
|
||||
run: rustup show
|
||||
@@ -207,12 +207,12 @@ jobs:
|
||||
uses: rui314/setup-mold@725a8794d15fc7563f59595bd9556495c0564878 # v1
|
||||
- name: "Install cargo nextest"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
if: ${{ success() }}
|
||||
uses: taiki-e/install-action@f79fe7514db78f0a7bdba3cb6dd9c1baa7d046d9 # v2.62.56
|
||||
uses: taiki-e/install-action@3575e532701a5fc614b0c842e4119af4cc5fd16d # v2.62.60
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- name: Update snapshots
|
||||
|
||||
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
2
.github/workflows/ty-ecosystem-analyzer.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
2
.github/workflows/ty-ecosystem-report.yaml
vendored
2
.github/workflows/ty-ecosystem-report.yaml
vendored
@@ -33,7 +33,7 @@ jobs:
|
||||
with:
|
||||
enable-cache: true # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
lookup-only: false # zizmor: ignore[cache-poisoning] acceptable risk for CloudFlare pages artifact
|
||||
|
||||
2
.github/workflows/typing_conformance.yaml
vendored
2
.github/workflows/typing_conformance.yaml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
path: typing
|
||||
persist-credentials: false
|
||||
|
||||
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
|
||||
with:
|
||||
workspaces: "ruff"
|
||||
|
||||
|
||||
32
Cargo.lock
generated
32
Cargo.lock
generated
@@ -1108,7 +1108,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1763,7 +1763,7 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3570,7 +3570,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3588,7 +3588,7 @@ checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
|
||||
dependencies = [
|
||||
"boxcar",
|
||||
"compact_str",
|
||||
@@ -3612,12 +3612,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=17bc55d699565e5a1cb1bd42363b905af2f9f3e7#17bc55d699565e5a1cb1bd42363b905af2f9f3e7"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0#59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3971,7 +3971,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4216,9 +4216,9 @@ checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
@@ -4228,9 +4228,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4239,9 +4239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@@ -4283,9 +4283,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.20"
|
||||
version = "0.3.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"matchers",
|
||||
@@ -5024,7 +5024,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -146,7 +146,7 @@ regex-automata = { version = "0.4.9" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
rustc-stable-hash = { version = "0.1.2" }
|
||||
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "17bc55d699565e5a1cb1bd42363b905af2f9f3e7", default-features = false, features = [
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "59aa1075e837f5deb0d6ffb24b68fedc0f4bc5e0", default-features = false, features = [
|
||||
"compact_str",
|
||||
"macros",
|
||||
"salsa_unstable",
|
||||
|
||||
@@ -354,6 +354,13 @@ impl Diagnostic {
|
||||
Arc::make_mut(&mut self.inner).fix = Some(fix);
|
||||
}
|
||||
|
||||
/// If `fix` is `Some`, set the fix for this diagnostic.
|
||||
pub fn set_optional_fix(&mut self, fix: Option<Fix>) {
|
||||
if let Some(fix) = fix {
|
||||
self.set_fix(fix);
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the fix for this diagnostic.
|
||||
pub fn remove_fix(&mut self) {
|
||||
Arc::make_mut(&mut self.inner).fix = None;
|
||||
|
||||
@@ -149,6 +149,10 @@ impl Fix {
|
||||
&self.edits
|
||||
}
|
||||
|
||||
pub fn into_edits(self) -> Vec<Edit> {
|
||||
self.edits
|
||||
}
|
||||
|
||||
/// Return the [`Applicability`] of the [`Fix`].
|
||||
pub fn applicability(&self) -> Applicability {
|
||||
self.applicability
|
||||
|
||||
@@ -216,3 +216,15 @@ def get_items_list():
|
||||
|
||||
def get_items_set():
|
||||
return tuple({item for item in items}) or None # OK
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/21473
|
||||
tuple("") or True # SIM222
|
||||
tuple(t"") or True # OK
|
||||
tuple(0) or True # OK
|
||||
tuple(1) or True # OK
|
||||
tuple(False) or True # OK
|
||||
tuple(None) or True # OK
|
||||
tuple(...) or True # OK
|
||||
tuple(lambda x: x) or True # OK
|
||||
tuple(x for x in range(0)) or True # OK
|
||||
|
||||
@@ -157,3 +157,15 @@ print(f"{1}{''}" and "bar")
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/7127
|
||||
def f(a: "'' and 'b'"): ...
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/21473
|
||||
tuple("") and False # SIM223
|
||||
tuple(t"") and False # OK
|
||||
tuple(0) and False # OK
|
||||
tuple(1) and False # OK
|
||||
tuple(False) and False # OK
|
||||
tuple(None) and False # OK
|
||||
tuple(...) and False # OK
|
||||
tuple(lambda x: x) and False # OK
|
||||
tuple(x for x in range(0)) and False # OK
|
||||
|
||||
@@ -1144,3 +1144,23 @@ help: Replace with `(i for i in range(1))`
|
||||
208 | # https://github.com/astral-sh/ruff/issues/21136
|
||||
209 | def get_items():
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM222 [*] Use `True` instead of `... or True`
|
||||
--> SIM222.py:222:1
|
||||
|
|
||||
221 | # https://github.com/astral-sh/ruff/issues/21473
|
||||
222 | tuple("") or True # SIM222
|
||||
| ^^^^^^^^^^^^^^^^^
|
||||
223 | tuple(t"") or True # OK
|
||||
224 | tuple(0) or True # OK
|
||||
|
|
||||
help: Replace with `True`
|
||||
219 |
|
||||
220 |
|
||||
221 | # https://github.com/astral-sh/ruff/issues/21473
|
||||
- tuple("") or True # SIM222
|
||||
222 + True # SIM222
|
||||
223 | tuple(t"") or True # OK
|
||||
224 | tuple(0) or True # OK
|
||||
225 | tuple(1) or True # OK
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -1025,3 +1025,23 @@ help: Replace with `f"{''}{''}"`
|
||||
156 |
|
||||
157 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
SIM223 [*] Use `tuple("")` instead of `tuple("") and ...`
|
||||
--> SIM223.py:163:1
|
||||
|
|
||||
162 | # https://github.com/astral-sh/ruff/issues/21473
|
||||
163 | tuple("") and False # SIM223
|
||||
| ^^^^^^^^^^^^^^^^^^^
|
||||
164 | tuple(t"") and False # OK
|
||||
165 | tuple(0) and False # OK
|
||||
|
|
||||
help: Replace with `tuple("")`
|
||||
160 |
|
||||
161 |
|
||||
162 | # https://github.com/astral-sh/ruff/issues/21473
|
||||
- tuple("") and False # SIM223
|
||||
163 + tuple("") # SIM223
|
||||
164 | tuple(t"") and False # OK
|
||||
165 | tuple(0) and False # OK
|
||||
166 | tuple(1) and False # OK
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
@@ -57,7 +57,7 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
|
||||
fn_argument: &str,
|
||||
fix_enabled: bool,
|
||||
violation: impl Violation,
|
||||
applicability: Option<Applicability>,
|
||||
applicability: Applicability,
|
||||
) {
|
||||
if call.arguments.len() != 1 {
|
||||
return;
|
||||
@@ -91,18 +91,14 @@ pub(crate) fn check_os_pathlib_single_arg_calls(
|
||||
|
||||
let edit = Edit::range_replacement(replacement, range);
|
||||
|
||||
let fix = match applicability {
|
||||
Some(Applicability::Unsafe) => Fix::unsafe_edits(edit, [import_edit]),
|
||||
_ => {
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
Fix::applicable_edits(edit, [import_edit], applicability)
|
||||
}
|
||||
let applicability = match applicability {
|
||||
Applicability::DisplayOnly => Applicability::DisplayOnly,
|
||||
_ if checker.comment_ranges().intersects(range) => Applicability::Unsafe,
|
||||
_ => applicability,
|
||||
};
|
||||
|
||||
let fix = Fix::applicable_edits(edit, [import_edit], applicability);
|
||||
|
||||
Ok(fix)
|
||||
});
|
||||
}
|
||||
@@ -138,6 +134,7 @@ pub(crate) fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool
|
||||
typing::is_int(binding, semantic)
|
||||
}
|
||||
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
pub(crate) fn check_os_pathlib_two_arg_calls(
|
||||
checker: &Checker,
|
||||
call: &ExprCall,
|
||||
@@ -146,6 +143,7 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
|
||||
second_arg: &str,
|
||||
fix_enabled: bool,
|
||||
violation: impl Violation,
|
||||
applicability: Applicability,
|
||||
) {
|
||||
let range = call.range();
|
||||
let mut diagnostic = checker.report_diagnostic(violation, call.func.range());
|
||||
@@ -174,10 +172,10 @@ pub(crate) fn check_os_pathlib_two_arg_calls(
|
||||
format!("{binding}({path_code}).{attr}({second_code})")
|
||||
};
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
let applicability = match applicability {
|
||||
Applicability::DisplayOnly => Applicability::DisplayOnly,
|
||||
_ if checker.comment_ranges().intersects(range) => Applicability::Unsafe,
|
||||
_ => applicability,
|
||||
};
|
||||
|
||||
Ok(Fix::applicable_edits(
|
||||
@@ -209,3 +207,9 @@ pub(crate) fn is_argument_non_default(arguments: &Arguments, name: &str, positio
|
||||
.find_argument_value(name, position)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
}
|
||||
|
||||
/// Returns `true` if the given call is a top-level expression in its statement.
|
||||
/// This means the call's return value is not used, so return type changes don't matter.
|
||||
pub(crate) fn is_top_level_expression_call(checker: &Checker) -> bool {
|
||||
checker.semantic().current_expression_parent().is_none()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::preview::is_fix_os_getcwd_enabled;
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::preview::is_fix_os_getcwd_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::is_top_level_expression_call;
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.getcwd` and `os.getcwdb`.
|
||||
///
|
||||
@@ -37,6 +39,8 @@ use ruff_text_size::Ranged;
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
|
||||
/// from `str` or `bytes` to a `Path` object.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd)
|
||||
@@ -83,7 +87,10 @@ pub(crate) fn os_getcwd(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
checker.semantic(),
|
||||
)?;
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
// Unsafe when the fix would delete comments or change a used return value
|
||||
let applicability = if checker.comment_ranges().intersects(range)
|
||||
|| !is_top_level_expression_call(checker)
|
||||
{
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
|
||||
@@ -45,6 +45,10 @@ use crate::{FixAvailability, Violation};
|
||||
/// behaviors is required, there's no existing `pathlib` alternative. See CPython issue
|
||||
/// [#69200](https://github.com/python/cpython/issues/69200).
|
||||
///
|
||||
/// Additionally, the fix is marked as unsafe because `os.path.abspath()` returns `str` or `bytes` (`AnyStr`),
|
||||
/// while `Path.resolve()` returns a `Path` object. This change in return type can break code that uses
|
||||
/// the return value.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
|
||||
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
|
||||
@@ -85,6 +89,6 @@ pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall, segments: &[&s
|
||||
"path",
|
||||
is_fix_os_path_abspath_enabled(checker.settings()),
|
||||
OsPathAbspath,
|
||||
Some(Applicability::Unsafe),
|
||||
Applicability::Unsafe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,6 +82,6 @@ pub(crate) fn os_path_basename(checker: &Checker, call: &ExprCall, segments: &[&
|
||||
"p",
|
||||
is_fix_os_path_basename_enabled(checker.settings()),
|
||||
OsPathBasename,
|
||||
Some(Applicability::Unsafe),
|
||||
Applicability::Unsafe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,6 +42,10 @@ use crate::{FixAvailability, Violation};
|
||||
/// As a result, code relying on the exact string returned by `os.path.dirname`
|
||||
/// may behave differently after the fix.
|
||||
///
|
||||
/// Additionally, the fix is marked as unsafe because `os.path.dirname()` returns `str` or `bytes` (`AnyStr`),
|
||||
/// while `Path.parent` returns a `Path` object. This change in return type can break code that uses
|
||||
/// the return value.
|
||||
///
|
||||
/// ## Known issues
|
||||
/// While using `pathlib` can improve the readability and type safety of your code,
|
||||
/// it can be less performant than the lower-level alternatives that work directly with strings,
|
||||
@@ -82,6 +86,6 @@ pub(crate) fn os_path_dirname(checker: &Checker, call: &ExprCall, segments: &[&s
|
||||
"p",
|
||||
is_fix_os_path_dirname_enabled(checker.settings()),
|
||||
OsPathDirname,
|
||||
Some(Applicability::Unsafe),
|
||||
Applicability::Unsafe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -72,6 +73,6 @@ pub(crate) fn os_path_exists(checker: &Checker, call: &ExprCall, segments: &[&st
|
||||
"path",
|
||||
is_fix_os_path_exists_enabled(checker.settings()),
|
||||
OsPathExists,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@ use crate::{FixAvailability, Violation};
|
||||
/// directory can't be resolved: `os.path.expanduser` returns the
|
||||
/// input unchanged, while `Path.expanduser` raises `RuntimeError`.
|
||||
///
|
||||
/// Additionally, the fix is marked as unsafe because `os.path.expanduser()` returns `str` or `bytes` (`AnyStr`),
|
||||
/// while `Path.expanduser()` returns a `Path` object. This change in return type can break code that uses
|
||||
/// the return value.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
|
||||
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
|
||||
@@ -76,6 +80,6 @@ pub(crate) fn os_path_expanduser(checker: &Checker, call: &ExprCall, segments: &
|
||||
"path",
|
||||
is_fix_os_path_expanduser_enabled(checker.settings()),
|
||||
OsPathExpanduser,
|
||||
Some(Applicability::Unsafe),
|
||||
Applicability::Unsafe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -75,6 +76,6 @@ pub(crate) fn os_path_getatime(checker: &Checker, call: &ExprCall, segments: &[&
|
||||
"filename",
|
||||
is_fix_os_path_getatime_enabled(checker.settings()),
|
||||
OsPathGetatime,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -76,6 +77,6 @@ pub(crate) fn os_path_getctime(checker: &Checker, call: &ExprCall, segments: &[&
|
||||
"filename",
|
||||
is_fix_os_path_getctime_enabled(checker.settings()),
|
||||
OsPathGetctime,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -76,6 +77,6 @@ pub(crate) fn os_path_getmtime(checker: &Checker, call: &ExprCall, segments: &[&
|
||||
"filename",
|
||||
is_fix_os_path_getmtime_enabled(checker.settings()),
|
||||
OsPathGetmtime,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -76,6 +77,6 @@ pub(crate) fn os_path_getsize(checker: &Checker, call: &ExprCall, segments: &[&s
|
||||
"filename",
|
||||
is_fix_os_path_getsize_enabled(checker.settings()),
|
||||
OsPathGetsize,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -71,6 +72,6 @@ pub(crate) fn os_path_isabs(checker: &Checker, call: &ExprCall, segments: &[&str
|
||||
"s",
|
||||
is_fix_os_path_isabs_enabled(checker.settings()),
|
||||
OsPathIsabs,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -73,6 +74,6 @@ pub(crate) fn os_path_isdir(checker: &Checker, call: &ExprCall, segments: &[&str
|
||||
"s",
|
||||
is_fix_os_path_isdir_enabled(checker.settings()),
|
||||
OsPathIsdir,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -73,6 +74,6 @@ pub(crate) fn os_path_isfile(checker: &Checker, call: &ExprCall, segments: &[&st
|
||||
"path",
|
||||
is_fix_os_path_isfile_enabled(checker.settings()),
|
||||
OsPathIsfile,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -73,6 +74,6 @@ pub(crate) fn os_path_islink(checker: &Checker, call: &ExprCall, segments: &[&st
|
||||
"path",
|
||||
is_fix_os_path_islink_enabled(checker.settings()),
|
||||
OsPathIslink,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_path_samefile_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.path.samefile`.
|
||||
@@ -79,5 +81,6 @@ pub(crate) fn os_path_samefile(checker: &Checker, call: &ExprCall, segments: &[&
|
||||
"f2",
|
||||
fix_enabled,
|
||||
OsPathSamefile,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::{ExprCall, PythonVersion};
|
||||
|
||||
@@ -5,6 +6,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_readlink_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_single_arg_calls, is_keyword_only_argument_non_default,
|
||||
is_top_level_expression_call,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
|
||||
@@ -38,6 +40,8 @@ use crate::{FixAvailability, Violation};
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
|
||||
/// from `str` or `bytes` (`AnyStr`) to a `Path` object.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
|
||||
@@ -82,6 +86,13 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
return;
|
||||
}
|
||||
|
||||
let applicability = if !is_top_level_expression_call(checker) {
|
||||
// Unsafe because the return type changes (str/bytes -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
check_os_pathlib_single_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
@@ -89,6 +100,6 @@ pub(crate) fn os_readlink(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
"path",
|
||||
is_fix_os_readlink_enabled(checker.settings()),
|
||||
OsReadlink,
|
||||
None,
|
||||
applicability,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -84,6 +85,6 @@ pub(crate) fn os_remove(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
"path",
|
||||
is_fix_os_remove_enabled(checker.settings()),
|
||||
OsRemove,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_rename_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_call,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.rename`.
|
||||
@@ -38,6 +40,8 @@ use ruff_python_ast::ExprCall;
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
|
||||
/// from `None` to a `Path` object.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
|
||||
@@ -87,5 +91,22 @@ pub(crate) fn os_rename(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
|
||||
);
|
||||
|
||||
check_os_pathlib_two_arg_calls(checker, call, "rename", "src", "dst", fix_enabled, OsRename);
|
||||
// Unsafe when the fix would delete comments or change a used return value
|
||||
let applicability = if !is_top_level_expression_call(checker) {
|
||||
// Unsafe because the return type changes (None -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
"rename",
|
||||
"src",
|
||||
"dst",
|
||||
fix_enabled,
|
||||
OsRename,
|
||||
applicability,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::preview::is_fix_os_replace_enabled;
|
||||
use crate::rules::flake8_use_pathlib::helpers::{
|
||||
check_os_pathlib_two_arg_calls, has_unknown_keywords_or_starred_expr,
|
||||
is_keyword_only_argument_non_default,
|
||||
is_keyword_only_argument_non_default, is_top_level_expression_call,
|
||||
};
|
||||
use crate::{FixAvailability, Violation};
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `os.replace`.
|
||||
@@ -41,6 +43,8 @@ use ruff_python_ast::ExprCall;
|
||||
///
|
||||
/// ## Fix Safety
|
||||
/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression.
|
||||
/// Additionally, the fix is marked as unsafe when the return value is used because the type changes
|
||||
/// from `None` to a `Path` object.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
|
||||
@@ -90,6 +94,14 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
&["src", "dst", "src_dir_fd", "dst_dir_fd"],
|
||||
);
|
||||
|
||||
// Unsafe when the fix would delete comments or change a used return value
|
||||
let applicability = if !is_top_level_expression_call(checker) {
|
||||
// Unsafe because the return type changes (None -> Path)
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
check_os_pathlib_two_arg_calls(
|
||||
checker,
|
||||
call,
|
||||
@@ -98,5 +110,6 @@ pub(crate) fn os_replace(checker: &Checker, call: &ExprCall, segments: &[&str])
|
||||
"dst",
|
||||
fix_enabled,
|
||||
OsReplace,
|
||||
applicability,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -84,6 +85,6 @@ pub(crate) fn os_rmdir(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
"path",
|
||||
is_fix_os_rmdir_enabled(checker.settings()),
|
||||
OsRmdir,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruff_diagnostics::Applicability;
|
||||
use ruff_macros::{ViolationMetadata, derive_message_formats};
|
||||
use ruff_python_ast::ExprCall;
|
||||
|
||||
@@ -84,6 +85,6 @@ pub(crate) fn os_unlink(checker: &Checker, call: &ExprCall, segments: &[&str]) {
|
||||
"path",
|
||||
is_fix_os_unlink_enabled(checker.settings()),
|
||||
OsUnlink,
|
||||
None,
|
||||
Applicability::Safe,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1322,14 +1322,22 @@ impl Truthiness {
|
||||
&& arguments.keywords.is_empty()
|
||||
{
|
||||
// Ex) `list([1, 2, 3])`
|
||||
// For tuple(generator), we can't determine statically if the result will
|
||||
// be empty or not, so return Unknown. The generator itself is truthy, but
|
||||
// tuple(empty_generator) is falsy. ListComp and SetComp are handled by
|
||||
// recursing into Self::from_expr below, which returns Unknown for them.
|
||||
if argument.is_generator_expr() {
|
||||
Self::Unknown
|
||||
} else {
|
||||
Self::from_expr(argument, is_builtin)
|
||||
match argument {
|
||||
// Return Unknown for types with definite truthiness that might
|
||||
// result in empty iterables (t-strings and generators) or will
|
||||
// raise a type error (non-iterable types like numbers, booleans,
|
||||
// None, etc.).
|
||||
Expr::NumberLiteral(_)
|
||||
| Expr::BooleanLiteral(_)
|
||||
| Expr::NoneLiteral(_)
|
||||
| Expr::EllipsisLiteral(_)
|
||||
| Expr::TString(_)
|
||||
| Expr::Lambda(_)
|
||||
| Expr::Generator(_) => Self::Unknown,
|
||||
// Recurse for all other types - collections, comprehensions, variables, etc.
|
||||
// StringLiteral, FString, and BytesLiteral recurse because Self::from_expr
|
||||
// correctly handles their truthiness (checking if empty or not).
|
||||
_ => Self::from_expr(argument, is_builtin),
|
||||
}
|
||||
} else {
|
||||
Self::Unknown
|
||||
|
||||
@@ -74,7 +74,7 @@ def f(): # a
|
||||
The other option is to use the playground (also check the playground README):
|
||||
|
||||
```shell
|
||||
cd playground && npm install && npm run dev:wasm && npm run dev
|
||||
cd playground && npm ci --ignore-scripts && npm run dev:wasm && npm run dev
|
||||
```
|
||||
|
||||
Run`npm run dev:wasm` and reload the page in the browser to refresh.
|
||||
|
||||
204
crates/ty/docs/rules.md
generated
204
crates/ty/docs/rules.md
generated
@@ -39,7 +39,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20call-non-callable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L134" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L135" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ Calling a non-callable object will raise a `TypeError` at runtime.
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20conflicting-argument-forms" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L178" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L179" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ f(int) # error
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20conflicting-declarations" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L204" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L205" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ a = 1
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20conflicting-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L229" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L230" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20cyclic-class-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L255" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L256" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ class B(A): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20cyclic-type-alias-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L281" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L282" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ type B = A
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20duplicate-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L342" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L343" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -245,7 +245,7 @@ class B(A, A): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20duplicate-kw-only" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L363" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L364" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -357,7 +357,7 @@ def test(): -> "Literal[5]":
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20inconsistent-mro" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L567" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L590" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -387,7 +387,7 @@ class C(A, B): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20index-out-of-bounds" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L591" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L614" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -413,7 +413,7 @@ t[3] # IndexError: tuple index out of range
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.12">0.0.1-alpha.12</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20instance-layout-conflict" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L395" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L396" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -502,7 +502,7 @@ an atypical memory layout.
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-argument-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L645" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L668" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -529,7 +529,7 @@ func("foo") # error: [invalid-argument-type]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-assignment" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L685" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L708" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -557,7 +557,7 @@ a: int = ''
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-attribute-access" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1948" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1998" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -591,7 +591,7 @@ C.instance_var = 3 # error: Cannot assign to instance variable
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-await" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L707" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L730" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -627,7 +627,7 @@ asyncio.run(main())
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L737" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L760" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -651,7 +651,7 @@ class A(42): ... # error: [invalid-base]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-context-manager" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L788" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L811" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -678,7 +678,7 @@ with 1:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-declaration" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L809" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -707,7 +707,7 @@ a: str
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-exception-caught" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L832" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L855" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -751,7 +751,7 @@ except ZeroDivisionError:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.28">0.0.1-alpha.28</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-explicit-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1645" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1668" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -793,7 +793,7 @@ class D(A):
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-generic-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L868" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L891" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -826,7 +826,7 @@ class C[U](Generic[T]): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.17">0.0.1-alpha.17</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L612" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L635" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -865,7 +865,7 @@ carol = Person(name="Carol", age=25) # typo!
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-legacy-type-variable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L894" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L917" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -900,7 +900,7 @@ def f(t: TypeVar("U")): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-metaclass" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L991" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1014" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -934,7 +934,7 @@ class B(metaclass=f): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-method-override" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2076" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2126" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1041,7 +1041,7 @@ Correct use of `@override` is enforced by ty's `invalid-explicit-override` rule.
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.19">0.0.1-alpha.19</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-named-tuple" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L541" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L542" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1052,7 +1052,8 @@ Checks for invalidly defined `NamedTuple` classes.
|
||||
**Why is this bad?**
|
||||
|
||||
An invalidly defined `NamedTuple` class may lead to the type checker
|
||||
drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
|
||||
drawing incorrect conclusions. It may also lead to `TypeError`s or
|
||||
`AttributeError`s at runtime.
|
||||
|
||||
**Examples**
|
||||
|
||||
@@ -1067,13 +1068,34 @@ in a class's bases list.
|
||||
TypeError: can only inherit from a NamedTuple type and Generic
|
||||
```
|
||||
|
||||
Further, `NamedTuple` field names cannot start with an underscore:
|
||||
|
||||
```pycon
|
||||
>>> from typing import NamedTuple
|
||||
>>> class Foo(NamedTuple):
|
||||
... _bar: int
|
||||
ValueError: Field names cannot start with an underscore: '_bar'
|
||||
```
|
||||
|
||||
`NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,
|
||||
`_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes
|
||||
without a type annotation will raise an `AttributeError` at runtime.
|
||||
|
||||
```pycon
|
||||
>>> from typing import NamedTuple
|
||||
>>> class Foo(NamedTuple):
|
||||
... x: int
|
||||
... _asdict = 42
|
||||
AttributeError: Cannot overwrite NamedTuple attribute _asdict
|
||||
```
|
||||
|
||||
## `invalid-newtype`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/1.0.0">1.0.0</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-newtype" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L967" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L990" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1103,7 +1125,7 @@ Baz = NewType("Baz", int | str) # error: invalid base for `typing.NewType`
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1018" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1041" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1153,7 +1175,7 @@ def foo(x: int) -> int: ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-parameter-default" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1117" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1140" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1179,7 +1201,7 @@ def f(a: int = ''): ...
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-paramspec" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L922" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L945" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1210,7 +1232,7 @@ P2 = ParamSpec("S2") # error: ParamSpec name must match the variable it's assig
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-protocol" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L477" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L478" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1244,7 +1266,7 @@ TypeError: Protocols can only inherit from other protocols, got <class 'int'>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-raise" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1137" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1160" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1293,7 +1315,7 @@ def g():
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-return-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L666" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L689" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1318,7 +1340,7 @@ def func() -> int:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-super-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1180" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1203" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1376,7 +1398,7 @@ TODO #14889
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.6">0.0.1-alpha.6</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-alias-type" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L946" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L969" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1403,7 +1425,7 @@ NewAlias = TypeAliasType(get_name(), int) # error: TypeAliasType name mus
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1412" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1435" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1450,7 +1472,7 @@ Bar[int] # error: too few arguments
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-type-checking-constant" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1219" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1242" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1480,7 +1502,7 @@ TYPE_CHECKING = ''
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-type-form" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1243" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1266" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1510,7 +1532,7 @@ b: Annotated[int] # `Annotated` expects at least two arguments
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1295" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1318" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1544,7 +1566,7 @@ f(10) # Error
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.11">0.0.1-alpha.11</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20invalid-type-guard-definition" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1267" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1290" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1578,7 +1600,7 @@ class C:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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-type-variable-constraints" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1323" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1346" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1613,7 +1635,7 @@ T = TypeVar('T', bound=str) # valid bound TypeVar
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20missing-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1352" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1375" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1638,7 +1660,7 @@ func() # TypeError: func() missing 1 required positional argument: 'x'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20missing-typed-dict-key" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2049" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2099" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1671,7 +1693,7 @@ alice["age"] # KeyError
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20no-matching-overload" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1371" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1700,7 +1722,7 @@ func("string") # error: [no-matching-overload]
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20non-subscriptable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1394" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1417" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1724,7 +1746,7 @@ Subscripting an object that does not support it will raise a `TypeError` at runt
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20not-iterable" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1453" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1476" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1750,7 +1772,7 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.29">0.0.1-alpha.29</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20override-of-final-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1641" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1783,7 +1805,7 @@ class B(A):
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20parameter-already-assigned" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1504" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1527" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1810,7 +1832,7 @@ f(1, x=2) # Error raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Added in <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20positional-only-parameter-as-kwarg" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1802" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1852" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1868,7 +1890,7 @@ def test(): -> "int":
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20static-assert-error" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1924" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1974" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1898,7 +1920,7 @@ static_assert(int(2.0 * 3.0) == 6) # error: does not have a statically known tr
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20subclass-of-final-class" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1595" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1618" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1921,13 +1943,47 @@ class A: ...
|
||||
class B(A): ... # Error raised here
|
||||
```
|
||||
|
||||
## `super-call-in-named-tuple-method`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</code></a> ·
|
||||
Preview (since <a href="https://github.com/astral-sh/ty/releases/tag/0.0.1-alpha.30">0.0.1-alpha.30</a>) ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20super-call-in-named-tuple-method" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1786" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for calls to `super()` inside methods of `NamedTuple` classes.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime.
|
||||
|
||||
**Examples**
|
||||
|
||||
```python
|
||||
from typing import NamedTuple
|
||||
|
||||
class F(NamedTuple):
|
||||
x: int
|
||||
|
||||
def method(self):
|
||||
super() # error: super() is not supported in methods of NamedTuple classes
|
||||
```
|
||||
|
||||
**References**
|
||||
|
||||
- [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
|
||||
|
||||
## `too-many-positional-arguments`
|
||||
|
||||
<small>
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20too-many-positional-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1703" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1726" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1954,7 +2010,7 @@ f("foo") # Error raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20type-assertion-failure" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1681" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1704" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -1982,7 +2038,7 @@ def _(x: int):
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20unavailable-implicit-super-arguments" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1724" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1747" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2028,7 +2084,7 @@ class A:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20unknown-argument" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1781" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1831" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2055,7 +2111,7 @@ f(x=1, y=2) # Error raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20unresolved-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1823" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1873" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2083,7 +2139,7 @@ A().foo # AttributeError: 'A' object has no attribute 'foo'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20unresolved-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1845" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1895" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2108,7 +2164,7 @@ import foo # ModuleNotFoundError: No module named 'foo'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1864" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1914" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2133,7 +2189,7 @@ print(x) # NameError: name 'x' is not defined
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20unsupported-bool-conversion" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1473" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1496" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2170,7 +2226,7 @@ b1 < b2 < b1 # exception raised here
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20unsupported-operator" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1883" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1933" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2198,7 +2254,7 @@ A() + A() # TypeError: unsupported operand type(s) for +: 'A' and 'A'
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'error'."><code>error</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%20zero-stepsize-in-slice" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1905" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1955" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2223,7 +2279,7 @@ l[1:10:0] # ValueError: slice step cannot be zero
|
||||
Default level: <a href="../rules.md#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.20">0.0.1-alpha.20</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20ambiguous-protocol-member" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L506" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L507" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2264,7 +2320,7 @@ class SubProto(BaseProto, Protocol):
|
||||
Default level: <a href="../rules.md#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.16">0.0.1-alpha.16</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20deprecated" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L321" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L322" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2352,7 +2408,7 @@ a = 20 / 0 # type: ignore
|
||||
Default level: <a href="../rules.md#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.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-attribute" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1525" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1548" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2380,7 +2436,7 @@ A.c # AttributeError: type object 'A' has no attribute 'c'
|
||||
Default level: <a href="../rules.md#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.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-implicit-call" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L152" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L153" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2412,7 +2468,7 @@ A()[0] # TypeError: 'A' object is not subscriptable
|
||||
Default level: <a href="../rules.md#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.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20possibly-missing-import" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1547" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1570" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2444,7 +2500,7 @@ from module import a # ImportError: cannot import name 'a' from 'module'
|
||||
Default level: <a href="../rules.md#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%20redundant-cast" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1976" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2026" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2471,7 +2527,7 @@ cast(int, f()) # Redundant
|
||||
Default level: <a href="../rules.md#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%20undefined-reveal" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1763" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1813" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2495,7 +2551,7 @@ reveal_type(1) # NameError: name 'reveal_type' is not defined
|
||||
Default level: <a href="../rules.md#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.15">0.0.1-alpha.15</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unresolved-global" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1997" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L2047" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2553,7 +2609,7 @@ def g():
|
||||
Default level: <a href="../rules.md#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.7">0.0.1-alpha.7</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20unsupported-base" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L755" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L778" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2592,7 +2648,7 @@ class D(C): ... # error: [unsupported-base]
|
||||
Default level: <a href="../rules.md#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.22">0.0.1-alpha.22</a> ·
|
||||
<a href="https://github.com/astral-sh/ty/issues?q=sort%3Aupdated-desc%20is%3Aissue%20is%3Aopen%20useless-overload-body" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1061" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1084" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2655,7 +2711,7 @@ def foo(x: int | str) -> int | str:
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</code></a> ·
|
||||
Preview (since <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%20division-by-zero" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L303" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L304" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
@@ -2679,7 +2735,7 @@ Dividing by zero raises a `ZeroDivisionError` at runtime.
|
||||
Default level: <a href="../rules.md#rule-levels" title="This lint has a default level of 'ignore'."><code>ignore</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%20possibly-unresolved-reference" target="_blank">Related issues</a> ·
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1573" target="_blank">View source</a>
|
||||
<a href="https://github.com/astral-sh/ruff/blob/main/crates%2Fty_python_semantic%2Fsrc%2Ftypes%2Fdiagnostic.rs#L1596" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
|
||||
@@ -25,4 +25,4 @@ scope-simple-long-identifier,main.py,0,1
|
||||
tstring-completions,main.py,0,1
|
||||
ty-extensions-lower-stdlib,main.py,0,8
|
||||
type-var-typing-over-ast,main.py,0,3
|
||||
type-var-typing-over-ast,main.py,1,278
|
||||
type-var-typing-over-ast,main.py,1,275
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use ruff_db::files::File;
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::{Module, all_modules};
|
||||
use ty_python_semantic::{Module, ModuleName, all_modules, resolve_real_shadowable_module};
|
||||
|
||||
use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
|
||||
|
||||
@@ -8,12 +8,20 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
|
||||
///
|
||||
/// Returns symbols from all files in the workspace and dependencies, filtered
|
||||
/// by the query.
|
||||
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
|
||||
pub fn all_symbols<'db>(
|
||||
db: &'db dyn Db,
|
||||
importing_from: File,
|
||||
query: &QueryPattern,
|
||||
) -> Vec<AllSymbolInfo<'db>> {
|
||||
// If the query is empty, return immediately to avoid expensive file scanning
|
||||
if query.will_match_everything() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
|
||||
let is_typing_extensions_available = importing_from.is_stub(db)
|
||||
|| resolve_real_shadowable_module(db, &typing_extensions).is_some();
|
||||
|
||||
let results = std::sync::Mutex::new(Vec::new());
|
||||
{
|
||||
let modules = all_modules(db);
|
||||
@@ -28,6 +36,11 @@ pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolI
|
||||
let Some(file) = module.file(&*db) else {
|
||||
continue;
|
||||
};
|
||||
// TODO: also make it available in `TYPE_CHECKING` blocks
|
||||
// (we'd need https://github.com/astral-sh/ty/issues/1553 to do this well)
|
||||
if !is_typing_extensions_available && module.name(&*db) == &typing_extensions {
|
||||
continue;
|
||||
}
|
||||
s.spawn(move |_| {
|
||||
for (_, symbol) in symbols_for_file_global_only(&*db, file).search(query) {
|
||||
// It seems like we could do better here than
|
||||
@@ -143,7 +156,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
|
||||
|
||||
impl CursorTest {
|
||||
fn all_symbols(&self, query: &str) -> String {
|
||||
let symbols = all_symbols(&self.db, &QueryPattern::fuzzy(query));
|
||||
let symbols = all_symbols(&self.db, self.cursor.file, &QueryPattern::fuzzy(query));
|
||||
|
||||
if symbols.is_empty() {
|
||||
return "No symbols found".to_string();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::{completion, find_node::covering_node};
|
||||
|
||||
use ruff_db::{files::File, parsed::parsed_module};
|
||||
use ruff_diagnostics::Edit;
|
||||
use ruff_text_size::TextRange;
|
||||
use ty_project::Db;
|
||||
use ty_python_semantic::create_suppression_fix;
|
||||
use ty_python_semantic::types::UNRESOLVED_REFERENCE;
|
||||
|
||||
/// A `QuickFix` Code Action
|
||||
@@ -18,26 +20,501 @@ pub fn code_actions(
|
||||
file: File,
|
||||
diagnostic_range: TextRange,
|
||||
diagnostic_id: &str,
|
||||
) -> Option<Vec<QuickFix>> {
|
||||
) -> Vec<QuickFix> {
|
||||
let registry = db.lint_registry();
|
||||
let Ok(lint_id) = registry.get(diagnostic_id) else {
|
||||
return None;
|
||||
return Vec::new();
|
||||
};
|
||||
if lint_id.name() == UNRESOLVED_REFERENCE.name() {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
|
||||
let symbol = &node.expr_name()?.id;
|
||||
|
||||
let fixes = completion::missing_imports(db, file, &parsed, symbol, node)
|
||||
let mut actions = Vec::new();
|
||||
|
||||
if lint_id.name() == UNRESOLVED_REFERENCE.name()
|
||||
&& let Some(import_quick_fix) = create_import_symbol_quick_fix(db, file, diagnostic_range)
|
||||
{
|
||||
actions.extend(import_quick_fix);
|
||||
}
|
||||
|
||||
actions.push(QuickFix {
|
||||
title: format!("Ignore '{}' for this line", lint_id.name()),
|
||||
edits: create_suppression_fix(db, file, lint_id, diagnostic_range).into_edits(),
|
||||
preferred: false,
|
||||
});
|
||||
|
||||
actions
|
||||
}
|
||||
|
||||
fn create_import_symbol_quick_fix(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
diagnostic_range: TextRange,
|
||||
) -> Option<impl Iterator<Item = QuickFix>> {
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let node = covering_node(parsed.syntax().into(), diagnostic_range).node();
|
||||
let symbol = &node.expr_name()?.id;
|
||||
|
||||
Some(
|
||||
completion::missing_imports(db, file, &parsed, symbol, node)
|
||||
.into_iter()
|
||||
.map(|import| QuickFix {
|
||||
title: import.label,
|
||||
edits: vec![import.edit],
|
||||
preferred: true,
|
||||
})
|
||||
.collect();
|
||||
Some(fixes)
|
||||
} else {
|
||||
None
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::code_actions;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::{
|
||||
diagnostic::{
|
||||
Annotation, Diagnostic, DiagnosticFormat, DiagnosticId, DisplayDiagnosticConfig,
|
||||
LintName, Span, SubDiagnostic,
|
||||
},
|
||||
files::{File, system_path_to_file},
|
||||
system::{DbWithWritableSystem, SystemPathBuf},
|
||||
};
|
||||
use ruff_diagnostics::Fix;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use ty_project::ProjectMetadata;
|
||||
use ty_python_semantic::{lint::LintMetadata, types::UNRESOLVED_REFERENCE};
|
||||
|
||||
#[test]
|
||||
fn add_ignore() {
|
||||
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10"#);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:1:5
|
||||
|
|
||||
1 | b = a / 10
|
||||
| ^
|
||||
|
|
||||
- b = a / 10
|
||||
1 + b = a / 10 # ty:ignore[unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ignore_existing_comment() {
|
||||
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 # fmt: off"#);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:1:5
|
||||
|
|
||||
1 | b = a / 10 # fmt: off
|
||||
| ^
|
||||
|
|
||||
- b = a / 10 # fmt: off
|
||||
1 + b = a / 10 # fmt: off # ty:ignore[unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_ignore_trailing_whitespace() {
|
||||
let test = CodeActionTest::with_source(r#"b = <START>a<END> / 10 "#);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:1:5
|
||||
|
|
||||
1 | b = a / 10
|
||||
| ^
|
||||
|
|
||||
- b = a / 10
|
||||
1 + b = a / 10 # ty:ignore[unresolved-reference]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_existing_ignore() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = <START>a<END> / 0 # ty:ignore[division-by-zero]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero]
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
3 |
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_existing_ignore_trailing_comma() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = <START>a<END> / 0 # ty:ignore[division-by-zero,]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero,]
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero,]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
3 |
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_existing_ignore_trailing_whitespace() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = <START>a<END> / 0 # ty:ignore[division-by-zero ]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero ]
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero ]
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero, unresolved-reference ]
|
||||
3 |
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_existing_ignore_with_reason() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = <START>a<END> / 0 # ty:ignore[division-by-zero] some explanation
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||
| ^
|
||||
|
|
||||
1 |
|
||||
- b = a / 0 # ty:ignore[division-by-zero] some explanation
|
||||
2 + b = a / 0 # ty:ignore[division-by-zero] some explanation # ty:ignore[unresolved-reference]
|
||||
3 |
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_existing_ignore_start_line() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = (
|
||||
<START>a # ty:ignore[division-by-zero]
|
||||
/
|
||||
0<END>
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:21
|
||||
|
|
||||
2 | b = (
|
||||
3 | / a # ty:ignore[division-by-zero]
|
||||
4 | | /
|
||||
5 | | 0
|
||||
| |_____________________^
|
||||
6 | )
|
||||
|
|
||||
1 |
|
||||
2 | b = (
|
||||
- a # ty:ignore[division-by-zero]
|
||||
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||
4 | /
|
||||
5 | 0
|
||||
6 | )
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_existing_ignore_end_line() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = (
|
||||
<START>a
|
||||
/
|
||||
0<END> # ty:ignore[division-by-zero]
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:21
|
||||
|
|
||||
2 | b = (
|
||||
3 | / a
|
||||
4 | | /
|
||||
5 | | 0 # ty:ignore[division-by-zero]
|
||||
| |_____________________^
|
||||
6 | )
|
||||
|
|
||||
2 | b = (
|
||||
3 | a
|
||||
4 | /
|
||||
- 0 # ty:ignore[division-by-zero]
|
||||
5 + 0 # ty:ignore[division-by-zero, unresolved-reference]
|
||||
6 | )
|
||||
7 |
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_existing_ignores() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = (
|
||||
<START>a # ty:ignore[division-by-zero]
|
||||
/
|
||||
0<END> # ty:ignore[division-by-zero]
|
||||
)
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:21
|
||||
|
|
||||
2 | b = (
|
||||
3 | / a # ty:ignore[division-by-zero]
|
||||
4 | | /
|
||||
5 | | 0 # ty:ignore[division-by-zero]
|
||||
| |_____________________^
|
||||
6 | )
|
||||
|
|
||||
1 |
|
||||
2 | b = (
|
||||
- a # ty:ignore[division-by-zero]
|
||||
3 + a # ty:ignore[division-by-zero, unresolved-reference]
|
||||
4 | /
|
||||
5 | 0 # ty:ignore[division-by-zero]
|
||||
6 | )
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_interpolated_string() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = f"""
|
||||
{<START>a<END>}
|
||||
more text
|
||||
"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:3:18
|
||||
|
|
||||
2 | b = f"""
|
||||
3 | {a}
|
||||
| ^
|
||||
4 | more text
|
||||
5 | """
|
||||
|
|
||||
2 | b = f"""
|
||||
3 | {a}
|
||||
4 | more text
|
||||
- """
|
||||
5 + """ # ty:ignore[unresolved-reference]
|
||||
6 |
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_multiline_interpolation() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = f"""
|
||||
{
|
||||
<START>a<END>
|
||||
}
|
||||
more text
|
||||
"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:4:17
|
||||
|
|
||||
2 | b = f"""
|
||||
3 | {
|
||||
4 | a
|
||||
| ^
|
||||
5 | }
|
||||
6 | more text
|
||||
|
|
||||
1 |
|
||||
2 | b = f"""
|
||||
3 | {
|
||||
- a
|
||||
4 + a # ty:ignore[unresolved-reference]
|
||||
5 | }
|
||||
6 | more text
|
||||
7 | """
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_followed_by_multiline_string() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = <START>a<END> + """
|
||||
more text
|
||||
"""
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | b = a + """
|
||||
| ^
|
||||
3 | more text
|
||||
4 | """
|
||||
|
|
||||
1 |
|
||||
2 | b = a + """
|
||||
3 | more text
|
||||
- """
|
||||
4 + """ # ty:ignore[unresolved-reference]
|
||||
5 |
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_code_followed_by_continuation() {
|
||||
let test = CodeActionTest::with_source(
|
||||
r#"
|
||||
b = <START>a<END> \
|
||||
+ "test"
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.code_actions(&UNRESOLVED_REFERENCE), @r#"
|
||||
info[code-action]: Ignore 'unresolved-reference' for this line
|
||||
--> main.py:2:17
|
||||
|
|
||||
2 | b = a \
|
||||
| ^
|
||||
3 | + "test"
|
||||
|
|
||||
1 |
|
||||
2 | b = a \
|
||||
- + "test"
|
||||
3 + + "test" # ty:ignore[unresolved-reference]
|
||||
4 |
|
||||
"#);
|
||||
}
|
||||
|
||||
pub(super) struct CodeActionTest {
|
||||
pub(super) db: ty_project::TestDb,
|
||||
pub(super) file: File,
|
||||
pub(super) diagnostic_range: TextRange,
|
||||
}
|
||||
|
||||
impl CodeActionTest {
|
||||
pub(super) fn with_source(source: &str) -> Self {
|
||||
let mut db = ty_project::TestDb::new(ProjectMetadata::new(
|
||||
"test".into(),
|
||||
SystemPathBuf::from("/"),
|
||||
));
|
||||
|
||||
db.init_program().unwrap();
|
||||
|
||||
let mut cleansed = source.to_string();
|
||||
|
||||
let start = cleansed
|
||||
.find("<START>")
|
||||
.expect("source text should contain a `<START>` marker");
|
||||
cleansed.replace_range(start..start + "<START>".len(), "");
|
||||
|
||||
let end = cleansed
|
||||
.find("<END>")
|
||||
.expect("source text should contain a `<END>` marker");
|
||||
|
||||
cleansed.replace_range(end..end + "<END>".len(), "");
|
||||
|
||||
assert!(start <= end, "<START> marker should be before <END> marker");
|
||||
|
||||
db.write_file("main.py", cleansed)
|
||||
.expect("write to memory file system to be successful");
|
||||
|
||||
let file = system_path_to_file(&db, "main.py").expect("newly written file to existing");
|
||||
|
||||
Self {
|
||||
db,
|
||||
file,
|
||||
diagnostic_range: TextRange::new(
|
||||
TextSize::try_from(start).unwrap(),
|
||||
TextSize::try_from(end).unwrap(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn code_actions(&self, lint: &'static LintMetadata) -> String {
|
||||
use std::fmt::Write;
|
||||
|
||||
let mut buf = String::new();
|
||||
|
||||
let config = DisplayDiagnosticConfig::default()
|
||||
.color(false)
|
||||
.show_fix_diff(true)
|
||||
.format(DiagnosticFormat::Full);
|
||||
|
||||
for mut action in code_actions(&self.db, self.file, self.diagnostic_range, &lint.name) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DiagnosticId::Lint(LintName::of("code-action")),
|
||||
ruff_db::diagnostic::Severity::Info,
|
||||
action.title,
|
||||
);
|
||||
|
||||
diagnostic.annotate(Annotation::primary(
|
||||
Span::from(self.file).with_range(self.diagnostic_range),
|
||||
));
|
||||
|
||||
if action.preferred {
|
||||
diagnostic.sub(SubDiagnostic::new(
|
||||
ruff_db::diagnostic::SubDiagnosticSeverity::Help,
|
||||
"This is a preferred code action",
|
||||
));
|
||||
}
|
||||
|
||||
let first_edit = action.edits.remove(0);
|
||||
diagnostic.set_fix(Fix::safe_edits(first_edit, action.edits));
|
||||
|
||||
write!(buf, "{}", diagnostic.display(&self.db, &config)).unwrap();
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +417,16 @@ pub fn completion<'db>(
|
||||
}
|
||||
if settings.auto_import {
|
||||
if let Some(scoped) = scoped {
|
||||
add_unimported_completions(db, file, &parsed, scoped, &mut completions);
|
||||
add_unimported_completions(
|
||||
db,
|
||||
file,
|
||||
&parsed,
|
||||
scoped,
|
||||
|module_name: &ModuleName, symbol: &str| {
|
||||
ImportRequest::import_from(module_name.as_str(), symbol)
|
||||
},
|
||||
&mut completions,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -453,7 +462,16 @@ pub(crate) fn missing_imports(
|
||||
) -> Vec<ImportEdit> {
|
||||
let mut completions = Completions::exactly(db, symbol);
|
||||
let scoped = ScopedTarget { node };
|
||||
add_unimported_completions(db, file, parsed, scoped, &mut completions);
|
||||
add_unimported_completions(
|
||||
db,
|
||||
file,
|
||||
parsed,
|
||||
scoped,
|
||||
|module_name: &ModuleName, symbol: &str| {
|
||||
ImportRequest::import_from(module_name.as_str(), symbol).force()
|
||||
},
|
||||
&mut completions,
|
||||
);
|
||||
|
||||
completions.into_imports()
|
||||
}
|
||||
@@ -502,6 +520,7 @@ fn add_unimported_completions<'db>(
|
||||
file: File,
|
||||
parsed: &ParsedModuleRef,
|
||||
scoped: ScopedTarget<'_>,
|
||||
create_import_request: impl for<'a> Fn(&'a ModuleName, &'a str) -> ImportRequest<'a>,
|
||||
completions: &mut Completions<'db>,
|
||||
) {
|
||||
// This is redundant since `all_symbols` will also bail
|
||||
@@ -517,14 +536,13 @@ fn add_unimported_completions<'db>(
|
||||
let importer = Importer::new(db, &stylist, file, source.as_str(), parsed);
|
||||
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
|
||||
|
||||
for symbol in all_symbols(db, &completions.query) {
|
||||
for symbol in all_symbols(db, file, &completions.query) {
|
||||
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let request =
|
||||
ImportRequest::import_from(symbol.module.name(db).as_str(), &symbol.symbol.name);
|
||||
let request = create_import_request(symbol.module.name(db), &symbol.symbol.name);
|
||||
// FIXME: `all_symbols` doesn't account for wildcard imports.
|
||||
// Since we're looking at every module, this is probably
|
||||
// "fine," but it might mean that we import a symbol from the
|
||||
@@ -5566,10 +5584,7 @@ def foo(param: s<CURSOR>)
|
||||
#[test]
|
||||
fn from_import_no_space_not_suggests_import() {
|
||||
let builder = completion_test_builder("from typing<CURSOR>");
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
typing
|
||||
typing_extensions
|
||||
");
|
||||
assert_snapshot!(builder.build().snapshot(), @"typing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5785,6 +5800,86 @@ from .imp<CURSOR>
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_extensions_excluded_from_import() {
|
||||
let builder = completion_test_builder("from typing<CURSOR>").module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @"typing :: Current module");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_extensions_excluded_from_auto_import() {
|
||||
let builder = completion_test_builder("deprecated<CURSOR>")
|
||||
.auto_import()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
Deprecated :: importlib.metadata
|
||||
DeprecatedList :: importlib.metadata
|
||||
DeprecatedNonAbstract :: importlib.metadata
|
||||
DeprecatedTuple :: importlib.metadata
|
||||
deprecated :: warnings
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_extensions_included_from_import() {
|
||||
let builder = CursorTest::builder()
|
||||
.source("typing_extensions.py", "deprecated = 1")
|
||||
.source("foo.py", "from typing<CURSOR>")
|
||||
.completion_test_builder()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
typing :: Current module
|
||||
typing_extensions :: Current module
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_extensions_included_from_auto_import() {
|
||||
let builder = CursorTest::builder()
|
||||
.source("typing_extensions.py", "deprecated = 1")
|
||||
.source("foo.py", "deprecated<CURSOR>")
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
Deprecated :: importlib.metadata
|
||||
DeprecatedList :: importlib.metadata
|
||||
DeprecatedNonAbstract :: importlib.metadata
|
||||
DeprecatedTuple :: importlib.metadata
|
||||
deprecated :: typing_extensions
|
||||
deprecated :: warnings
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_extensions_included_from_import_in_stub() {
|
||||
let builder = CursorTest::builder()
|
||||
.source("foo.pyi", "from typing<CURSOR>")
|
||||
.completion_test_builder()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
typing :: Current module
|
||||
typing_extensions :: Current module
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_extensions_included_from_auto_import_in_stub() {
|
||||
let builder = CursorTest::builder()
|
||||
.source("foo.pyi", "deprecated<CURSOR>")
|
||||
.completion_test_builder()
|
||||
.auto_import()
|
||||
.module_names();
|
||||
assert_snapshot!(builder.build().snapshot(), @r"
|
||||
Deprecated :: importlib.metadata
|
||||
DeprecatedList :: importlib.metadata
|
||||
DeprecatedNonAbstract :: importlib.metadata
|
||||
DeprecatedTuple :: importlib.metadata
|
||||
deprecated :: typing_extensions
|
||||
deprecated :: warnings
|
||||
");
|
||||
}
|
||||
|
||||
/// A way to create a simple single-file (named `main.py`) completion test
|
||||
/// builder.
|
||||
///
|
||||
|
||||
@@ -1656,4 +1656,218 @@ func<CURSOR>_alias()
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stub_target() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"path.pyi",
|
||||
r#"
|
||||
class Path:
|
||||
def __init__(self, path: str): ...
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"path.py",
|
||||
r#"
|
||||
class Path:
|
||||
def __init__(self, path: str):
|
||||
self.path = path
|
||||
"#,
|
||||
)
|
||||
.source(
|
||||
"importer.py",
|
||||
r#"
|
||||
from path import Path<CURSOR>
|
||||
|
||||
a: Path = Path("test")
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.references(), @r###"
|
||||
info[references]: Reference 1
|
||||
--> path.pyi:2:7
|
||||
|
|
||||
2 | class Path:
|
||||
| ^^^^
|
||||
3 | def __init__(self, path: str): ...
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> importer.py:2:18
|
||||
|
|
||||
2 | from path import Path
|
||||
| ^^^^
|
||||
3 |
|
||||
4 | a: Path = Path("test")
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> importer.py:4:4
|
||||
|
|
||||
2 | from path import Path
|
||||
3 |
|
||||
4 | a: Path = Path("test")
|
||||
| ^^^^
|
||||
|
|
||||
|
||||
info[references]: Reference 4
|
||||
--> importer.py:4:11
|
||||
|
|
||||
2 | from path import Path
|
||||
3 |
|
||||
4 | a: Path = Path("test")
|
||||
| ^^^^
|
||||
|
|
||||
"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_alias() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
import warnings
|
||||
import warnings as <CURSOR>abc
|
||||
|
||||
x = abc
|
||||
y = warnings
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:3:20
|
||||
|
|
||||
2 | import warnings
|
||||
3 | import warnings as abc
|
||||
| ^^^
|
||||
4 |
|
||||
5 | x = abc
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:5
|
||||
|
|
||||
3 | import warnings as abc
|
||||
4 |
|
||||
5 | x = abc
|
||||
| ^^^
|
||||
6 | y = warnings
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_alias_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
import warnings
|
||||
import warnings as abc
|
||||
|
||||
x = abc<CURSOR>
|
||||
y = warnings
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:3:20
|
||||
|
|
||||
2 | import warnings
|
||||
3 | import warnings as abc
|
||||
| ^^^
|
||||
4 |
|
||||
5 | x = abc
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:5
|
||||
|
|
||||
3 | import warnings as abc
|
||||
4 |
|
||||
5 | x = abc
|
||||
| ^^^
|
||||
6 | y = warnings
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_from_alias() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from warnings import deprecated as xyz<CURSOR>
|
||||
from warnings import deprecated
|
||||
|
||||
y = xyz
|
||||
z = deprecated
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:36
|
||||
|
|
||||
2 | from warnings import deprecated as xyz
|
||||
| ^^^
|
||||
3 | from warnings import deprecated
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:5
|
||||
|
|
||||
3 | from warnings import deprecated
|
||||
4 |
|
||||
5 | y = xyz
|
||||
| ^^^
|
||||
6 | z = deprecated
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_from_alias_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
from warnings import deprecated as xyz
|
||||
from warnings import deprecated
|
||||
|
||||
y = xyz<CURSOR>
|
||||
z = deprecated
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:36
|
||||
|
|
||||
2 | from warnings import deprecated as xyz
|
||||
| ^^^
|
||||
3 | from warnings import deprecated
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:5
|
||||
|
|
||||
3 | from warnings import deprecated
|
||||
4 |
|
||||
5 | y = xyz
|
||||
| ^^^
|
||||
6 | z = deprecated
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,15 +334,24 @@ impl GotoTarget<'_> {
|
||||
let (_, ty) = ty_python_semantic::definitions_for_unary_op(model, expression)?;
|
||||
ty
|
||||
}
|
||||
// TODO: Support identifier targets
|
||||
GotoTarget::PatternMatchRest(_)
|
||||
| GotoTarget::PatternKeywordArgument(_)
|
||||
| GotoTarget::PatternMatchStarName(_)
|
||||
| GotoTarget::PatternMatchAsName(_)
|
||||
| GotoTarget::TypeParamParamSpecName(_)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(_)
|
||||
| GotoTarget::NonLocal { .. }
|
||||
| GotoTarget::Globals { .. } => return None,
|
||||
GotoTarget::PatternMatchRest(pattern) => {
|
||||
model.inferred_type_for_identifier(pattern.rest.as_ref()?)
|
||||
}
|
||||
GotoTarget::PatternKeywordArgument(pattern) => {
|
||||
model.inferred_type_for_identifier(&pattern.attr)
|
||||
}
|
||||
GotoTarget::PatternMatchStarName(pattern) => {
|
||||
model.inferred_type_for_identifier(pattern.name.as_ref()?)
|
||||
}
|
||||
GotoTarget::PatternMatchAsName(pattern) => {
|
||||
model.inferred_type_for_identifier(pattern.name.as_ref()?)
|
||||
}
|
||||
GotoTarget::NonLocal { identifier } => model.inferred_type_for_identifier(identifier),
|
||||
GotoTarget::Globals { identifier } => model.inferred_type_for_identifier(identifier),
|
||||
// These don't really... *have* a type?
|
||||
GotoTarget::TypeParamParamSpecName(_) | GotoTarget::TypeParamTypeVarTupleName(_) => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(ty)
|
||||
@@ -396,13 +405,19 @@ impl GotoTarget<'_> {
|
||||
GotoTarget::ImportSymbolAlias {
|
||||
alias, import_from, ..
|
||||
} => {
|
||||
let symbol_name = alias.name.as_str();
|
||||
Some(definitions_for_imported_symbol(
|
||||
model,
|
||||
import_from,
|
||||
symbol_name,
|
||||
alias_resolution,
|
||||
))
|
||||
if let Some(asname) = alias.asname.as_ref()
|
||||
&& alias_resolution == ImportAliasResolution::PreserveAliases
|
||||
{
|
||||
Some(definitions_for_name(model, asname.as_str(), asname.into()))
|
||||
} else {
|
||||
let symbol_name = alias.name.as_str();
|
||||
Some(definitions_for_imported_symbol(
|
||||
model,
|
||||
import_from,
|
||||
symbol_name,
|
||||
alias_resolution,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
GotoTarget::ImportModuleComponent {
|
||||
@@ -418,12 +433,12 @@ impl GotoTarget<'_> {
|
||||
|
||||
// Handle import aliases (offset within 'z' in "import x.y as z")
|
||||
GotoTarget::ImportModuleAlias { alias } => {
|
||||
if alias_resolution == ImportAliasResolution::ResolveAliases {
|
||||
definitions_for_module(model, Some(alias.name.as_str()), 0)
|
||||
if let Some(asname) = alias.asname.as_ref()
|
||||
&& alias_resolution == ImportAliasResolution::PreserveAliases
|
||||
{
|
||||
Some(definitions_for_name(model, asname.as_str(), asname.into()))
|
||||
} else {
|
||||
alias.asname.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
})
|
||||
definitions_for_module(model, Some(alias.name.as_str()), 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -975,7 +975,26 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1003,7 +1022,26 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1031,7 +1069,26 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1065,7 +1122,26 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1143,7 +1219,26 @@ mod tests {
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:10:23
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^^^^^
|
||||
11 | x = ab
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1395,7 +1490,26 @@ def outer():
|
||||
);
|
||||
|
||||
// Should find the variable declaration in the outer scope, not the nonlocal statement
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:6:18
|
||||
|
|
||||
5 | def inner():
|
||||
6 | nonlocal xy
|
||||
| ^^
|
||||
7 | xy = "modified"
|
||||
8 | return x # Should find the nonlocal x declaration in outer scope
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1447,7 +1561,26 @@ def function():
|
||||
);
|
||||
|
||||
// Should find the global variable declaration, not the global statement
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
19 | # Types
|
||||
20 | Unknown = object()
|
||||
| ^^^^^^^
|
||||
21 | AlwaysTruthy = object()
|
||||
22 | AlwaysFalsy = object()
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:12
|
||||
|
|
||||
4 | def function():
|
||||
5 | global global_var
|
||||
| ^^^^^^^^^^
|
||||
6 | global_var = "modified"
|
||||
7 | return global_var # Should find the global variable declaration
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1704,7 +1704,26 @@ def outer():
|
||||
);
|
||||
|
||||
// Should find the variable declaration in the outer scope, not the nonlocal statement
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:6:18
|
||||
|
|
||||
5 | def inner():
|
||||
6 | nonlocal xy
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
7 | xy = "modified"
|
||||
8 | return x # Should find the nonlocal x declaration in outer scope
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1756,7 +1775,26 @@ def function():
|
||||
);
|
||||
|
||||
// Should find the global variable declaration, not the global statement
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:5:12
|
||||
|
|
||||
4 | def function():
|
||||
5 | global global_var
|
||||
| ^^^^^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
6 | global_var = "modified"
|
||||
7 | return global_var # Should find the global variable declaration
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1770,7 +1808,26 @@ def function():
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1816,7 +1873,26 @@ def function():
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1862,7 +1938,26 @@ def function():
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1914,7 +2009,26 @@ def function():
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
11 | x = ab
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2011,7 +2125,26 @@ def function():
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
Unknown
|
||||
---------------------------------------------
|
||||
```python
|
||||
Unknown
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:10:23
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
11 | x = ab
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -553,6 +553,16 @@ impl<'a> ImportRequest<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Causes this request to become a command. This will force the
|
||||
/// requested import style, even if another style would be more
|
||||
/// appropriate generally.
|
||||
pub(crate) fn force(mut self) -> Self {
|
||||
Self {
|
||||
force_style: true,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to change the import request style so that the chances
|
||||
/// of an import conflict are minimized (although not always reduced
|
||||
/// to zero).
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
use crate::find_node::CoveringNode;
|
||||
use crate::goto::GotoTarget;
|
||||
use crate::{Db, NavigationTarget, ReferenceKind, ReferenceTarget};
|
||||
use crate::{Db, NavigationTargets, ReferenceKind, ReferenceTarget};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{
|
||||
self as ast, AnyNodeRef,
|
||||
@@ -49,10 +49,9 @@ pub(crate) fn references(
|
||||
|
||||
// When finding references, do not resolve any local aliases.
|
||||
let model = SemanticModel::new(db, file);
|
||||
let target_definitions_nav = goto_target
|
||||
let target_definitions = goto_target
|
||||
.get_definition_targets(&model, ImportAliasResolution::PreserveAliases)?
|
||||
.definition_targets(db)?;
|
||||
let target_definitions: Vec<NavigationTarget> = target_definitions_nav.into_iter().collect();
|
||||
.declaration_targets(db)?;
|
||||
|
||||
// Extract the target text from the goto target for fast comparison
|
||||
let target_text = goto_target.to_string()?;
|
||||
@@ -115,7 +114,7 @@ pub(crate) fn references(
|
||||
fn references_for_file(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
target_definitions: &[NavigationTarget],
|
||||
target_definitions: &NavigationTargets,
|
||||
target_text: &str,
|
||||
mode: ReferencesMode,
|
||||
references: &mut Vec<ReferenceTarget>,
|
||||
@@ -159,7 +158,7 @@ fn is_symbol_externally_visible(goto_target: &GotoTarget<'_>) -> bool {
|
||||
struct LocalReferencesFinder<'a> {
|
||||
model: &'a SemanticModel<'a>,
|
||||
tokens: &'a Tokens,
|
||||
target_definitions: &'a [NavigationTarget],
|
||||
target_definitions: &'a NavigationTargets,
|
||||
references: &'a mut Vec<ReferenceTarget>,
|
||||
mode: ReferencesMode,
|
||||
target_text: &'a str,
|
||||
@@ -318,12 +317,10 @@ impl LocalReferencesFinder<'_> {
|
||||
GotoTarget::from_covering_node(self.model, covering_node, offset, self.tokens)
|
||||
{
|
||||
// Get the definitions for this goto target
|
||||
if let Some(current_definitions_nav) = goto_target
|
||||
if let Some(current_definitions) = goto_target
|
||||
.get_definition_targets(self.model, ImportAliasResolution::PreserveAliases)
|
||||
.and_then(|definitions| definitions.declaration_targets(self.model.db()))
|
||||
{
|
||||
let current_definitions: Vec<NavigationTarget> =
|
||||
current_definitions_nav.into_iter().collect();
|
||||
// Check if any of the current definitions match our target definitions
|
||||
if self.navigation_targets_match(¤t_definitions) {
|
||||
// Determine if this is a read or write reference
|
||||
@@ -337,7 +334,7 @@ impl LocalReferencesFinder<'_> {
|
||||
}
|
||||
|
||||
/// Check if `Vec<NavigationTarget>` match our target definitions
|
||||
fn navigation_targets_match(&self, current_targets: &[NavigationTarget]) -> bool {
|
||||
fn navigation_targets_match(&self, current_targets: &NavigationTargets) -> bool {
|
||||
// Since we're comparing the same symbol, all definitions should be equivalent
|
||||
// We only need to check against the first target definition
|
||||
if let Some(first_target) = self.target_definitions.iter().next() {
|
||||
|
||||
@@ -163,7 +163,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepare_rename_parameter() {
|
||||
fn prepare_rename_parameter() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def func(<CURSOR>value: int) -> int:
|
||||
@@ -178,7 +178,7 @@ value = 0
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_parameter() {
|
||||
fn rename_parameter() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def func(<CURSOR>value: int) -> int:
|
||||
@@ -207,7 +207,7 @@ func(value=42)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_function() {
|
||||
fn rename_function() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def fu<CURSOR>nc():
|
||||
@@ -235,7 +235,7 @@ x = func
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_class() {
|
||||
fn rename_class() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class My<CURSOR>Class:
|
||||
@@ -265,7 +265,7 @@ cls = MyClass
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_invalid_name() {
|
||||
fn rename_invalid_name() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def fu<CURSOR>nc():
|
||||
@@ -286,7 +286,7 @@ def fu<CURSOR>nc():
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_file_function_rename() {
|
||||
fn multi_file_function_rename() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"utils.py",
|
||||
@@ -312,7 +312,7 @@ from utils import helper_function
|
||||
class DataProcessor:
|
||||
def __init__(self):
|
||||
self.multiplier = helper_function
|
||||
|
||||
|
||||
def process(self, value):
|
||||
return helper_function(value)
|
||||
",
|
||||
@@ -654,7 +654,7 @@ class DataProcessor:
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=a<CURSOR>b):
|
||||
@@ -685,7 +685,7 @@ class DataProcessor:
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, button=ab):
|
||||
@@ -716,7 +716,7 @@ class DataProcessor:
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Cl<CURSOR>ick(x, button=ab):
|
||||
@@ -756,7 +756,7 @@ class DataProcessor:
|
||||
def __init__(self, pos, btn):
|
||||
self.position: int = pos
|
||||
self.button: str = btn
|
||||
|
||||
|
||||
def my_func(event: Click):
|
||||
match event:
|
||||
case Click(x, but<CURSOR>ton=ab):
|
||||
@@ -880,7 +880,7 @@ class DataProcessor:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_rename_import_module_component() {
|
||||
fn cannot_rename_import_module_component() {
|
||||
// Test that we cannot rename parts of module names in import statements
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -893,7 +893,7 @@ x = os.path.join('a', 'b')
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_rename_from_import_module_component() {
|
||||
fn cannot_rename_from_import_module_component() {
|
||||
// Test that we cannot rename parts of module names in from import statements
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -906,7 +906,7 @@ result = join('a', 'b')
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_rename_external_file() {
|
||||
fn cannot_rename_external_file() {
|
||||
// This test verifies that we cannot rename a symbol when it's defined in a file
|
||||
// that's outside the project (like a standard library function)
|
||||
let test = cursor_test(
|
||||
@@ -920,7 +920,7 @@ x = <CURSOR>os.path.join('a', 'b')
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_alias_at_import_statement() {
|
||||
fn rename_alias_at_import_statement() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"utils.py",
|
||||
@@ -931,8 +931,8 @@ def test(): pass
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from utils import test as test_<CURSOR>alias
|
||||
result = test_alias()
|
||||
from utils import test as <CURSOR>alias
|
||||
result = alias()
|
||||
",
|
||||
)
|
||||
.build();
|
||||
@@ -941,16 +941,16 @@ result = test_alias()
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:2:27
|
||||
|
|
||||
2 | from utils import test as test_alias
|
||||
| ^^^^^^^^^^
|
||||
3 | result = test_alias()
|
||||
| ----------
|
||||
2 | from utils import test as alias
|
||||
| ^^^^^
|
||||
3 | result = alias()
|
||||
| -----
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_alias_at_usage_site() {
|
||||
fn rename_alias_at_usage_site() {
|
||||
// Test renaming an alias when the cursor is on the alias in the usage statement
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
@@ -962,8 +962,8 @@ def test(): pass
|
||||
.source(
|
||||
"main.py",
|
||||
"
|
||||
from utils import test as test_alias
|
||||
result = test_<CURSOR>alias()
|
||||
from utils import test as alias
|
||||
result = <CURSOR>alias()
|
||||
",
|
||||
)
|
||||
.build();
|
||||
@@ -972,16 +972,16 @@ result = test_<CURSOR>alias()
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:2:27
|
||||
|
|
||||
2 | from utils import test as test_alias
|
||||
| ^^^^^^^^^^
|
||||
3 | result = test_alias()
|
||||
| ----------
|
||||
2 | from utils import test as alias
|
||||
| ^^^^^
|
||||
3 | result = alias()
|
||||
| -----
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_across_import_chain_with_mixed_aliases() {
|
||||
fn rename_across_import_chain_with_mixed_aliases() {
|
||||
// Test renaming a symbol that's imported across multiple files with mixed alias patterns
|
||||
// File 1 (source.py): defines the original function
|
||||
// File 2 (middle.py): imports without alias from source.py
|
||||
@@ -1049,7 +1049,7 @@ value1 = func_alias()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_alias_in_import_chain() {
|
||||
fn rename_alias_in_import_chain() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"file1.py",
|
||||
@@ -1101,7 +1101,7 @@ class App:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_rename_keyword() {
|
||||
fn cannot_rename_keyword() {
|
||||
// Test that we cannot rename Python keywords like "None"
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -1116,7 +1116,7 @@ def process_value(value):
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_rename_builtin_type() {
|
||||
fn cannot_rename_builtin_type() {
|
||||
// Test that we cannot rename Python builtin types like "int"
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -1129,7 +1129,7 @@ def convert_to_number(value):
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_keyword_argument() {
|
||||
fn rename_keyword_argument() {
|
||||
// Test renaming a keyword argument and its corresponding parameter
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -1156,7 +1156,7 @@ result = func(10, <CURSOR>y=20)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_parameter_with_keyword_argument() {
|
||||
fn rename_parameter_with_keyword_argument() {
|
||||
// Test renaming a parameter and its corresponding keyword argument
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -1181,4 +1181,64 @@ result = func(10, y=20)
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_alias() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
import warnings
|
||||
import warnings as <CURSOR>abc
|
||||
|
||||
x = abc
|
||||
y = warnings
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("z"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:3:20
|
||||
|
|
||||
2 | import warnings
|
||||
3 | import warnings as abc
|
||||
| ^^^
|
||||
4 |
|
||||
5 | x = abc
|
||||
| ---
|
||||
6 | y = warnings
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_alias_use() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"main.py",
|
||||
r#"
|
||||
import warnings
|
||||
import warnings as abc
|
||||
|
||||
x = abc<CURSOR>
|
||||
y = warnings
|
||||
"#,
|
||||
)
|
||||
.build();
|
||||
|
||||
assert_snapshot!(test.rename("z"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:3:20
|
||||
|
|
||||
2 | import warnings
|
||||
3 | import warnings as abc
|
||||
| ^^^
|
||||
4 |
|
||||
5 | x = abc
|
||||
| ---
|
||||
6 | y = warnings
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
# GenericAlias in type expressions
|
||||
|
||||
We recognize if a `types.GenericAlias` instance is created by specializing a generic class. We don't
|
||||
explicitly mention it in our type display, but `list[int]` in the example below is a `GenericAlias`
|
||||
instance at runtime:
|
||||
|
||||
```py
|
||||
Numbers = list[int]
|
||||
|
||||
# At runtime, `Numbers` is an instance of `types.GenericAlias`. Showing
|
||||
# this as `list[int]` is more helpful, though:
|
||||
reveal_type(Numbers) # revealed: <class 'list[int]'>
|
||||
|
||||
def _(numbers: Numbers) -> None:
|
||||
reveal_type(numbers) # revealed: list[int]
|
||||
```
|
||||
|
||||
It is also valid to create `GenericAlias` instances manually:
|
||||
|
||||
```py
|
||||
from types import GenericAlias
|
||||
|
||||
Strings = GenericAlias(list, (str,))
|
||||
|
||||
reveal_type(Strings) # revealed: GenericAlias
|
||||
```
|
||||
|
||||
However, using such a `GenericAlias` instance in a type expression is currently not supported:
|
||||
|
||||
```py
|
||||
# error: [invalid-type-form] "Variable of type `GenericAlias` is not allowed in a type expression"
|
||||
def _(strings: Strings) -> None:
|
||||
reveal_type(strings) # revealed: Unknown
|
||||
```
|
||||
@@ -1,24 +1,16 @@
|
||||
# NewType
|
||||
|
||||
## Valid forms
|
||||
## Basic usage
|
||||
|
||||
`NewType` can be used to create distinct types that are based on existing types:
|
||||
|
||||
```py
|
||||
from typing_extensions import NewType
|
||||
from types import GenericAlias
|
||||
|
||||
X = GenericAlias(type, ())
|
||||
A = NewType("A", int)
|
||||
# TODO: typeshed for `typing.GenericAlias` uses `type` for the first argument. `NewType` should be special-cased
|
||||
# to be compatible with `type`
|
||||
# error: [invalid-argument-type] "Argument to function `__new__` is incorrect: Expected `type`, found `<NewType pseudo-class 'A'>`"
|
||||
B = GenericAlias(A, ())
|
||||
UserId = NewType("UserId", int)
|
||||
|
||||
def _(
|
||||
a: A,
|
||||
b: B,
|
||||
):
|
||||
reveal_type(a) # revealed: A
|
||||
reveal_type(b) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
|
||||
def _(user_id: UserId):
|
||||
reveal_type(user_id) # revealed: UserId
|
||||
```
|
||||
|
||||
## Subtyping
|
||||
|
||||
@@ -52,6 +52,10 @@ def f(x: A):
|
||||
|
||||
JSONPrimitive = Union[str, int, float, bool, None]
|
||||
JSONValue = TypeAliasType("JSONValue", 'Union[JSONPrimitive, Sequence["JSONValue"], Mapping[str, "JSONValue"]]')
|
||||
|
||||
def _(x: JSONValue):
|
||||
# TODO: should be `JSONValue`
|
||||
reveal_type(x) # revealed: Divergent
|
||||
```
|
||||
|
||||
## Self-referential legacy type variables
|
||||
|
||||
@@ -284,10 +284,17 @@ python-version = "3.12"
|
||||
```py
|
||||
from typing import assert_never
|
||||
|
||||
class A[T]: ...
|
||||
class A[T]:
|
||||
value: T
|
||||
|
||||
class ASub[T](A[T]): ...
|
||||
class B[T]: ...
|
||||
class C[T]: ...
|
||||
|
||||
class B[T]:
|
||||
value: T
|
||||
|
||||
class C[T]:
|
||||
value: T
|
||||
|
||||
class D: ...
|
||||
class E: ...
|
||||
class F: ...
|
||||
|
||||
@@ -51,6 +51,10 @@ class Parent:
|
||||
@final
|
||||
def my_property2(self) -> int: ...
|
||||
|
||||
@property
|
||||
@final
|
||||
def my_property3(self) -> int: ...
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def class_method1(cls) -> int: ...
|
||||
@@ -86,6 +90,13 @@ class Child(Parent):
|
||||
|
||||
@property
|
||||
def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
@my_property2.setter
|
||||
def my_property2(self, x: int) -> None: ...
|
||||
|
||||
@property
|
||||
def my_property3(self) -> int: ... # error: [override-of-final-method]
|
||||
@my_property3.deleter
|
||||
def my_proeprty3(self) -> None: ...
|
||||
|
||||
@classmethod
|
||||
def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
@@ -230,7 +241,7 @@ class ChildOfBad(Bad):
|
||||
def bar(self, x: str) -> str: ...
|
||||
@overload
|
||||
def bar(self, x: int) -> int: ... # error: [override-of-final-method]
|
||||
|
||||
|
||||
@overload
|
||||
def baz(self, x: str) -> str: ...
|
||||
@overload
|
||||
@@ -461,14 +472,17 @@ class B(A):
|
||||
def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
|
||||
# check that autofixes don't introduce invalid syntax
|
||||
# if there are multiple statements on one line
|
||||
#
|
||||
# TODO: we should emit a Liskov violation here too
|
||||
# error: [override-of-final-method]
|
||||
method4 = 42; unrelated = 56 # fmt: skip
|
||||
|
||||
# Possible overrides of possibly `@final` methods...
|
||||
class C(A):
|
||||
if coinflip():
|
||||
# TODO: the autofix here introduces invalid syntax because there are now no
|
||||
# statements inside the `if:` branch
|
||||
# (but it might still be a useful autofix in an IDE context?)
|
||||
def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
else:
|
||||
pass
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
At its simplest, to define a generic class using the legacy syntax, you inherit from the
|
||||
`typing.Generic` special form, which is "specialized" with the generic class's type variables.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context
|
||||
from typing_extensions import Generic, TypeVar, TypeVarTuple, ParamSpec, Unpack
|
||||
@@ -19,7 +24,9 @@ class MultipleTypevars(Generic[T, S]): ...
|
||||
class SingleParamSpec(Generic[P]): ...
|
||||
class TypeVarAndParamSpec(Generic[P, T]): ...
|
||||
class SingleTypeVarTuple(Generic[Unpack[Ts]]): ...
|
||||
class StarredSingleTypeVarTuple(Generic[*Ts]): ...
|
||||
class TypeVarAndTypeVarTuple(Generic[T, Unpack[Ts]]): ...
|
||||
class StarredTypeVarAndTypeVarTuple(Generic[T, *Ts]): ...
|
||||
|
||||
# revealed: ty_extensions.GenericContext[T@SingleTypevar]
|
||||
reveal_type(generic_context(SingleTypevar))
|
||||
@@ -34,6 +41,8 @@ reveal_type(generic_context(TypeVarAndParamSpec))
|
||||
# TODO: support `TypeVarTuple` properly (these should not reveal `None`)
|
||||
reveal_type(generic_context(SingleTypeVarTuple)) # revealed: None
|
||||
reveal_type(generic_context(TypeVarAndTypeVarTuple)) # revealed: None
|
||||
reveal_type(generic_context(StarredSingleTypeVarTuple)) # revealed: None
|
||||
reveal_type(generic_context(StarredTypeVarAndTypeVarTuple)) # revealed: None
|
||||
```
|
||||
|
||||
Inheriting from `Generic` multiple times yields a `duplicate-base` diagnostic, just like any other
|
||||
@@ -210,6 +219,37 @@ reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str]
|
||||
reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int]
|
||||
```
|
||||
|
||||
## Diagnostics for bad specializations
|
||||
|
||||
We show the user where the type variable was defined if a specialization is given that doesn't
|
||||
satisfy the type variable's upper bound or constraints:
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`library.py`:
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic
|
||||
|
||||
T = TypeVar("T", bound=str)
|
||||
U = TypeVar("U", int, bytes)
|
||||
|
||||
class Bounded(Generic[T]):
|
||||
x: T
|
||||
|
||||
class Constrained(Generic[U]):
|
||||
x: U
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from library import Bounded, Constrained
|
||||
|
||||
x: Bounded[int] # error: [invalid-type-arguments]
|
||||
y: Constrained[str] # error: [invalid-type-arguments]
|
||||
```
|
||||
|
||||
## Inferring generic class parameters
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# `ParamSpec`
|
||||
# Legacy `ParamSpec`
|
||||
|
||||
## Definition
|
||||
|
||||
@@ -115,59 +115,3 @@ P = ParamSpec("P", default=[A, B])
|
||||
class A: ...
|
||||
class B: ...
|
||||
```
|
||||
|
||||
### PEP 695
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
#### Valid
|
||||
|
||||
```py
|
||||
def foo1[**P]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
|
||||
def foo2[**P = ...]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
|
||||
def foo3[**P = [int, str]]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
|
||||
def foo4[**P, **Q = P]():
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
reveal_type(Q) # revealed: typing.ParamSpec
|
||||
```
|
||||
|
||||
#### Invalid
|
||||
|
||||
ParamSpec, when defined using the new syntax, does not allow defining bounds or constraints.
|
||||
|
||||
This results in a lot of syntax errors mainly because the AST doesn't accept them in this position.
|
||||
The parser could do a better job in recovering from these errors.
|
||||
|
||||
<!-- blacken-docs:off -->
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
def foo[**P: int]() -> None:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
```
|
||||
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
#### Invalid default
|
||||
|
||||
```py
|
||||
# error: [invalid-paramspec]
|
||||
def foo[**P = int]() -> None:
|
||||
pass
|
||||
```
|
||||
@@ -191,6 +191,32 @@ reveal_type(WithDefault[str, str]()) # revealed: WithDefault[str, str]
|
||||
reveal_type(WithDefault[str]()) # revealed: WithDefault[str, int]
|
||||
```
|
||||
|
||||
## Diagnostics for bad specializations
|
||||
|
||||
We show the user where the type variable was defined if a specialization is given that doesn't
|
||||
satisfy the type variable's upper bound or constraints:
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`library.py`:
|
||||
|
||||
```py
|
||||
class Bounded[T: str]:
|
||||
x: T
|
||||
|
||||
class Constrained[U: (int, bytes)]:
|
||||
x: U
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from library import Bounded, Constrained
|
||||
|
||||
x: Bounded[int] # error: [invalid-type-arguments]
|
||||
y: Constrained[str] # error: [invalid-type-arguments]
|
||||
```
|
||||
|
||||
## Inferring generic class parameters
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# PEP 695 `ParamSpec`
|
||||
|
||||
`ParamSpec` was introduced in Python 3.12 while the support for specifying defaults was added in
|
||||
Python 3.13.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
## Definition
|
||||
|
||||
```py
|
||||
def foo1[**P]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
```
|
||||
|
||||
## Bounds and constraints
|
||||
|
||||
`ParamSpec`, when defined using the new syntax, does not allow defining bounds or constraints.
|
||||
|
||||
TODO: This results in a lot of syntax errors mainly because the AST doesn't accept them in this
|
||||
position. The parser could do a better job in recovering from these errors.
|
||||
|
||||
<!-- blacken-docs:off -->
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
def foo[**P: int]() -> None:
|
||||
# error: [invalid-syntax]
|
||||
# error: [invalid-syntax]
|
||||
pass
|
||||
```
|
||||
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
## Default
|
||||
|
||||
The default value for a `ParamSpec` can be either a list of types, `...`, or another `ParamSpec`.
|
||||
|
||||
```py
|
||||
def foo2[**P = ...]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
|
||||
def foo3[**P = [int, str]]() -> None:
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
|
||||
def foo4[**P, **Q = P]():
|
||||
reveal_type(P) # revealed: typing.ParamSpec
|
||||
reveal_type(Q) # revealed: typing.ParamSpec
|
||||
```
|
||||
|
||||
Other values are invalid.
|
||||
|
||||
```py
|
||||
# error: [invalid-paramspec]
|
||||
def foo[**P = int]() -> None:
|
||||
pass
|
||||
```
|
||||
@@ -321,9 +321,8 @@ from typing import Never
|
||||
from ty_extensions import ConstraintSet, generic_context
|
||||
|
||||
def mentions[T, U]():
|
||||
# (T@mentions ≤ int) ∧ (U@mentions = list[T@mentions])
|
||||
constraints = ConstraintSet.range(Never, T, int) & ConstraintSet.range(list[T], U, list[T])
|
||||
# revealed: ty_extensions.ConstraintSet[((T@mentions ≤ int) ∧ (U@mentions = list[T@mentions]))]
|
||||
reveal_type(constraints)
|
||||
# revealed: ty_extensions.Specialization[T@mentions = int, U@mentions = list[int]]
|
||||
reveal_type(generic_context(mentions).specialize_constrained(constraints))
|
||||
```
|
||||
@@ -334,9 +333,8 @@ this case.
|
||||
|
||||
```py
|
||||
def divergent[T, U]():
|
||||
# (T@divergent = list[U@divergent]) ∧ (U@divergent = list[T@divergent]))
|
||||
constraints = ConstraintSet.range(list[U], T, list[U]) & ConstraintSet.range(list[T], U, list[T])
|
||||
# revealed: ty_extensions.ConstraintSet[((T@divergent = list[U@divergent]) ∧ (U@divergent = list[T@divergent]))]
|
||||
reveal_type(constraints)
|
||||
# revealed: None
|
||||
reveal_type(generic_context(divergent).specialize_constrained(constraints))
|
||||
```
|
||||
|
||||
660
crates/ty_python_semantic/resources/mdtest/import/workspaces.md
Normal file
660
crates/ty_python_semantic/resources/mdtest/import/workspaces.md
Normal file
@@ -0,0 +1,660 @@
|
||||
# Support for Resolving Imports In Workspaces
|
||||
|
||||
Python packages have fairly rigid structures that we rely on when resolving imports and merging
|
||||
namespace packages or stub packages. These rules go out the window when analyzing some random local
|
||||
python file in some random workspace, and so we need to be more tolerant of situations that wouldn't
|
||||
fly in a published package, cases where we're not configured as well as we'd like, or cases where
|
||||
two projects in a monorepo have conflicting definitions (but we want to analyze both at once).
|
||||
|
||||
## Invalid Names
|
||||
|
||||
While you can't syntactically refer to a module with an invalid name (i.e. one with a `-`, or that
|
||||
has the same name as a keyword) there are plenty of situations where a module with an invalid name
|
||||
can be run. For instance `python my-script.py` and `python my-proj/main.py` both work, even though
|
||||
we might in the course of analyzing the code compute the module name `my-script` or `my-proj.main`.
|
||||
|
||||
Also, a sufficiently motivated programmer can technically use `importlib.import_module` which takes
|
||||
strings and does in fact allow syntactically invalid module names.
|
||||
|
||||
### Current File Is Invalid Module Name
|
||||
|
||||
Relative and absolute imports should resolve fine in a file that isn't a valid module name.
|
||||
|
||||
`my-main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .mod1 import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import mod2
|
||||
import mod3
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(mod2.y) # revealed: Unknown
|
||||
reveal_type(mod3.z) # revealed: int
|
||||
```
|
||||
|
||||
`mod1.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`mod2.py`:
|
||||
|
||||
```py
|
||||
y: int = 2
|
||||
```
|
||||
|
||||
`mod3.py`:
|
||||
|
||||
```py
|
||||
z: int = 2
|
||||
```
|
||||
|
||||
### Current Directory Is Invalid Module Name
|
||||
|
||||
Relative and absolute imports should resolve fine in a dir that isn't a valid module name.
|
||||
|
||||
`my-tests/main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .mod1 import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import mod2
|
||||
import mod3
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(mod2.y) # revealed: Unknown
|
||||
reveal_type(mod3.z) # revealed: int
|
||||
```
|
||||
|
||||
`my-tests/mod1.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`my-tests/mod2.py`:
|
||||
|
||||
```py
|
||||
y: int = 2
|
||||
```
|
||||
|
||||
`mod3.py`:
|
||||
|
||||
```py
|
||||
z: int = 2
|
||||
```
|
||||
|
||||
### Current Directory Is Invalid Package Name
|
||||
|
||||
Relative and absolute imports should resolve fine in a dir that isn't a valid package name, even if
|
||||
it contains an `__init__.py`:
|
||||
|
||||
`my-tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`my-tests/main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .mod1 import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import mod2
|
||||
import mod3
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(mod2.y) # revealed: Unknown
|
||||
reveal_type(mod3.z) # revealed: int
|
||||
```
|
||||
|
||||
`my-tests/mod1.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`my-tests/mod2.py`:
|
||||
|
||||
```py
|
||||
y: int = 2
|
||||
```
|
||||
|
||||
`mod3.py`:
|
||||
|
||||
```py
|
||||
z: int = 2
|
||||
```
|
||||
|
||||
## Multiple Projects
|
||||
|
||||
It's common for a monorepo to define many separate projects that may or may not depend on eachother
|
||||
and are stitched together with a package manager like `uv` or `poetry`, often as editables. In this
|
||||
case, especially when running as an LSP, we want to be able to analyze all of the projects at once,
|
||||
allowing us to reuse results between projects, without getting confused about things that only make
|
||||
sense when analyzing the project separately.
|
||||
|
||||
The following tests will feature two projects, `a` and `b` where the "real" packages are found under
|
||||
`src/` subdirectories (and we've been configured to understand that), but each project also contains
|
||||
other python files in their roots or subdirectories that contains python files which relatively
|
||||
import eachother and also absolutely import the main package of the project. All of these imports
|
||||
*should* resolve.
|
||||
|
||||
Often the fact that there is both an `a` and `b` project seemingly won't matter, but many possible
|
||||
solutions will misbehave under these conditions, as e.g. if both define a `main.py` and test code
|
||||
has `import main`, we need to resolve each project's main as appropriate.
|
||||
|
||||
One key hint we will have in these situations is the existence of a `pyproject.toml`, so the
|
||||
following examples include them in case they help.
|
||||
|
||||
### Tests Directory With Overlapping Names
|
||||
|
||||
Here we have fairly typical situation where there are two projects `aproj` and `bproj` where the
|
||||
"real" packages are found under `src/` subdirectories, but each project also contains a `tests/`
|
||||
directory that contains python files which relatively import eachother and also absolutely import
|
||||
the package they test. All of these imports *should* resolve.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
# This is similar to what we would compute for installed editables
|
||||
extra-paths = ["aproj/src/", "bproj/src/"]
|
||||
```
|
||||
|
||||
`aproj/tests/test1.py`:
|
||||
|
||||
```py
|
||||
from .setup import x
|
||||
from . import setup
|
||||
from a import y
|
||||
import a
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(setup.x) # revealed: int
|
||||
reveal_type(y) # revealed: int
|
||||
reveal_type(a.y) # revealed: int
|
||||
```
|
||||
|
||||
`aproj/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`aproj/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`aproj/src/a/__init__.py`:
|
||||
|
||||
```py
|
||||
y: int = 10
|
||||
```
|
||||
|
||||
`bproj/tests/test1.py`:
|
||||
|
||||
```py
|
||||
from .setup import x
|
||||
from . import setup
|
||||
from b import y
|
||||
import b
|
||||
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(setup.x) # revealed: str
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(b.y) # revealed: str
|
||||
```
|
||||
|
||||
`bproj/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: str = "2"
|
||||
```
|
||||
|
||||
`bproj/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`bproj/src/b/__init__.py`:
|
||||
|
||||
```py
|
||||
y: str = "20"
|
||||
```
|
||||
|
||||
### Tests Directory With Ambiguous Project Directories
|
||||
|
||||
The same situation as the previous test but instead of the project `a` being in a directory `aproj`
|
||||
to disambiguate, we now need to avoid getting confused about whether `a/` or `a/src/a/` is the
|
||||
package `a` while still resolving imports.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
# This is similar to what we would compute for installed editables
|
||||
extra-paths = ["a/src/", "b/src/"]
|
||||
```
|
||||
|
||||
`a/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import setup
|
||||
from a import y
|
||||
import a
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: int
|
||||
reveal_type(a.y) # revealed: int
|
||||
```
|
||||
|
||||
`a/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`a/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`a/src/a/__init__.py`:
|
||||
|
||||
```py
|
||||
y: int = 10
|
||||
```
|
||||
|
||||
`b/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import setup
|
||||
from b import y
|
||||
import b
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(b.y) # revealed: str
|
||||
```
|
||||
|
||||
`b/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: str = "2"
|
||||
```
|
||||
|
||||
`b/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`b/src/b/__init__.py`:
|
||||
|
||||
```py
|
||||
y: str = "20"
|
||||
```
|
||||
|
||||
### Tests Package With Ambiguous Project Directories
|
||||
|
||||
The same situation as the previous test but `tests/__init__.py` is also defined, in case that
|
||||
complicates the situation.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
extra-paths = ["a/src/", "b/src/"]
|
||||
```
|
||||
|
||||
`a/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import setup
|
||||
from a import y
|
||||
import a
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: int
|
||||
reveal_type(a.y) # revealed: int
|
||||
```
|
||||
|
||||
`a/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`a/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`a/src/a/__init__.py`:
|
||||
|
||||
```py
|
||||
y: int = 10
|
||||
```
|
||||
|
||||
`b/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
# error: [unresolved-import]
|
||||
from .setup import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
from . import setup
|
||||
from b import y
|
||||
import b
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(setup.x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(b.y) # revealed: str
|
||||
```
|
||||
|
||||
`b/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`b/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: str = "2"
|
||||
```
|
||||
|
||||
`b/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`b/src/b/__init__.py`:
|
||||
|
||||
```py
|
||||
y: str = "20"
|
||||
```
|
||||
|
||||
### Tests Directory Absolute Importing `main.py`
|
||||
|
||||
Here instead of defining packages we have a couple simple applications with a `main.py` and tests
|
||||
that `import main` and expect that to work.
|
||||
|
||||
`a/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(setup.x) # revealed: int
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`a/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`a/main.py`:
|
||||
|
||||
```py
|
||||
y: int = 10
|
||||
```
|
||||
|
||||
`b/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(setup.x) # revealed: str
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: str = "2"
|
||||
```
|
||||
|
||||
`b/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`b/main.py`:
|
||||
|
||||
```py
|
||||
y: str = "20"
|
||||
```
|
||||
|
||||
### Tests Package Absolute Importing `main.py`
|
||||
|
||||
The same as the previous case but `tests/__init__.py` exists in case that causes different issues.
|
||||
|
||||
`a/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(setup.x) # revealed: int
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`a/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`a/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`a/main.py`:
|
||||
|
||||
```py
|
||||
y: int = 10
|
||||
```
|
||||
|
||||
`b/tests/test1.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file
|
||||
|
||||
from .setup import x
|
||||
from . import setup
|
||||
|
||||
# error: [unresolved-import]
|
||||
from main import y
|
||||
|
||||
# error: [unresolved-import]
|
||||
import main
|
||||
|
||||
reveal_type(x) # revealed: str
|
||||
reveal_type(setup.x) # revealed: str
|
||||
reveal_type(y) # revealed: Unknown
|
||||
reveal_type(main.y) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b/tests/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`b/tests/setup.py`:
|
||||
|
||||
```py
|
||||
x: str = "2"
|
||||
```
|
||||
|
||||
`b/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`b/main.py`:
|
||||
|
||||
```py
|
||||
y: str = "20"
|
||||
```
|
||||
|
||||
### `main.py` absolute importing private package
|
||||
|
||||
In this case each project has a `main.py` that defines a "private" `utils` package and absolute
|
||||
imports it.
|
||||
|
||||
`a/main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from utils import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
import utils
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(utils.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
`a/utils/__init__.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
`a/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
|
||||
`b/main.py`:
|
||||
|
||||
```py
|
||||
# TODO: there should be no errors in this file.
|
||||
|
||||
# error: [unresolved-import]
|
||||
from utils import x
|
||||
|
||||
# error: [unresolved-import]
|
||||
import utils
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(utils.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
`b/utils/__init__.py`:
|
||||
|
||||
```py
|
||||
x: str = "2"
|
||||
```
|
||||
|
||||
`b/pyproject.toml`:
|
||||
|
||||
```text
|
||||
name = "a"
|
||||
version = "0.1.0"
|
||||
```
|
||||
@@ -408,3 +408,205 @@ class Vec2(NamedTuple):
|
||||
|
||||
Vec2(0.0, 0.0)
|
||||
```
|
||||
|
||||
## `super()` is not supported in NamedTuple methods
|
||||
|
||||
Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime. In Python
|
||||
3.14+, a `TypeError` is raised; in earlier versions, a confusing `RuntimeError` about
|
||||
`__classcell__` is raised.
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class F(NamedTuple):
|
||||
x: int
|
||||
|
||||
def method(self):
|
||||
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
|
||||
super()
|
||||
|
||||
def method_with_args(self):
|
||||
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
|
||||
super(F, self)
|
||||
|
||||
def method_with_different_pivot(self):
|
||||
# Even passing a different pivot class fails.
|
||||
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
|
||||
super(tuple, self)
|
||||
|
||||
@classmethod
|
||||
def class_method(cls):
|
||||
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
|
||||
super()
|
||||
|
||||
@staticmethod
|
||||
def static_method():
|
||||
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
|
||||
super()
|
||||
|
||||
@property
|
||||
def prop(self):
|
||||
# error: [super-call-in-named-tuple-method] "Cannot use `super()` in a method of NamedTuple class `F`"
|
||||
return super()
|
||||
```
|
||||
|
||||
However, classes that **inherit from** a `NamedTuple` class (but don't directly inherit from
|
||||
`NamedTuple`) can use `super()` normally:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class Base(NamedTuple):
|
||||
x: int
|
||||
|
||||
class Child(Base):
|
||||
def method(self):
|
||||
super()
|
||||
```
|
||||
|
||||
And regular classes that don't inherit from `NamedTuple` at all can use `super()` as normal:
|
||||
|
||||
```py
|
||||
class Regular:
|
||||
def method(self):
|
||||
super() # fine
|
||||
```
|
||||
|
||||
Using `super()` on a `NamedTuple` class also works fine if it occurs outside the class:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class F(NamedTuple):
|
||||
x: int
|
||||
|
||||
super(F, F(42)) # fine
|
||||
```
|
||||
|
||||
## NamedTuples cannot have field names starting with underscores
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class Foo(NamedTuple):
|
||||
# error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore"
|
||||
_bar: int
|
||||
|
||||
class Bar(NamedTuple):
|
||||
x: int
|
||||
|
||||
class Baz(Bar):
|
||||
_whatever: str # `Baz` is not a NamedTuple class, so this is fine
|
||||
```
|
||||
|
||||
## Prohibited NamedTuple attributes
|
||||
|
||||
`NamedTuple` classes have certain synthesized attributes that cannot be overwritten. Attempting to
|
||||
assign to these attributes (without type annotations) will raise an `AttributeError` at runtime.
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class F(NamedTuple):
|
||||
x: int
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
|
||||
_asdict = 42
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_make`"
|
||||
_make = "foo"
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_replace`"
|
||||
_replace = lambda self: self
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_fields`"
|
||||
_fields = ()
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_field_defaults`"
|
||||
_field_defaults = {}
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__new__`"
|
||||
__new__ = None
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__init__`"
|
||||
__init__ = None
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `__getnewargs__`"
|
||||
__getnewargs__ = None
|
||||
```
|
||||
|
||||
However, other attributes (including those starting with underscores) can be assigned without error:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class G(NamedTuple):
|
||||
x: int
|
||||
|
||||
# These are fine (not prohibited attributes)
|
||||
_custom = 42
|
||||
__custom__ = "ok"
|
||||
regular_attr = "value"
|
||||
```
|
||||
|
||||
Note that type-annotated attributes become NamedTuple fields, not attribute overrides. They are not
|
||||
flagged as prohibited attribute overrides (though field names starting with `_` are caught by the
|
||||
underscore field name check):
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class H(NamedTuple):
|
||||
x: int
|
||||
# This is a field declaration, not an override. It's not flagged as an override,
|
||||
# but is flagged because field names cannot start with underscores.
|
||||
# error: [invalid-named-tuple] "NamedTuple field `_asdict` cannot start with an underscore"
|
||||
_asdict: int = 0
|
||||
```
|
||||
|
||||
The check also applies to assignments within conditional blocks:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class I(NamedTuple):
|
||||
x: int
|
||||
|
||||
if True:
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
|
||||
_asdict = 42
|
||||
```
|
||||
|
||||
Method definitions with prohibited names are also flagged:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class J(NamedTuple):
|
||||
x: int
|
||||
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
|
||||
def _asdict(self):
|
||||
return {}
|
||||
|
||||
@classmethod
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_make`"
|
||||
def _make(cls, iterable):
|
||||
return cls(*iterable)
|
||||
```
|
||||
|
||||
Classes that inherit from a `NamedTuple` class (but don't directly inherit from `NamedTuple`) are
|
||||
not subject to these restrictions:
|
||||
|
||||
```py
|
||||
from typing import NamedTuple
|
||||
|
||||
class Base(NamedTuple):
|
||||
x: int
|
||||
|
||||
class Child(Base):
|
||||
# This is fine - Child is not directly a NamedTuple
|
||||
_asdict = 42
|
||||
```
|
||||
|
||||
@@ -283,8 +283,7 @@ class MyNamedTuple(NamedTuple):
|
||||
x: int
|
||||
|
||||
@override
|
||||
# TODO: this raises an exception at runtime (which we should emit a diagnostic for).
|
||||
# It shouldn't be an `invalid-explicit-override` diagnostic, however.
|
||||
# error: [invalid-named-tuple] "Cannot overwrite NamedTuple attribute `_asdict`"
|
||||
def _asdict(self, /) -> dict[str, Any]: ...
|
||||
|
||||
class MyNamedTupleParent(NamedTuple):
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: classes.md - Generic classes: Legacy syntax - Diagnostics for bad specializations
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/legacy/classes.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## library.py
|
||||
|
||||
```
|
||||
1 | from typing import TypeVar, Generic
|
||||
2 |
|
||||
3 | T = TypeVar("T", bound=str)
|
||||
4 | U = TypeVar("U", int, bytes)
|
||||
5 |
|
||||
6 | class Bounded(Generic[T]):
|
||||
7 | x: T
|
||||
8 |
|
||||
9 | class Constrained(Generic[U]):
|
||||
10 | x: U
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from library import Bounded, Constrained
|
||||
2 |
|
||||
3 | x: Bounded[int] # error: [invalid-type-arguments]
|
||||
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` of type variable `T@Bounded`
|
||||
--> src/main.py:3:12
|
||||
|
|
||||
1 | from library import Bounded, Constrained
|
||||
2 |
|
||||
3 | x: Bounded[int] # error: [invalid-type-arguments]
|
||||
| ^^^
|
||||
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
||||
|
|
||||
::: src/library.py:3:1
|
||||
|
|
||||
1 | from typing import TypeVar, Generic
|
||||
2 |
|
||||
3 | T = TypeVar("T", bound=str)
|
||||
| - Type variable defined here
|
||||
4 | U = TypeVar("U", int, bytes)
|
||||
|
|
||||
info: rule `invalid-type-arguments` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `bytes` of type variable `U@Constrained`
|
||||
--> src/main.py:4:16
|
||||
|
|
||||
3 | x: Bounded[int] # error: [invalid-type-arguments]
|
||||
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
||||
| ^^^
|
||||
|
|
||||
::: src/library.py:4:1
|
||||
|
|
||||
3 | T = TypeVar("T", bound=str)
|
||||
4 | U = TypeVar("U", int, bytes)
|
||||
| - Type variable defined here
|
||||
5 |
|
||||
6 | class Bounded(Generic[T]):
|
||||
|
|
||||
info: rule `invalid-type-arguments` is enabled by default
|
||||
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: classes.md - Generic classes: PEP 695 syntax - Diagnostics for bad specializations
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/generics/pep695/classes.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## library.py
|
||||
|
||||
```
|
||||
1 | class Bounded[T: str]:
|
||||
2 | x: T
|
||||
3 |
|
||||
4 | class Constrained[U: (int, bytes)]:
|
||||
5 | x: U
|
||||
```
|
||||
|
||||
## main.py
|
||||
|
||||
```
|
||||
1 | from library import Bounded, Constrained
|
||||
2 |
|
||||
3 | x: Bounded[int] # error: [invalid-type-arguments]
|
||||
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-type-arguments]: Type `int` is not assignable to upper bound `str` of type variable `T@Bounded`
|
||||
--> src/main.py:3:12
|
||||
|
|
||||
1 | from library import Bounded, Constrained
|
||||
2 |
|
||||
3 | x: Bounded[int] # error: [invalid-type-arguments]
|
||||
| ^^^
|
||||
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
||||
|
|
||||
::: src/library.py:1:15
|
||||
|
|
||||
1 | class Bounded[T: str]:
|
||||
| - Type variable defined here
|
||||
2 | x: T
|
||||
|
|
||||
info: rule `invalid-type-arguments` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[invalid-type-arguments]: Type `str` does not satisfy constraints `int`, `bytes` of type variable `U@Constrained`
|
||||
--> src/main.py:4:16
|
||||
|
|
||||
3 | x: Bounded[int] # error: [invalid-type-arguments]
|
||||
4 | y: Constrained[str] # error: [invalid-type-arguments]
|
||||
| ^^^
|
||||
|
|
||||
::: src/library.py:4:19
|
||||
|
|
||||
2 | x: T
|
||||
3 |
|
||||
4 | class Constrained[U: (int, bytes)]:
|
||||
| - Type variable defined here
|
||||
5 | x: U
|
||||
|
|
||||
info: rule `invalid-type-arguments` is enabled by default
|
||||
|
||||
```
|
||||
@@ -49,26 +49,29 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
|
||||
35 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
39 |
|
||||
40 | # Possible overrides of possibly `@final` methods...
|
||||
41 | class C(A):
|
||||
42 | if coinflip():
|
||||
43 | # TODO: the autofix here introduces invalid syntax because there are now no
|
||||
44 | # statements inside the `if:` branch
|
||||
45 | # (but it might still be a useful autofix in an IDE context?)
|
||||
46 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
47 | else:
|
||||
48 | pass
|
||||
49 |
|
||||
50 | if coinflip():
|
||||
51 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
|
||||
52 | else:
|
||||
53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
|
||||
54 |
|
||||
55 | if coinflip():
|
||||
56 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
57 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
38 |
|
||||
39 | # check that autofixes don't introduce invalid syntax
|
||||
40 | # if there are multiple statements on one line
|
||||
41 | #
|
||||
42 | # TODO: we should emit a Liskov violation here too
|
||||
43 | # error: [override-of-final-method]
|
||||
44 | method4 = 42; unrelated = 56 # fmt: skip
|
||||
45 |
|
||||
46 | # Possible overrides of possibly `@final` methods...
|
||||
47 | class C(A):
|
||||
48 | if coinflip():
|
||||
49 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
50 | else:
|
||||
51 | pass
|
||||
52 |
|
||||
53 | if coinflip():
|
||||
54 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
|
||||
55 | else:
|
||||
56 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
|
||||
57 |
|
||||
58 | if coinflip():
|
||||
59 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
60 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
@@ -104,7 +107,7 @@ info: rule `override-of-final-method` is enabled by default
|
||||
35 + # error: [override-of-final-method]
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
38 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
@@ -118,7 +121,6 @@ error[override-of-final-method]: Cannot override `A.method2`
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
|
|
||||
info: `A.method2` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:16:9
|
||||
@@ -140,8 +142,8 @@ info: rule `override-of-final-method` is enabled by default
|
||||
- def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
36 + # error: [override-of-final-method]
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
39 |
|
||||
38 |
|
||||
39 | # check that autofixes don't introduce invalid syntax
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
@@ -154,7 +156,8 @@ error[override-of-final-method]: Cannot override `A.method3`
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
38 |
|
||||
39 | # check that autofixes don't introduce invalid syntax
|
||||
|
|
||||
info: `A.method3` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:20:9
|
||||
@@ -174,23 +177,23 @@ info: rule `override-of-final-method` is enabled by default
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
- def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
37 + # error: [override-of-final-method]
|
||||
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
39 |
|
||||
40 | # Possible overrides of possibly `@final` methods...
|
||||
38 |
|
||||
39 | # check that autofixes don't introduce invalid syntax
|
||||
40 | # if there are multiple statements on one line
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method4`
|
||||
--> src/mdtest_snippet.py:38:9
|
||||
--> src/mdtest_snippet.py:44:5
|
||||
|
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
38 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
39 |
|
||||
40 | # Possible overrides of possibly `@final` methods...
|
||||
42 | # TODO: we should emit a Liskov violation here too
|
||||
43 | # error: [override-of-final-method]
|
||||
44 | method4 = 42; unrelated = 56 # fmt: skip
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
45 |
|
||||
46 | # Possible overrides of possibly `@final` methods...
|
||||
|
|
||||
info: `A.method4` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:29:9
|
||||
@@ -206,28 +209,19 @@ info: `A.method4` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `method4`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
35 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
- def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
38 + # error: [override-of-final-method]
|
||||
39 |
|
||||
40 | # Possible overrides of possibly `@final` methods...
|
||||
41 | class C(A):
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method1`
|
||||
--> src/mdtest_snippet.py:46:13
|
||||
--> src/mdtest_snippet.py:49:13
|
||||
|
|
||||
44 | # statements inside the `if:` branch
|
||||
45 | # (but it might still be a useful autofix in an IDE context?)
|
||||
46 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
47 | class C(A):
|
||||
48 | if coinflip():
|
||||
49 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
47 | else:
|
||||
48 | pass
|
||||
50 | else:
|
||||
51 | pass
|
||||
|
|
||||
info: `A.method1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:8:9
|
||||
@@ -243,26 +237,17 @@ info: `A.method1` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `method1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
43 | # TODO: the autofix here introduces invalid syntax because there are now no
|
||||
44 | # statements inside the `if:` branch
|
||||
45 | # (but it might still be a useful autofix in an IDE context?)
|
||||
- def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
46 + # error: [override-of-final-method]
|
||||
47 | else:
|
||||
48 | pass
|
||||
49 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method3`
|
||||
--> src/mdtest_snippet.py:56:13
|
||||
--> src/mdtest_snippet.py:59:13
|
||||
|
|
||||
55 | if coinflip():
|
||||
56 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
58 | if coinflip():
|
||||
59 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
57 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
60 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
|
|
||||
info: `A.method3` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:20:9
|
||||
@@ -277,23 +262,16 @@ info: `A.method3` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `method3`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
53 | def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
|
||||
54 |
|
||||
55 | if coinflip():
|
||||
- def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
56 + # error: [override-of-final-method]
|
||||
57 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method4`
|
||||
--> src/mdtest_snippet.py:57:13
|
||||
--> src/mdtest_snippet.py:60:13
|
||||
|
|
||||
55 | if coinflip():
|
||||
56 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
57 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
58 | if coinflip():
|
||||
59 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
60 | def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
|
|
||||
info: `A.method4` is decorated with `@final`, forbidding overrides
|
||||
@@ -310,11 +288,5 @@ info: `A.method4` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `method4`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
54 |
|
||||
55 | if coinflip():
|
||||
56 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
- def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
57 + # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -28,93 +28,93 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
|
||||
14 | @final
|
||||
15 | def my_property2(self) -> int: ...
|
||||
16 |
|
||||
17 | @final
|
||||
18 | @classmethod
|
||||
19 | def class_method1(cls) -> int: ...
|
||||
17 | @property
|
||||
18 | @final
|
||||
19 | def my_property3(self) -> int: ...
|
||||
20 |
|
||||
21 | @classmethod
|
||||
22 | @final
|
||||
23 | def class_method2(cls) -> int: ...
|
||||
21 | @final
|
||||
22 | @classmethod
|
||||
23 | def class_method1(cls) -> int: ...
|
||||
24 |
|
||||
25 | @final
|
||||
26 | @staticmethod
|
||||
27 | def static_method1() -> int: ...
|
||||
25 | @classmethod
|
||||
26 | @final
|
||||
27 | def class_method2(cls) -> int: ...
|
||||
28 |
|
||||
29 | @staticmethod
|
||||
30 | @final
|
||||
31 | def static_method2() -> int: ...
|
||||
29 | @final
|
||||
30 | @staticmethod
|
||||
31 | def static_method1() -> int: ...
|
||||
32 |
|
||||
33 | @lossy_decorator
|
||||
33 | @staticmethod
|
||||
34 | @final
|
||||
35 | def decorated_1(self): ...
|
||||
35 | def static_method2() -> int: ...
|
||||
36 |
|
||||
37 | @final
|
||||
38 | @lossy_decorator
|
||||
39 | def decorated_2(self): ...
|
||||
37 | @lossy_decorator
|
||||
38 | @final
|
||||
39 | def decorated_1(self): ...
|
||||
40 |
|
||||
41 | class Child(Parent):
|
||||
42 | # explicitly test the concise diagnostic message,
|
||||
43 | # which is different to the verbose diagnostic summary message:
|
||||
44 | #
|
||||
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
46 | def foo(self): ...
|
||||
47 | @property
|
||||
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
49 |
|
||||
50 | @property
|
||||
51 | def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
52 |
|
||||
53 | @classmethod
|
||||
54 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
55 |
|
||||
56 | @staticmethod
|
||||
57 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
41 | @final
|
||||
42 | @lossy_decorator
|
||||
43 | def decorated_2(self): ...
|
||||
44 |
|
||||
45 | class Child(Parent):
|
||||
46 | # explicitly test the concise diagnostic message,
|
||||
47 | # which is different to the verbose diagnostic summary message:
|
||||
48 | #
|
||||
49 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
50 | def foo(self): ...
|
||||
51 | @property
|
||||
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
53 |
|
||||
54 | @property
|
||||
55 | def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
56 | @my_property2.setter
|
||||
57 | def my_property2(self, x: int) -> None: ...
|
||||
58 |
|
||||
59 | @classmethod
|
||||
60 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
61 |
|
||||
62 | @staticmethod
|
||||
63 | def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
64 |
|
||||
65 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
||||
59 | @property
|
||||
60 | def my_property3(self) -> int: ... # error: [override-of-final-method]
|
||||
61 | @my_property3.deleter
|
||||
62 | def my_proeprty3(self) -> None: ...
|
||||
63 |
|
||||
64 | @classmethod
|
||||
65 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
66 |
|
||||
67 | @lossy_decorator
|
||||
68 | def decorated_2(self): ... # TODO: should emit [override-of-final-method]
|
||||
67 | @staticmethod
|
||||
68 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
69 |
|
||||
70 | class OtherChild(Parent): ...
|
||||
71 |
|
||||
72 | class Grandchild(OtherChild):
|
||||
70 | @classmethod
|
||||
71 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
72 |
|
||||
73 | @staticmethod
|
||||
74 | # TODO: we should emit a Liskov violation here too
|
||||
75 | # error: [override-of-final-method]
|
||||
76 | def foo(): ...
|
||||
77 | @property
|
||||
78 | # TODO: we should emit a Liskov violation here too
|
||||
79 | # error: [override-of-final-method]
|
||||
80 | def my_property1(self) -> str: ...
|
||||
81 | # TODO: we should emit a Liskov violation here too
|
||||
82 | # error: [override-of-final-method]
|
||||
83 | class_method1 = None
|
||||
84 |
|
||||
85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
|
||||
86 |
|
||||
87 | T = TypeVar("T")
|
||||
88 |
|
||||
89 | def identity(x: T) -> T: ...
|
||||
90 |
|
||||
91 | class Foo:
|
||||
92 | @final
|
||||
93 | @identity
|
||||
94 | @identity
|
||||
95 | @identity
|
||||
96 | @identity
|
||||
97 | @identity
|
||||
98 | @identity
|
||||
99 | @identity
|
||||
100 | @identity
|
||||
101 | @identity
|
||||
102 | @identity
|
||||
103 | @identity
|
||||
74 | def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
75 |
|
||||
76 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
||||
77 |
|
||||
78 | @lossy_decorator
|
||||
79 | def decorated_2(self): ... # TODO: should emit [override-of-final-method]
|
||||
80 |
|
||||
81 | class OtherChild(Parent): ...
|
||||
82 |
|
||||
83 | class Grandchild(OtherChild):
|
||||
84 | @staticmethod
|
||||
85 | # TODO: we should emit a Liskov violation here too
|
||||
86 | # error: [override-of-final-method]
|
||||
87 | def foo(): ...
|
||||
88 | @property
|
||||
89 | # TODO: we should emit a Liskov violation here too
|
||||
90 | # error: [override-of-final-method]
|
||||
91 | def my_property1(self) -> str: ...
|
||||
92 | # TODO: we should emit a Liskov violation here too
|
||||
93 | # error: [override-of-final-method]
|
||||
94 | class_method1 = None
|
||||
95 |
|
||||
96 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
|
||||
97 |
|
||||
98 | T = TypeVar("T")
|
||||
99 |
|
||||
100 | def identity(x: T) -> T: ...
|
||||
101 |
|
||||
102 | class Foo:
|
||||
103 | @final
|
||||
104 | @identity
|
||||
105 | @identity
|
||||
106 | @identity
|
||||
@@ -122,24 +122,35 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
|
||||
108 | @identity
|
||||
109 | @identity
|
||||
110 | @identity
|
||||
111 | def bar(self): ...
|
||||
112 |
|
||||
113 | class Baz(Foo):
|
||||
114 | def bar(self): ... # error: [override-of-final-method]
|
||||
111 | @identity
|
||||
112 | @identity
|
||||
113 | @identity
|
||||
114 | @identity
|
||||
115 | @identity
|
||||
116 | @identity
|
||||
117 | @identity
|
||||
118 | @identity
|
||||
119 | @identity
|
||||
120 | @identity
|
||||
121 | @identity
|
||||
122 | def bar(self): ...
|
||||
123 |
|
||||
124 | class Baz(Foo):
|
||||
125 | def bar(self): ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.foo`
|
||||
--> src/mdtest_snippet.pyi:46:9
|
||||
--> src/mdtest_snippet.pyi:50:9
|
||||
|
|
||||
44 | #
|
||||
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
46 | def foo(self): ...
|
||||
48 | #
|
||||
49 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
50 | def foo(self): ...
|
||||
| ^^^ Overrides a definition from superclass `Parent`
|
||||
47 | @property
|
||||
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
51 | @property
|
||||
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
|
|
||||
info: `Parent.foo` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:6:5
|
||||
@@ -154,28 +165,28 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `foo`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
43 | # which is different to the verbose diagnostic summary message:
|
||||
44 | #
|
||||
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
47 | # which is different to the verbose diagnostic summary message:
|
||||
48 | #
|
||||
49 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
- def foo(self): ...
|
||||
46 +
|
||||
47 | @property
|
||||
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
49 |
|
||||
50 +
|
||||
51 | @property
|
||||
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
53 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.my_property1`
|
||||
--> src/mdtest_snippet.pyi:48:9
|
||||
--> src/mdtest_snippet.pyi:52:9
|
||||
|
|
||||
46 | def foo(self): ...
|
||||
47 | @property
|
||||
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
50 | def foo(self): ...
|
||||
51 | @property
|
||||
52 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
49 |
|
||||
50 | @property
|
||||
53 |
|
||||
54 | @property
|
||||
|
|
||||
info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:9:5
|
||||
@@ -192,28 +203,18 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `my_property1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
44 | #
|
||||
45 | # error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
46 | def foo(self): ...
|
||||
- @property
|
||||
- def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
47 + # error: [override-of-final-method]
|
||||
48 |
|
||||
49 | @property
|
||||
50 | def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.my_property2`
|
||||
--> src/mdtest_snippet.pyi:51:9
|
||||
--> src/mdtest_snippet.pyi:55:9
|
||||
|
|
||||
50 | @property
|
||||
51 | def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
54 | @property
|
||||
55 | def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
52 |
|
||||
53 | @classmethod
|
||||
56 | @my_property2.setter
|
||||
57 | def my_property2(self, x: int) -> None: ...
|
||||
|
|
||||
info: `Parent.my_property2` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:14:5
|
||||
@@ -224,181 +225,197 @@ info: `Parent.my_property2` is decorated with `@final`, forbidding overrides
|
||||
15 | def my_property2(self) -> int: ...
|
||||
| ------------ `Parent.my_property2` defined here
|
||||
16 |
|
||||
17 | @final
|
||||
17 | @property
|
||||
|
|
||||
help: Remove the override of `my_property2`
|
||||
help: Remove the getter and setter for `my_property2`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.my_property3`
|
||||
--> src/mdtest_snippet.pyi:60:9
|
||||
|
|
||||
59 | @property
|
||||
60 | def my_property3(self) -> int: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
61 | @my_property3.deleter
|
||||
62 | def my_proeprty3(self) -> None: ...
|
||||
|
|
||||
info: `Parent.my_property3` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:18:5
|
||||
|
|
||||
17 | @property
|
||||
18 | @final
|
||||
| ------
|
||||
19 | def my_property3(self) -> int: ...
|
||||
| ------------ `Parent.my_property3` defined here
|
||||
20 |
|
||||
21 | @final
|
||||
|
|
||||
help: Remove the override of `my_property3`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
47 | @property
|
||||
48 | def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
49 |
|
||||
- @property
|
||||
- def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
50 + # error: [override-of-final-method]
|
||||
51 |
|
||||
52 | @classmethod
|
||||
53 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.class_method1`
|
||||
--> src/mdtest_snippet.pyi:54:9
|
||||
--> src/mdtest_snippet.pyi:65:9
|
||||
|
|
||||
53 | @classmethod
|
||||
54 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
64 | @classmethod
|
||||
65 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
55 |
|
||||
56 | @staticmethod
|
||||
66 |
|
||||
67 | @staticmethod
|
||||
|
|
||||
info: `Parent.class_method1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:17:5
|
||||
--> src/mdtest_snippet.pyi:21:5
|
||||
|
|
||||
15 | def my_property2(self) -> int: ...
|
||||
16 |
|
||||
17 | @final
|
||||
| ------
|
||||
18 | @classmethod
|
||||
19 | def class_method1(cls) -> int: ...
|
||||
| ------------- `Parent.class_method1` defined here
|
||||
19 | def my_property3(self) -> int: ...
|
||||
20 |
|
||||
21 | @classmethod
|
||||
21 | @final
|
||||
| ------
|
||||
22 | @classmethod
|
||||
23 | def class_method1(cls) -> int: ...
|
||||
| ------------- `Parent.class_method1` defined here
|
||||
24 |
|
||||
25 | @classmethod
|
||||
|
|
||||
help: Remove the override of `class_method1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
50 | @property
|
||||
51 | def my_property2(self) -> int: ... # error: [override-of-final-method]
|
||||
52 |
|
||||
61 | @my_property3.deleter
|
||||
62 | def my_proeprty3(self) -> None: ...
|
||||
63 |
|
||||
- @classmethod
|
||||
- def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
53 + # error: [override-of-final-method]
|
||||
54 |
|
||||
55 | @staticmethod
|
||||
56 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
64 + # error: [override-of-final-method]
|
||||
65 |
|
||||
66 | @staticmethod
|
||||
67 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.static_method1`
|
||||
--> src/mdtest_snippet.pyi:57:9
|
||||
--> src/mdtest_snippet.pyi:68:9
|
||||
|
|
||||
56 | @staticmethod
|
||||
57 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
67 | @staticmethod
|
||||
68 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
58 |
|
||||
59 | @classmethod
|
||||
69 |
|
||||
70 | @classmethod
|
||||
|
|
||||
info: `Parent.static_method1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:25:5
|
||||
--> src/mdtest_snippet.pyi:29:5
|
||||
|
|
||||
23 | def class_method2(cls) -> int: ...
|
||||
24 |
|
||||
25 | @final
|
||||
| ------
|
||||
26 | @staticmethod
|
||||
27 | def static_method1() -> int: ...
|
||||
| -------------- `Parent.static_method1` defined here
|
||||
27 | def class_method2(cls) -> int: ...
|
||||
28 |
|
||||
29 | @staticmethod
|
||||
29 | @final
|
||||
| ------
|
||||
30 | @staticmethod
|
||||
31 | def static_method1() -> int: ...
|
||||
| -------------- `Parent.static_method1` defined here
|
||||
32 |
|
||||
33 | @staticmethod
|
||||
|
|
||||
help: Remove the override of `static_method1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
53 | @classmethod
|
||||
54 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
55 |
|
||||
64 | @classmethod
|
||||
65 | def class_method1(cls) -> int: ... # error: [override-of-final-method]
|
||||
66 |
|
||||
- @staticmethod
|
||||
- def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
56 + # error: [override-of-final-method]
|
||||
57 |
|
||||
58 | @classmethod
|
||||
59 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
67 + # error: [override-of-final-method]
|
||||
68 |
|
||||
69 | @classmethod
|
||||
70 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.class_method2`
|
||||
--> src/mdtest_snippet.pyi:60:9
|
||||
--> src/mdtest_snippet.pyi:71:9
|
||||
|
|
||||
59 | @classmethod
|
||||
60 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
70 | @classmethod
|
||||
71 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
61 |
|
||||
62 | @staticmethod
|
||||
72 |
|
||||
73 | @staticmethod
|
||||
|
|
||||
info: `Parent.class_method2` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:22:5
|
||||
--> src/mdtest_snippet.pyi:26:5
|
||||
|
|
||||
21 | @classmethod
|
||||
22 | @final
|
||||
25 | @classmethod
|
||||
26 | @final
|
||||
| ------
|
||||
23 | def class_method2(cls) -> int: ...
|
||||
27 | def class_method2(cls) -> int: ...
|
||||
| ------------- `Parent.class_method2` defined here
|
||||
24 |
|
||||
25 | @final
|
||||
28 |
|
||||
29 | @final
|
||||
|
|
||||
help: Remove the override of `class_method2`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
56 | @staticmethod
|
||||
57 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
58 |
|
||||
67 | @staticmethod
|
||||
68 | def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
69 |
|
||||
- @classmethod
|
||||
- def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
59 + # error: [override-of-final-method]
|
||||
60 |
|
||||
61 | @staticmethod
|
||||
62 | def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
70 + # error: [override-of-final-method]
|
||||
71 |
|
||||
72 | @staticmethod
|
||||
73 | def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.static_method2`
|
||||
--> src/mdtest_snippet.pyi:63:9
|
||||
--> src/mdtest_snippet.pyi:74:9
|
||||
|
|
||||
62 | @staticmethod
|
||||
63 | def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
73 | @staticmethod
|
||||
74 | def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
64 |
|
||||
65 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
||||
75 |
|
||||
76 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
||||
|
|
||||
info: `Parent.static_method2` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:30:5
|
||||
--> src/mdtest_snippet.pyi:34:5
|
||||
|
|
||||
29 | @staticmethod
|
||||
30 | @final
|
||||
33 | @staticmethod
|
||||
34 | @final
|
||||
| ------
|
||||
31 | def static_method2() -> int: ...
|
||||
35 | def static_method2() -> int: ...
|
||||
| -------------- `Parent.static_method2` defined here
|
||||
32 |
|
||||
33 | @lossy_decorator
|
||||
36 |
|
||||
37 | @lossy_decorator
|
||||
|
|
||||
help: Remove the override of `static_method2`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
59 | @classmethod
|
||||
60 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
61 |
|
||||
70 | @classmethod
|
||||
71 | def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
72 |
|
||||
- @staticmethod
|
||||
- def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
62 + # error: [override-of-final-method]
|
||||
63 |
|
||||
64 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
||||
65 |
|
||||
73 + # error: [override-of-final-method]
|
||||
74 |
|
||||
75 | def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
||||
76 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.foo`
|
||||
--> src/mdtest_snippet.pyi:76:9
|
||||
--> src/mdtest_snippet.pyi:87:9
|
||||
|
|
||||
74 | # TODO: we should emit a Liskov violation here too
|
||||
75 | # error: [override-of-final-method]
|
||||
76 | def foo(): ...
|
||||
85 | # TODO: we should emit a Liskov violation here too
|
||||
86 | # error: [override-of-final-method]
|
||||
87 | def foo(): ...
|
||||
| ^^^ Overrides a definition from superclass `Parent`
|
||||
77 | @property
|
||||
78 | # TODO: we should emit a Liskov violation here too
|
||||
88 | @property
|
||||
89 | # TODO: we should emit a Liskov violation here too
|
||||
|
|
||||
info: `Parent.foo` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:6:5
|
||||
@@ -413,31 +430,31 @@ info: `Parent.foo` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `foo`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
70 | class OtherChild(Parent): ...
|
||||
71 |
|
||||
72 | class Grandchild(OtherChild):
|
||||
81 | class OtherChild(Parent): ...
|
||||
82 |
|
||||
83 | class Grandchild(OtherChild):
|
||||
- @staticmethod
|
||||
- # TODO: we should emit a Liskov violation here too
|
||||
- # error: [override-of-final-method]
|
||||
- def foo(): ...
|
||||
73 +
|
||||
74 | @property
|
||||
75 | # TODO: we should emit a Liskov violation here too
|
||||
76 | # error: [override-of-final-method]
|
||||
84 +
|
||||
85 | @property
|
||||
86 | # TODO: we should emit a Liskov violation here too
|
||||
87 | # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.my_property1`
|
||||
--> src/mdtest_snippet.pyi:80:9
|
||||
--> src/mdtest_snippet.pyi:91:9
|
||||
|
|
||||
78 | # TODO: we should emit a Liskov violation here too
|
||||
79 | # error: [override-of-final-method]
|
||||
80 | def my_property1(self) -> str: ...
|
||||
89 | # TODO: we should emit a Liskov violation here too
|
||||
90 | # error: [override-of-final-method]
|
||||
91 | def my_property1(self) -> str: ...
|
||||
| ^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
81 | # TODO: we should emit a Liskov violation here too
|
||||
82 | # error: [override-of-final-method]
|
||||
92 | # TODO: we should emit a Liskov violation here too
|
||||
93 | # error: [override-of-final-method]
|
||||
|
|
||||
info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:9:5
|
||||
@@ -454,92 +471,71 @@ info: `Parent.my_property1` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `my_property1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
74 | # TODO: we should emit a Liskov violation here too
|
||||
75 | # error: [override-of-final-method]
|
||||
76 | def foo(): ...
|
||||
- @property
|
||||
- # TODO: we should emit a Liskov violation here too
|
||||
- # error: [override-of-final-method]
|
||||
- def my_property1(self) -> str: ...
|
||||
77 +
|
||||
78 | # TODO: we should emit a Liskov violation here too
|
||||
79 | # error: [override-of-final-method]
|
||||
80 | class_method1 = None
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Parent.class_method1`
|
||||
--> src/mdtest_snippet.pyi:83:5
|
||||
--> src/mdtest_snippet.pyi:94:5
|
||||
|
|
||||
81 | # TODO: we should emit a Liskov violation here too
|
||||
82 | # error: [override-of-final-method]
|
||||
83 | class_method1 = None
|
||||
92 | # TODO: we should emit a Liskov violation here too
|
||||
93 | # error: [override-of-final-method]
|
||||
94 | class_method1 = None
|
||||
| ^^^^^^^^^^^^^ Overrides a definition from superclass `Parent`
|
||||
84 |
|
||||
85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
|
||||
95 |
|
||||
96 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
|
||||
|
|
||||
info: `Parent.class_method1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:17:5
|
||||
--> src/mdtest_snippet.pyi:21:5
|
||||
|
|
||||
15 | def my_property2(self) -> int: ...
|
||||
16 |
|
||||
17 | @final
|
||||
| ------
|
||||
18 | @classmethod
|
||||
19 | def class_method1(cls) -> int: ...
|
||||
| ------------- `Parent.class_method1` defined here
|
||||
19 | def my_property3(self) -> int: ...
|
||||
20 |
|
||||
21 | @classmethod
|
||||
21 | @final
|
||||
| ------
|
||||
22 | @classmethod
|
||||
23 | def class_method1(cls) -> int: ...
|
||||
| ------------- `Parent.class_method1` defined here
|
||||
24 |
|
||||
25 | @classmethod
|
||||
|
|
||||
help: Remove the override of `class_method1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
80 | def my_property1(self) -> str: ...
|
||||
81 | # TODO: we should emit a Liskov violation here too
|
||||
82 | # error: [override-of-final-method]
|
||||
- class_method1 = None
|
||||
83 +
|
||||
84 |
|
||||
85 | # Diagnostic edge case: `final` is very far away from the method definition in the source code:
|
||||
86 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `Foo.bar`
|
||||
--> src/mdtest_snippet.pyi:114:9
|
||||
--> src/mdtest_snippet.pyi:125:9
|
||||
|
|
||||
113 | class Baz(Foo):
|
||||
114 | def bar(self): ... # error: [override-of-final-method]
|
||||
124 | class Baz(Foo):
|
||||
125 | def bar(self): ... # error: [override-of-final-method]
|
||||
| ^^^ Overrides a definition from superclass `Foo`
|
||||
|
|
||||
info: `Foo.bar` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.pyi:92:5
|
||||
--> src/mdtest_snippet.pyi:103:5
|
||||
|
|
||||
91 | class Foo:
|
||||
92 | @final
|
||||
102 | class Foo:
|
||||
103 | @final
|
||||
| ------
|
||||
93 | @identity
|
||||
94 | @identity
|
||||
104 | @identity
|
||||
105 | @identity
|
||||
|
|
||||
::: src/mdtest_snippet.pyi:111:9
|
||||
::: src/mdtest_snippet.pyi:122:9
|
||||
|
|
||||
109 | @identity
|
||||
110 | @identity
|
||||
111 | def bar(self): ...
|
||||
120 | @identity
|
||||
121 | @identity
|
||||
122 | def bar(self): ...
|
||||
| --- `Foo.bar` defined here
|
||||
112 |
|
||||
113 | class Baz(Foo):
|
||||
123 |
|
||||
124 | class Baz(Foo):
|
||||
|
|
||||
help: Remove the override of `bar`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
111 | def bar(self): ...
|
||||
112 |
|
||||
113 | class Baz(Foo):
|
||||
122 | def bar(self): ...
|
||||
123 |
|
||||
124 | class Baz(Foo):
|
||||
- def bar(self): ... # error: [override-of-final-method]
|
||||
114 + # error: [override-of-final-method]
|
||||
125 + pass # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -53,7 +53,7 @@ info: rule `override-of-final-method` is enabled by default
|
||||
2 |
|
||||
3 | class Foo(module1.Foo):
|
||||
- def f(self): ... # error: [override-of-final-method]
|
||||
4 + # error: [override-of-final-method]
|
||||
4 + pass # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ info: rule `override-of-final-method` is enabled by default
|
||||
7 | class B(A):
|
||||
- @final
|
||||
- def f(self): ... # error: [override-of-final-method]
|
||||
8 + # error: [override-of-final-method]
|
||||
8 + pass # error: [override-of-final-method]
|
||||
9 |
|
||||
10 | class C(B):
|
||||
11 | @final
|
||||
@@ -95,7 +95,7 @@ info: rule `override-of-final-method` is enabled by default
|
||||
- @final
|
||||
- # we only emit one error here, not two
|
||||
- def f(self): ... # error: [override-of-final-method]
|
||||
12 + # error: [override-of-final-method]
|
||||
12 + pass # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -58,7 +58,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
|
||||
44 | def bar(self, x: str) -> str: ...
|
||||
45 | @overload
|
||||
46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method]
|
||||
47 |
|
||||
47 |
|
||||
48 | @overload
|
||||
49 | def baz(self, x: str) -> str: ...
|
||||
50 | @overload
|
||||
@@ -265,7 +265,7 @@ error[override-of-final-method]: Cannot override `Bad.bar`
|
||||
45 | @overload
|
||||
46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method]
|
||||
| ^^^ Overrides a definition from superclass `Bad`
|
||||
47 |
|
||||
47 |
|
||||
48 | @overload
|
||||
|
|
||||
info: `Bad.bar` is decorated with `@final`, forbidding overrides
|
||||
@@ -287,12 +287,11 @@ info: rule `override-of-final-method` is enabled by default
|
||||
- def bar(self, x: str) -> str: ...
|
||||
- @overload
|
||||
- def bar(self, x: int) -> int: ... # error: [override-of-final-method]
|
||||
43 |
|
||||
43 +
|
||||
44 + # error: [override-of-final-method]
|
||||
45 +
|
||||
45 |
|
||||
46 | @overload
|
||||
47 | def baz(self, x: str) -> str: ...
|
||||
48 | @overload
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
@@ -319,7 +318,7 @@ help: Remove all overloads for `baz`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
45 | @overload
|
||||
46 | def bar(self, x: int) -> int: ... # error: [override-of-final-method]
|
||||
47 |
|
||||
47 |
|
||||
- @overload
|
||||
- def baz(self, x: str) -> str: ...
|
||||
- @overload
|
||||
@@ -360,12 +359,12 @@ info: rule `override-of-final-method` is enabled by default
|
||||
- def f(self, x: str) -> str: ...
|
||||
- @overload
|
||||
- def f(self, x: int) -> int: ...
|
||||
13 +
|
||||
14 +
|
||||
13 + pass
|
||||
14 + pass
|
||||
15 | # error: [override-of-final-method]
|
||||
- def f(self, x: int | str) -> int | str:
|
||||
- return x
|
||||
16 +
|
||||
16 + pass
|
||||
17 |
|
||||
18 | class Bad:
|
||||
19 | @overload
|
||||
@@ -459,15 +458,6 @@ info: `Bad.f` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `f`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
57 |
|
||||
58 | class ChildOfBad(Bad):
|
||||
59 | # TODO: these should all cause us to emit Liskov violations as well
|
||||
- f = None # error: [override-of-final-method]
|
||||
60 + # error: [override-of-final-method]
|
||||
61 | g = None # error: [override-of-final-method]
|
||||
62 | h = None # error: [override-of-final-method]
|
||||
63 | i = None # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -493,14 +483,6 @@ info: `Bad.g` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `g`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
58 | class ChildOfBad(Bad):
|
||||
59 | # TODO: these should all cause us to emit Liskov violations as well
|
||||
60 | f = None # error: [override-of-final-method]
|
||||
- g = None # error: [override-of-final-method]
|
||||
61 + # error: [override-of-final-method]
|
||||
62 | h = None # error: [override-of-final-method]
|
||||
63 | i = None # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -525,13 +507,6 @@ info: `Bad.h` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `h`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
59 | # TODO: these should all cause us to emit Liskov violations as well
|
||||
60 | f = None # error: [override-of-final-method]
|
||||
61 | g = None # error: [override-of-final-method]
|
||||
- h = None # error: [override-of-final-method]
|
||||
62 + # error: [override-of-final-method]
|
||||
63 | i = None # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -555,11 +530,5 @@ info: `Bad.i` is decorated with `@final`, forbidding overrides
|
||||
|
|
||||
help: Remove the override of `i`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
60 | f = None # error: [override-of-final-method]
|
||||
61 | g = None # error: [override-of-final-method]
|
||||
62 | h = None # error: [override-of-final-method]
|
||||
- i = None # error: [override-of-final-method]
|
||||
63 + # error: [override-of-final-method]
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: named_tuple.md - `NamedTuple` - NamedTuples cannot have field names starting with underscores
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/named_tuple.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import NamedTuple
|
||||
2 |
|
||||
3 | class Foo(NamedTuple):
|
||||
4 | # error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore"
|
||||
5 | _bar: int
|
||||
6 |
|
||||
7 | class Bar(NamedTuple):
|
||||
8 | x: int
|
||||
9 |
|
||||
10 | class Baz(Bar):
|
||||
11 | _whatever: str # `Baz` is not a NamedTuple class, so this is fine
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error[invalid-named-tuple]: NamedTuple field name cannot start with an underscore
|
||||
--> src/mdtest_snippet.py:5:5
|
||||
|
|
||||
3 | class Foo(NamedTuple):
|
||||
4 | # error: [invalid-named-tuple] "NamedTuple field `_bar` cannot start with an underscore"
|
||||
5 | _bar: int
|
||||
| ^^^^^^^^^ Class definition will raise `TypeError` at runtime due to this field
|
||||
6 |
|
||||
7 | class Bar(NamedTuple):
|
||||
|
|
||||
info: rule `invalid-named-tuple` is enabled by default
|
||||
|
||||
```
|
||||
@@ -329,6 +329,16 @@ reveal_type(tuple[int, str]) # revealed: <class 'tuple[int, str]'>
|
||||
reveal_type(tuple[int, ...]) # revealed: <class 'tuple[int, ...]'>
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def _(a: type[tuple], b: type[tuple[int]], c: type[tuple[int, ...]], d: type[tuple[Any, ...]]) -> None:
|
||||
reveal_type(a) # revealed: type[tuple[Unknown, ...]]
|
||||
reveal_type(b) # revealed: type[tuple[int]]
|
||||
reveal_type(c) # revealed: type[tuple[int, ...]]
|
||||
reveal_type(d) # revealed: type[tuple[Any, ...]]
|
||||
```
|
||||
|
||||
## Inheritance
|
||||
|
||||
```toml
|
||||
@@ -392,7 +402,7 @@ class C(Tuple): ...
|
||||
reveal_mro(C)
|
||||
```
|
||||
|
||||
### Union subscript access
|
||||
## Union subscript access
|
||||
|
||||
```py
|
||||
def test(val: tuple[str] | tuple[int]):
|
||||
@@ -402,7 +412,7 @@ def test2(val: tuple[str, None] | list[int | float]):
|
||||
reveal_type(val[0]) # revealed: str | int | float
|
||||
```
|
||||
|
||||
### Union subscript access with non-indexable type
|
||||
## Union subscript access with non-indexable type
|
||||
|
||||
```py
|
||||
def test3(val: tuple[str] | tuple[int] | int):
|
||||
@@ -410,7 +420,7 @@ def test3(val: tuple[str] | tuple[int] | int):
|
||||
reveal_type(val[0]) # revealed: str | int | Unknown
|
||||
```
|
||||
|
||||
### Intersection subscript access
|
||||
## Intersection subscript access
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
|
||||
@@ -85,6 +85,50 @@ a = test \
|
||||
+ 2 # type: ignore
|
||||
```
|
||||
|
||||
## Interpolated strings
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.14"
|
||||
```
|
||||
|
||||
Suppressions for expressions within interpolated strings can be placed after the interpolated string
|
||||
if it's a single-line interpolation.
|
||||
|
||||
```py
|
||||
a = f"""
|
||||
{test}
|
||||
""" # type: ignore
|
||||
```
|
||||
|
||||
For multiline-interpolation, put the ignore comment on the expression's start or end line:
|
||||
|
||||
```py
|
||||
a = f"""
|
||||
{
|
||||
10 / # type: ignore
|
||||
0
|
||||
}
|
||||
"""
|
||||
|
||||
a = f"""
|
||||
{
|
||||
10 /
|
||||
0 # type: ignore
|
||||
}
|
||||
"""
|
||||
```
|
||||
|
||||
But not at the end of the f-string:
|
||||
|
||||
```py
|
||||
a = f"""
|
||||
{
|
||||
10 / 0 # error: [division-by-zero]
|
||||
}
|
||||
""" # error: [unused-ignore-comment] # type: ignore
|
||||
```
|
||||
|
||||
## Codes
|
||||
|
||||
Mypy supports `type: ignore[code]`. ty doesn't understand mypy's rule names. Therefore, ignore the
|
||||
|
||||
@@ -398,7 +398,7 @@ the expression `str`:
|
||||
from ty_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
# This is incorrect and therefore fails with ...
|
||||
# error: "Static assertion error: argument of type `ty_extensions.ConstraintSet[never]` is statically known to be falsy"
|
||||
# error: "Static assertion error: argument of type `ty_extensions.ConstraintSet` is statically known to be falsy"
|
||||
static_assert(is_subtype_of(str, type[str]))
|
||||
|
||||
# Correct, returns True:
|
||||
|
||||
@@ -123,11 +123,11 @@ class A:
|
||||
A class `A` is a subtype of `type[T]` if any instance of `A` is a subtype of `T`.
|
||||
|
||||
```py
|
||||
from typing import Callable, Protocol
|
||||
from typing import Any, Callable, Protocol
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, is_disjoint_from, static_assert
|
||||
|
||||
class IntCallback(Protocol):
|
||||
def __call__(self, *args, **kwargs) -> int: ...
|
||||
class Callback[T](Protocol):
|
||||
def __call__(self, *args, **kwargs) -> T: ...
|
||||
|
||||
def _[T](_: T):
|
||||
static_assert(not is_subtype_of(type[T], T))
|
||||
@@ -141,8 +141,11 @@ def _[T](_: T):
|
||||
static_assert(is_assignable_to(type[T], Callable[..., T]))
|
||||
static_assert(not is_disjoint_from(type[T], Callable[..., T]))
|
||||
|
||||
static_assert(not is_assignable_to(type[T], IntCallback))
|
||||
static_assert(not is_disjoint_from(type[T], IntCallback))
|
||||
static_assert(is_assignable_to(type[T], Callable[..., T] | Callable[..., Any]))
|
||||
static_assert(not is_disjoint_from(type[T], Callable[..., T] | Callable[..., Any]))
|
||||
|
||||
static_assert(not is_assignable_to(type[T], Callback[int]))
|
||||
static_assert(not is_disjoint_from(type[T], Callback[int]))
|
||||
|
||||
def _[T: int](_: T):
|
||||
static_assert(not is_subtype_of(type[T], T))
|
||||
@@ -157,14 +160,23 @@ def _[T: int](_: T):
|
||||
static_assert(is_subtype_of(type[T], type[int]))
|
||||
static_assert(not is_disjoint_from(type[T], type[int]))
|
||||
|
||||
static_assert(is_subtype_of(type[T], type[int] | None))
|
||||
static_assert(not is_disjoint_from(type[T], type[int] | None))
|
||||
|
||||
static_assert(is_subtype_of(type[T], type[T]))
|
||||
static_assert(not is_disjoint_from(type[T], type[T]))
|
||||
|
||||
static_assert(is_assignable_to(type[T], Callable[..., T]))
|
||||
static_assert(not is_disjoint_from(type[T], Callable[..., T]))
|
||||
|
||||
static_assert(is_assignable_to(type[T], IntCallback))
|
||||
static_assert(not is_disjoint_from(type[T], IntCallback))
|
||||
static_assert(is_assignable_to(type[T], Callable[..., T] | Callable[..., Any]))
|
||||
static_assert(not is_disjoint_from(type[T], Callable[..., T] | Callable[..., Any]))
|
||||
|
||||
static_assert(is_assignable_to(type[T], Callback[int]))
|
||||
static_assert(not is_disjoint_from(type[T], Callback[int]))
|
||||
|
||||
static_assert(is_assignable_to(type[T], Callback[int] | Callback[Any]))
|
||||
static_assert(not is_disjoint_from(type[T], Callback[int] | Callback[Any]))
|
||||
|
||||
static_assert(is_subtype_of(type[T], type[T] | None))
|
||||
static_assert(not is_disjoint_from(type[T], type[T] | None))
|
||||
@@ -183,8 +195,14 @@ def _[T: (int, str)](_: T):
|
||||
static_assert(is_assignable_to(type[T], Callable[..., T]))
|
||||
static_assert(not is_disjoint_from(type[T], Callable[..., T]))
|
||||
|
||||
static_assert(not is_assignable_to(type[T], IntCallback))
|
||||
static_assert(not is_disjoint_from(type[T], IntCallback))
|
||||
static_assert(is_assignable_to(type[T], Callable[..., T] | Callable[..., Any]))
|
||||
static_assert(not is_disjoint_from(type[T], Callable[..., T] | Callable[..., Any]))
|
||||
|
||||
static_assert(not is_assignable_to(type[T], Callback[int]))
|
||||
static_assert(not is_disjoint_from(type[T], Callback[int]))
|
||||
|
||||
static_assert(is_assignable_to(type[T], Callback[int | str]))
|
||||
static_assert(not is_disjoint_from(type[T], Callback[int] | Callback[str]))
|
||||
|
||||
static_assert(is_subtype_of(type[T], type[T] | None))
|
||||
static_assert(not is_disjoint_from(type[T], type[T] | None))
|
||||
|
||||
@@ -34,7 +34,7 @@ upper bound.
|
||||
|
||||
```py
|
||||
from typing import Any, final, Never, Sequence
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -44,8 +44,8 @@ class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Super))
|
||||
# (Sub ≤ T@_ ≤ Super)
|
||||
ConstraintSet.range(Sub, T, Super)
|
||||
```
|
||||
|
||||
Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower
|
||||
@@ -53,8 +53,8 @@ bound.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(Never, T, Base))
|
||||
# (T@_ ≤ Base)
|
||||
ConstraintSet.range(Never, T, Base)
|
||||
```
|
||||
|
||||
Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having
|
||||
@@ -62,8 +62,8 @@ no upper bound.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)]
|
||||
reveal_type(ConstraintSet.range(Base, T, object))
|
||||
# (Base ≤ T@_)
|
||||
ConstraintSet.range(Base, T, object)
|
||||
```
|
||||
|
||||
And a range constraint with a lower bound of `Never` and an upper bound of `object` allows the
|
||||
@@ -74,8 +74,8 @@ of `object`.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ = *)]
|
||||
reveal_type(ConstraintSet.range(Never, T, object))
|
||||
# (T@_ = *)
|
||||
ConstraintSet.range(Never, T, object)
|
||||
```
|
||||
|
||||
If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound)
|
||||
@@ -83,10 +83,8 @@ or incomparable, then there is no type that can satisfy the constraint.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(ConstraintSet.range(Super, T, Sub))
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(ConstraintSet.range(Base, T, Unrelated))
|
||||
static_assert(not ConstraintSet.range(Super, T, Sub))
|
||||
static_assert(not ConstraintSet.range(Base, T, Unrelated))
|
||||
```
|
||||
|
||||
The lower and upper bound can be the same type, in which case the typevar can only be specialized to
|
||||
@@ -94,8 +92,8 @@ that specific type.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ = Base)]
|
||||
reveal_type(ConstraintSet.range(Base, T, Base))
|
||||
# (T@_ = Base)
|
||||
ConstraintSet.range(Base, T, Base)
|
||||
```
|
||||
|
||||
Constraints can only refer to fully static types, so the lower and upper bounds are transformed into
|
||||
@@ -103,15 +101,21 @@ their bottom and top materializations, respectively.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_)]
|
||||
reveal_type(ConstraintSet.range(Base, T, Any))
|
||||
# revealed: ty_extensions.ConstraintSet[(Sequence[Base] ≤ T@_ ≤ Sequence[object])]
|
||||
reveal_type(ConstraintSet.range(Sequence[Base], T, Sequence[Any]))
|
||||
constraints = ConstraintSet.range(Base, T, Any)
|
||||
expected = ConstraintSet.range(Base, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(Any, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[(Sequence[Never] ≤ T@_ ≤ Sequence[Base])]
|
||||
reveal_type(ConstraintSet.range(Sequence[Any], T, Sequence[Base]))
|
||||
constraints = ConstraintSet.range(Sequence[Base], T, Sequence[Any])
|
||||
expected = ConstraintSet.range(Sequence[Base], T, Sequence[object])
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Any, T, Base)
|
||||
expected = ConstraintSet.range(Never, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Sequence[Any], T, Sequence[Base])
|
||||
expected = ConstraintSet.range(Sequence[Never], T, Sequence[Base])
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
### Negated range
|
||||
@@ -122,7 +126,7 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc
|
||||
|
||||
```py
|
||||
from typing import Any, final, Never, Sequence
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -132,8 +136,8 @@ class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Super))
|
||||
# ¬(Sub ≤ T@_ ≤ Super)
|
||||
~ConstraintSet.range(Sub, T, Super)
|
||||
```
|
||||
|
||||
Every type is a supertype of `Never`, so a lower bound of `Never` is the same as having no lower
|
||||
@@ -141,8 +145,8 @@ bound.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(Never, T, Base))
|
||||
# ¬(T@_ ≤ Base)
|
||||
~ConstraintSet.range(Never, T, Base)
|
||||
```
|
||||
|
||||
Similarly, every type is a subtype of `object`, so an upper bound of `object` is the same as having
|
||||
@@ -150,8 +154,8 @@ no upper bound.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)]
|
||||
reveal_type(~ConstraintSet.range(Base, T, object))
|
||||
# ¬(Base ≤ T@_)
|
||||
~ConstraintSet.range(Base, T, object)
|
||||
```
|
||||
|
||||
And a negated range constraint with _both_ a lower bound of `Never` and an upper bound of `object`
|
||||
@@ -159,8 +163,8 @@ cannot be satisfied at all.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ *)]
|
||||
reveal_type(~ConstraintSet.range(Never, T, object))
|
||||
# (T@_ ≠ *)
|
||||
~ConstraintSet.range(Never, T, object)
|
||||
```
|
||||
|
||||
If the lower bound and upper bounds are "inverted" (the upper bound is a subtype of the lower bound)
|
||||
@@ -168,10 +172,8 @@ or incomparable, then the negated range constraint can always be satisfied.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~ConstraintSet.range(Super, T, Sub))
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~ConstraintSet.range(Base, T, Unrelated))
|
||||
static_assert(~ConstraintSet.range(Super, T, Sub))
|
||||
static_assert(~ConstraintSet.range(Base, T, Unrelated))
|
||||
```
|
||||
|
||||
The lower and upper bound can be the same type, in which case the typevar can be specialized to any
|
||||
@@ -179,8 +181,8 @@ type other than that specific type.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)]
|
||||
reveal_type(~ConstraintSet.range(Base, T, Base))
|
||||
# (T@_ ≠ Base)
|
||||
~ConstraintSet.range(Base, T, Base)
|
||||
```
|
||||
|
||||
Constraints can only refer to fully static types, so the lower and upper bounds are transformed into
|
||||
@@ -188,15 +190,21 @@ their bottom and top materializations, respectively.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_)]
|
||||
reveal_type(~ConstraintSet.range(Base, T, Any))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sequence[Base] ≤ T@_ ≤ Sequence[object])]
|
||||
reveal_type(~ConstraintSet.range(Sequence[Base], T, Sequence[Any]))
|
||||
constraints = ~ConstraintSet.range(Base, T, Any)
|
||||
expected = ~ConstraintSet.range(Base, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(Any, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sequence[Never] ≤ T@_ ≤ Sequence[Base])]
|
||||
reveal_type(~ConstraintSet.range(Sequence[Any], T, Sequence[Base]))
|
||||
constraints = ~ConstraintSet.range(Sequence[Base], T, Sequence[Any])
|
||||
expected = ~ConstraintSet.range(Sequence[Base], T, Sequence[object])
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(Any, T, Base)
|
||||
expected = ~ConstraintSet.range(Never, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(Sequence[Any], T, Sequence[Base])
|
||||
expected = ~ConstraintSet.range(Sequence[Never], T, Sequence[Base])
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
## Intersection
|
||||
@@ -218,10 +226,10 @@ We cannot simplify the intersection of constraints that refer to different typev
|
||||
|
||||
```py
|
||||
def _[T, U]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[((Sub ≤ T@_ ≤ Base) ∧ (Sub ≤ U@_ ≤ Base))]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Sub, U, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ U@_ ≤ Base))]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, U, Base))
|
||||
# (Sub ≤ T@_ ≤ Base) ∧ (Sub ≤ U@_ ≤ Base)
|
||||
ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Sub, U, Base)
|
||||
# ¬(Sub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ U@_ ≤ Base)
|
||||
~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, U, Base)
|
||||
```
|
||||
|
||||
### Intersection of two ranges
|
||||
@@ -230,7 +238,7 @@ The intersection of two ranges is where the ranges "overlap".
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -241,24 +249,29 @@ class SubSub(Sub): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Base) & ConstraintSet.range(Sub, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Super) & ConstraintSet.range(Sub, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ = Base)]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super))
|
||||
constraints = ConstraintSet.range(SubSub, T, Base) & ConstraintSet.range(Sub, T, Super)
|
||||
expected = ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(SubSub, T, Super) & ConstraintSet.range(Sub, T, Base)
|
||||
expected = ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Sub, T, Base) & ConstraintSet.range(Base, T, Super)
|
||||
expected = ConstraintSet.range(Base, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Sub, T, Super) & ConstraintSet.range(Sub, T, Super)
|
||||
expected = ConstraintSet.range(Sub, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
If they don't overlap, the intersection is empty.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object))
|
||||
static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Base, T, Super))
|
||||
static_assert(not ConstraintSet.range(SubSub, T, Sub) & ConstraintSet.range(Unrelated, T, object))
|
||||
```
|
||||
|
||||
Expanding on this, when intersecting two upper bounds constraints (`(T ≤ Base) ∧ (T ≤ Other)`), we
|
||||
@@ -267,23 +280,17 @@ satisfy their intersection `T ≤ Base & Other`, and vice versa.
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import Intersection, static_assert
|
||||
from ty_extensions import Intersection
|
||||
|
||||
# This is not final, so it's possible for a subclass to inherit from both Base and Other.
|
||||
class Other: ...
|
||||
|
||||
def upper_bounds[T]():
|
||||
# (T@upper_bounds ≤ Base & Other)
|
||||
intersection_type = ConstraintSet.range(Never, T, Intersection[Base, Other])
|
||||
# revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
|
||||
reveal_type(intersection_type)
|
||||
|
||||
# (T@upper_bounds ≤ Base) ∧ (T@upper_bounds ≤ Other)
|
||||
intersection_constraint = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, T, Other)
|
||||
# revealed: ty_extensions.ConstraintSet[(T@upper_bounds ≤ Base & Other)]
|
||||
reveal_type(intersection_constraint)
|
||||
|
||||
# The two constraint sets are equivalent; each satisfies the other.
|
||||
static_assert(intersection_type.satisfies(intersection_constraint))
|
||||
static_assert(intersection_constraint.satisfies(intersection_type))
|
||||
static_assert(intersection_type == intersection_constraint)
|
||||
```
|
||||
|
||||
For an intersection of two lower bounds constraints (`(Base ≤ T) ∧ (Other ≤ T)`), we union the lower
|
||||
@@ -292,17 +299,11 @@ bounds. Any type that satisfies both `Base ≤ T` and `Other ≤ T` must necessa
|
||||
|
||||
```py
|
||||
def lower_bounds[T]():
|
||||
# (Base | Other ≤ T@lower_bounds)
|
||||
union_type = ConstraintSet.range(Base | Other, T, object)
|
||||
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
|
||||
reveal_type(union_type)
|
||||
|
||||
# (Base ≤ T@upper_bounds) ∧ (Other ≤ T@upper_bounds)
|
||||
intersection_constraint = ConstraintSet.range(Base, T, object) & ConstraintSet.range(Other, T, object)
|
||||
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@lower_bounds)]
|
||||
reveal_type(intersection_constraint)
|
||||
|
||||
# The two constraint sets are equivalent; each satisfies the other.
|
||||
static_assert(union_type.satisfies(intersection_constraint))
|
||||
static_assert(intersection_constraint.satisfies(union_type))
|
||||
static_assert(union_type == intersection_constraint)
|
||||
```
|
||||
|
||||
### Intersection of a range and a negated range
|
||||
@@ -313,7 +314,7 @@ the intersection as removing the hole from the range constraint.
|
||||
|
||||
```py
|
||||
from typing import final, Never
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -328,10 +329,8 @@ If the negative range completely contains the positive range, then the intersect
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(SubSub, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, T, Base))
|
||||
static_assert(not ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(SubSub, T, Super))
|
||||
static_assert(not ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Sub, T, Base))
|
||||
```
|
||||
|
||||
If the negative range is disjoint from the positive range, the negative range doesn't remove
|
||||
@@ -339,12 +338,17 @@ anything; the intersection is the positive range.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated))
|
||||
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub)]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super)]
|
||||
reveal_type(ConstraintSet.range(Base, T, Super) & ~ConstraintSet.range(SubSub, T, Sub))
|
||||
constraints = ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Never, T, Unrelated)
|
||||
expected = ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super)
|
||||
expected = ConstraintSet.range(SubSub, T, Sub)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Base, T, Super) & ~ConstraintSet.range(SubSub, T, Sub)
|
||||
expected = ConstraintSet.range(Base, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
Otherwise we clip the negative constraint to the mininum range that overlaps with the positive
|
||||
@@ -352,10 +356,9 @@ range.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ T@_ ≤ Base))]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base))
|
||||
constraints = ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super)
|
||||
expected = ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
### Intersection of two negated ranges
|
||||
@@ -365,7 +368,7 @@ smaller constraint. For negated ranges, the smaller constraint is the one with t
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -376,22 +379,25 @@ class SubSub(Sub): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Super)]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super))
|
||||
constraints = ~ConstraintSet.range(SubSub, T, Super) & ~ConstraintSet.range(Sub, T, Base)
|
||||
expected = ~ConstraintSet.range(SubSub, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(Sub, T, Super) & ~ConstraintSet.range(Sub, T, Super)
|
||||
expected = ~ConstraintSet.range(Sub, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
Otherwise, the intersection cannot be simplified.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_))]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object))
|
||||
# ¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))
|
||||
~ConstraintSet.range(Sub, T, Base) & ~ConstraintSet.range(Base, T, Super)
|
||||
# ¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))
|
||||
~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Base, T, Super)
|
||||
# ¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_)
|
||||
~ConstraintSet.range(SubSub, T, Sub) & ~ConstraintSet.range(Unrelated, T, object)
|
||||
```
|
||||
|
||||
In particular, the following does not simplify, even though it seems like it could simplify to
|
||||
@@ -408,8 +414,8 @@ way.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Base))]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super))
|
||||
# (¬(Sub ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Base))
|
||||
~ConstraintSet.range(SubSub, T, Base) & ~ConstraintSet.range(Sub, T, Super)
|
||||
```
|
||||
|
||||
## Union
|
||||
@@ -431,10 +437,10 @@ We cannot simplify the union of constraints that refer to different typevars.
|
||||
|
||||
```py
|
||||
def _[T, U]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ∨ (Sub ≤ U@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, U, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base) ∨ ¬(Sub ≤ U@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Sub, U, Base))
|
||||
# (Sub ≤ T@_ ≤ Base) ∨ (Sub ≤ U@_ ≤ Base)
|
||||
ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, U, Base)
|
||||
# ¬(Sub ≤ T@_ ≤ Base) ∨ ¬(Sub ≤ U@_ ≤ Base)
|
||||
~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Sub, U, Base)
|
||||
```
|
||||
|
||||
### Union of two ranges
|
||||
@@ -444,7 +450,7 @@ bounds.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -455,22 +461,25 @@ class SubSub(Sub): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Super)]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super)]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Super) | ConstraintSet.range(Sub, T, Super))
|
||||
constraints = ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base)
|
||||
expected = ConstraintSet.range(SubSub, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Sub, T, Super) | ConstraintSet.range(Sub, T, Super)
|
||||
expected = ConstraintSet.range(Sub, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
Otherwise, the union cannot be simplified.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) ∨ (Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Sub)]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) ∨ (Unrelated ≤ T@_)]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object))
|
||||
# (Base ≤ T@_ ≤ Super) ∨ (Sub ≤ T@_ ≤ Base)
|
||||
ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Base, T, Super)
|
||||
# (Base ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Sub)
|
||||
ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super)
|
||||
# (SubSub ≤ T@_ ≤ Sub) ∨ (Unrelated ≤ T@_)
|
||||
ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Unrelated, T, object)
|
||||
```
|
||||
|
||||
In particular, the following does not simplify, even though it seems like it could simplify to
|
||||
@@ -485,8 +494,8 @@ not include `Sub`. That means it should not be in the union. Since that type _is
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Base)]
|
||||
reveal_type(ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
|
||||
# (Sub ≤ T@_ ≤ Super) ∨ (SubSub ≤ T@_ ≤ Base)
|
||||
ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super)
|
||||
```
|
||||
|
||||
The union of two upper bound constraints (`(T ≤ Base) ∨ (T ≤ Other)`) is different than the single
|
||||
@@ -496,24 +505,18 @@ that satisfies the union constraint satisfies the union type.
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import static_assert
|
||||
|
||||
# This is not final, so it's possible for a subclass to inherit from both Base and Other.
|
||||
class Other: ...
|
||||
|
||||
def union[T]():
|
||||
# (T@union ≤ Base | Other)
|
||||
union_type = ConstraintSet.range(Never, T, Base | Other)
|
||||
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base | Other)]
|
||||
reveal_type(union_type)
|
||||
|
||||
# (T@union ≤ Base) ∨ (T@union ≤ Other)
|
||||
union_constraint = ConstraintSet.range(Never, T, Base) | ConstraintSet.range(Never, T, Other)
|
||||
# revealed: ty_extensions.ConstraintSet[(T@union ≤ Base) ∨ (T@union ≤ Other)]
|
||||
reveal_type(union_constraint)
|
||||
|
||||
# (T = Base | Other) satisfies (T ≤ Base | Other) but not (T ≤ Base ∨ T ≤ Other)
|
||||
specialization = ConstraintSet.range(Base | Other, T, Base | Other)
|
||||
# revealed: ty_extensions.ConstraintSet[(T@union = Base | Other)]
|
||||
reveal_type(specialization)
|
||||
static_assert(specialization.satisfies(union_type))
|
||||
static_assert(not specialization.satisfies(union_constraint))
|
||||
|
||||
@@ -528,18 +531,13 @@ satisfies the union constraint (`(Base ≤ T) ∨ (Other ≤ T)`) but not the un
|
||||
|
||||
```py
|
||||
def union[T]():
|
||||
# (Base | Other ≤ T@union)
|
||||
union_type = ConstraintSet.range(Base | Other, T, object)
|
||||
# revealed: ty_extensions.ConstraintSet[(Base | Other ≤ T@union)]
|
||||
reveal_type(union_type)
|
||||
|
||||
# (Base ≤ T@union) ∨ (Other ≤ T@union)
|
||||
union_constraint = ConstraintSet.range(Base, T, object) | ConstraintSet.range(Other, T, object)
|
||||
# revealed: ty_extensions.ConstraintSet[(Base ≤ T@union) ∨ (Other ≤ T@union)]
|
||||
reveal_type(union_constraint)
|
||||
|
||||
# (T = Base) satisfies (Base ≤ T ∨ Other ≤ T) but not (Base | Other ≤ T)
|
||||
specialization = ConstraintSet.range(Base, T, Base)
|
||||
# revealed: ty_extensions.ConstraintSet[(T@union = Base)]
|
||||
reveal_type(specialization)
|
||||
static_assert(not specialization.satisfies(union_type))
|
||||
static_assert(specialization.satisfies(union_constraint))
|
||||
|
||||
@@ -556,7 +554,7 @@ the union as filling part of the hole with the types from the range constraint.
|
||||
|
||||
```py
|
||||
from typing import final, Never
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -571,10 +569,8 @@ If the positive range completely contains the negative range, then the union is
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(SubSub, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, T, Base))
|
||||
static_assert(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(SubSub, T, Super))
|
||||
static_assert(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Sub, T, Base))
|
||||
```
|
||||
|
||||
If the negative range is disjoint from the positive range, the positive range doesn't add anything;
|
||||
@@ -582,12 +578,17 @@ the union is the negative range.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Sub)]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Base ≤ T@_ ≤ Super)]
|
||||
reveal_type(~ConstraintSet.range(Base, T, Super) | ConstraintSet.range(SubSub, T, Sub))
|
||||
constraints = ~ConstraintSet.range(Sub, T, Base) | ConstraintSet.range(Never, T, Unrelated)
|
||||
expected = ~ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(SubSub, T, Sub) | ConstraintSet.range(Base, T, Super)
|
||||
expected = ~ConstraintSet.range(SubSub, T, Sub)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(Base, T, Super) | ConstraintSet.range(SubSub, T, Sub)
|
||||
expected = ~ConstraintSet.range(Base, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
Otherwise we clip the positive constraint to the mininum range that overlaps with the negative
|
||||
@@ -595,10 +596,9 @@ range.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ∨ ¬(SubSub ≤ T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ∨ ¬(SubSub ≤ T@_ ≤ Super)]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Super) | ConstraintSet.range(Sub, T, Base))
|
||||
constraints = ~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Super)
|
||||
expected = ~ConstraintSet.range(SubSub, T, Base) | ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
### Union of two negated ranges
|
||||
@@ -607,7 +607,7 @@ The union of two negated ranges has a hole where the ranges "overlap".
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
@@ -618,24 +618,29 @@ class SubSub(Sub): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Base) | ~ConstraintSet.range(Sub, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Super) | ~ConstraintSet.range(Sub, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ Base)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Super)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super))
|
||||
constraints = ~ConstraintSet.range(SubSub, T, Base) | ~ConstraintSet.range(Sub, T, Super)
|
||||
expected = ~ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(SubSub, T, Super) | ~ConstraintSet.range(Sub, T, Base)
|
||||
expected = ~ConstraintSet.range(Sub, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(Sub, T, Base) | ~ConstraintSet.range(Base, T, Super)
|
||||
expected = ~ConstraintSet.range(Base, T, Base)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ~ConstraintSet.range(Sub, T, Super) | ~ConstraintSet.range(Sub, T, Super)
|
||||
expected = ~ConstraintSet.range(Sub, T, Super)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
If the holes don't overlap, the union is always satisfied.
|
||||
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super))
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object))
|
||||
static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Base, T, Super))
|
||||
static_assert(~ConstraintSet.range(SubSub, T, Sub) | ~ConstraintSet.range(Unrelated, T, object))
|
||||
```
|
||||
|
||||
## Negation
|
||||
@@ -644,21 +649,21 @@ def _[T]() -> None:
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
|
||||
def _[T]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base)]
|
||||
reveal_type(~ConstraintSet.range(Never, T, Base))
|
||||
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_)]
|
||||
reveal_type(~ConstraintSet.range(Sub, T, object))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@_ ≠ *)]
|
||||
reveal_type(~ConstraintSet.range(Never, T, object))
|
||||
# ¬(Sub ≤ T@_ ≤ Base)
|
||||
~ConstraintSet.range(Sub, T, Base)
|
||||
# ¬(T@_ ≤ Base)
|
||||
~ConstraintSet.range(Never, T, Base)
|
||||
# ¬(Sub ≤ T@_)
|
||||
~ConstraintSet.range(Sub, T, object)
|
||||
# (T@_ ≠ *)
|
||||
~ConstraintSet.range(Never, T, object)
|
||||
```
|
||||
|
||||
The union of a range constraint and its negation should always be satisfiable.
|
||||
@@ -666,15 +671,14 @@ The union of a range constraint and its negation should always be satisfiable.
|
||||
```py
|
||||
def _[T]() -> None:
|
||||
constraint = ConstraintSet.range(Sub, T, Base)
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(constraint | ~constraint)
|
||||
static_assert(constraint | ~constraint)
|
||||
```
|
||||
|
||||
### Negation of constraints involving two variables
|
||||
|
||||
```py
|
||||
from typing import final, Never
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
class Base: ...
|
||||
|
||||
@@ -682,8 +686,8 @@ class Base: ...
|
||||
class Unrelated: ...
|
||||
|
||||
def _[T, U]() -> None:
|
||||
# revealed: ty_extensions.ConstraintSet[¬(T@_ ≤ Base) ∨ ¬(U@_ ≤ Base)]
|
||||
reveal_type(~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)))
|
||||
# ¬(T@_ ≤ Base) ∨ ¬(U@_ ≤ Base)
|
||||
~(ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base))
|
||||
```
|
||||
|
||||
The union of a constraint and its negation should always be satisfiable.
|
||||
@@ -691,150 +695,91 @@ The union of a constraint and its negation should always be satisfiable.
|
||||
```py
|
||||
def _[T, U]() -> None:
|
||||
c1 = ConstraintSet.range(Never, T, Base) & ConstraintSet.range(Never, U, Base)
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(c1 | ~c1)
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~c1 | c1)
|
||||
static_assert(c1 | ~c1)
|
||||
static_assert(~c1 | c1)
|
||||
|
||||
c2 = ConstraintSet.range(Unrelated, T, object) & ConstraintSet.range(Unrelated, U, object)
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(c2 | ~c2)
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~c2 | c2)
|
||||
static_assert(c2 | ~c2)
|
||||
static_assert(~c2 | c2)
|
||||
|
||||
union = c1 | c2
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(union | ~union)
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(~union | union)
|
||||
static_assert(union | ~union)
|
||||
static_assert(~union | union)
|
||||
```
|
||||
|
||||
## Typevar ordering
|
||||
|
||||
Constraints can relate two typevars — i.e., `S ≤ T`. We could encode that in one of two ways:
|
||||
`Never ≤ S ≤ T` or `S ≤ T ≤ object`. In other words, we can decide whether `S` or `T` is the typevar
|
||||
being constrained. The other is then the lower or upper bound of the constraint.
|
||||
|
||||
To handle this, we enforce an arbitrary ordering on typevars, and always place the constraint on the
|
||||
"earlier" typevar. For the example above, that does not change how the constraint is displayed,
|
||||
since we always hide `Never` lower bounds and `object` upper bounds.
|
||||
being constrained. The other is then the lower or upper bound of the constraint. To handle this, we
|
||||
enforce an arbitrary ordering on typevars, and always place the constraint on the "earlier" typevar.
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
def f[S, T]():
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
|
||||
reveal_type(ConstraintSet.range(Never, S, T))
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
|
||||
reveal_type(ConstraintSet.range(S, T, object))
|
||||
# (S@f ≤ T@f)
|
||||
c1 = ConstraintSet.range(Never, S, T)
|
||||
c2 = ConstraintSet.range(S, T, object)
|
||||
static_assert(c1 == c2)
|
||||
|
||||
def f[T, S]():
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
|
||||
reveal_type(ConstraintSet.range(Never, S, T))
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f ≤ T@f)]
|
||||
reveal_type(ConstraintSet.range(S, T, object))
|
||||
# (S@f ≤ T@f)
|
||||
c1 = ConstraintSet.range(Never, S, T)
|
||||
c2 = ConstraintSet.range(S, T, object)
|
||||
static_assert(c1 == c2)
|
||||
```
|
||||
|
||||
Equivalence constraints are similar; internally we arbitrarily choose the "earlier" typevar to be
|
||||
the constraint, and the other the bound. But we display the result the same way no matter what.
|
||||
the constraint, and the other the bound.
|
||||
|
||||
```py
|
||||
def f[S, T]():
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
|
||||
reveal_type(ConstraintSet.range(T, S, T))
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
|
||||
reveal_type(ConstraintSet.range(S, T, S))
|
||||
# (S@f = T@f)
|
||||
c1 = ConstraintSet.range(T, S, T)
|
||||
c2 = ConstraintSet.range(S, T, S)
|
||||
static_assert(c1 == c2)
|
||||
|
||||
def f[T, S]():
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
|
||||
reveal_type(ConstraintSet.range(T, S, T))
|
||||
# revealed: ty_extensions.ConstraintSet[(S@f = T@f)]
|
||||
reveal_type(ConstraintSet.range(S, T, S))
|
||||
# (S@f = T@f)
|
||||
c1 = ConstraintSet.range(T, S, T)
|
||||
c2 = ConstraintSet.range(S, T, S)
|
||||
static_assert(c1 == c2)
|
||||
```
|
||||
|
||||
But in the case of `S ≤ T ≤ U`, we end up with an ambiguity. Depending on the typevar ordering, that
|
||||
might display as `S ≤ T ≤ U`, or as `(S ≤ T) ∧ (T ≤ U)`.
|
||||
might represented internally as `S ≤ T ≤ U`, or as `(S ≤ T) ∧ (T ≤ U)`. However, this should not
|
||||
affect any uses of the constraint set.
|
||||
|
||||
```py
|
||||
def f[S, T, U]():
|
||||
# Could be either of:
|
||||
# ty_extensions.ConstraintSet[(S@f ≤ T@f ≤ U@f)]
|
||||
# ty_extensions.ConstraintSet[(S@f ≤ T@f) ∧ (T@f ≤ U@f)]
|
||||
# reveal_type(ConstraintSet.range(S, T, U))
|
||||
# (S@f ≤ T@f ≤ U@f)
|
||||
# (S@f ≤ T@f) ∧ (T@f ≤ U@f)
|
||||
ConstraintSet.range(S, T, U)
|
||||
...
|
||||
```
|
||||
|
||||
## Other simplifications
|
||||
|
||||
### Displaying constraint sets
|
||||
### Ordering of intersection and union elements
|
||||
|
||||
When displaying a constraint set, we transform the internal BDD representation into a DNF formula
|
||||
(i.e., the logical OR of several clauses, each of which is the logical AND of several constraints).
|
||||
This section contains several examples that show that we simplify the DNF formula as much as we can
|
||||
before displaying it.
|
||||
|
||||
```py
|
||||
from ty_extensions import ConstraintSet
|
||||
|
||||
def f[T, U]():
|
||||
t1 = ConstraintSet.range(str, T, str)
|
||||
t2 = ConstraintSet.range(bool, T, bool)
|
||||
u1 = ConstraintSet.range(str, U, str)
|
||||
u2 = ConstraintSet.range(bool, U, bool)
|
||||
|
||||
# revealed: ty_extensions.ConstraintSet[(T@f = bool) ∨ (T@f = str)]
|
||||
reveal_type(t1 | t2)
|
||||
# revealed: ty_extensions.ConstraintSet[(U@f = bool) ∨ (U@f = str)]
|
||||
reveal_type(u1 | u2)
|
||||
# revealed: ty_extensions.ConstraintSet[((T@f = bool) ∧ (U@f = bool)) ∨ ((T@f = bool) ∧ (U@f = str)) ∨ ((T@f = str) ∧ (U@f = bool)) ∨ ((T@f = str) ∧ (U@f = str))]
|
||||
reveal_type((t1 | t2) & (u1 | u2))
|
||||
```
|
||||
|
||||
We might simplify a BDD so much that we can no longer see the constraints that we used to construct
|
||||
it!
|
||||
The ordering of elements in a union or intersection do not affect what types satisfy a constraint
|
||||
set.
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import static_assert
|
||||
from ty_extensions import ConstraintSet, Intersection, static_assert
|
||||
|
||||
def f[T]():
|
||||
t_int = ConstraintSet.range(Never, T, int)
|
||||
t_bool = ConstraintSet.range(Never, T, bool)
|
||||
c1 = ConstraintSet.range(Never, T, str | int)
|
||||
c2 = ConstraintSet.range(Never, T, int | str)
|
||||
static_assert(c1 == c2)
|
||||
|
||||
# `T ≤ bool` implies `T ≤ int`: if a type satisfies the former, it must always satisfy the
|
||||
# latter. We can turn that into a constraint set, using the equivalence `p → q == ¬p ∨ q`:
|
||||
implication = ~t_bool | t_int
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(implication)
|
||||
static_assert(implication)
|
||||
|
||||
# However, because of that implication, some inputs aren't valid: it's not possible for
|
||||
# `T ≤ bool` to be true and `T ≤ int` to be false. This is reflected in the constraint set's
|
||||
# "domain", which maps valid inputs to `true` and invalid inputs to `false`. This means that two
|
||||
# constraint sets that are both always satisfied will not be identical if they have different
|
||||
# domains!
|
||||
always = ConstraintSet.always()
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(always)
|
||||
static_assert(always)
|
||||
static_assert(implication != always)
|
||||
```
|
||||
|
||||
### Normalized bounds
|
||||
|
||||
The lower and upper bounds of a constraint are normalized, so that we equate unions and
|
||||
intersections whose elements appear in different orders.
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import ConstraintSet
|
||||
|
||||
def f[T]():
|
||||
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
|
||||
reveal_type(ConstraintSet.range(Never, T, str | int))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
|
||||
reveal_type(ConstraintSet.range(Never, T, int | str))
|
||||
c1 = ConstraintSet.range(Never, T, Intersection[str, int])
|
||||
c2 = ConstraintSet.range(Never, T, Intersection[int, str])
|
||||
static_assert(c1 == c2)
|
||||
```
|
||||
|
||||
### Constraints on the same typevar
|
||||
@@ -846,15 +791,20 @@ static types.)
|
||||
|
||||
```py
|
||||
from typing import Never
|
||||
from ty_extensions import ConstraintSet
|
||||
from ty_extensions import ConstraintSet, static_assert
|
||||
|
||||
def same_typevar[T]():
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
|
||||
reveal_type(ConstraintSet.range(Never, T, T))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
|
||||
reveal_type(ConstraintSet.range(T, T, object))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
|
||||
reveal_type(ConstraintSet.range(T, T, T))
|
||||
constraints = ConstraintSet.range(Never, T, T)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(T, T, object)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(T, T, T)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
This is also true when the typevar appears in a union in the upper bound, or in an intersection in
|
||||
@@ -865,12 +815,17 @@ as shown above.)
|
||||
from ty_extensions import Intersection
|
||||
|
||||
def same_typevar[T]():
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
|
||||
reveal_type(ConstraintSet.range(Never, T, T | None))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
|
||||
reveal_type(ConstraintSet.range(Intersection[T, None], T, object))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar = *)]
|
||||
reveal_type(ConstraintSet.range(Intersection[T, None], T, T | None))
|
||||
constraints = ConstraintSet.range(Never, T, T | None)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Intersection[T, None], T, object)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Intersection[T, None], T, T | None)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
Similarly, if the lower bound is an intersection containing the _negation_ of the typevar, then the
|
||||
@@ -880,8 +835,11 @@ constraint set can never be satisfied, since every type is disjoint with its neg
|
||||
from ty_extensions import Not
|
||||
|
||||
def same_typevar[T]():
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar ≠ *)]
|
||||
reveal_type(ConstraintSet.range(Intersection[Not[T], None], T, object))
|
||||
# revealed: ty_extensions.ConstraintSet[(T@same_typevar ≠ *)]
|
||||
reveal_type(ConstraintSet.range(Not[T], T, object))
|
||||
constraints = ConstraintSet.range(Intersection[Not[T], None], T, object)
|
||||
expected = ~ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = ConstraintSet.range(Not[T], T, object)
|
||||
expected = ~ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
@@ -50,27 +50,37 @@ question when considering a typevar, by translating the desired relationship int
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from ty_extensions import is_assignable_to, is_subtype_of
|
||||
from ty_extensions import ConstraintSet, is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
def assignability[T]():
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ bool]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_assignable_to(T, bool))
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ int]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_assignable_to(T, int))
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(is_assignable_to(T, object))
|
||||
constraints = is_assignable_to(T, bool)
|
||||
# TODO: expected = ConstraintSet.range(Never, T, bool)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_assignable_to(T, int)
|
||||
# TODO: expected = ConstraintSet.range(Never, T, int)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_assignable_to(T, object)
|
||||
expected = ConstraintSet.always()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
def subtyping[T]():
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ bool]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(T, bool))
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ int]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(T, int))
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(is_subtype_of(T, object))
|
||||
constraints = is_subtype_of(T, bool)
|
||||
# TODO: expected = ConstraintSet.range(Never, T, bool)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_subtype_of(T, int)
|
||||
# TODO: expected = ConstraintSet.range(Never, T, int)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_subtype_of(T, object)
|
||||
expected = ConstraintSet.always()
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
When checking assignability with a dynamic type, we use the bottom and top materializations of the
|
||||
@@ -88,50 +98,64 @@ class Contravariant[T]:
|
||||
pass
|
||||
|
||||
def assignability[T]():
|
||||
# aka [T@assignability ≤ object], which is always satisfiable
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(is_assignable_to(T, Any))
|
||||
constraints = is_assignable_to(T, Any)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# aka [Never ≤ T@assignability], which is always satisfiable
|
||||
# revealed: ty_extensions.ConstraintSet[always]
|
||||
reveal_type(is_assignable_to(Any, T))
|
||||
constraints = is_assignable_to(Any, T)
|
||||
expected = ConstraintSet.range(Never, T, object)
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Covariant[object]]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_assignable_to(T, Covariant[Any]))
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[Covariant[Never] ≤ T@assignability]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_assignable_to(Covariant[Any], T))
|
||||
constraints = is_assignable_to(T, Covariant[Any])
|
||||
# TODO: expected = ConstraintSet.range(Never, T, Covariant[object])
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@assignability ≤ Contravariant[Never]]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_assignable_to(T, Contravariant[Any]))
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[Contravariant[object] ≤ T@assignability]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_assignable_to(Contravariant[Any], T))
|
||||
constraints = is_assignable_to(Covariant[Any], T)
|
||||
# TODO: expected = ConstraintSet.range(Covariant[Never], T, object)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_assignable_to(T, Contravariant[Any])
|
||||
# TODO: expected = ConstraintSet.range(Never, T, Contravariant[Never])
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_assignable_to(Contravariant[Any], T)
|
||||
# TODO: expected = ConstraintSet.range(Contravariant[object], T, object)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
def subtyping[T]():
|
||||
# aka [T@assignability ≤ object], which is always satisfiable
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(T, Any))
|
||||
constraints = is_subtype_of(T, Any)
|
||||
# TODO: expected = ConstraintSet.range(Never, T, Never)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# aka [Never ≤ T@assignability], which is always satisfiable
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(Any, T))
|
||||
constraints = is_subtype_of(Any, T)
|
||||
# TODO: expected = ConstraintSet.range(object, T, object)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Covariant[Never]]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(T, Covariant[Any]))
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[Covariant[object] ≤ T@subtyping]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(Covariant[Any], T))
|
||||
constraints = is_subtype_of(T, Covariant[Any])
|
||||
# TODO: expected = ConstraintSet.range(Never, T, Covariant[Never])
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[T@subtyping ≤ Contravariant[object]]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(T, Contravariant[Any]))
|
||||
# TODO: revealed: ty_extensions.ConstraintSet[Contravariant[Never] ≤ T@subtyping]
|
||||
# revealed: ty_extensions.ConstraintSet[never]
|
||||
reveal_type(is_subtype_of(Contravariant[Any], T))
|
||||
constraints = is_subtype_of(Covariant[Any], T)
|
||||
# TODO: expected = ConstraintSet.range(Covariant[object], T, object)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_subtype_of(T, Contravariant[Any])
|
||||
# TODO: expected = ConstraintSet.range(Never, T, Contravariant[object])
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
|
||||
constraints = is_subtype_of(Contravariant[Any], T)
|
||||
# TODO: expected = ConstraintSet.range(Contravariant[Never], T, object)
|
||||
expected = ConstraintSet.never()
|
||||
static_assert(constraints == expected)
|
||||
```
|
||||
|
||||
At some point, though, we need to resolve a constraint set; at that point, we can no longer punt on
|
||||
|
||||
@@ -14,6 +14,7 @@ altair
|
||||
antidote
|
||||
anyio
|
||||
apprise
|
||||
archinstall
|
||||
artigraph
|
||||
arviz
|
||||
async-utils
|
||||
@@ -25,7 +26,9 @@ bidict
|
||||
black
|
||||
bokeh
|
||||
boostedblob
|
||||
build
|
||||
check-jsonschema
|
||||
cibuildwheel
|
||||
cki-lib
|
||||
cloud-init
|
||||
colour
|
||||
@@ -104,6 +107,7 @@ pylox
|
||||
pyodide
|
||||
pyp
|
||||
pyppeteer
|
||||
pyproject-metadata
|
||||
pytest
|
||||
pytest-robotframework
|
||||
python-chess
|
||||
@@ -118,6 +122,7 @@ schemathesis
|
||||
scikit-build-core
|
||||
scikit-learn
|
||||
scipy
|
||||
scipy-stubs
|
||||
scrapy
|
||||
setuptools
|
||||
sockeye
|
||||
|
||||
@@ -13,7 +13,8 @@ pub use diagnostic::add_inferred_python_version_hint_to_diagnostic;
|
||||
pub use module_name::{ModuleName, ModuleNameResolutionError};
|
||||
pub use module_resolver::{
|
||||
KnownModule, Module, SearchPath, SearchPathValidationError, SearchPaths, all_modules,
|
||||
list_modules, resolve_module, resolve_real_module, system_module_search_paths,
|
||||
list_modules, resolve_module, resolve_real_module, resolve_real_shadowable_module,
|
||||
system_module_search_paths,
|
||||
};
|
||||
pub use program::{
|
||||
Program, ProgramSettings, PythonVersionFileSource, PythonVersionSource,
|
||||
@@ -25,6 +26,7 @@ pub use semantic_model::{
|
||||
Completion, HasDefinition, HasType, MemberDefinition, NameKind, SemanticModel,
|
||||
};
|
||||
pub use site_packages::{PythonEnvironment, SitePackagesPaths, SysPrefixPathOrigin};
|
||||
pub use suppression::create_suppression_fix;
|
||||
pub use types::DisplaySettings;
|
||||
pub use types::ide_support::{
|
||||
ImportAliasResolution, ResolvedDefinition, definitions_for_attribute, definitions_for_bin_op,
|
||||
|
||||
@@ -8,9 +8,7 @@ use crate::program::Program;
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SystemOrVendoredPathRef};
|
||||
use super::resolver::{
|
||||
ModuleResolveMode, ResolverContext, is_non_shadowable, resolve_file_module, search_paths,
|
||||
};
|
||||
use super::resolver::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths};
|
||||
|
||||
/// List all available modules, including all sub-modules, sorted in lexicographic order.
|
||||
pub fn all_modules(db: &dyn Db) -> Vec<Module<'_>> {
|
||||
@@ -309,7 +307,8 @@ impl<'db> Lister<'db> {
|
||||
|
||||
/// Returns true if the given module name cannot be shadowable.
|
||||
fn is_non_shadowable(&self, name: &ModuleName) -> bool {
|
||||
is_non_shadowable(self.python_version().minor, name.as_str())
|
||||
ModuleResolveMode::StubsAllowed
|
||||
.is_non_shadowable(self.python_version().minor, name.as_str())
|
||||
}
|
||||
|
||||
/// Returns the Python version we want to perform module resolution
|
||||
|
||||
@@ -6,7 +6,7 @@ pub use module::Module;
|
||||
pub use path::{SearchPath, SearchPathValidationError};
|
||||
pub use resolver::SearchPaths;
|
||||
pub(crate) use resolver::file_to_module;
|
||||
pub use resolver::{resolve_module, resolve_real_module};
|
||||
pub use resolver::{resolve_module, resolve_real_module, resolve_real_shadowable_module};
|
||||
use ruff_db::system::SystemPath;
|
||||
|
||||
use crate::Db;
|
||||
|
||||
@@ -47,8 +47,33 @@ pub fn resolve_real_module<'db>(db: &'db dyn Db, module_name: &ModuleName) -> Op
|
||||
resolve_module_query(db, interned_name)
|
||||
}
|
||||
|
||||
/// Resolves a module name to a module (stubs not allowed, some shadowing is
|
||||
/// allowed).
|
||||
///
|
||||
/// In particular, this allows `typing_extensions` to be shadowed by a
|
||||
/// non-standard library module. This is useful in the context of the LSP
|
||||
/// where we don't want to pretend as if these modules are always available at
|
||||
/// runtime.
|
||||
///
|
||||
/// This should generally only be used within the context of the LSP. Using it
|
||||
/// within ty proper risks being unable to resolve builtin modules since they
|
||||
/// are involved in an import cycle with `builtins`.
|
||||
pub fn resolve_real_shadowable_module<'db>(
|
||||
db: &'db dyn Db,
|
||||
module_name: &ModuleName,
|
||||
) -> Option<Module<'db>> {
|
||||
let interned_name = ModuleNameIngredient::new(
|
||||
db,
|
||||
module_name,
|
||||
ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed,
|
||||
);
|
||||
|
||||
resolve_module_query(db, interned_name)
|
||||
}
|
||||
|
||||
/// Which files should be visible when doing a module query
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(crate) enum ModuleResolveMode {
|
||||
/// Stubs are allowed to appear.
|
||||
///
|
||||
@@ -61,6 +86,13 @@ pub(crate) enum ModuleResolveMode {
|
||||
/// implementations. When querying searchpaths this also notably replaces typeshed with
|
||||
/// the "real" stdlib.
|
||||
StubsNotAllowed,
|
||||
/// Like `StubsNotAllowed`, but permits some modules to be shadowed.
|
||||
///
|
||||
/// In particular, this allows `typing_extensions` to be shadowed by a
|
||||
/// non-standard library module. This is useful in the context of the LSP
|
||||
/// where we don't want to pretend as if these modules are always available
|
||||
/// at runtime.
|
||||
StubsNotAllowedSomeShadowingAllowed,
|
||||
}
|
||||
|
||||
#[salsa::interned(heap_size=ruff_memory_usage::heap_size)]
|
||||
@@ -73,6 +105,39 @@ impl ModuleResolveMode {
|
||||
fn stubs_allowed(self) -> bool {
|
||||
matches!(self, Self::StubsAllowed)
|
||||
}
|
||||
|
||||
/// Returns `true` if the module name refers to a standard library module
|
||||
/// which can't be shadowed by a first-party module.
|
||||
///
|
||||
/// This includes "builtin" modules, which can never be shadowed at runtime
|
||||
/// either. Additionally, certain other modules that are involved in an
|
||||
/// import cycle with `builtins` (`types`, `typing_extensions`, etc.) are
|
||||
/// also considered non-shadowable, unless the module resolution mode
|
||||
/// specifically opts into allowing some of them to be shadowed. This
|
||||
/// latter set of modules cannot be allowed to be shadowed by first-party
|
||||
/// or "extra-path" modules in ty proper, or we risk panics in unexpected
|
||||
/// places due to being unable to resolve builtin symbols. This is similar
|
||||
/// behaviour to other type checkers such as mypy:
|
||||
/// <https://github.com/python/mypy/blob/3807423e9d98e678bf16b13ec8b4f909fe181908/mypy/build.py#L104-L117>
|
||||
pub(super) fn is_non_shadowable(self, minor_version: u8, module_name: &str) -> bool {
|
||||
// Builtin modules are never shadowable, no matter what.
|
||||
if ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name) {
|
||||
return true;
|
||||
}
|
||||
// Similarly for `types`, which is always available at runtime.
|
||||
if module_name == "types" {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise, some modules should only be conditionally allowed
|
||||
// to be shadowed, depending on the module resolution mode.
|
||||
match self {
|
||||
ModuleResolveMode::StubsAllowed | ModuleResolveMode::StubsNotAllowed => {
|
||||
module_name == "typing_extensions"
|
||||
}
|
||||
ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Salsa query that resolves an interned [`ModuleNameIngredient`] to a module.
|
||||
@@ -386,7 +451,10 @@ impl SearchPaths {
|
||||
pub(crate) fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> {
|
||||
match mode {
|
||||
ModuleResolveMode::StubsAllowed => self.stdlib_path.as_ref(),
|
||||
ModuleResolveMode::StubsNotAllowed => self.real_stdlib_path.as_ref(),
|
||||
ModuleResolveMode::StubsNotAllowed
|
||||
| ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => {
|
||||
self.real_stdlib_path.as_ref()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +507,8 @@ pub(crate) fn dynamic_resolution_paths<'db>(
|
||||
// Use the `ModuleResolveMode` to determine which stdlib (if any) to mark as existing
|
||||
let stdlib = match mode.mode(db) {
|
||||
ModuleResolveMode::StubsAllowed => stdlib_path,
|
||||
ModuleResolveMode::StubsNotAllowed => real_stdlib_path,
|
||||
ModuleResolveMode::StubsNotAllowed
|
||||
| ModuleResolveMode::StubsNotAllowedSomeShadowingAllowed => real_stdlib_path,
|
||||
};
|
||||
if let Some(path) = stdlib.as_ref().and_then(SearchPath::as_system_path) {
|
||||
existing_paths.insert(Cow::Borrowed(path));
|
||||
@@ -684,27 +753,13 @@ struct ModuleNameIngredient<'db> {
|
||||
pub(super) mode: ModuleResolveMode,
|
||||
}
|
||||
|
||||
/// Returns `true` if the module name refers to a standard library module which can't be shadowed
|
||||
/// by a first-party module.
|
||||
///
|
||||
/// This includes "builtin" modules, which can never be shadowed at runtime either, as well as
|
||||
/// certain other modules that are involved in an import cycle with `builtins` (`types`,
|
||||
/// `typing_extensions`, etc.). This latter set of modules cannot be allowed to be shadowed by
|
||||
/// first-party or "extra-path" modules, or we risk panics in unexpected places due to being
|
||||
/// unable to resolve builtin symbols. This is similar behaviour to other type checkers such
|
||||
/// as mypy: <https://github.com/python/mypy/blob/3807423e9d98e678bf16b13ec8b4f909fe181908/mypy/build.py#L104-L117>
|
||||
pub(super) fn is_non_shadowable(minor_version: u8, module_name: &str) -> bool {
|
||||
matches!(module_name, "types" | "typing_extensions")
|
||||
|| ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name)
|
||||
}
|
||||
|
||||
/// Given a module name and a list of search paths in which to lookup modules,
|
||||
/// attempt to resolve the module name
|
||||
fn resolve_name(db: &dyn Db, name: &ModuleName, mode: ModuleResolveMode) -> Option<ResolvedName> {
|
||||
let program = Program::get(db);
|
||||
let python_version = program.python_version(db);
|
||||
let resolver_state = ResolverContext::new(db, python_version, mode);
|
||||
let is_non_shadowable = is_non_shadowable(python_version.minor, name.as_str());
|
||||
let is_non_shadowable = mode.is_non_shadowable(python_version.minor, name.as_str());
|
||||
|
||||
let name = RelaxedModuleName::new(name);
|
||||
let stub_name = name.to_stub_package();
|
||||
|
||||
@@ -1478,6 +1478,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
}
|
||||
|
||||
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
|
||||
self.scopes_by_expression
|
||||
.record_expression(asname, self.current_scope());
|
||||
(asname.id.clone(), asname.id == alias.name.id)
|
||||
} else {
|
||||
(Name::new(alias.name.id.split('.').next().unwrap()), false)
|
||||
@@ -1651,6 +1653,8 @@ impl<'ast> Visitor<'ast> for SemanticIndexBuilder<'_, 'ast> {
|
||||
}
|
||||
|
||||
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
|
||||
self.scopes_by_expression
|
||||
.record_expression(asname, self.current_scope());
|
||||
// It's re-exported if it's `from ... import x as x`
|
||||
(&asname.id, asname.id == alias.name.id)
|
||||
} else {
|
||||
|
||||
@@ -336,6 +336,7 @@ fn pattern_kind_to_type<'db>(db: &'db dyn Db, kind: &PatternPredicateKind<'db>)
|
||||
infer_expression_type(db, *class_expr, TypeContext::default())
|
||||
.to_instance(db)
|
||||
.unwrap_or(Type::Never)
|
||||
.top_materialization(db)
|
||||
} else {
|
||||
Type::Never
|
||||
}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use ruff_db::files::{File, FilePath};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ruff_python_ast::{self as ast, ExprStringLiteral, ModExpression};
|
||||
use ruff_python_ast::{self as ast, ExprStringLiteral, Identifier, ModExpression};
|
||||
use ruff_python_ast::{Expr, ExprRef, HasNodeIndex, name::Name};
|
||||
use ruff_python_parser::Parsed;
|
||||
use ruff_source_file::LineIndex;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{KnownModule, Module, list_modules, resolve_module};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::scope::FileScopeId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::ide_support::{Member, all_declarations_and_bindings, all_members};
|
||||
use crate::types::list_members::{Member, all_members, all_members_of_scope};
|
||||
use crate::types::{Type, binding_type, infer_scope_types};
|
||||
use crate::{Db, resolve_real_shadowable_module};
|
||||
|
||||
/// The primary interface the LSP should use for querying semantic information about a [`File`].
|
||||
///
|
||||
@@ -76,7 +76,7 @@ impl<'db> SemanticModel<'db> {
|
||||
|
||||
for (file_scope, _) in index.ancestor_scopes(file_scope) {
|
||||
for memberdef in
|
||||
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
|
||||
all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file))
|
||||
{
|
||||
members.insert(
|
||||
memberdef.member.name,
|
||||
@@ -90,6 +90,19 @@ impl<'db> SemanticModel<'db> {
|
||||
members
|
||||
}
|
||||
|
||||
pub fn inferred_type_for_identifier(&self, identifier: &Identifier) -> Type<'db> {
|
||||
// TODO(#1637): semantic tokens is making this crash even with
|
||||
// `try_expr_ref_in_ast` guarding this, for now just use `try_expression_scope_id`.
|
||||
// The problematic input is `x: "float` (with a dangling quote). I imagine the issue
|
||||
// is we're too eagerly setting `is_string_annotation` in inference.
|
||||
let Some(file_scope) = self.scope(identifier.into()) else {
|
||||
return Type::unknown();
|
||||
};
|
||||
let scope = file_scope.to_scope_id(self.db, self.file);
|
||||
|
||||
infer_scope_types(self.db, scope).expression_type(identifier)
|
||||
}
|
||||
|
||||
/// Resolve the given import made in this file to a Type
|
||||
pub fn resolve_module_type(&self, module: Option<&str>, level: u32) -> Option<Type<'db>> {
|
||||
let module = self.resolve_module(module, level)?;
|
||||
@@ -105,8 +118,14 @@ impl<'db> SemanticModel<'db> {
|
||||
|
||||
/// Returns completions for symbols available in a `import <CURSOR>` context.
|
||||
pub fn import_completions(&self) -> Vec<Completion<'db>> {
|
||||
let typing_extensions = ModuleName::new("typing_extensions").unwrap();
|
||||
let is_typing_extensions_available = self.file.is_stub(self.db)
|
||||
|| resolve_real_shadowable_module(self.db, &typing_extensions).is_some();
|
||||
list_modules(self.db)
|
||||
.into_iter()
|
||||
.filter(|module| {
|
||||
is_typing_extensions_available || module.name(self.db) != &typing_extensions
|
||||
})
|
||||
.map(|module| {
|
||||
let builtin = module.is_known(self.db, KnownModule::Builtins);
|
||||
let ty = Type::module_literal(self.db, self.file, module);
|
||||
@@ -215,12 +234,13 @@ impl<'db> SemanticModel<'db> {
|
||||
let mut completions = vec![];
|
||||
for (file_scope, _) in index.ancestor_scopes(file_scope) {
|
||||
completions.extend(
|
||||
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
|
||||
.map(|memberdef| Completion {
|
||||
all_members_of_scope(self.db, file_scope.to_scope_id(self.db, self.file)).map(
|
||||
|memberdef| Completion {
|
||||
name: memberdef.member.name,
|
||||
ty: Some(memberdef.member.ty),
|
||||
builtin: false,
|
||||
}),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// Builtins are available in all scopes.
|
||||
@@ -397,7 +417,7 @@ pub trait HasType {
|
||||
}
|
||||
|
||||
pub trait HasDefinition {
|
||||
/// Returns the inferred type of `self`.
|
||||
/// Returns the definition of `self`.
|
||||
///
|
||||
/// ## Panics
|
||||
/// May panic if `self` is from another file than `model`.
|
||||
|
||||
@@ -375,6 +375,77 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a fix for adding a suppression comment to suppress `lint` for `range`.
|
||||
///
|
||||
/// The fix prefers adding the code to an existing `ty: ignore[]` comment over
|
||||
/// adding a new suppression comment.
|
||||
pub fn create_suppression_fix(db: &dyn Db, file: File, id: LintId, range: TextRange) -> Fix {
|
||||
let suppressions = suppressions(db, file);
|
||||
let source = source_text(db, file);
|
||||
|
||||
let mut existing_suppressions = suppressions.line_suppressions(range).filter(|suppression| {
|
||||
matches!(
|
||||
suppression.target,
|
||||
SuppressionTarget::Lint(_) | SuppressionTarget::Empty,
|
||||
)
|
||||
});
|
||||
|
||||
// If there's an existing `ty: ignore[]` comment, append the code to it instead of creating a new suppression comment.
|
||||
if let Some(existing) = existing_suppressions.next() {
|
||||
let comment_text = &source[existing.comment_range];
|
||||
// Only add to the existing ignore comment if it has no reason.
|
||||
if let Some(before_closing_paren) = comment_text.trim_end().strip_suffix(']') {
|
||||
let up_to_last_code = before_closing_paren.trim_end();
|
||||
|
||||
let insertion = if up_to_last_code.ends_with(',') {
|
||||
format!(" {id}", id = id.name())
|
||||
} else {
|
||||
format!(", {id}", id = id.name())
|
||||
};
|
||||
|
||||
let relative_offset_from_end = comment_text.text_len() - up_to_last_code.text_len();
|
||||
|
||||
return Fix::safe_edit(Edit::insertion(
|
||||
insertion,
|
||||
existing.comment_range.end() - relative_offset_from_end,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Always insert a new suppression at the end of the range to avoid having to deal with multiline strings
|
||||
// etc.
|
||||
let parsed = parsed_module(db, file).load(db);
|
||||
let tokens_after = parsed.tokens().after(range.end());
|
||||
|
||||
// Same as for `line_end` when building up the `suppressions`: Ignore newlines
|
||||
// in multiline-strings, inside f-strings, or after a line continuation because we can't
|
||||
// place a comment on those lines.
|
||||
let line_end = tokens_after
|
||||
.iter()
|
||||
.find(|token| {
|
||||
matches!(
|
||||
token.kind(),
|
||||
TokenKind::Newline | TokenKind::NonLogicalNewline
|
||||
)
|
||||
})
|
||||
.map(Ranged::start)
|
||||
.unwrap_or(source.text_len());
|
||||
|
||||
let up_to_line_end = &source[..line_end.to_usize()];
|
||||
let up_to_first_content = up_to_line_end.trim_end();
|
||||
let trailing_whitespace_len = up_to_line_end.text_len() - up_to_first_content.text_len();
|
||||
|
||||
let insertion = format!(" # ty:ignore[{id}]", id = id.name());
|
||||
|
||||
Fix::safe_edit(if trailing_whitespace_len == TextSize::ZERO {
|
||||
Edit::insertion(insertion, line_end)
|
||||
} else {
|
||||
// `expr # fmt: off<trailing_whitespace>`
|
||||
// Trim the trailing whitespace
|
||||
Edit::replacement(insertion, line_end - trailing_whitespace_len, line_end)
|
||||
})
|
||||
}
|
||||
|
||||
struct CheckSuppressionsContext<'a> {
|
||||
db: &'a dyn Db,
|
||||
file: File,
|
||||
|
||||
@@ -96,11 +96,12 @@ mod generics;
|
||||
pub mod ide_support;
|
||||
mod infer;
|
||||
mod instance;
|
||||
mod liskov;
|
||||
pub mod list_members;
|
||||
mod member;
|
||||
mod mro;
|
||||
mod narrow;
|
||||
mod newtype;
|
||||
mod overrides;
|
||||
mod protocol_class;
|
||||
mod signatures;
|
||||
mod special_form;
|
||||
@@ -573,20 +574,19 @@ impl<'db> PropertyInstanceType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
let getter = match self.getter(db) {
|
||||
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true, visitor)?),
|
||||
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?),
|
||||
Some(ty) => Some(
|
||||
ty.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
ty.recursive_type_normalized_impl(db, div, true)
|
||||
.unwrap_or(div),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
let setter = match self.setter(db) {
|
||||
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true, visitor)?),
|
||||
Some(ty) if nested => Some(ty.recursive_type_normalized_impl(db, div, true)?),
|
||||
Some(ty) => Some(
|
||||
ty.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
ty.recursive_type_normalized_impl(db, div, true)
|
||||
.unwrap_or(div),
|
||||
),
|
||||
None => None,
|
||||
@@ -957,8 +957,13 @@ impl<'db> Type<'db> {
|
||||
self.is_instance_of(db, KnownClass::NotImplementedType)
|
||||
}
|
||||
|
||||
pub(crate) const fn is_todo(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
|
||||
pub(crate) fn is_todo(&self) -> bool {
|
||||
self.as_dynamic().is_some_and(|dynamic| match dynamic {
|
||||
DynamicType::Any | DynamicType::Unknown | DynamicType::Divergent(_) => false,
|
||||
DynamicType::Todo(_) | DynamicType::TodoStarredExpression | DynamicType::TodoUnpack => {
|
||||
true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn is_generic_alias(&self) -> bool {
|
||||
@@ -1553,15 +1558,14 @@ impl<'db> Type<'db> {
|
||||
#[must_use]
|
||||
pub(crate) fn recursive_type_normalized(self, db: &'db dyn Db, cycle: &salsa::Cycle) -> Self {
|
||||
cycle.head_ids().fold(self, |ty, id| {
|
||||
let visitor = NormalizedVisitor::new(Type::divergent(id));
|
||||
ty.recursive_type_normalized_impl(db, Type::divergent(id), false, &visitor)
|
||||
ty.recursive_type_normalized_impl(db, Type::divergent(id), false)
|
||||
.unwrap_or(Type::divergent(id))
|
||||
})
|
||||
}
|
||||
|
||||
/// Normalizes types including divergent types (recursive types), which is necessary for convergence of fixed-point iteration.
|
||||
/// When nested is true, propagate `None`. That is, if the type contains a `Divergent` type, the return value of this method is `None`.
|
||||
/// When nested is false, create a type containing `Divergent` types instead of propagating `None`.
|
||||
/// When `nested` is true, propagate `None`. That is, if the type contains a `Divergent` type, the return value of this method is `None` (so we can use the `?` operator).
|
||||
/// When `nested` is false, create a type containing `Divergent` types instead of propagating `None` (we should use `unwrap_or(Divergent)`).
|
||||
/// This is to preserve the structure of the non-divergent parts of the type instead of completely collapsing the type containing a `Divergent` type into a `Divergent` type.
|
||||
/// ```python
|
||||
/// tuple[tuple[Divergent, Literal[1]], Literal[1]].recursive_type_normalized(nested: false)
|
||||
@@ -1580,102 +1584,73 @@ impl<'db> Type<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
if nested && self == div {
|
||||
return None;
|
||||
}
|
||||
match self {
|
||||
Type::Union(union) => visitor.try_visit(self, || {
|
||||
union.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
}),
|
||||
Type::Intersection(intersection) => visitor.try_visit(self, || {
|
||||
intersection
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::Intersection)
|
||||
}),
|
||||
Type::Callable(callable) => visitor.try_visit(self, || {
|
||||
callable
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::Callable)
|
||||
}),
|
||||
Type::ProtocolInstance(protocol) => visitor.try_visit(self, || {
|
||||
protocol
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::ProtocolInstance)
|
||||
}),
|
||||
Type::NominalInstance(instance) => visitor.try_visit(self, || {
|
||||
instance
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::NominalInstance)
|
||||
}),
|
||||
Type::FunctionLiteral(function) => visitor.try_visit(self, || {
|
||||
function
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::FunctionLiteral)
|
||||
}),
|
||||
Type::PropertyInstance(property) => visitor.try_visit(self, || {
|
||||
property
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::PropertyInstance)
|
||||
}),
|
||||
Type::KnownBoundMethod(method_kind) => visitor.try_visit(self, || {
|
||||
method_kind
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::KnownBoundMethod)
|
||||
}),
|
||||
Type::BoundMethod(method) => visitor.try_visit(self, || {
|
||||
method
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::BoundMethod)
|
||||
}),
|
||||
Type::BoundSuper(bound_super) => visitor.try_visit(self, || {
|
||||
bound_super
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::BoundSuper)
|
||||
}),
|
||||
Type::GenericAlias(generic) => visitor.try_visit(self, || {
|
||||
generic
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::GenericAlias)
|
||||
}),
|
||||
Type::SubclassOf(subclass_of) => visitor.try_visit(self, || {
|
||||
subclass_of
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::SubclassOf)
|
||||
}),
|
||||
Type::Union(union) => union.recursive_type_normalized_impl(db, div, nested),
|
||||
Type::Intersection(intersection) => intersection
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::Intersection),
|
||||
Type::Callable(callable) => callable
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::Callable),
|
||||
Type::ProtocolInstance(protocol) => protocol
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::ProtocolInstance),
|
||||
Type::NominalInstance(instance) => instance
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::NominalInstance),
|
||||
Type::FunctionLiteral(function) => function
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::FunctionLiteral),
|
||||
Type::PropertyInstance(property) => property
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::PropertyInstance),
|
||||
Type::KnownBoundMethod(method_kind) => method_kind
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::KnownBoundMethod),
|
||||
Type::BoundMethod(method) => method
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::BoundMethod),
|
||||
Type::BoundSuper(bound_super) => bound_super
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::BoundSuper),
|
||||
Type::GenericAlias(generic) => generic
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::GenericAlias),
|
||||
Type::SubclassOf(subclass_of) => subclass_of
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::SubclassOf),
|
||||
Type::TypeVar(_) => Some(self),
|
||||
Type::KnownInstance(known_instance) => visitor.try_visit(self, || {
|
||||
known_instance
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.map(Type::KnownInstance)
|
||||
}),
|
||||
Type::TypeIs(type_is) => visitor.try_visit(self, || {
|
||||
Type::KnownInstance(known_instance) => known_instance
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Type::KnownInstance),
|
||||
Type::TypeIs(type_is) => {
|
||||
let ty = if nested {
|
||||
type_is
|
||||
.return_type(db)
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)?
|
||||
.recursive_type_normalized_impl(db, div, true)?
|
||||
} else {
|
||||
type_is
|
||||
.return_type(db)
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
.recursive_type_normalized_impl(db, div, true)
|
||||
.unwrap_or(div)
|
||||
};
|
||||
Some(type_is.with_type(db, ty))
|
||||
}),
|
||||
}
|
||||
Type::Dynamic(dynamic) => Some(Type::Dynamic(dynamic.recursive_type_normalized())),
|
||||
Type::TypedDict(_) => {
|
||||
// TODO: Normalize TypedDicts
|
||||
Some(self)
|
||||
}
|
||||
Type::TypeAlias(_) => Some(self),
|
||||
Type::NewTypeInstance(newtype) => visitor.try_visit(self, || {
|
||||
newtype
|
||||
.try_map_base_class_type(db, |class_type| {
|
||||
class_type.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
})
|
||||
.map(Type::NewTypeInstance)
|
||||
}),
|
||||
Type::NewTypeInstance(newtype) => newtype
|
||||
.try_map_base_class_type(db, |class_type| {
|
||||
class_type.recursive_type_normalized_impl(db, div, nested)
|
||||
})
|
||||
.map(Type::NewTypeInstance),
|
||||
Type::LiteralString
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
@@ -2115,18 +2090,25 @@ impl<'db> Type<'db> {
|
||||
// `type[T]` is a subtype of the class object `A` if every instance of `T` is a subtype of an instance
|
||||
// of `A`, and vice versa.
|
||||
(Type::SubclassOf(subclass_of), _)
|
||||
if subclass_of.is_type_var()
|
||||
&& !matches!(target, Type::Callable(_) | Type::ProtocolInstance(_)) =>
|
||||
if !subclass_of
|
||||
.into_type_var()
|
||||
.zip(target.to_instance(db))
|
||||
.when_some_and(|(this_instance, other_instance)| {
|
||||
Type::TypeVar(this_instance).has_relation_to_impl(
|
||||
db,
|
||||
other_instance,
|
||||
inferable,
|
||||
relation,
|
||||
relation_visitor,
|
||||
disjointness_visitor,
|
||||
)
|
||||
})
|
||||
.is_never_satisfied(db) =>
|
||||
{
|
||||
// TODO: The repetition here isn't great, but we really need the fallthrough logic,
|
||||
// where this arm only engages if it returns true.
|
||||
let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
|
||||
let other_instance = match target {
|
||||
Type::Union(union) => Some(
|
||||
union.map(db, |element| element.to_instance(db).unwrap_or(Type::Never)),
|
||||
),
|
||||
_ => target.to_instance(db),
|
||||
};
|
||||
|
||||
other_instance.when_some_and(|other_instance| {
|
||||
target.to_instance(db).when_some_and(|other_instance| {
|
||||
this_instance.has_relation_to_impl(
|
||||
db,
|
||||
other_instance,
|
||||
@@ -2137,6 +2119,7 @@ impl<'db> Type<'db> {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
(_, Type::SubclassOf(subclass_of)) if subclass_of.is_type_var() => {
|
||||
let other_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
|
||||
self.to_instance(db).when_some_and(|this_instance| {
|
||||
@@ -2673,6 +2656,10 @@ impl<'db> Type<'db> {
|
||||
disjointness_visitor,
|
||||
),
|
||||
|
||||
(Type::SubclassOf(subclass_of), _) if subclass_of.is_type_var() => {
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
// `Literal[<class 'C'>]` is a subtype of `type[B]` if `C` is a subclass of `B`,
|
||||
// since `type[B]` describes all possible runtime subclasses of the class object `B`.
|
||||
(Type::ClassLiteral(class), Type::SubclassOf(target_subclass_ty)) => target_subclass_ty
|
||||
@@ -3107,8 +3094,7 @@ impl<'db> Type<'db> {
|
||||
ConstraintSet::from(false)
|
||||
}
|
||||
|
||||
// `type[T]` is disjoint from a callable or protocol instance if its upper bound or
|
||||
// constraints are.
|
||||
// `type[T]` is disjoint from a callable or protocol instance if its upper bound or constraints are.
|
||||
(Type::SubclassOf(subclass_of), Type::Callable(_) | Type::ProtocolInstance(_))
|
||||
| (Type::Callable(_) | Type::ProtocolInstance(_), Type::SubclassOf(subclass_of))
|
||||
if subclass_of.is_type_var() =>
|
||||
@@ -3130,13 +3116,14 @@ impl<'db> Type<'db> {
|
||||
|
||||
// `type[T]` is disjoint from a class object `A` if every instance of `T` is disjoint from an instance of `A`.
|
||||
(Type::SubclassOf(subclass_of), other) | (other, Type::SubclassOf(subclass_of))
|
||||
if subclass_of.is_type_var() =>
|
||||
if subclass_of.is_type_var()
|
||||
&& (other.to_instance(db).is_some()
|
||||
|| other.as_typevar().is_some_and(|type_var| {
|
||||
type_var.typevar(db).bound_or_constraints(db).is_none()
|
||||
})) =>
|
||||
{
|
||||
let this_instance = Type::TypeVar(subclass_of.into_type_var().unwrap());
|
||||
let other_instance = match other {
|
||||
Type::Union(union) => Some(
|
||||
union.map(db, |element| element.to_instance(db).unwrap_or(Type::Never)),
|
||||
),
|
||||
// An unbounded typevar `U` may have instances of type `object` if specialized to
|
||||
// an instance of `type`.
|
||||
Type::TypeVar(typevar)
|
||||
@@ -3490,6 +3477,12 @@ impl<'db> Type<'db> {
|
||||
})
|
||||
}
|
||||
|
||||
(Type::SubclassOf(subclass_of_ty), _) | (_, Type::SubclassOf(subclass_of_ty))
|
||||
if subclass_of_ty.is_type_var() =>
|
||||
{
|
||||
ConstraintSet::from(true)
|
||||
}
|
||||
|
||||
(Type::SubclassOf(subclass_of_ty), Type::ClassLiteral(class_b))
|
||||
| (Type::ClassLiteral(class_b), Type::SubclassOf(subclass_of_ty)) => {
|
||||
match subclass_of_ty.subclass_of() {
|
||||
@@ -3519,31 +3512,27 @@ impl<'db> Type<'db> {
|
||||
// for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`,
|
||||
// so although the type is dynamic we can still determine disjointedness in some situations
|
||||
(Type::SubclassOf(subclass_of_ty), other)
|
||||
| (other, Type::SubclassOf(subclass_of_ty))
|
||||
if !subclass_of_ty.is_type_var() =>
|
||||
{
|
||||
match subclass_of_ty.subclass_of() {
|
||||
SubclassOfInner::Dynamic(_) => {
|
||||
KnownClass::Type.to_instance(db).is_disjoint_from_impl(
|
||||
db,
|
||||
other,
|
||||
inferable,
|
||||
disjointness_visitor,
|
||||
relation_visitor,
|
||||
)
|
||||
}
|
||||
SubclassOfInner::Class(class) => {
|
||||
class.metaclass_instance_type(db).is_disjoint_from_impl(
|
||||
db,
|
||||
other,
|
||||
inferable,
|
||||
disjointness_visitor,
|
||||
relation_visitor,
|
||||
)
|
||||
}
|
||||
SubclassOfInner::TypeVar(_) => unreachable!(),
|
||||
| (other, Type::SubclassOf(subclass_of_ty)) => match subclass_of_ty.subclass_of() {
|
||||
SubclassOfInner::Dynamic(_) => {
|
||||
KnownClass::Type.to_instance(db).is_disjoint_from_impl(
|
||||
db,
|
||||
other,
|
||||
inferable,
|
||||
disjointness_visitor,
|
||||
relation_visitor,
|
||||
)
|
||||
}
|
||||
}
|
||||
SubclassOfInner::Class(class) => {
|
||||
class.metaclass_instance_type(db).is_disjoint_from_impl(
|
||||
db,
|
||||
other,
|
||||
inferable,
|
||||
disjointness_visitor,
|
||||
relation_visitor,
|
||||
)
|
||||
}
|
||||
SubclassOfInner::TypeVar(_) => unreachable!(),
|
||||
},
|
||||
|
||||
(Type::SpecialForm(special_form), Type::NominalInstance(instance))
|
||||
| (Type::NominalInstance(instance), Type::SpecialForm(special_form)) => {
|
||||
@@ -3805,11 +3794,6 @@ impl<'db> Type<'db> {
|
||||
relation_visitor,
|
||||
)
|
||||
}
|
||||
|
||||
(Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => {
|
||||
// All cases should have been handled above.
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7403,9 +7387,6 @@ impl<'db> Type<'db> {
|
||||
Some(KnownClass::TypeVarTuple) => Ok(todo_type!(
|
||||
"Support for `typing.TypeVarTuple` instances in type expressions"
|
||||
)),
|
||||
Some(KnownClass::GenericAlias) => Ok(todo_type!(
|
||||
"Support for `typing.GenericAlias` instances in type expressions"
|
||||
)),
|
||||
_ => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::InvalidType(*self, scope_id)
|
||||
@@ -8167,7 +8148,7 @@ impl<'db> Type<'db> {
|
||||
Self::AlwaysFalsy => Type::SpecialForm(SpecialFormType::AlwaysFalsy).definition(db),
|
||||
|
||||
// These types have no definition
|
||||
Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack)
|
||||
Self::Dynamic(DynamicType::Divergent(_) | DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression)
|
||||
| Self::Callable(_)
|
||||
| Self::TypeIs(_) => None,
|
||||
}
|
||||
@@ -8702,7 +8683,6 @@ impl<'db> KnownInstanceType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
match self {
|
||||
// Nothing to normalize
|
||||
@@ -8712,37 +8692,37 @@ impl<'db> KnownInstanceType<'db> {
|
||||
Self::ConstraintSet(set) => Some(Self::ConstraintSet(set)),
|
||||
Self::TypeVar(typevar) => Some(Self::TypeVar(typevar)),
|
||||
Self::TypeAliasType(type_alias) => type_alias
|
||||
.recursive_type_normalized_impl(db, div, visitor)
|
||||
.recursive_type_normalized_impl(db, div)
|
||||
.map(Self::TypeAliasType),
|
||||
Self::Field(field) => field
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Self::Field),
|
||||
Self::UnionType(union_type) => union_type
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Self::UnionType),
|
||||
Self::Literal(ty) => ty
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
.recursive_type_normalized_impl(db, div, true)
|
||||
.map(Self::Literal),
|
||||
Self::Annotated(ty) => ty
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
.recursive_type_normalized_impl(db, div, true)
|
||||
.map(Self::Annotated),
|
||||
Self::TypeGenericAlias(ty) => ty
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
.recursive_type_normalized_impl(db, div, true)
|
||||
.map(Self::TypeGenericAlias),
|
||||
Self::LiteralStringAlias(ty) => ty
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
.recursive_type_normalized_impl(db, div, true)
|
||||
.map(Self::LiteralStringAlias),
|
||||
Self::Callable(callable) => callable
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.map(Self::Callable),
|
||||
Self::NewType(newtype) => newtype
|
||||
.try_map_base_class_type(db, |class_type| {
|
||||
class_type.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
class_type.recursive_type_normalized_impl(db, div, true)
|
||||
})
|
||||
.map(Self::NewType),
|
||||
Self::GenericContext(generic) => Some(Self::GenericContext(generic)),
|
||||
Self::Specialization(specialization) => specialization
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
.recursive_type_normalized_impl(db, div, true)
|
||||
.map(Self::Specialization),
|
||||
}
|
||||
}
|
||||
@@ -8829,6 +8809,8 @@ pub enum DynamicType {
|
||||
Todo(TodoType),
|
||||
/// A special Todo-variant for `Unpack[Ts]`, so that we can treat it specially in `Generic[Unpack[Ts]]`
|
||||
TodoUnpack,
|
||||
/// A special Todo-variant for `*Ts`, so that we can treat it specially in `Generic[Unpack[Ts]]`
|
||||
TodoStarredExpression,
|
||||
/// A type that is determined to be divergent during recursive type inference.
|
||||
Divergent(DivergentType),
|
||||
}
|
||||
@@ -8859,13 +8841,8 @@ impl std::fmt::Display for DynamicType {
|
||||
// `DynamicType::Todo`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
DynamicType::Todo(todo) => write!(f, "@Todo{todo}"),
|
||||
DynamicType::TodoUnpack => {
|
||||
if cfg!(debug_assertions) {
|
||||
f.write_str("@Todo(typing.Unpack)")
|
||||
} else {
|
||||
f.write_str("@Todo")
|
||||
}
|
||||
}
|
||||
DynamicType::TodoUnpack => f.write_str("@Todo(typing.Unpack)"),
|
||||
DynamicType::TodoStarredExpression => f.write_str("@Todo(StarredExpression)"),
|
||||
DynamicType::Divergent(_) => f.write_str("Divergent"),
|
||||
}
|
||||
}
|
||||
@@ -9231,15 +9208,12 @@ impl<'db> FieldInstance<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
let default_type = match self.default_type(db) {
|
||||
Some(default) if nested => {
|
||||
Some(default.recursive_type_normalized_impl(db, div, true, visitor)?)
|
||||
}
|
||||
Some(default) if nested => Some(default.recursive_type_normalized_impl(db, div, true)?),
|
||||
Some(default) => Some(
|
||||
default
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)
|
||||
.recursive_type_normalized_impl(db, div, true)
|
||||
.unwrap_or(div),
|
||||
),
|
||||
None => None,
|
||||
@@ -10184,7 +10158,6 @@ impl<'db> UnionTypeInstance<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
// The `Divergent` elimination rules are different within union types.
|
||||
// See `UnionType::recursive_type_normalized_impl` for details.
|
||||
@@ -10192,14 +10165,14 @@ impl<'db> UnionTypeInstance<'db> {
|
||||
Some(types) if nested => Some(
|
||||
types
|
||||
.iter()
|
||||
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested, visitor))
|
||||
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested))
|
||||
.collect::<Option<Box<_>>>()?,
|
||||
),
|
||||
Some(types) => Some(
|
||||
types
|
||||
.iter()
|
||||
.map(|ty| {
|
||||
ty.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
ty.recursive_type_normalized_impl(db, div, nested)
|
||||
.unwrap_or(div)
|
||||
})
|
||||
.collect::<Box<_>>(),
|
||||
@@ -10207,9 +10180,9 @@ impl<'db> UnionTypeInstance<'db> {
|
||||
None => None,
|
||||
};
|
||||
let union_type = match self.union_type(db).clone() {
|
||||
Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested, visitor)?),
|
||||
Ok(ty) if nested => Ok(ty.recursive_type_normalized_impl(db, div, nested)?),
|
||||
Ok(ty) => Ok(ty
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.unwrap_or(div)),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
@@ -10241,14 +10214,13 @@ impl<'db> InternedType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
let inner = if nested {
|
||||
self.inner(db)
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)?
|
||||
.recursive_type_normalized_impl(db, div, nested)?
|
||||
} else {
|
||||
self.inner(db)
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
.recursive_type_normalized_impl(db, div, nested)
|
||||
.unwrap_or(div)
|
||||
};
|
||||
Some(InternedType::new(db, inner))
|
||||
@@ -11560,14 +11532,13 @@ impl<'db> BoundMethodType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
Some(Self::new(
|
||||
db,
|
||||
self.function(db)
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
.recursive_type_normalized_impl(db, div, nested)?,
|
||||
self.self_instance(db)
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)?,
|
||||
.recursive_type_normalized_impl(db, div, true)?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -11732,12 +11703,11 @@ impl<'db> CallableType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
Some(CallableType::new(
|
||||
db,
|
||||
self.signatures(db)
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
.recursive_type_normalized_impl(db, div, nested)?,
|
||||
self.is_function_like(db),
|
||||
))
|
||||
}
|
||||
@@ -12175,27 +12145,26 @@ impl<'db> KnownBoundMethodType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
match self {
|
||||
KnownBoundMethodType::FunctionTypeDunderGet(function) => {
|
||||
Some(KnownBoundMethodType::FunctionTypeDunderGet(
|
||||
function.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
function.recursive_type_normalized_impl(db, div, nested)?,
|
||||
))
|
||||
}
|
||||
KnownBoundMethodType::FunctionTypeDunderCall(function) => {
|
||||
Some(KnownBoundMethodType::FunctionTypeDunderCall(
|
||||
function.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
function.recursive_type_normalized_impl(db, div, nested)?,
|
||||
))
|
||||
}
|
||||
KnownBoundMethodType::PropertyDunderGet(property) => {
|
||||
Some(KnownBoundMethodType::PropertyDunderGet(
|
||||
property.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
property.recursive_type_normalized_impl(db, div, nested)?,
|
||||
))
|
||||
}
|
||||
KnownBoundMethodType::PropertyDunderSet(property) => {
|
||||
Some(KnownBoundMethodType::PropertyDunderSet(
|
||||
property.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
property.recursive_type_normalized_impl(db, div, nested)?,
|
||||
))
|
||||
}
|
||||
KnownBoundMethodType::StrStartswith(_)
|
||||
@@ -12858,18 +12827,14 @@ impl<'db> ManualPEP695TypeAliasType<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
fn recursive_type_normalized_impl(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
// TODO: with full support for manual PEP-695 style type aliases, this method should become unnecessary.
|
||||
fn recursive_type_normalized_impl(self, db: &'db dyn Db, div: Type<'db>) -> Option<Self> {
|
||||
Some(Self::new(
|
||||
db,
|
||||
self.name(db),
|
||||
self.definition(db),
|
||||
self.value(db)
|
||||
.recursive_type_normalized_impl(db, div, true, visitor)?,
|
||||
.recursive_type_normalized_impl(db, div, true)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -12914,16 +12879,11 @@ impl<'db> TypeAliasType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn recursive_type_normalized_impl(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
fn recursive_type_normalized_impl(self, db: &'db dyn Db, div: Type<'db>) -> Option<Self> {
|
||||
match self {
|
||||
TypeAliasType::PEP695(type_alias) => Some(TypeAliasType::PEP695(type_alias)),
|
||||
TypeAliasType::ManualPEP695(type_alias) => Some(TypeAliasType::ManualPEP695(
|
||||
type_alias.recursive_type_normalized_impl(db, div, visitor)?,
|
||||
type_alias.recursive_type_normalized_impl(db, div)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -13248,7 +13208,6 @@ impl<'db> UnionType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
let mut builder = UnionBuilder::new(db)
|
||||
.order_elements(false)
|
||||
@@ -13258,7 +13217,7 @@ impl<'db> UnionType<'db> {
|
||||
for ty in self.elements(db) {
|
||||
if nested {
|
||||
// list[T | Divergent] => list[Divergent]
|
||||
let ty = ty.recursive_type_normalized_impl(db, div, nested, visitor)?;
|
||||
let ty = ty.recursive_type_normalized_impl(db, div, nested)?;
|
||||
if ty == div {
|
||||
return Some(ty);
|
||||
}
|
||||
@@ -13271,7 +13230,7 @@ impl<'db> UnionType<'db> {
|
||||
continue;
|
||||
}
|
||||
builder = builder.add(
|
||||
ty.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
ty.recursive_type_normalized_impl(db, div, nested)
|
||||
.unwrap_or(div),
|
||||
);
|
||||
empty = false;
|
||||
@@ -13389,18 +13348,16 @@ impl<'db> IntersectionType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
fn opt_normalized_set<'db>(
|
||||
db: &'db dyn Db,
|
||||
elements: &FxOrderSet<Type<'db>>,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<FxOrderSet<Type<'db>>> {
|
||||
elements
|
||||
.iter()
|
||||
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested, visitor))
|
||||
.map(|ty| ty.recursive_type_normalized_impl(db, div, nested))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -13409,26 +13366,25 @@ impl<'db> IntersectionType<'db> {
|
||||
elements: &FxOrderSet<Type<'db>>,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> FxOrderSet<Type<'db>> {
|
||||
elements
|
||||
.iter()
|
||||
.map(|ty| {
|
||||
ty.recursive_type_normalized_impl(db, div, nested, visitor)
|
||||
ty.recursive_type_normalized_impl(db, div, nested)
|
||||
.unwrap_or(div)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
let positive = if nested {
|
||||
opt_normalized_set(db, self.positive(db), div, nested, visitor)?
|
||||
opt_normalized_set(db, self.positive(db), div, nested)?
|
||||
} else {
|
||||
normalized_set(db, self.positive(db), div, nested, visitor)
|
||||
normalized_set(db, self.positive(db), div, nested)
|
||||
};
|
||||
let negative = if nested {
|
||||
opt_normalized_set(db, self.negative(db), div, nested, visitor)?
|
||||
opt_normalized_set(db, self.negative(db), div, nested)?
|
||||
} else {
|
||||
normalized_set(db, self.negative(db), div, nested, visitor)
|
||||
normalized_set(db, self.negative(db), div, nested)
|
||||
};
|
||||
|
||||
Some(IntersectionType::new(db, positive, negative))
|
||||
@@ -13911,16 +13867,15 @@ pub(crate) mod tests {
|
||||
nested_rec.display(&db).to_string(),
|
||||
"list[list[Divergent] | None]"
|
||||
);
|
||||
let visitor = NormalizedVisitor::default();
|
||||
let normalized = nested_rec
|
||||
.recursive_type_normalized_impl(&db, div, false, &visitor)
|
||||
.recursive_type_normalized_impl(&db, div, false)
|
||||
.unwrap();
|
||||
assert_eq!(normalized.display(&db).to_string(), "list[Divergent]");
|
||||
|
||||
let union = UnionType::from_elements(&db, [div, KnownClass::Int.to_instance(&db)]);
|
||||
assert_eq!(union.display(&db).to_string(), "Divergent | int");
|
||||
let normalized = union
|
||||
.recursive_type_normalized_impl(&db, div, false, &visitor)
|
||||
.recursive_type_normalized_impl(&db, div, false)
|
||||
.unwrap();
|
||||
assert_eq!(normalized.display(&db).to_string(), "int");
|
||||
|
||||
|
||||
@@ -197,17 +197,16 @@ impl<'db> SuperOwnerKind<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
match self {
|
||||
SuperOwnerKind::Dynamic(dynamic) => {
|
||||
Some(SuperOwnerKind::Dynamic(dynamic.recursive_type_normalized()))
|
||||
}
|
||||
SuperOwnerKind::Class(class) => Some(SuperOwnerKind::Class(
|
||||
class.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
class.recursive_type_normalized_impl(db, div, nested)?,
|
||||
)),
|
||||
SuperOwnerKind::Instance(instance) => Some(SuperOwnerKind::Instance(
|
||||
instance.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
instance.recursive_type_normalized_impl(db, div, nested)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -620,14 +619,13 @@ impl<'db> BoundSuperType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
Some(Self::new(
|
||||
db,
|
||||
self.pivot_class(db)
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
.recursive_type_normalized_impl(db, div, nested)?,
|
||||
self.owner(db)
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
.recursive_type_normalized_impl(db, div, nested)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ use crate::types::{
|
||||
DataclassParams, FieldInstance, KnownBoundMethodType, KnownClass, KnownInstanceType,
|
||||
MemberLookupPolicy, NominalInstanceType, PropertyInstanceType, SpecialFormType,
|
||||
TrackedConstraintSet, TypeAliasType, TypeContext, TypeVarVariance, UnionBuilder, UnionType,
|
||||
WrapperDescriptorKind, enums, ide_support, todo_type,
|
||||
WrapperDescriptorKind, enums, list_members, todo_type,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_python_ast::{self as ast, ArgOrKeyword, PythonVersion};
|
||||
@@ -888,7 +888,7 @@ impl<'db> Bindings<'db> {
|
||||
if let [Some(ty)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::heterogeneous_tuple(
|
||||
db,
|
||||
ide_support::all_members(db, *ty)
|
||||
list_members::all_members(db, *ty)
|
||||
.into_iter()
|
||||
.sorted()
|
||||
.map(|member| Type::string_literal(db, &member.name)),
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::semantic_index::{
|
||||
use crate::types::bound_super::BoundSuperError;
|
||||
use crate::types::constraints::{ConstraintSet, IteratorConstraintsExtension};
|
||||
use crate::types::context::InferContext;
|
||||
use crate::types::diagnostic::INVALID_TYPE_ALIAS_TYPE;
|
||||
use crate::types::diagnostic::{INVALID_TYPE_ALIAS_TYPE, SUPER_CALL_IN_NAMED_TUPLE_METHOD};
|
||||
use crate::types::enums::enum_metadata;
|
||||
use crate::types::function::{DataclassTransformerParams, KnownFunction};
|
||||
use crate::types::generics::{
|
||||
@@ -284,13 +284,12 @@ impl<'db> GenericAlias<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
Some(Self::new(
|
||||
db,
|
||||
self.origin(db),
|
||||
self.specialization(db)
|
||||
.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
.recursive_type_normalized_impl(db, div, nested)?,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -443,12 +442,11 @@ impl<'db> ClassType<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
match self {
|
||||
Self::NonGeneric(_) => Some(self),
|
||||
Self::Generic(generic) => Some(Self::Generic(
|
||||
generic.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
generic.recursive_type_normalized_impl(db, div, nested)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
@@ -5546,6 +5544,20 @@ impl KnownClass {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
|
||||
if let Some(builder) = context
|
||||
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Cannot use `super()` in a method of NamedTuple class `{}`",
|
||||
enclosing_class.name(db)
|
||||
));
|
||||
}
|
||||
overload.set_return_type(Type::unknown());
|
||||
return;
|
||||
}
|
||||
|
||||
// The type of the first parameter if the given scope is function-like (i.e. function or lambda).
|
||||
// `None` if the scope is not function-like, or has no parameters.
|
||||
let first_param = match scope.node(db) {
|
||||
@@ -5585,6 +5597,22 @@ impl KnownClass {
|
||||
overload.set_return_type(bound_super);
|
||||
}
|
||||
[Some(pivot_class_type), Some(owner_type)] => {
|
||||
// Check if the enclosing class is a `NamedTuple`, which forbids the use of `super()`.
|
||||
if let Some(enclosing_class) = nearest_enclosing_class(db, index, scope) {
|
||||
if CodeGeneratorKind::NamedTuple.matches(db, enclosing_class, None) {
|
||||
if let Some(builder) = context
|
||||
.report_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD, call_expression)
|
||||
{
|
||||
builder.into_diagnostic(format_args!(
|
||||
"Cannot use `super()` in a method of NamedTuple class `{}`",
|
||||
enclosing_class.name(db)
|
||||
));
|
||||
}
|
||||
overload.set_return_type(Type::unknown());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let bound_super = BoundSuperType::build(db, *pivot_class_type, *owner_type)
|
||||
.unwrap_or_else(|err| {
|
||||
err.report_diagnostic(context, call_expression.into());
|
||||
|
||||
@@ -48,12 +48,11 @@ impl<'db> ClassBase<'db> {
|
||||
db: &'db dyn Db,
|
||||
div: Type<'db>,
|
||||
nested: bool,
|
||||
visitor: &NormalizedVisitor<'db>,
|
||||
) -> Option<Self> {
|
||||
match self {
|
||||
Self::Dynamic(dynamic) => Some(Self::Dynamic(dynamic.recursive_type_normalized())),
|
||||
Self::Class(class) => Some(Self::Class(
|
||||
class.recursive_type_normalized_impl(db, div, nested, visitor)?,
|
||||
class.recursive_type_normalized_impl(db, div, nested)?,
|
||||
)),
|
||||
Self::Protocol | Self::Generic | Self::TypedDict => Some(self),
|
||||
}
|
||||
@@ -64,7 +63,9 @@ impl<'db> ClassBase<'db> {
|
||||
ClassBase::Class(class) => class.name(db),
|
||||
ClassBase::Dynamic(DynamicType::Any) => "Any",
|
||||
ClassBase::Dynamic(DynamicType::Unknown) => "Unknown",
|
||||
ClassBase::Dynamic(DynamicType::Todo(_) | DynamicType::TodoUnpack) => "@Todo",
|
||||
ClassBase::Dynamic(
|
||||
DynamicType::Todo(_) | DynamicType::TodoUnpack | DynamicType::TodoStarredExpression,
|
||||
) => "@Todo",
|
||||
ClassBase::Dynamic(DynamicType::Divergent(_)) => "Divergent",
|
||||
ClassBase::Protocol => "Protocol",
|
||||
ClassBase::Generic => "Generic",
|
||||
|
||||
@@ -418,6 +418,7 @@ impl<'db> ConstraintSet<'db> {
|
||||
Self::constrain_typevar(db, typevar, lower, upper, TypeRelation::Assignability)
|
||||
}
|
||||
|
||||
#[expect(dead_code)] // Keep this around for debugging purposes
|
||||
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
|
||||
self.node.simplify_for_display(db).display(db)
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use crate::types::class::{
|
||||
CodeGeneratorKind, DisjointBase, DisjointBaseKind, Field, MethodDecorator,
|
||||
};
|
||||
use crate::types::function::{FunctionDecorators, FunctionType, KnownFunction, OverloadLiteral};
|
||||
use crate::types::liskov::MethodKind;
|
||||
use crate::types::overrides::MethodKind;
|
||||
use crate::types::string_annotation::{
|
||||
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
|
||||
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
|
||||
@@ -121,6 +121,7 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
|
||||
registry.register_lint(&MISSING_TYPED_DICT_KEY);
|
||||
registry.register_lint(&INVALID_METHOD_OVERRIDE);
|
||||
registry.register_lint(&INVALID_EXPLICIT_OVERRIDE);
|
||||
registry.register_lint(&SUPER_CALL_IN_NAMED_TUPLE_METHOD);
|
||||
|
||||
// String annotations
|
||||
registry.register_lint(&BYTE_STRING_TYPE_ANNOTATION);
|
||||
@@ -544,7 +545,8 @@ declare_lint! {
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// An invalidly defined `NamedTuple` class may lead to the type checker
|
||||
/// drawing incorrect conclusions. It may also lead to `TypeError`s at runtime.
|
||||
/// drawing incorrect conclusions. It may also lead to `TypeError`s or
|
||||
/// `AttributeError`s at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// A class definition cannot combine `NamedTuple` with other base classes
|
||||
@@ -557,6 +559,27 @@ declare_lint! {
|
||||
/// >>> class Foo(NamedTuple, object): ...
|
||||
/// TypeError: can only inherit from a NamedTuple type and Generic
|
||||
/// ```
|
||||
///
|
||||
/// Further, `NamedTuple` field names cannot start with an underscore:
|
||||
///
|
||||
/// ```pycon
|
||||
/// >>> from typing import NamedTuple
|
||||
/// >>> class Foo(NamedTuple):
|
||||
/// ... _bar: int
|
||||
/// ValueError: Field names cannot start with an underscore: '_bar'
|
||||
/// ```
|
||||
///
|
||||
/// `NamedTuple` classes also have certain synthesized attributes (like `_asdict`, `_make`,
|
||||
/// `_replace`, etc.) that cannot be overwritten. Attempting to assign to these attributes
|
||||
/// without a type annotation will raise an `AttributeError` at runtime.
|
||||
///
|
||||
/// ```pycon
|
||||
/// >>> from typing import NamedTuple
|
||||
/// >>> class Foo(NamedTuple):
|
||||
/// ... x: int
|
||||
/// ... _asdict = 42
|
||||
/// AttributeError: Cannot overwrite NamedTuple attribute _asdict
|
||||
/// ```
|
||||
pub(crate) static INVALID_NAMED_TUPLE = {
|
||||
summary: "detects invalid `NamedTuple` class definitions",
|
||||
status: LintStatus::stable("0.0.1-alpha.19"),
|
||||
@@ -1760,6 +1783,33 @@ declare_lint! {
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to `super()` inside methods of `NamedTuple` classes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Using `super()` in a method of a `NamedTuple` class will raise an exception at runtime.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// from typing import NamedTuple
|
||||
///
|
||||
/// class F(NamedTuple):
|
||||
/// x: int
|
||||
///
|
||||
/// def method(self):
|
||||
/// super() # error: super() is not supported in methods of NamedTuple classes
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: super()](https://docs.python.org/3/library/functions.html#super)
|
||||
pub(crate) static SUPER_CALL_IN_NAMED_TUPLE_METHOD = {
|
||||
summary: "detects `super()` calls in methods of `NamedTuple` classes",
|
||||
status: LintStatus::preview("0.0.1-alpha.30"),
|
||||
default_level: Level::Error,
|
||||
}
|
||||
}
|
||||
|
||||
declare_lint! {
|
||||
/// ## What it does
|
||||
/// Checks for calls to `reveal_type` without importing it.
|
||||
@@ -3501,7 +3551,7 @@ pub(crate) fn report_invalid_key_on_typed_dict<'db>(
|
||||
pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
class: ClassLiteral<'db>,
|
||||
(field, field_def): &(Name, Option<Definition<'db>>),
|
||||
(field, field_def): (&str, Option<Definition<'db>>),
|
||||
(field_with_default, field_with_default_def): &(Name, Option<Definition<'db>>),
|
||||
) {
|
||||
let db = context.db();
|
||||
@@ -3514,9 +3564,9 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'
|
||||
let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, diagnostic_range) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
let mut diagnostic = builder.into_diagnostic(
|
||||
"NamedTuple field without default value cannot follow field(s) with default value(s)",
|
||||
));
|
||||
);
|
||||
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Field `{field}` defined here without a default value",
|
||||
@@ -3547,6 +3597,40 @@ pub(super) fn report_namedtuple_field_without_default_after_field_with_default<'
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn report_named_tuple_field_with_leading_underscore<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
class: ClassLiteral<'db>,
|
||||
field_name: &str,
|
||||
field_definition: Option<Definition<'db>>,
|
||||
) {
|
||||
let db = context.db();
|
||||
let module = context.module();
|
||||
|
||||
let diagnostic_range = field_definition
|
||||
.map(|definition| definition.kind(db).full_range(module))
|
||||
.unwrap_or_else(|| class.header_range(db));
|
||||
|
||||
let Some(builder) = context.report_lint(&INVALID_NAMED_TUPLE, diagnostic_range) else {
|
||||
return;
|
||||
};
|
||||
let mut diagnostic =
|
||||
builder.into_diagnostic("NamedTuple field name cannot start with an underscore");
|
||||
|
||||
if field_definition.is_some() {
|
||||
diagnostic.set_primary_message(
|
||||
"Class definition will raise `TypeError` at runtime due to this field",
|
||||
);
|
||||
} else {
|
||||
diagnostic.set_primary_message(format_args!(
|
||||
"Class definition will raise `TypeError` at runtime due to field `{field_name}`",
|
||||
));
|
||||
}
|
||||
|
||||
diagnostic.set_concise_message(format_args!(
|
||||
"NamedTuple field `{field_name}` cannot start with an underscore"
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn report_missing_typed_dict_key<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
constructor_node: AnyNodeRef,
|
||||
@@ -3804,6 +3888,7 @@ pub(super) fn report_overridden_final_method<'db>(
|
||||
context: &InferContext<'db, '_>,
|
||||
member: &str,
|
||||
subclass_definition: Definition<'db>,
|
||||
// N.B. the type of the *definition*, not the type on an instance of the subclass
|
||||
subclass_type: Type<'db>,
|
||||
superclass: ClassType<'db>,
|
||||
subclass: ClassType<'db>,
|
||||
@@ -3811,6 +3896,23 @@ pub(super) fn report_overridden_final_method<'db>(
|
||||
) {
|
||||
let db = context.db();
|
||||
|
||||
// Some hijinks so that we emit a diagnostic on the property getter rather than the property setter
|
||||
let property_getter_definition = if subclass_definition.kind(db).is_function_def()
|
||||
&& let Type::PropertyInstance(property) = subclass_type
|
||||
&& let Some(Type::FunctionLiteral(getter)) = property.getter(db)
|
||||
{
|
||||
let getter_definition = getter.definition(db);
|
||||
if getter_definition.scope(db) == subclass_definition.scope(db) {
|
||||
Some(getter_definition)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let subclass_definition = property_getter_definition.unwrap_or(subclass_definition);
|
||||
|
||||
let Some(builder) = context.report_lint(
|
||||
&OVERRIDE_OF_FINAL_METHOD,
|
||||
subclass_definition.focus_range(db, context.module()),
|
||||
@@ -3871,37 +3973,69 @@ pub(super) fn report_overridden_final_method<'db>(
|
||||
|
||||
diagnostic.sub(sub);
|
||||
|
||||
let underlying_function = match subclass_type {
|
||||
Type::FunctionLiteral(function) => Some(function),
|
||||
Type::BoundMethod(method) => Some(method.function(db)),
|
||||
_ => None,
|
||||
};
|
||||
// It's tempting to autofix properties as well,
|
||||
// but you'd want to delete the `@my_property.deleter` as well as the getter and the deleter,
|
||||
// and we don't model property deleters at all right now.
|
||||
if let Type::FunctionLiteral(function) = subclass_type {
|
||||
let class_node = subclass
|
||||
.class_literal(db)
|
||||
.0
|
||||
.body_scope(db)
|
||||
.node(db)
|
||||
.expect_class()
|
||||
.node(context.module());
|
||||
|
||||
let (overloads, implementation) = function.overloads_and_implementation(db);
|
||||
let overload_count = overloads.len() + usize::from(implementation.is_some());
|
||||
let is_only = overload_count >= class_node.body.len();
|
||||
|
||||
if let Some(function) = underlying_function {
|
||||
let overload_deletion = |overload: &OverloadLiteral<'db>| {
|
||||
Edit::range_deletion(overload.node(db, context.file(), context.module()).range())
|
||||
let range = overload.node(db, context.file(), context.module()).range();
|
||||
if is_only {
|
||||
Edit::range_replacement("pass".to_string(), range)
|
||||
} else {
|
||||
Edit::range_deletion(range)
|
||||
}
|
||||
};
|
||||
|
||||
let should_fix = overloads
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(implementation)
|
||||
.all(|overload| {
|
||||
class_node
|
||||
.body
|
||||
.iter()
|
||||
.filter_map(ast::Stmt::as_function_def_stmt)
|
||||
.contains(overload.node(db, context.file(), context.module()))
|
||||
});
|
||||
|
||||
match function.overloads_and_implementation(db) {
|
||||
([first_overload, rest @ ..], None) => {
|
||||
diagnostic.help(format_args!("Remove all overloads for `{member}`"));
|
||||
diagnostic.set_fix(Fix::unsafe_edits(
|
||||
overload_deletion(first_overload),
|
||||
rest.iter().map(overload_deletion),
|
||||
));
|
||||
diagnostic.set_optional_fix(should_fix.then(|| {
|
||||
Fix::unsafe_edits(
|
||||
overload_deletion(first_overload),
|
||||
rest.iter().map(overload_deletion),
|
||||
)
|
||||
}));
|
||||
}
|
||||
([first_overload, rest @ ..], Some(implementation)) => {
|
||||
diagnostic.help(format_args!(
|
||||
"Remove all overloads and the implementation for `{member}`"
|
||||
));
|
||||
diagnostic.set_fix(Fix::unsafe_edits(
|
||||
overload_deletion(first_overload),
|
||||
rest.iter().chain([&implementation]).map(overload_deletion),
|
||||
));
|
||||
diagnostic.set_optional_fix(should_fix.then(|| {
|
||||
Fix::unsafe_edits(
|
||||
overload_deletion(first_overload),
|
||||
rest.iter().chain([&implementation]).map(overload_deletion),
|
||||
)
|
||||
}));
|
||||
}
|
||||
([], Some(implementation)) => {
|
||||
diagnostic.help(format_args!("Remove the override of `{member}`"));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(overload_deletion(&implementation)));
|
||||
diagnostic.set_optional_fix(
|
||||
should_fix.then(|| Fix::unsafe_edit(overload_deletion(&implementation))),
|
||||
);
|
||||
}
|
||||
([], None) => {
|
||||
// Should be impossible to get here: how would we even infer a function as a function
|
||||
@@ -3911,11 +4045,12 @@ pub(super) fn report_overridden_final_method<'db>(
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if let Type::PropertyInstance(property) = subclass_type
|
||||
&& property.setter(db).is_some()
|
||||
{
|
||||
diagnostic.help(format_args!("Remove the getter and setter for `{member}`"));
|
||||
} else {
|
||||
diagnostic.help(format_args!("Remove the override of `{member}`"));
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_deletion(
|
||||
subclass_definition.full_range(db, context.module()).range(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user