Compare commits
54 Commits
david/impl
...
david/defa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4833614c2 | ||
|
|
508c0a0861 | ||
|
|
0d2792517d | ||
|
|
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 | ||
|
|
ecab623fb2 | ||
|
|
42f152108a | ||
|
|
594b7b04d3 | ||
|
|
b5b4917d7f | ||
|
|
0084e94f78 | ||
|
|
566c959add | ||
|
|
8bcfc198b8 | ||
|
|
c534bfaf01 | ||
|
|
5e1b2eef57 | ||
|
|
98681b9356 | ||
|
|
3ed537e9f1 |
@@ -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).
|
||||
|
||||
7
.github/renovate.json5
vendored
7
.github/renovate.json5
vendored
@@ -2,12 +2,11 @@
|
||||
$schema: "https://docs.renovatebot.com/renovate-schema.json",
|
||||
dependencyDashboard: true,
|
||||
suppressNotifications: ["prEditedNotification"],
|
||||
extends: ["config:recommended"],
|
||||
extends: ["github>astral-sh/renovate-config"],
|
||||
labels: ["internal"],
|
||||
schedule: ["before 4am on Monday"],
|
||||
semanticCommits: "disabled",
|
||||
separateMajorMinor: false,
|
||||
prHourlyLimit: 10,
|
||||
enabledManagers: ["github-actions", "pre-commit", "cargo", "pep621", "pip_requirements", "npm"],
|
||||
cargo: {
|
||||
// See https://docs.renovatebot.com/configuration-options/#rangestrategy
|
||||
@@ -16,7 +15,7 @@
|
||||
pep621: {
|
||||
// The default for this package manager is to only search for `pyproject.toml` files
|
||||
// found at the repository root: https://docs.renovatebot.com/modules/manager/pep621/#file-matching
|
||||
fileMatch: ["^(python|scripts)/.*pyproject\\.toml$"],
|
||||
managerFilePatterns: ["^(python|scripts)/.*pyproject\\.toml$"],
|
||||
},
|
||||
pip_requirements: {
|
||||
// The default for this package manager is to run on all requirements.txt files:
|
||||
@@ -34,7 +33,7 @@
|
||||
npm: {
|
||||
// The default for this package manager is to only search for `package.json` files
|
||||
// found at the repository root: https://docs.renovatebot.com/modules/manager/npm/#file-matching
|
||||
fileMatch: ["^playground/.*package\\.json$"],
|
||||
managerFilePatterns: ["^playground/.*package\\.json$"],
|
||||
},
|
||||
"pre-commit": {
|
||||
enabled: true,
|
||||
|
||||
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"
|
||||
|
||||
|
||||
35
CHANGELOG.md
35
CHANGELOG.md
@@ -1,5 +1,40 @@
|
||||
# Changelog
|
||||
|
||||
## 0.14.7
|
||||
|
||||
Released on 2025-11-28.
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bandit`\] Handle string literal bindings in suspicious-url-open-usage (`S310`) ([#21469](https://github.com/astral-sh/ruff/pull/21469))
|
||||
- \[`pylint`\] Fix `PLR1708` false positives on nested functions ([#21177](https://github.com/astral-sh/ruff/pull/21177))
|
||||
- \[`pylint`\] Fix suppression for empty dict without tuple key annotation (`PLE1141`) ([#21290](https://github.com/astral-sh/ruff/pull/21290))
|
||||
- \[`ruff`\] Add rule `RUF066` to detect unnecessary class properties ([#21535](https://github.com/astral-sh/ruff/pull/21535))
|
||||
- \[`ruff`\] Catch more dummy variable uses (`RUF052`) ([#19799](https://github.com/astral-sh/ruff/pull/19799))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- [server] Set severity for non-rule diagnostics ([#21559](https://github.com/astral-sh/ruff/pull/21559))
|
||||
- \[`flake8-implicit-str-concat`\] Avoid invalid fix in (`ISC003`) ([#21517](https://github.com/astral-sh/ruff/pull/21517))
|
||||
- \[`parser`\] Fix panic when parsing IPython escape command expressions ([#21480](https://github.com/astral-sh/ruff/pull/21480))
|
||||
|
||||
### CLI
|
||||
|
||||
- Show partial fixability indicator in statistics output ([#21513](https://github.com/astral-sh/ruff/pull/21513))
|
||||
|
||||
### Contributors
|
||||
|
||||
- [@mikeleppane](https://github.com/mikeleppane)
|
||||
- [@senekor](https://github.com/senekor)
|
||||
- [@ShaharNaveh](https://github.com/ShaharNaveh)
|
||||
- [@JumboBear](https://github.com/JumboBear)
|
||||
- [@prakhar1144](https://github.com/prakhar1144)
|
||||
- [@tsvikas](https://github.com/tsvikas)
|
||||
- [@danparizher](https://github.com/danparizher)
|
||||
- [@chirizxc](https://github.com/chirizxc)
|
||||
- [@AlexWaygood](https://github.com/AlexWaygood)
|
||||
- [@MichaReiser](https://github.com/MichaReiser)
|
||||
|
||||
## 0.14.6
|
||||
|
||||
Released on 2025-11-21.
|
||||
|
||||
38
Cargo.lock
generated
38
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]]
|
||||
@@ -2859,7 +2859,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.6"
|
||||
version = "0.14.7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -3117,7 +3117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.6"
|
||||
version = "0.14.7"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -3472,7 +3472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.6"
|
||||
version = "0.14.7"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -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",
|
||||
|
||||
@@ -147,8 +147,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.14.6/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.6/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.14.7/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.14.7/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -181,7 +181,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.6
|
||||
rev: v0.14.7
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.14.6"
|
||||
version = "0.14.7"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -120,7 +120,7 @@ static COLOUR_SCIENCE: Benchmark = Benchmark::new(
|
||||
max_dep_date: "2025-06-17",
|
||||
python_version: PythonVersion::PY310,
|
||||
},
|
||||
600,
|
||||
1070,
|
||||
);
|
||||
|
||||
static FREQTRADE: Benchmark = Benchmark::new(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.14.6"
|
||||
version = "0.14.7"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.14.6"
|
||||
version = "0.14.7"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
235
crates/ty/docs/rules.md
generated
235
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#L133" 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#L177" 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#L203" 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#L228" 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#L254" 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#L280" 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#L341" 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#L362" 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#L566" 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#L590" 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#L394" 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#L644" 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#L684" 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#L1920" 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#L706" 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#L736" 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#L787" 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#L808" 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#L831" 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#L1617" 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#L867" 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#L611" 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#L893" 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#L990" 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#L2048" 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#L540" 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#L966" 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#L1017" 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#L1116" 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#L921" 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#L476" 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#L1136" 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#L665" 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#L1179" 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#L945" 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#L1411" 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#L1218" 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#L1242" 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#L1294" 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#L1266" 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#L1322" 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#L1351" 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#L2021" 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#L1370" 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#L1393" 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#L1452" 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>
|
||||
|
||||
|
||||
@@ -1744,13 +1766,46 @@ for i in 34: # TypeError: 'int' object is not iterable
|
||||
pass
|
||||
```
|
||||
|
||||
## `override-of-final-method`
|
||||
|
||||
<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.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#L1641" target="_blank">View source</a>
|
||||
</small>
|
||||
|
||||
|
||||
**What it does**
|
||||
|
||||
Checks for methods on subclasses that override superclass methods decorated with `@final`.
|
||||
|
||||
**Why is this bad?**
|
||||
|
||||
Decorating a method with `@final` declares to the type checker that it should not be
|
||||
overridden on any subclass.
|
||||
|
||||
**Example**
|
||||
|
||||
|
||||
```python
|
||||
from typing import final
|
||||
|
||||
class A:
|
||||
@final
|
||||
def foo(self): ...
|
||||
|
||||
class B(A):
|
||||
def foo(self): ... # Error raised here
|
||||
```
|
||||
|
||||
## `parameter-already-assigned`
|
||||
|
||||
<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%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#L1503" 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>
|
||||
|
||||
|
||||
@@ -1777,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#L1774" 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>
|
||||
|
||||
|
||||
@@ -1835,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#L1896" 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>
|
||||
|
||||
|
||||
@@ -1865,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#L1594" 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>
|
||||
|
||||
|
||||
@@ -1888,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#L1675" 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>
|
||||
|
||||
|
||||
@@ -1921,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#L1653" 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>
|
||||
|
||||
|
||||
@@ -1949,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#L1696" 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>
|
||||
|
||||
|
||||
@@ -1995,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#L1753" 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>
|
||||
|
||||
|
||||
@@ -2022,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#L1795" 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>
|
||||
|
||||
|
||||
@@ -2050,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#L1817" 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>
|
||||
|
||||
|
||||
@@ -2075,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#L1836" 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>
|
||||
|
||||
|
||||
@@ -2100,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#L1472" 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>
|
||||
|
||||
|
||||
@@ -2137,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#L1855" 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>
|
||||
|
||||
|
||||
@@ -2165,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#L1877" 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>
|
||||
|
||||
|
||||
@@ -2190,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#L505" 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>
|
||||
|
||||
|
||||
@@ -2231,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#L320" 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>
|
||||
|
||||
|
||||
@@ -2319,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#L1524" 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>
|
||||
|
||||
|
||||
@@ -2347,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#L151" 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>
|
||||
|
||||
|
||||
@@ -2379,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#L1546" 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>
|
||||
|
||||
|
||||
@@ -2411,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#L1948" 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>
|
||||
|
||||
|
||||
@@ -2438,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#L1735" 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>
|
||||
|
||||
|
||||
@@ -2462,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#L1969" 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>
|
||||
|
||||
|
||||
@@ -2520,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#L754" 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>
|
||||
|
||||
|
||||
@@ -2559,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#L1060" 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>
|
||||
|
||||
|
||||
@@ -2622,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#L302" 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>
|
||||
|
||||
|
||||
@@ -2646,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#L1572" 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use ty_python_semantic::{
|
||||
|
||||
use crate::docstring::Docstring;
|
||||
use crate::find_node::covering_node;
|
||||
use crate::goto::DefinitionsOrTargets;
|
||||
use crate::goto::Definitions;
|
||||
use crate::importer::{ImportRequest, Importer};
|
||||
use crate::symbols::QueryPattern;
|
||||
use crate::{Db, all_symbols};
|
||||
@@ -220,9 +220,7 @@ impl<'db> Completion<'db> {
|
||||
db: &'db dyn Db,
|
||||
semantic: SemanticCompletion<'db>,
|
||||
) -> Completion<'db> {
|
||||
let definition = semantic
|
||||
.ty
|
||||
.and_then(|ty| DefinitionsOrTargets::from_ty(db, ty));
|
||||
let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, ty));
|
||||
let documentation = definition.and_then(|def| def.docstring(db));
|
||||
let is_type_check_only = semantic.is_type_check_only(db);
|
||||
Completion {
|
||||
@@ -419,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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -455,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()
|
||||
}
|
||||
@@ -504,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
|
||||
@@ -519,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
|
||||
@@ -2898,7 +2914,7 @@ Answer.<CURSOR>
|
||||
__itemsize__ :: int
|
||||
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__]
|
||||
__len__ :: bound method <class 'Answer'>.__len__() -> int
|
||||
__members__ :: MappingProxyType[str, Unknown]
|
||||
__members__ :: MappingProxyType[str, Answer]
|
||||
__module__ :: str
|
||||
__mro__ :: tuple[type, ...]
|
||||
__name__ :: str
|
||||
@@ -5568,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]
|
||||
@@ -5787,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.
|
||||
///
|
||||
|
||||
@@ -182,6 +182,11 @@ fn documentation_trim(docs: &str) -> String {
|
||||
/// </code>
|
||||
/// ```
|
||||
fn render_markdown(docstring: &str) -> String {
|
||||
// Here lies a monumemnt to robust parsing and escaping:
|
||||
// a codefence with SO MANY backticks that surely no one will ever accidentally
|
||||
// break out of it, even if they're writing python documentation about markdown
|
||||
// code fences and are showing off how you can use more than 3 backticks.
|
||||
const FENCE: &str = "```````````";
|
||||
// TODO: there is a convention that `singletick` is for items that can
|
||||
// be looked up in-scope while ``multitick`` is for opaque inline code.
|
||||
// While rendering this we should make note of all the `singletick` locations
|
||||
@@ -191,9 +196,10 @@ fn render_markdown(docstring: &str) -> String {
|
||||
let mut first_line = true;
|
||||
let mut block_indent = 0;
|
||||
let mut in_doctest = false;
|
||||
let mut starting_literal = false;
|
||||
let mut starting_literal = None;
|
||||
let mut in_literal = false;
|
||||
let mut in_any_code = false;
|
||||
let mut temp_owned_line;
|
||||
for untrimmed_line in docstring.lines() {
|
||||
// We can assume leading whitespace has been normalized
|
||||
let mut line = untrimmed_line.trim_start_matches(' ');
|
||||
@@ -207,7 +213,7 @@ fn render_markdown(docstring: &str) -> String {
|
||||
output.push_str(" ");
|
||||
}
|
||||
// Only push newlines if we're not scanning for a real line
|
||||
if !starting_literal {
|
||||
if starting_literal.is_none() {
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
@@ -219,21 +225,23 @@ fn render_markdown(docstring: &str) -> String {
|
||||
in_literal = false;
|
||||
in_any_code = false;
|
||||
block_indent = 0;
|
||||
output.push_str("```\n");
|
||||
output.push_str(FENCE);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
// We previously entered a literal block and we just found our first non-blank line
|
||||
// So now we're actually in the literal block
|
||||
if starting_literal && !line.is_empty() {
|
||||
starting_literal = false;
|
||||
if let Some(literal) = starting_literal
|
||||
&& !line.is_empty()
|
||||
{
|
||||
starting_literal = None;
|
||||
in_literal = true;
|
||||
in_any_code = true;
|
||||
block_indent = line_indent;
|
||||
// TODO: I hope people don't have literal blocks about markdown code fence syntax
|
||||
// TODO: should we not be this aggressive? Let it autodetect?
|
||||
// TODO: respect `.. code-block::` directives:
|
||||
// <https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#directive-code-block>
|
||||
output.push_str("\n```python\n");
|
||||
output.push('\n');
|
||||
output.push_str(FENCE);
|
||||
output.push_str(literal);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
// If we're not in a codeblock and we see something that signals a doctest, start one
|
||||
@@ -242,25 +250,79 @@ fn render_markdown(docstring: &str) -> String {
|
||||
in_doctest = true;
|
||||
in_any_code = true;
|
||||
// TODO: is there something more specific? `pycon`?
|
||||
output.push_str("```python\n");
|
||||
output.push_str(FENCE);
|
||||
output.push_str("python\n");
|
||||
}
|
||||
|
||||
// If we're not in a codeblock and we see something that signals a literal block, start one
|
||||
if !in_any_code && let Some(without_lit) = line.strip_suffix("::") {
|
||||
let trimmed_without_lit = without_lit.trim();
|
||||
if let Some(character) = trimmed_without_lit.chars().next_back() {
|
||||
if character.is_whitespace() {
|
||||
// Remove the marker completely
|
||||
line = trimmed_without_lit;
|
||||
} else {
|
||||
// Only remove the first `:`
|
||||
line = line.strip_suffix(":").unwrap();
|
||||
}
|
||||
let parsed_lit = line
|
||||
// first check for a line ending with `::`
|
||||
.strip_suffix("::")
|
||||
.map(|prefix| (prefix, None))
|
||||
// if that fails, look for a line ending with `:: lang`
|
||||
.or_else(|| {
|
||||
let (prefix, lang) = line.rsplit_once(' ')?;
|
||||
let prefix = prefix.trim_end().strip_suffix("::")?;
|
||||
Some((prefix, Some(lang)))
|
||||
});
|
||||
if !in_any_code && let Some((without_lit, lang)) = parsed_lit {
|
||||
let mut without_directive = without_lit;
|
||||
let mut directive = None;
|
||||
// Parse out a directive like `.. warning::`
|
||||
if let Some((prefix, directive_str)) = without_lit.rsplit_once(' ')
|
||||
&& let Some(without_directive_str) = prefix.strip_suffix("..")
|
||||
{
|
||||
directive = Some(directive_str);
|
||||
without_directive = without_directive_str;
|
||||
}
|
||||
|
||||
// Whether the `::` should become `:` or be erased
|
||||
let include_colon = if let Some(character) = without_directive.chars().next_back() {
|
||||
// If lang is set then we're either deleting the whole line or
|
||||
// the special rendering below will add it itself
|
||||
lang.is_none() && !character.is_whitespace()
|
||||
} else {
|
||||
// Delete whole line
|
||||
line = trimmed_without_lit;
|
||||
false
|
||||
};
|
||||
|
||||
if include_colon {
|
||||
line = line.strip_suffix(":").unwrap();
|
||||
} else {
|
||||
line = without_directive.trim_end();
|
||||
}
|
||||
starting_literal = true;
|
||||
|
||||
starting_literal = match directive {
|
||||
// Special directives that should be plaintext
|
||||
Some(
|
||||
"attention" | "caution" | "danger" | "error" | "hint" | "important" | "note"
|
||||
| "tip" | "warning" | "admonition" | "versionadded" | "version-added"
|
||||
| "versionchanged" | "version-changed" | "version-deprecated" | "deprecated"
|
||||
| "version-removed" | "versionremoved",
|
||||
) => {
|
||||
// Render the argument of things like `.. version-added:: 4.0`
|
||||
let suffix = if let Some(lang) = lang {
|
||||
format!(" *{lang}*")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
// We prepend without_directive here out of caution for preserving input.
|
||||
// This is probably gibberish/invalid syntax? But it's a no-op in normal cases.
|
||||
temp_owned_line =
|
||||
format!("**{without_directive}{}:**{suffix}", directive.unwrap());
|
||||
|
||||
line = temp_owned_line.as_str();
|
||||
None
|
||||
}
|
||||
// Things that just mean "it's code"
|
||||
Some(
|
||||
"code-block" | "sourcecode" | "code" | "testcode" | "testsetup" | "testcleanup",
|
||||
) => lang.or(Some("python")),
|
||||
// Unknown (python I guess?)
|
||||
Some(_) => lang.or(Some("python")),
|
||||
// default to python
|
||||
None => lang.or(Some("python")),
|
||||
};
|
||||
}
|
||||
|
||||
// Add this line's indentation.
|
||||
@@ -349,7 +411,7 @@ fn render_markdown(docstring: &str) -> String {
|
||||
block_indent = 0;
|
||||
in_any_code = false;
|
||||
in_literal = false;
|
||||
output.push_str("```");
|
||||
output.push_str(FENCE);
|
||||
}
|
||||
} else {
|
||||
// Print the line verbatim, it's in code
|
||||
@@ -360,7 +422,8 @@ fn render_markdown(docstring: &str) -> String {
|
||||
}
|
||||
// Flush codeblock
|
||||
if in_any_code {
|
||||
output.push_str("\n```");
|
||||
output.push('\n');
|
||||
output.push_str(FENCE);
|
||||
}
|
||||
|
||||
output
|
||||
@@ -730,28 +793,6 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r"
|
||||
Here _this_ and ___that__ should be escaped
|
||||
Here *this* and **that** should be untouched
|
||||
Here `this` and ``that`` should be untouched
|
||||
|
||||
Here `_this_` and ``__that__`` should be untouched
|
||||
Here `_this_` ``__that__`` should be untouched
|
||||
`_this_too_should_be_untouched_`
|
||||
|
||||
Here `_this_```__that__`` should be untouched but this_is_escaped
|
||||
Here ``_this_```__that__` should be untouched but this_is_escaped
|
||||
|
||||
Here `_this_ and _that_ should be escaped (but isn't)
|
||||
Here _this_ and _that_` should be escaped
|
||||
`Here _this_ and _that_ should be escaped (but isn't)
|
||||
Here _this_ and _that_ should be escaped`
|
||||
|
||||
Here ```_is_``__a__`_balanced_``_mess_```
|
||||
Here ```_is_`````__a__``_random_````_mess__````
|
||||
```_is_`````__a__``_random_````_mess__````
|
||||
");
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
Here \_this\_ and \_\_\_that\_\_ should be escaped
|
||||
Here *this* and **that** should be untouched
|
||||
@@ -796,24 +837,9 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r#"
|
||||
Check out this great example code::
|
||||
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
print(x_y)
|
||||
else:
|
||||
print("too short :(")
|
||||
|
||||
print("done")
|
||||
|
||||
You love to see it.
|
||||
"#);
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Check out this great example code:
|
||||
```python
|
||||
```````````python
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
@@ -823,7 +849,7 @@ mod tests {
|
||||
|
||||
print("done")
|
||||
|
||||
```
|
||||
```````````
|
||||
You love to see it.
|
||||
"#);
|
||||
}
|
||||
@@ -849,24 +875,9 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r#"
|
||||
Check out this great example code ::
|
||||
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
print(x_y)
|
||||
else:
|
||||
print("too short :(")
|
||||
|
||||
print("done")
|
||||
|
||||
You love to see it.
|
||||
"#);
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Check out this great example code :
|
||||
```python
|
||||
Check out this great example code
|
||||
```````````python
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
@@ -876,7 +887,7 @@ mod tests {
|
||||
|
||||
print("done")
|
||||
|
||||
```
|
||||
```````````
|
||||
You love to see it.
|
||||
"#);
|
||||
}
|
||||
@@ -903,26 +914,10 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r#"
|
||||
Check out this great example code
|
||||
::
|
||||
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
print(x_y)
|
||||
else:
|
||||
print("too short :(")
|
||||
|
||||
print("done")
|
||||
|
||||
You love to see it.
|
||||
"#);
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Check out this great example code
|
||||
|
||||
```python
|
||||
```````````python
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
@@ -932,7 +927,7 @@ mod tests {
|
||||
|
||||
print("done")
|
||||
|
||||
```
|
||||
```````````
|
||||
You love to see it.
|
||||
"#);
|
||||
}
|
||||
@@ -956,22 +951,9 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r#"
|
||||
Check out this great example code::
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
print(x_y)
|
||||
else:
|
||||
print("too short :(")
|
||||
|
||||
print("done")
|
||||
You love to see it.
|
||||
"#);
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Check out this great example code:
|
||||
```python
|
||||
```````````python
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
@@ -980,7 +962,7 @@ mod tests {
|
||||
print("too short :(")
|
||||
|
||||
print("done")
|
||||
```
|
||||
```````````
|
||||
You love to see it.
|
||||
"#);
|
||||
}
|
||||
@@ -1003,22 +985,9 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r#"
|
||||
Check out this great example code::
|
||||
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
print(x_y)
|
||||
else:
|
||||
print("too short :(")
|
||||
|
||||
print("done")
|
||||
"#);
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Check out this great example code:
|
||||
```python
|
||||
```````````python
|
||||
x_y = "hello"
|
||||
|
||||
if len(x_y) > 4:
|
||||
@@ -1027,7 +996,224 @@ mod tests {
|
||||
print("too short :(")
|
||||
|
||||
print("done")
|
||||
```
|
||||
```````````
|
||||
"#);
|
||||
}
|
||||
|
||||
// `warning` and several other directives are special languages that should actually
|
||||
// still be shown as text and not ```code```.
|
||||
#[test]
|
||||
fn warning_block() {
|
||||
let docstring = r#"
|
||||
The thing you need to understand is that computers are hard.
|
||||
|
||||
.. warning::
|
||||
Now listen here buckaroo you might have seen me say computers are hard,
|
||||
and though "yeah I know computers are hard but NO you DON'T KNOW.
|
||||
|
||||
Listen:
|
||||
|
||||
- Computers
|
||||
- Are
|
||||
- Hard
|
||||
|
||||
Ok!?!?!?
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
The thing you need to understand is that computers are hard.
|
||||
|
||||
**warning:**
|
||||
Now listen here buckaroo you might have seen me say computers are hard,
|
||||
and though "yeah I know computers are hard but NO you DON'T KNOW.
|
||||
|
||||
Listen:
|
||||
|
||||
- Computers
|
||||
- Are
|
||||
- Hard
|
||||
|
||||
Ok!?!?!?
|
||||
"#);
|
||||
}
|
||||
|
||||
// `warning` and several other directives are special languages that should actually
|
||||
// still be shown as text and not ```code```.
|
||||
#[test]
|
||||
fn version_blocks() {
|
||||
let docstring = r#"
|
||||
Some much-updated docs
|
||||
|
||||
.. version-added:: 3.0
|
||||
Function added
|
||||
|
||||
.. version-changed:: 4.0
|
||||
The `spam` argument was added
|
||||
.. version-changed:: 4.1
|
||||
The `spam` argument is considered evil now.
|
||||
|
||||
You really shouldnt use it
|
||||
|
||||
And that's the docs
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
Some much-updated docs
|
||||
|
||||
**version-added:** *3.0*
|
||||
Function added
|
||||
|
||||
**version-changed:** *4.0*
|
||||
The `spam` argument was added
|
||||
**version-changed:** *4.1*
|
||||
The `spam` argument is considered evil now.
|
||||
|
||||
You really shouldnt use it
|
||||
|
||||
And that's the docs
|
||||
");
|
||||
}
|
||||
|
||||
// I don't know if this is valid syntax but we preserve stuff before non-code blocks like
|
||||
// `..deprecated ::`
|
||||
#[test]
|
||||
fn deprecated_prefix_gunk() {
|
||||
let docstring = r#"
|
||||
wow this is some changes .. deprecated:: 1.2.3
|
||||
x = 2
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
**wow this is some changes deprecated:** *1.2.3*
|
||||
x = 2
|
||||
");
|
||||
}
|
||||
|
||||
// `.. code::` is a literal block and the `.. code::` should be deleted
|
||||
#[test]
|
||||
fn code_block() {
|
||||
let docstring = r#"
|
||||
Here's some code!
|
||||
|
||||
.. code::
|
||||
def main() {
|
||||
print("hello world!")
|
||||
}
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Here's some code!
|
||||
|
||||
|
||||
```````````python
|
||||
def main() {
|
||||
print("hello world!")
|
||||
}
|
||||
```````````
|
||||
"#);
|
||||
}
|
||||
|
||||
// `.. code:: rust` is a literal block with rust syntax highlighting
|
||||
#[test]
|
||||
fn code_block_lang() {
|
||||
let docstring = r#"
|
||||
Here's some Rust code!
|
||||
|
||||
.. code:: rust
|
||||
fn main() {
|
||||
println!("hello world!");
|
||||
}
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Here's some Rust code!
|
||||
|
||||
|
||||
```````````rust
|
||||
fn main() {
|
||||
println!("hello world!");
|
||||
}
|
||||
```````````
|
||||
"#);
|
||||
}
|
||||
|
||||
// I don't know if this is valid syntax but we preserve stuff before `..code ::`
|
||||
#[test]
|
||||
fn code_block_prefix_gunk() {
|
||||
let docstring = r#"
|
||||
wow this is some code.. code:: abc
|
||||
x = 2
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
wow this is some code
|
||||
```````````abc
|
||||
x = 2
|
||||
```````````
|
||||
");
|
||||
}
|
||||
|
||||
// `.. asdgfhjkl-unknown::` is treated the same as `.. code::`
|
||||
#[test]
|
||||
fn unknown_block() {
|
||||
let docstring = r#"
|
||||
Here's some code!
|
||||
|
||||
.. asdgfhjkl-unknown::
|
||||
fn main() {
|
||||
println!("hello world!");
|
||||
}
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Here's some code!
|
||||
|
||||
|
||||
```````````python
|
||||
fn main() {
|
||||
println!("hello world!");
|
||||
}
|
||||
```````````
|
||||
"#);
|
||||
}
|
||||
|
||||
// `.. asdgfhjkl-unknown:: rust` is treated the same as `.. code:: rust`
|
||||
#[test]
|
||||
fn unknown_block_lang() {
|
||||
let docstring = r#"
|
||||
Here's some Rust code!
|
||||
|
||||
.. asdgfhjkl-unknown:: rust
|
||||
fn main() {
|
||||
print("hello world!")
|
||||
}
|
||||
"#;
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r#"
|
||||
Here's some Rust code!
|
||||
|
||||
|
||||
```````````rust
|
||||
fn main() {
|
||||
print("hello world!")
|
||||
}
|
||||
```````````
|
||||
"#);
|
||||
}
|
||||
|
||||
@@ -1047,26 +1233,15 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r"
|
||||
This is a function description
|
||||
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
|
||||
As you can see it did the thing!
|
||||
");
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
This is a function description
|
||||
|
||||
```python
|
||||
```````````python
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
```
|
||||
```````````
|
||||
As you can see it did the thing!
|
||||
");
|
||||
}
|
||||
@@ -1087,26 +1262,15 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r"
|
||||
This is a function description
|
||||
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
|
||||
As you can see it did the thing!
|
||||
");
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
This is a function description
|
||||
|
||||
```python
|
||||
```````````python
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
```
|
||||
```````````
|
||||
As you can see it did the thing!
|
||||
");
|
||||
}
|
||||
@@ -1121,20 +1285,13 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r"
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
");
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
```python
|
||||
```````````python
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
```
|
||||
```````````
|
||||
");
|
||||
}
|
||||
|
||||
@@ -1154,26 +1311,15 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r"
|
||||
This is a function description::
|
||||
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
|
||||
As you can see it did the thing!
|
||||
");
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
This is a function description:
|
||||
```python
|
||||
```````````python
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
|
||||
```
|
||||
```````````
|
||||
As you can see it did the thing!
|
||||
");
|
||||
}
|
||||
@@ -1189,22 +1335,14 @@ mod tests {
|
||||
|
||||
let docstring = Docstring::new(docstring.to_owned());
|
||||
|
||||
assert_snapshot!(docstring.render_plaintext(), @r"
|
||||
And so you can see that
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
");
|
||||
|
||||
assert_snapshot!(docstring.render_markdown(), @r"
|
||||
And so you can see that
|
||||
```python
|
||||
```````````python
|
||||
>>> thing.do_thing()
|
||||
wow it did the thing
|
||||
>>> thing.do_other_thing()
|
||||
it sure did the thing
|
||||
```
|
||||
```````````
|
||||
");
|
||||
}
|
||||
|
||||
@@ -1383,14 +1521,14 @@ mod tests {
|
||||
This is a continuation of param2 description.
|
||||
'param3' -- A parameter without type annotation
|
||||
|
||||
```python
|
||||
```````````python
|
||||
>>> print repr(foo.__doc__)
|
||||
'\n This is the second line of the docstring.\n '
|
||||
>>> foo.__doc__.splitlines()
|
||||
['', ' This is the second line of the docstring.', ' ']
|
||||
>>> trim(foo.__doc__)
|
||||
'This is the second line of the docstring.'
|
||||
```
|
||||
```````````
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use ty_python_semantic::SemanticModel;
|
||||
|
||||
/// Find all references to a symbol at the given position.
|
||||
/// Search for references across all files in the project.
|
||||
pub fn goto_references(
|
||||
pub fn find_references(
|
||||
db: &dyn Db,
|
||||
file: File,
|
||||
offset: TextSize,
|
||||
@@ -41,7 +41,7 @@ mod tests {
|
||||
impl CursorTest {
|
||||
fn references(&self) -> String {
|
||||
let Some(mut reference_results) =
|
||||
goto_references(&self.db, self.cursor.file, self.cursor.offset, true)
|
||||
find_references(&self.db, self.cursor.file, self.cursor.offset, true)
|
||||
else {
|
||||
return "No references found".to_string();
|
||||
};
|
||||
@@ -84,7 +84,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parameter_references_in_function() {
|
||||
fn parameter_references_in_function() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def calculate_sum(<CURSOR>value: int) -> int:
|
||||
@@ -149,28 +149,28 @@ result = calculate_sum(value=42)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonlocal_variable_references() {
|
||||
fn nonlocal_variable_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def outer_function():
|
||||
coun<CURSOR>ter = 0
|
||||
|
||||
|
||||
def increment():
|
||||
nonlocal counter
|
||||
counter += 1
|
||||
return counter
|
||||
|
||||
|
||||
def decrement():
|
||||
nonlocal counter
|
||||
counter -= 1
|
||||
return counter
|
||||
|
||||
|
||||
# Use counter in outer scope
|
||||
initial = counter
|
||||
increment()
|
||||
decrement()
|
||||
final = counter
|
||||
|
||||
|
||||
return increment, decrement
|
||||
",
|
||||
);
|
||||
@@ -272,7 +272,7 @@ def outer_function():
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_variable_references() {
|
||||
fn global_variable_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
glo<CURSOR>bal_counter = 0
|
||||
@@ -389,7 +389,7 @@ final_value = global_counter
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_except_handler_variable_references() {
|
||||
fn except_handler_variable_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
try:
|
||||
@@ -450,7 +450,7 @@ except ValueError as err:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_match_as_references() {
|
||||
fn pattern_match_as_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
match x:
|
||||
@@ -498,7 +498,7 @@ match x:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pattern_match_mapping_rest_references() {
|
||||
fn pattern_match_mapping_rest_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
match data:
|
||||
@@ -553,7 +553,7 @@ match data:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_function_definition_references() {
|
||||
fn function_definition_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
def my_func<CURSOR>tion():
|
||||
@@ -632,7 +632,7 @@ value = my_function
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_class_definition_references() {
|
||||
fn class_definition_references() {
|
||||
let test = cursor_test(
|
||||
"
|
||||
class My<CURSOR>Class:
|
||||
@@ -899,7 +899,553 @@ cls = MyClass
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_file_function_references() {
|
||||
fn references_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:11:17
|
||||
|
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
11 | x = ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:11:17
|
||||
|
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
11 | x = ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r#"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Click:
|
||||
| ^^^^^
|
||||
3 | __match_args__ = ("position", "button")
|
||||
4 | def __init__(self, pos, btn):
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:8:20
|
||||
|
|
||||
6 | self.button: str = btn
|
||||
7 |
|
||||
8 | def my_func(event: Click):
|
||||
| ^^^^^
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:10:14
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^^^^
|
||||
11 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @"No references found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:46
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:46
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:3:53
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:3:53
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:38
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:50
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.references(), @r"
|
||||
info[references]: Reference 1
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 2
|
||||
--> main.py:2:38
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
|
||||
info[references]: Reference 3
|
||||
--> main.py:2:50
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_file_function_references() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"utils.py",
|
||||
@@ -925,7 +1471,7 @@ from utils import func
|
||||
class DataProcessor:
|
||||
def __init__(self):
|
||||
self.multiplier = func
|
||||
|
||||
|
||||
def process(self, value):
|
||||
return func(value)
|
||||
",
|
||||
@@ -989,14 +1535,14 @@ class DataProcessor:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_file_class_attribute_references() {
|
||||
fn multi_file_class_attribute_references() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"models.py",
|
||||
"
|
||||
class MyModel:
|
||||
a<CURSOR>ttr = 42
|
||||
|
||||
|
||||
def get_attribute(self):
|
||||
return MyModel.attr
|
||||
",
|
||||
@@ -1067,7 +1613,7 @@ def process_model():
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_alias_references_should_not_resolve_to_original() {
|
||||
fn import_alias_references_should_not_resolve_to_original() {
|
||||
let test = CursorTest::builder()
|
||||
.source(
|
||||
"original.py",
|
||||
@@ -1110,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
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
@@ -212,16 +212,9 @@ pub(crate) enum GotoTarget<'a> {
|
||||
|
||||
/// The resolved definitions for a `GotoTarget`
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum DefinitionsOrTargets<'db> {
|
||||
/// We computed actual Definitions we can do followup queries on.
|
||||
Definitions(Vec<ResolvedDefinition<'db>>),
|
||||
/// We directly computed a navigation.
|
||||
///
|
||||
/// We can't get docs or usefully compute goto-definition for this.
|
||||
Targets(crate::NavigationTargets),
|
||||
}
|
||||
pub(crate) struct Definitions<'db>(pub Vec<ResolvedDefinition<'db>>);
|
||||
|
||||
impl<'db> DefinitionsOrTargets<'db> {
|
||||
impl<'db> Definitions<'db> {
|
||||
pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option<Self> {
|
||||
let ty_def = ty.definition(db)?;
|
||||
let resolved = match ty_def {
|
||||
@@ -237,7 +230,7 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
ResolvedDefinition::Definition(definition)
|
||||
}
|
||||
};
|
||||
Some(DefinitionsOrTargets::Definitions(vec![resolved]))
|
||||
Some(Definitions(vec![resolved]))
|
||||
}
|
||||
|
||||
/// Get the "goto-declaration" interpretation of this definition
|
||||
@@ -247,12 +240,7 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
self,
|
||||
db: &'db dyn ty_python_semantic::Db,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
match self {
|
||||
DefinitionsOrTargets::Definitions(definitions) => {
|
||||
definitions_to_navigation_targets(db, None, definitions)
|
||||
}
|
||||
DefinitionsOrTargets::Targets(targets) => Some(targets),
|
||||
}
|
||||
definitions_to_navigation_targets(db, None, self.0)
|
||||
}
|
||||
|
||||
/// Get the "goto-definition" interpretation of this definition
|
||||
@@ -263,12 +251,7 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
self,
|
||||
db: &'db dyn ty_python_semantic::Db,
|
||||
) -> Option<crate::NavigationTargets> {
|
||||
match self {
|
||||
DefinitionsOrTargets::Definitions(definitions) => {
|
||||
definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), definitions)
|
||||
}
|
||||
DefinitionsOrTargets::Targets(targets) => Some(targets),
|
||||
}
|
||||
definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), self.0)
|
||||
}
|
||||
|
||||
/// Get the docstring for this definition
|
||||
@@ -277,13 +260,7 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
/// so this will check both the goto-declarations and goto-definitions (in that order)
|
||||
/// and return the first one found.
|
||||
pub(crate) fn docstring(self, db: &'db dyn crate::Db) -> Option<Docstring> {
|
||||
let definitions = match self {
|
||||
DefinitionsOrTargets::Definitions(definitions) => definitions,
|
||||
// Can't find docs for these
|
||||
// (make more cases DefinitionOrTargets::Definitions to get more docs!)
|
||||
DefinitionsOrTargets::Targets(_) => return None,
|
||||
};
|
||||
for definition in &definitions {
|
||||
for definition in &self.0 {
|
||||
// If we got a docstring from the original definition, use it
|
||||
if let Some(docstring) = definition.docstring(db) {
|
||||
return Some(Docstring::new(docstring));
|
||||
@@ -296,7 +273,7 @@ impl<'db> DefinitionsOrTargets<'db> {
|
||||
let stub_mapper = StubMapper::new(db);
|
||||
|
||||
// Try to find the corresponding implementation definition
|
||||
for definition in stub_mapper.map_definitions(definitions) {
|
||||
for definition in stub_mapper.map_definitions(self.0) {
|
||||
if let Some(docstring) = definition.docstring(db) {
|
||||
return Some(Docstring::new(docstring));
|
||||
}
|
||||
@@ -399,37 +376,39 @@ impl GotoTarget<'_> {
|
||||
&self,
|
||||
model: &SemanticModel<'db>,
|
||||
alias_resolution: ImportAliasResolution,
|
||||
) -> Option<DefinitionsOrTargets<'db>> {
|
||||
use crate::NavigationTarget;
|
||||
match self {
|
||||
GotoTarget::Expression(expression) => definitions_for_expression(model, *expression)
|
||||
.map(DefinitionsOrTargets::Definitions),
|
||||
) -> Option<Definitions<'db>> {
|
||||
let definitions = match self {
|
||||
GotoTarget::Expression(expression) => definitions_for_expression(model, *expression),
|
||||
// For already-defined symbols, they are their own definitions
|
||||
GotoTarget::FunctionDef(function) => Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(function.definition(model)),
|
||||
])),
|
||||
GotoTarget::FunctionDef(function) => Some(vec![ResolvedDefinition::Definition(
|
||||
function.definition(model),
|
||||
)]),
|
||||
|
||||
GotoTarget::ClassDef(class) => Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(class.definition(model)),
|
||||
])),
|
||||
GotoTarget::ClassDef(class) => Some(vec![ResolvedDefinition::Definition(
|
||||
class.definition(model),
|
||||
)]),
|
||||
|
||||
GotoTarget::Parameter(parameter) => Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(parameter.definition(model)),
|
||||
])),
|
||||
GotoTarget::Parameter(parameter) => Some(vec![ResolvedDefinition::Definition(
|
||||
parameter.definition(model),
|
||||
)]),
|
||||
|
||||
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
|
||||
GotoTarget::ImportSymbolAlias {
|
||||
alias, import_from, ..
|
||||
} => {
|
||||
let symbol_name = alias.name.as_str();
|
||||
Some(DefinitionsOrTargets::Definitions(
|
||||
definitions_for_imported_symbol(
|
||||
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 {
|
||||
@@ -445,17 +424,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 {
|
||||
let alias_range = alias.asname.as_ref().unwrap().range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget {
|
||||
file: model.file(),
|
||||
focus_range: alias_range,
|
||||
full_range: alias.range(),
|
||||
}),
|
||||
))
|
||||
definitions_for_module(model, Some(alias.name.as_str()), 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,45 +437,44 @@ impl GotoTarget<'_> {
|
||||
GotoTarget::KeywordArgument {
|
||||
keyword,
|
||||
call_expression,
|
||||
} => Some(DefinitionsOrTargets::Definitions(
|
||||
definitions_for_keyword_argument(model, keyword, call_expression),
|
||||
} => Some(definitions_for_keyword_argument(
|
||||
model,
|
||||
keyword,
|
||||
call_expression,
|
||||
)),
|
||||
|
||||
// For exception variables, they are their own definitions (like parameters)
|
||||
GotoTarget::ExceptVariable(except_handler) => {
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Definition(except_handler.definition(model)),
|
||||
]))
|
||||
Some(vec![ResolvedDefinition::Definition(
|
||||
except_handler.definition(model),
|
||||
)])
|
||||
}
|
||||
|
||||
// For pattern match rest variables, they are their own definitions
|
||||
// Patterns are glorified assignments but we have to look them up by ident
|
||||
// because they're not expressions
|
||||
GotoTarget::PatternMatchRest(pattern_mapping) => {
|
||||
if let Some(rest_name) = &pattern_mapping.rest {
|
||||
let range = rest_name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget::new(
|
||||
model.file(),
|
||||
range,
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
pattern_mapping.rest.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
})
|
||||
}
|
||||
|
||||
// For pattern match as names, they are their own definitions
|
||||
GotoTarget::PatternMatchAsName(pattern_as) => {
|
||||
if let Some(name) = &pattern_as.name {
|
||||
let range = name.range;
|
||||
Some(DefinitionsOrTargets::Targets(
|
||||
crate::NavigationTargets::single(NavigationTarget::new(
|
||||
model.file(),
|
||||
range,
|
||||
)),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
GotoTarget::PatternMatchAsName(pattern_as) => pattern_as.name.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
}),
|
||||
|
||||
GotoTarget::PatternKeywordArgument(pattern_keyword) => {
|
||||
let name = &pattern_keyword.attr;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
|
||||
GotoTarget::PatternMatchStarName(pattern_star) => {
|
||||
pattern_star.name.as_ref().map(|name| {
|
||||
definitions_for_name(model, name.as_str(), AnyNodeRef::Identifier(name))
|
||||
})
|
||||
}
|
||||
|
||||
// For callables, both the definition of the callable and the actual function impl are relevant.
|
||||
@@ -516,7 +489,7 @@ impl GotoTarget<'_> {
|
||||
if definitions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(DefinitionsOrTargets::Definitions(definitions))
|
||||
Some(definitions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,14 +497,14 @@ impl GotoTarget<'_> {
|
||||
let (definitions, _) =
|
||||
ty_python_semantic::definitions_for_bin_op(model, expression)?;
|
||||
|
||||
Some(DefinitionsOrTargets::Definitions(definitions))
|
||||
Some(definitions)
|
||||
}
|
||||
|
||||
GotoTarget::UnaryOp { expression, .. } => {
|
||||
let (definitions, _) =
|
||||
ty_python_semantic::definitions_for_unary_op(model, expression)?;
|
||||
|
||||
Some(DefinitionsOrTargets::Definitions(definitions))
|
||||
Some(definitions)
|
||||
}
|
||||
|
||||
// String annotations sub-expressions require us to recurse into the sub-AST
|
||||
@@ -545,23 +518,47 @@ impl GotoTarget<'_> {
|
||||
.node()
|
||||
.as_expr_ref()?;
|
||||
definitions_for_expression(&submodel, subexpr)
|
||||
.map(DefinitionsOrTargets::Definitions)
|
||||
}
|
||||
|
||||
// nonlocal and global are essentially loads, but again they're statements,
|
||||
// so we need to look them up by ident
|
||||
GotoTarget::NonLocal { identifier } | GotoTarget::Globals { identifier } => {
|
||||
Some(DefinitionsOrTargets::Definitions(definitions_for_name(
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
identifier.as_str(),
|
||||
AnyNodeRef::Identifier(identifier),
|
||||
)))
|
||||
))
|
||||
}
|
||||
|
||||
// TODO: implement these
|
||||
GotoTarget::PatternKeywordArgument(..)
|
||||
| GotoTarget::PatternMatchStarName(..)
|
||||
| GotoTarget::TypeParamTypeVarName(..)
|
||||
| GotoTarget::TypeParamParamSpecName(..)
|
||||
| GotoTarget::TypeParamTypeVarTupleName(..) => None,
|
||||
}
|
||||
// These are declarations of sorts, but they're stmts and not exprs, so look up by ident.
|
||||
GotoTarget::TypeParamTypeVarName(type_var) => {
|
||||
let name = &type_var.name;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
|
||||
GotoTarget::TypeParamParamSpecName(name) => {
|
||||
let name = &name.name;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
|
||||
GotoTarget::TypeParamTypeVarTupleName(name) => {
|
||||
let name = &name.name;
|
||||
Some(definitions_for_name(
|
||||
model,
|
||||
name.as_str(),
|
||||
AnyNodeRef::Identifier(name),
|
||||
))
|
||||
}
|
||||
};
|
||||
definitions.map(Definitions)
|
||||
}
|
||||
|
||||
/// Returns the text representation of this goto target.
|
||||
@@ -1050,12 +1047,10 @@ fn definitions_for_module<'db>(
|
||||
model: &SemanticModel<'db>,
|
||||
module: Option<&str>,
|
||||
level: u32,
|
||||
) -> Option<DefinitionsOrTargets<'db>> {
|
||||
) -> Option<Vec<ResolvedDefinition<'db>>> {
|
||||
let module = model.resolve_module(module, level)?;
|
||||
let file = module.file(model.db())?;
|
||||
Some(DefinitionsOrTargets::Definitions(vec![
|
||||
ResolvedDefinition::Module(file),
|
||||
]))
|
||||
Some(vec![ResolvedDefinition::Module(file)])
|
||||
}
|
||||
|
||||
/// Helper function to extract module component information from a dotted module name
|
||||
|
||||
@@ -1397,6 +1397,486 @@ def function():
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
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]
|
||||
fn goto_declaration_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:4:37
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
5 | x = ab
|
||||
| ^^
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
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]
|
||||
fn goto_declaration_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
|
|
||||
info: Source
|
||||
--> main.py:11:17
|
||||
|
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
11 | x = ab
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r#"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Click:
|
||||
| ^^^^^
|
||||
3 | __match_args__ = ("position", "button")
|
||||
4 | def __init__(self, pos, btn):
|
||||
|
|
||||
info: Source
|
||||
--> main.py:10:14
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^^^^
|
||||
11 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_declaration(), @r"
|
||||
info[goto-declaration]: Declaration
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:38
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_declaration_property_getter_setter() {
|
||||
let test = cursor_test(
|
||||
|
||||
@@ -964,6 +964,282 @@ mod tests {
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r#"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Click:
|
||||
| ^^^^^
|
||||
3 | __match_args__ = ("position", "button")
|
||||
4 | def __init__(self, pos, btn):
|
||||
|
|
||||
info: Source
|
||||
--> main.py:10:14
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^^^^
|
||||
11 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @r"
|
||||
info[goto-type-definition]: Type definition
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
info: Source
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No goto target found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.goto_type_definition(), @"No type definitions found");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn goto_type_on_keyword_argument() {
|
||||
let test = cursor_test(
|
||||
|
||||
@@ -1759,6 +1759,398 @@ def function():
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```python
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
5 | x = ab
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```python
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
5 | x = ab
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r#"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```python
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:5:17
|
||||
|
|
||||
3 | match command.split():
|
||||
4 | case ["get", ("a" | "b") as ab]:
|
||||
5 | x = ab
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```python
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:11:17
|
||||
|
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
11 | x = ab
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
<class 'Click'>
|
||||
---------------------------------------------
|
||||
```python
|
||||
<class 'Click'>
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:10:14
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^-^^
|
||||
| | |
|
||||
| | Cursor offset
|
||||
| source
|
||||
11 | x = ab
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
AB@Alias1 (invariant)
|
||||
---------------------------------------------
|
||||
```python
|
||||
AB@Alias1 (invariant)
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
AB@Alias1 (invariant)
|
||||
---------------------------------------------
|
||||
```python
|
||||
AB@Alias1 (invariant)
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:37
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
(
|
||||
...
|
||||
) -> tuple[typing.ParamSpec]
|
||||
---------------------------------------------
|
||||
```python
|
||||
(
|
||||
...
|
||||
) -> tuple[typing.ParamSpec]
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:3:43
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @"Hover provided no content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.hover(), @r"
|
||||
@Todo
|
||||
---------------------------------------------
|
||||
```python
|
||||
@Todo
|
||||
```
|
||||
---------------------------------------------
|
||||
info[hover]: Hovered content is
|
||||
--> main.py:2:38
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^-
|
||||
| ||
|
||||
| |Cursor offset
|
||||
| source
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_module_import() {
|
||||
let mut test = cursor_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).
|
||||
|
||||
@@ -1946,6 +1946,131 @@ mod tests {
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_name_binding() {
|
||||
let mut test = inlay_hint_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x[: @Todo] = ab
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_rest_binding() {
|
||||
let mut test = inlay_hint_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x[: @Todo] = ab
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_as_binding() {
|
||||
let mut test = inlay_hint_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x[: @Todo] = ab
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_match_keyword_binding() {
|
||||
let mut test = inlay_hint_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x[: @Todo] = ab
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_typevar_name_binding() {
|
||||
let mut test = inlay_hint_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @"type Alias1[AB: int = bool] = tuple[AB, list[AB]]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_typevar_spec_binding() {
|
||||
let mut test = inlay_hint_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_typevar_tuple_binding() {
|
||||
let mut test = inlay_hint_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @"type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_many_literals() {
|
||||
let mut test = inlay_hint_test(
|
||||
|
||||
@@ -9,10 +9,10 @@ mod doc_highlights;
|
||||
mod docstring;
|
||||
mod document_symbols;
|
||||
mod find_node;
|
||||
mod find_references;
|
||||
mod goto;
|
||||
mod goto_declaration;
|
||||
mod goto_definition;
|
||||
mod goto_references;
|
||||
mod goto_type_definition;
|
||||
mod hover;
|
||||
mod importer;
|
||||
@@ -32,8 +32,8 @@ pub use code_action::{QuickFix, code_actions};
|
||||
pub use completion::{Completion, CompletionKind, CompletionSettings, completion};
|
||||
pub use doc_highlights::document_highlights;
|
||||
pub use document_symbols::document_symbols;
|
||||
pub use find_references::find_references;
|
||||
pub use goto::{goto_declaration, goto_definition, goto_type_definition};
|
||||
pub use goto_references::goto_references;
|
||||
pub use hover::hover;
|
||||
pub use inlay_hints::{
|
||||
InlayHintKind, InlayHintLabel, InlayHintSettings, InlayHintTextEdit, inlay_hints,
|
||||
|
||||
@@ -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,
|
||||
@@ -219,6 +218,11 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
|
||||
self.check_identifier_reference(name);
|
||||
}
|
||||
}
|
||||
AnyNodeRef::PatternMatchStar(pattern_star) if self.should_include_declaration() => {
|
||||
if let Some(name) = &pattern_star.name {
|
||||
self.check_identifier_reference(name);
|
||||
}
|
||||
}
|
||||
AnyNodeRef::PatternMatchMapping(pattern_mapping)
|
||||
if self.should_include_declaration() =>
|
||||
{
|
||||
@@ -226,6 +230,15 @@ impl<'a> SourceOrderVisitor<'a> for LocalReferencesFinder<'a> {
|
||||
self.check_identifier_reference(rest_name);
|
||||
}
|
||||
}
|
||||
AnyNodeRef::TypeParamParamSpec(param_spec) if self.should_include_declaration() => {
|
||||
self.check_identifier_reference(¶m_spec.name);
|
||||
}
|
||||
AnyNodeRef::TypeParamTypeVarTuple(param_tuple) if self.should_include_declaration() => {
|
||||
self.check_identifier_reference(¶m_tuple.name);
|
||||
}
|
||||
AnyNodeRef::TypeParamTypeVar(param_var) if self.should_include_declaration() => {
|
||||
self.check_identifier_reference(¶m_var.name);
|
||||
}
|
||||
AnyNodeRef::ExprStringLiteral(string_expr) if self.should_include_declaration() => {
|
||||
// Highlight the sub-AST of a string annotation
|
||||
if let Some((sub_ast, sub_model)) = self.model.enter_string_annotation(string_expr)
|
||||
@@ -304,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
|
||||
@@ -323,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)
|
||||
",
|
||||
@@ -496,7 +496,391 @@ class DataProcessor:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_rename_import_module_component() {
|
||||
fn rename_match_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:22
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_rest_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_rest_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", *ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:4:23
|
||||
|
|
||||
2 | def my_func(command: str):
|
||||
3 | match command.split():
|
||||
4 | case ["get", *ab]:
|
||||
| ^^
|
||||
5 | x = ab
|
||||
| --
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_as_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as a<CURSOR>b]:
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> 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]
|
||||
fn rename_match_as_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
def my_func(command: str):
|
||||
match command.split():
|
||||
case ["get", ("a" | "b") as ab]:
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> 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]
|
||||
fn rename_match_keyword_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
| --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_keyword_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = a<CURSOR>b
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 2 locations)
|
||||
--> main.py:10:30
|
||||
|
|
||||
8 | def my_func(event: Click):
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| ^^
|
||||
11 | x = ab
|
||||
| --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_class_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r#"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Click:
|
||||
| ^^^^^
|
||||
3 | __match_args__ = ("position", "button")
|
||||
4 | def __init__(self, pos, btn):
|
||||
|
|
||||
::: main.py:8:20
|
||||
|
|
||||
6 | self.button: str = btn
|
||||
7 |
|
||||
8 | def my_func(event: Click):
|
||||
| -----
|
||||
9 | match event:
|
||||
10 | case Click(x, button=ab):
|
||||
| -----
|
||||
11 | x = ab
|
||||
|
|
||||
"#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_match_class_field_name() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
class Click:
|
||||
__match_args__ = ("position", "button")
|
||||
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):
|
||||
x = ab
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @"Cannot rename");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_name_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[A<CURSOR>B: int = bool] = tuple[AB, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_name_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias1[AB: int = bool] = tuple[A<CURSOR>B, list[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:13
|
||||
|
|
||||
2 | type Alias1[AB: int = bool] = tuple[AB, list[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_spec_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**A<CURSOR>B = [int, str]] = Callable[AB, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_spec_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
from typing import Callable
|
||||
type Alias2[**AB = [int, str]] = Callable[A<CURSOR>B, tuple[AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:3:15
|
||||
|
|
||||
2 | from typing import Callable
|
||||
3 | type Alias2[**AB = [int, str]] = Callable[AB, tuple[AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_tuple_stmt() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*A<CURSOR>B = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rename_typevar_tuple_binding() {
|
||||
let test = cursor_test(
|
||||
r#"
|
||||
type Alias3[*AB = ()] = tuple[tuple[*A<CURSOR>B], tuple[*AB]]
|
||||
"#,
|
||||
);
|
||||
|
||||
assert_snapshot!(test.rename("XY"), @r"
|
||||
info[rename]: Rename symbol (found 3 locations)
|
||||
--> main.py:2:14
|
||||
|
|
||||
2 | type Alias3[*AB = ()] = tuple[tuple[*AB], tuple[*AB]]
|
||||
| ^^ -- --
|
||||
|
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_rename_import_module_component() {
|
||||
// Test that we cannot rename parts of module names in import statements
|
||||
let test = cursor_test(
|
||||
"
|
||||
@@ -509,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(
|
||||
"
|
||||
@@ -522,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(
|
||||
@@ -536,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",
|
||||
@@ -547,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();
|
||||
@@ -557,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(
|
||||
@@ -578,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();
|
||||
@@ -588,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
|
||||
@@ -665,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",
|
||||
@@ -717,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(
|
||||
"
|
||||
@@ -732,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(
|
||||
"
|
||||
@@ -745,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(
|
||||
"
|
||||
@@ -772,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(
|
||||
"
|
||||
@@ -797,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
|
||||
|
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1060,6 +1060,16 @@ impl SourceOrderVisitor<'_> for SemanticTokenVisitor<'_> {
|
||||
);
|
||||
}
|
||||
}
|
||||
ast::Pattern::MatchStar(pattern_star) => {
|
||||
// Just the one ident here
|
||||
if let Some(rest_name) = &pattern_star.name {
|
||||
self.add_token(
|
||||
rest_name.range(),
|
||||
SemanticTokenType::Variable,
|
||||
SemanticTokenModifier::empty(),
|
||||
);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// For all other pattern types, use the default walker
|
||||
ruff_python_ast::visitor::source_order::walk_pattern(self, pattern);
|
||||
@@ -2485,6 +2495,7 @@ def process_data(data):
|
||||
"rest" @ 154..158: Variable
|
||||
"person" @ 181..187: Variable
|
||||
"first" @ 202..207: Variable
|
||||
"remaining" @ 210..219: Variable
|
||||
"sequence" @ 224..232: Variable
|
||||
"print" @ 246..251: Function
|
||||
"First: " @ 254..261: String
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! and overloads.
|
||||
|
||||
use crate::docstring::Docstring;
|
||||
use crate::goto::DefinitionsOrTargets;
|
||||
use crate::goto::Definitions;
|
||||
use crate::{Db, find_node::covering_node};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
@@ -214,8 +214,7 @@ fn get_callable_documentation(
|
||||
db: &dyn crate::Db,
|
||||
definition: Option<Definition>,
|
||||
) -> Option<Docstring> {
|
||||
DefinitionsOrTargets::Definitions(vec![ResolvedDefinition::Definition(definition?)])
|
||||
.docstring(db)
|
||||
Definitions(vec![ResolvedDefinition::Definition(definition?)]).docstring(db)
|
||||
}
|
||||
|
||||
/// Create `ParameterDetails` objects from parameter label offsets.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -232,6 +232,32 @@ class C:
|
||||
reveal_type(not_a_method) # revealed: def not_a_method(self) -> Unknown
|
||||
```
|
||||
|
||||
## Different occurrences of `Self` represent different types
|
||||
|
||||
Here, both `Foo.foo` and `Bar.bar` use `Self`. When accessing a bound method, we replace any
|
||||
occurrences of `Self` with the bound `self` type. In this example, when we access `x.foo`, we only
|
||||
want to substitute the occurrences of `Self` in `Foo.foo` — that is, occurrences of `Self@foo`. The
|
||||
fact that `x` is an instance of `Foo[Self@bar]` (a completely different `Self` type) should not
|
||||
affect that subtitution. If we blindly substitute all occurrences of `Self`, we would get
|
||||
`Foo[Self@bar]` as the return type of the bound method.
|
||||
|
||||
```py
|
||||
from typing import Self
|
||||
|
||||
class Foo[T]:
|
||||
def foo(self: Self) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
class Bar:
|
||||
def bar(self: Self, x: Foo[Self]):
|
||||
# revealed: bound method Foo[Self@bar].foo() -> Self@bar
|
||||
reveal_type(x.foo)
|
||||
|
||||
def f[U: Bar](x: Foo[U]):
|
||||
# revealed: bound method Foo[U@f].foo() -> U@f
|
||||
reveal_type(x.foo)
|
||||
```
|
||||
|
||||
## typing_extensions
|
||||
|
||||
```toml
|
||||
@@ -260,15 +286,13 @@ class Shape:
|
||||
|
||||
@classmethod
|
||||
def bar(cls: type[Self]) -> Self:
|
||||
# TODO: type[Shape]
|
||||
reveal_type(cls) # revealed: @Todo(unsupported type[X] special form)
|
||||
reveal_type(cls) # revealed: type[Self@bar]
|
||||
return cls()
|
||||
|
||||
class Circle(Shape): ...
|
||||
|
||||
reveal_type(Shape().foo()) # revealed: Shape
|
||||
# TODO: Shape
|
||||
reveal_type(Shape.bar()) # revealed: Unknown
|
||||
reveal_type(Shape.bar()) # revealed: Shape
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
@@ -61,8 +61,7 @@ async def main():
|
||||
|
||||
result = await task
|
||||
|
||||
# TODO: this should be `int`
|
||||
reveal_type(result) # revealed: Unknown
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
### `asyncio.gather`
|
||||
@@ -79,9 +78,8 @@ async def main():
|
||||
task("B"),
|
||||
)
|
||||
|
||||
# TODO: these should be `int`
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
## Under the hood
|
||||
|
||||
@@ -2650,7 +2650,7 @@ reveal_type(C().x) # revealed: int
|
||||
```py
|
||||
import enum
|
||||
|
||||
reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Unknown]
|
||||
reveal_type(enum.Enum.__members__) # revealed: MappingProxyType[str, Enum]
|
||||
|
||||
class Answer(enum.Enum):
|
||||
NO = 0
|
||||
@@ -2658,7 +2658,7 @@ class Answer(enum.Enum):
|
||||
|
||||
reveal_type(Answer.NO) # revealed: Literal[Answer.NO]
|
||||
reveal_type(Answer.NO.value) # revealed: Literal[0]
|
||||
reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Unknown]
|
||||
reveal_type(Answer.__members__) # revealed: MappingProxyType[str, Answer]
|
||||
```
|
||||
|
||||
## Divergent inferred implicit instance attribute types
|
||||
|
||||
@@ -210,9 +210,7 @@ class BuilderMeta2(type):
|
||||
) -> BuilderMeta2:
|
||||
# revealed: <super: <class 'BuilderMeta2'>, <class 'BuilderMeta2'>>
|
||||
s = reveal_type(super())
|
||||
# TODO: should be `BuilderMeta2` (needs https://github.com/astral-sh/ty/issues/501)
|
||||
# revealed: Unknown
|
||||
return reveal_type(s.__new__(cls, name, bases, dct))
|
||||
return reveal_type(s.__new__(cls, name, bases, dct)) # revealed: BuilderMeta2
|
||||
|
||||
class Foo[T]:
|
||||
x: T
|
||||
@@ -395,6 +393,14 @@ class E(Enum):
|
||||
reveal_type(super(E, E.X)) # revealed: <super: <class 'E'>, E>
|
||||
```
|
||||
|
||||
## `type[Self]`
|
||||
|
||||
```py
|
||||
class Foo:
|
||||
def method(self):
|
||||
super(self.__class__, self)
|
||||
```
|
||||
|
||||
## Descriptor Behavior with Super
|
||||
|
||||
Accessing attributes through `super` still invokes descriptor protocol. However, the behavior can
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,10 +15,8 @@ reveal_type(Color.RED) # revealed: Literal[Color.RED]
|
||||
reveal_type(Color.RED.name) # revealed: Literal["RED"]
|
||||
reveal_type(Color.RED.value) # revealed: Literal[1]
|
||||
|
||||
# TODO: Should be `Color` or `Literal[Color.RED]`
|
||||
reveal_type(Color["RED"]) # revealed: Unknown
|
||||
|
||||
# TODO: Could be `Literal[Color.RED]` to be more precise
|
||||
reveal_type(Color["RED"]) # revealed: Color
|
||||
reveal_type(Color(1)) # revealed: Color
|
||||
|
||||
reveal_type(Color.RED in Color) # revealed: bool
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Tests for the `@typing(_extensions).final` decorator
|
||||
|
||||
## Cannot subclass
|
||||
## Cannot subclass a class decorated with `@final`
|
||||
|
||||
Don't do this:
|
||||
|
||||
@@ -29,3 +29,470 @@ class H(
|
||||
G,
|
||||
): ...
|
||||
```
|
||||
|
||||
## Cannot override a method decorated with `@final`
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```pyi
|
||||
from typing_extensions import final, Callable, TypeVar
|
||||
|
||||
def lossy_decorator(fn: Callable) -> Callable: ...
|
||||
|
||||
class Parent:
|
||||
@final
|
||||
def foo(self): ...
|
||||
|
||||
@final
|
||||
@property
|
||||
def my_property1(self) -> int: ...
|
||||
|
||||
@property
|
||||
@final
|
||||
def my_property2(self) -> int: ...
|
||||
|
||||
@property
|
||||
@final
|
||||
def my_property3(self) -> int: ...
|
||||
|
||||
@final
|
||||
@classmethod
|
||||
def class_method1(cls) -> int: ...
|
||||
|
||||
@classmethod
|
||||
@final
|
||||
def class_method2(cls) -> int: ...
|
||||
|
||||
@final
|
||||
@staticmethod
|
||||
def static_method1() -> int: ...
|
||||
|
||||
@staticmethod
|
||||
@final
|
||||
def static_method2() -> int: ...
|
||||
|
||||
@lossy_decorator
|
||||
@final
|
||||
def decorated_1(self): ...
|
||||
|
||||
@final
|
||||
@lossy_decorator
|
||||
def decorated_2(self): ...
|
||||
|
||||
class Child(Parent):
|
||||
# explicitly test the concise diagnostic message,
|
||||
# which is different to the verbose diagnostic summary message:
|
||||
#
|
||||
# error: [override-of-final-method] "Cannot override final member `foo` from superclass `Parent`"
|
||||
def foo(self): ...
|
||||
@property
|
||||
def my_property1(self) -> int: ... # error: [override-of-final-method]
|
||||
|
||||
@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]
|
||||
|
||||
@staticmethod
|
||||
def static_method1() -> int: ... # error: [override-of-final-method]
|
||||
|
||||
@classmethod
|
||||
def class_method2(cls) -> int: ... # error: [override-of-final-method]
|
||||
|
||||
@staticmethod
|
||||
def static_method2() -> int: ... # error: [override-of-final-method]
|
||||
|
||||
def decorated_1(self): ... # TODO: should emit [override-of-final-method]
|
||||
|
||||
@lossy_decorator
|
||||
def decorated_2(self): ... # TODO: should emit [override-of-final-method]
|
||||
|
||||
class OtherChild(Parent): ...
|
||||
|
||||
class Grandchild(OtherChild):
|
||||
@staticmethod
|
||||
# TODO: we should emit a Liskov violation here too
|
||||
# error: [override-of-final-method]
|
||||
def foo(): ...
|
||||
@property
|
||||
# TODO: we should emit a Liskov violation here too
|
||||
# error: [override-of-final-method]
|
||||
def my_property1(self) -> str: ...
|
||||
# TODO: we should emit a Liskov violation here too
|
||||
# error: [override-of-final-method]
|
||||
class_method1 = None
|
||||
|
||||
# Diagnostic edge case: `final` is very far away from the method definition in the source code:
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def identity(x: T) -> T: ...
|
||||
|
||||
class Foo:
|
||||
@final
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
@identity
|
||||
def bar(self): ...
|
||||
|
||||
class Baz(Foo):
|
||||
def bar(self): ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
## Diagnostic edge case: superclass with `@final` method has the same name as the subclass
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`module1.py`:
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class Foo:
|
||||
@final
|
||||
def f(self): ...
|
||||
```
|
||||
|
||||
`module2.py`:
|
||||
|
||||
```py
|
||||
import module1
|
||||
|
||||
class Foo(module1.Foo):
|
||||
def f(self): ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
## Overloaded methods decorated with `@final`
|
||||
|
||||
In a stub file, `@final` should be applied to the first overload. In a runtime file, `@final` should
|
||||
only be applied to the implementation function.
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
`stub.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import final, overload
|
||||
|
||||
class Good:
|
||||
@overload
|
||||
@final
|
||||
def bar(self, x: str) -> str: ...
|
||||
@overload
|
||||
def bar(self, x: int) -> int: ...
|
||||
|
||||
@final
|
||||
@overload
|
||||
def baz(self, x: str) -> str: ...
|
||||
@overload
|
||||
def baz(self, x: int) -> int: ...
|
||||
|
||||
class ChildOfGood(Good):
|
||||
@overload
|
||||
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
|
||||
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
|
||||
|
||||
class Bad:
|
||||
@overload
|
||||
def bar(self, x: str) -> str: ...
|
||||
@overload
|
||||
@final
|
||||
# error: [invalid-overload]
|
||||
def bar(self, x: int) -> int: ...
|
||||
|
||||
@overload
|
||||
def baz(self, x: str) -> str: ...
|
||||
@final
|
||||
@overload
|
||||
# error: [invalid-overload]
|
||||
def baz(self, x: int) -> int: ...
|
||||
|
||||
class ChildOfBad(Bad):
|
||||
@overload
|
||||
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
|
||||
def baz(self, x: int) -> int: ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from typing import overload, final
|
||||
|
||||
class Good:
|
||||
@overload
|
||||
def f(self, x: str) -> str: ...
|
||||
@overload
|
||||
def f(self, x: int) -> int: ...
|
||||
@final
|
||||
def f(self, x: int | str) -> int | str:
|
||||
return x
|
||||
|
||||
class ChildOfGood(Good):
|
||||
@overload
|
||||
def f(self, x: str) -> str: ...
|
||||
@overload
|
||||
def f(self, x: int) -> int: ...
|
||||
# error: [override-of-final-method]
|
||||
def f(self, x: int | str) -> int | str:
|
||||
return x
|
||||
|
||||
class Bad:
|
||||
@overload
|
||||
@final
|
||||
def f(self, x: str) -> str: ...
|
||||
@overload
|
||||
def f(self, x: int) -> int: ...
|
||||
# error: [invalid-overload]
|
||||
def f(self, x: int | str) -> int | str:
|
||||
return x
|
||||
|
||||
@final
|
||||
@overload
|
||||
def g(self, x: str) -> str: ...
|
||||
@overload
|
||||
def g(self, x: int) -> int: ...
|
||||
# error: [invalid-overload]
|
||||
def g(self, x: int | str) -> int | str:
|
||||
return x
|
||||
|
||||
@overload
|
||||
def h(self, x: str) -> str: ...
|
||||
@overload
|
||||
@final
|
||||
def h(self, x: int) -> int: ...
|
||||
# error: [invalid-overload]
|
||||
def h(self, x: int | str) -> int | str:
|
||||
return x
|
||||
|
||||
@overload
|
||||
def i(self, x: str) -> str: ...
|
||||
@final
|
||||
@overload
|
||||
def i(self, x: int) -> int: ...
|
||||
# error: [invalid-overload]
|
||||
def i(self, x: int | str) -> int | str:
|
||||
return x
|
||||
|
||||
class ChildOfBad(Bad):
|
||||
# TODO: these should all cause us to emit Liskov violations as well
|
||||
f = None # error: [override-of-final-method]
|
||||
g = None # error: [override-of-final-method]
|
||||
h = None # error: [override-of-final-method]
|
||||
i = None # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
## Edge case: the function is decorated with `@final` but originally defined elsewhere
|
||||
|
||||
As of 2025-11-26, pyrefly emits a diagnostic on this, but mypy and pyright do not. For mypy and
|
||||
pyright to emit a diagnostic, the superclass definition decorated with `@final` must be a literal
|
||||
function definition: an assignment definition where the right-hand side of the assignment is a
|
||||
`@final-decorated` function is not sufficient for them to consider the superclass definition as
|
||||
being `@final`.
|
||||
|
||||
For now, we choose to follow mypy's and pyright's behaviour here, in order to maximise compatibility
|
||||
with other type checkers. We may decide to change this in the future, however, as it would simplify
|
||||
our implementation. Mypy's and pyright's behaviour here is also arguably inconsistent with their
|
||||
treatment of other type qualifiers such as `Final`. As discussed in
|
||||
<https://discuss.python.org/t/imported-final-variable/82429>, both type checkers view the `Final`
|
||||
type qualifier as travelling *across* scopes.
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class A:
|
||||
@final
|
||||
def method(self) -> None: ...
|
||||
|
||||
class B:
|
||||
method = A.method
|
||||
|
||||
class C(B):
|
||||
def method(self) -> None: ... # no diagnostic here (see prose discussion above)
|
||||
```
|
||||
|
||||
## Constructor methods are also checked
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class A:
|
||||
@final
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
class B(A):
|
||||
def __init__(self) -> None: ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
## Only the first `@final` violation is reported
|
||||
|
||||
(Don't do this.)
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
class A:
|
||||
@final
|
||||
def f(self): ...
|
||||
|
||||
class B(A):
|
||||
@final
|
||||
def f(self): ... # error: [override-of-final-method]
|
||||
|
||||
class C(B):
|
||||
@final
|
||||
# we only emit one error here, not two
|
||||
def f(self): ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
## For when you just really want to drive the point home
|
||||
|
||||
```py
|
||||
from typing import final, Final
|
||||
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
class A:
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
def method(self): ...
|
||||
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
@final
|
||||
class B:
|
||||
method: Final = A.method
|
||||
|
||||
class C(A): # error: [subclass-of-final-class]
|
||||
def method(self): ... # error: [override-of-final-method]
|
||||
|
||||
class D(B): # error: [subclass-of-final-class]
|
||||
# TODO: we should emit a diagnostic here
|
||||
def method(self): ...
|
||||
```
|
||||
|
||||
## An `@final` method is overridden by an implicit instance attribute
|
||||
|
||||
```py
|
||||
from typing import final, Any
|
||||
|
||||
class Parent:
|
||||
@final
|
||||
def method(self) -> None: ...
|
||||
|
||||
class Child(Parent):
|
||||
def __init__(self) -> None:
|
||||
self.method: Any = 42 # TODO: we should emit `[override-of-final-method]` here
|
||||
```
|
||||
|
||||
## A possibly-undefined `@final` method is overridden
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import final
|
||||
|
||||
def coinflip() -> bool:
|
||||
return False
|
||||
|
||||
class A:
|
||||
if coinflip():
|
||||
@final
|
||||
def method1(self) -> None: ...
|
||||
else:
|
||||
def method1(self) -> None: ...
|
||||
|
||||
if coinflip():
|
||||
def method2(self) -> None: ...
|
||||
else:
|
||||
@final
|
||||
def method2(self) -> None: ...
|
||||
|
||||
if coinflip():
|
||||
@final
|
||||
def method3(self) -> None: ...
|
||||
else:
|
||||
@final
|
||||
def method3(self) -> None: ...
|
||||
|
||||
if coinflip():
|
||||
def method4(self) -> None: ...
|
||||
elif coinflip():
|
||||
@final
|
||||
def method4(self) -> None: ...
|
||||
else:
|
||||
def method4(self) -> None: ...
|
||||
|
||||
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]
|
||||
|
||||
# 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():
|
||||
def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
else:
|
||||
pass
|
||||
|
||||
if coinflip():
|
||||
def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
|
||||
else:
|
||||
def method2(self) -> None: ... # TODO: should emit [override-of-final-method]
|
||||
|
||||
if coinflip():
|
||||
def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
def method4(self) -> None: ... # error: [override-of-final-method]
|
||||
```
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -106,7 +106,7 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
|
||||
def takes_in_type(x: type[T]) -> type[T]:
|
||||
return x
|
||||
|
||||
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
|
||||
reveal_type(takes_in_type(int)) # revealed: type[int]
|
||||
```
|
||||
|
||||
This also works when passing in arguments that are subclasses of the parameter type.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -383,8 +383,7 @@ def constrained(f: T):
|
||||
|
||||
## Meta-type
|
||||
|
||||
The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the
|
||||
meta-types of the constraints:
|
||||
The meta-type of a typevar is `type[T]`.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
@@ -392,22 +391,22 @@ from typing import TypeVar
|
||||
T_normal = TypeVar("T_normal")
|
||||
|
||||
def normal(x: T_normal):
|
||||
reveal_type(type(x)) # revealed: type
|
||||
reveal_type(type(x)) # revealed: type[T_normal@normal]
|
||||
|
||||
T_bound_object = TypeVar("T_bound_object", bound=object)
|
||||
|
||||
def bound_object(x: T_bound_object):
|
||||
reveal_type(type(x)) # revealed: type
|
||||
reveal_type(type(x)) # revealed: type[T_bound_object@bound_object]
|
||||
|
||||
T_bound_int = TypeVar("T_bound_int", bound=int)
|
||||
|
||||
def bound_int(x: T_bound_int):
|
||||
reveal_type(type(x)) # revealed: type[int]
|
||||
reveal_type(type(x)) # revealed: type[T_bound_int@bound_int]
|
||||
|
||||
T_constrained = TypeVar("T_constrained", int, str)
|
||||
|
||||
def constrained(x: T_constrained):
|
||||
reveal_type(type(x)) # revealed: type[int] | type[str]
|
||||
reveal_type(type(x)) # revealed: type[T_constrained@constrained]
|
||||
```
|
||||
|
||||
## Cycles
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -101,7 +101,7 @@ def deeper_explicit(x: ExplicitlyImplements[set[str]]) -> None:
|
||||
def takes_in_type[T](x: type[T]) -> type[T]:
|
||||
return x
|
||||
|
||||
reveal_type(takes_in_type(int)) # revealed: @Todo(unsupported type[X] special form)
|
||||
reveal_type(takes_in_type(int)) # revealed: type[int]
|
||||
```
|
||||
|
||||
This also works when passing in arguments that are subclasses of the parameter type.
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -754,21 +754,20 @@ def constrained[T: (Callable[[], int], Callable[[], str])](f: T):
|
||||
|
||||
## Meta-type
|
||||
|
||||
The meta-type of a typevar is the same as the meta-type of the upper bound, or the union of the
|
||||
meta-types of the constraints:
|
||||
The meta-type of a typevar is `type[T]`.
|
||||
|
||||
```py
|
||||
def normal[T](x: T):
|
||||
reveal_type(type(x)) # revealed: type
|
||||
reveal_type(type(x)) # revealed: type[T@normal]
|
||||
|
||||
def bound_object[T: object](x: T):
|
||||
reveal_type(type(x)) # revealed: type
|
||||
reveal_type(type(x)) # revealed: type[T@bound_object]
|
||||
|
||||
def bound_int[T: int](x: T):
|
||||
reveal_type(type(x)) # revealed: type[int]
|
||||
reveal_type(type(x)) # revealed: type[T@bound_int]
|
||||
|
||||
def constrained[T: (int, str)](x: T):
|
||||
reveal_type(type(x)) # revealed: type[int] | type[str]
|
||||
reveal_type(type(x)) # revealed: type[T@constrained]
|
||||
```
|
||||
|
||||
## Cycles
|
||||
|
||||
@@ -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))
|
||||
```
|
||||
|
||||
@@ -110,6 +110,11 @@ static_assert(not has_member(C(), "non_existent"))
|
||||
|
||||
### Class objects
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Class-level attributes can also be accessed through the class itself:
|
||||
|
||||
```py
|
||||
@@ -154,7 +159,13 @@ static_assert(has_member(D, "meta_attr"))
|
||||
static_assert(has_member(D, "base_attr"))
|
||||
static_assert(has_member(D, "class_attr"))
|
||||
|
||||
def f(x: type[D]):
|
||||
def _(x: type[D]):
|
||||
static_assert(has_member(x, "meta_base_attr"))
|
||||
static_assert(has_member(x, "meta_attr"))
|
||||
static_assert(has_member(x, "base_attr"))
|
||||
static_assert(has_member(x, "class_attr"))
|
||||
|
||||
def _[T: D](x: type[T]):
|
||||
static_assert(has_member(x, "meta_base_attr"))
|
||||
static_assert(has_member(x, "meta_attr"))
|
||||
static_assert(has_member(x, "base_attr"))
|
||||
|
||||
@@ -11,36 +11,17 @@ valid type for use in a type expression:
|
||||
```py
|
||||
MyInt = int
|
||||
|
||||
reveal_type(MyInt) # revealed: <class 'int'>
|
||||
reveal_type(MyInt(1)) # revealed: int
|
||||
|
||||
def f(x: MyInt):
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
f(1)
|
||||
```
|
||||
|
||||
This also works for generic aliases:
|
||||
|
||||
```py
|
||||
ListOfStr = list[str]
|
||||
|
||||
reveal_type(ListOfStr) # revealed: <class 'list[str]'>
|
||||
reveal_type(ListOfStr(["a", "b"])) # revealed: list[str]
|
||||
|
||||
def g(x: ListOfStr):
|
||||
reveal_type(x) # revealed: list[str]
|
||||
|
||||
g(["a", "b"])
|
||||
```
|
||||
|
||||
## None
|
||||
|
||||
```py
|
||||
MyNone = None
|
||||
|
||||
reveal_type(MyNone) # revealed: None
|
||||
|
||||
def g(x: MyNone):
|
||||
reveal_type(x) # revealed: None
|
||||
|
||||
@@ -209,14 +190,10 @@ def _(
|
||||
reveal_type(type_of_str_or_int) # revealed: type[str] | int
|
||||
reveal_type(int_or_callable) # revealed: int | ((str, /) -> bytes)
|
||||
reveal_type(callable_or_int) # revealed: ((str, /) -> bytes) | int
|
||||
# TODO should be Unknown | int
|
||||
reveal_type(type_var_or_int) # revealed: typing.TypeVar | int
|
||||
# TODO should be int | Unknown
|
||||
reveal_type(int_or_type_var) # revealed: int | typing.TypeVar
|
||||
# TODO should be Unknown | None
|
||||
reveal_type(type_var_or_none) # revealed: typing.TypeVar | None
|
||||
# TODO should be None | Unknown
|
||||
reveal_type(none_or_type_var) # revealed: None | typing.TypeVar
|
||||
reveal_type(type_var_or_int) # revealed: Unknown | int
|
||||
reveal_type(int_or_type_var) # revealed: int | Unknown
|
||||
reveal_type(type_var_or_none) # revealed: Unknown | None
|
||||
reveal_type(none_or_type_var) # revealed: None | Unknown
|
||||
```
|
||||
|
||||
If a type is unioned with itself in a value expression, the result is just that type. No
|
||||
@@ -234,13 +211,6 @@ def _(int_or_int: IntOrInt, list_of_int_or_list_of_int: ListOfIntOrListOfInt):
|
||||
reveal_type(list_of_int_or_list_of_int) # revealed: list[int]
|
||||
```
|
||||
|
||||
`types.UnionType` instances can not be instantiated:
|
||||
|
||||
```py
|
||||
# error: [call-non-callable] "Object of type `UnionType` is not callable"
|
||||
IntOrStr()
|
||||
```
|
||||
|
||||
`NoneType` has no special or-operator behavior, so this is an error:
|
||||
|
||||
```py
|
||||
@@ -392,7 +362,9 @@ def g(obj: Y):
|
||||
reveal_type(obj) # revealed: list[int | str]
|
||||
```
|
||||
|
||||
## Generic types
|
||||
## Generic implicit type aliases
|
||||
|
||||
### Functionality
|
||||
|
||||
Implicit type aliases can also be generic:
|
||||
|
||||
@@ -414,73 +386,62 @@ ListOrTuple = list[T] | tuple[T, ...]
|
||||
ListOrTupleLegacy = Union[list[T], tuple[T, ...]]
|
||||
MyCallable = Callable[P, T]
|
||||
AnnotatedType = Annotated[T, "tag"]
|
||||
TransparentAlias = T
|
||||
MyOptional = T | None
|
||||
|
||||
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below)
|
||||
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
|
||||
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
|
||||
reveal_type(MyList) # revealed: <class 'list[T@MyList]'>
|
||||
reveal_type(MyDict) # revealed: <class 'dict[T@MyDict, U@MyDict]'>
|
||||
reveal_type(MyType) # revealed: GenericAlias
|
||||
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'>
|
||||
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
|
||||
reveal_type(Sum) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
|
||||
reveal_type(IntAndType) # revealed: <class 'tuple[int, T@IntAndType]'>
|
||||
reveal_type(Pair) # revealed: <class 'tuple[T@Pair, T@Pair]'>
|
||||
reveal_type(Sum) # revealed: <class 'tuple[T@Sum, U@Sum]'>
|
||||
reveal_type(ListOrTuple) # revealed: types.UnionType
|
||||
reveal_type(ListOrTupleLegacy) # revealed: types.UnionType
|
||||
reveal_type(MyCallable) # revealed: GenericAlias
|
||||
reveal_type(MyCallable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||
reveal_type(AnnotatedType) # revealed: <typing.Annotated special form>
|
||||
reveal_type(TransparentAlias) # revealed: typing.TypeVar
|
||||
reveal_type(MyOptional) # revealed: types.UnionType
|
||||
|
||||
def _(
|
||||
list_of_ints: MyList[int],
|
||||
dict_str_to_int: MyDict[str, int],
|
||||
# TODO: no error here
|
||||
# error: [invalid-type-form] "`typing.TypeVar` is not a generic class"
|
||||
subclass_of_int: MyType[int],
|
||||
int_and_str: IntAndType[str],
|
||||
pair_of_ints: Pair[int],
|
||||
int_and_bytes: Sum[int, bytes],
|
||||
list_or_tuple: ListOrTuple[int],
|
||||
list_or_tuple_legacy: ListOrTupleLegacy[int],
|
||||
# TODO: no error here
|
||||
# error: [invalid-type-form] "List literals are not allowed in this context in a type expression: Did you mean `tuple[str, bytes]`?"
|
||||
my_callable: MyCallable[[str, bytes], int],
|
||||
annotated_int: AnnotatedType[int],
|
||||
transparent_alias: TransparentAlias[int],
|
||||
optional_int: MyOptional[int],
|
||||
):
|
||||
# TODO: This should be `list[int]`
|
||||
reveal_type(list_of_ints) # revealed: @Todo(specialized generic alias in type expression)
|
||||
# TODO: This should be `dict[str, int]`
|
||||
reveal_type(dict_str_to_int) # revealed: @Todo(specialized generic alias in type expression)
|
||||
# TODO: This should be `type[int]`
|
||||
reveal_type(subclass_of_int) # revealed: Unknown
|
||||
# TODO: This should be `tuple[int, str]`
|
||||
reveal_type(int_and_str) # revealed: @Todo(specialized generic alias in type expression)
|
||||
# TODO: This should be `tuple[int, int]`
|
||||
reveal_type(pair_of_ints) # revealed: @Todo(specialized generic alias in type expression)
|
||||
# TODO: This should be `tuple[int, bytes]`
|
||||
reveal_type(int_and_bytes) # revealed: @Todo(specialized generic alias in type expression)
|
||||
# TODO: This should be `list[int] | tuple[int, ...]`
|
||||
reveal_type(list_or_tuple) # revealed: @Todo(Generic specialization of types.UnionType)
|
||||
# TODO: This should be `list[int] | tuple[int, ...]`
|
||||
reveal_type(list_or_tuple_legacy) # revealed: @Todo(Generic specialization of types.UnionType)
|
||||
reveal_type(list_of_ints) # revealed: list[int]
|
||||
reveal_type(dict_str_to_int) # revealed: dict[str, int]
|
||||
reveal_type(subclass_of_int) # revealed: type[int]
|
||||
reveal_type(int_and_str) # revealed: tuple[int, str]
|
||||
reveal_type(pair_of_ints) # revealed: tuple[int, int]
|
||||
reveal_type(int_and_bytes) # revealed: tuple[int, bytes]
|
||||
reveal_type(list_or_tuple) # revealed: list[int] | tuple[int, ...]
|
||||
reveal_type(list_or_tuple_legacy) # revealed: list[int] | tuple[int, ...]
|
||||
# TODO: This should be `(str, bytes) -> int`
|
||||
reveal_type(my_callable) # revealed: @Todo(Generic specialization of typing.Callable)
|
||||
# TODO: This should be `int`
|
||||
reveal_type(annotated_int) # revealed: @Todo(Generic specialization of typing.Annotated)
|
||||
reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||
reveal_type(annotated_int) # revealed: int
|
||||
reveal_type(transparent_alias) # revealed: int
|
||||
reveal_type(optional_int) # revealed: int | None
|
||||
```
|
||||
|
||||
Generic implicit type aliases can be partially specialized:
|
||||
|
||||
```py
|
||||
U = TypeVar("U")
|
||||
|
||||
DictStrTo = MyDict[str, U]
|
||||
|
||||
reveal_type(DictStrTo) # revealed: GenericAlias
|
||||
reveal_type(DictStrTo) # revealed: <class 'dict[str, U@DictStrTo]'>
|
||||
|
||||
def _(
|
||||
# TODO: No error here
|
||||
# error: [invalid-type-form] "Invalid subscript of object of type `GenericAlias` in type expression"
|
||||
dict_str_to_int: DictStrTo[int],
|
||||
):
|
||||
# TODO: This should be `dict[str, int]`
|
||||
reveal_type(dict_str_to_int) # revealed: Unknown
|
||||
reveal_type(dict_str_to_int) # revealed: dict[str, int]
|
||||
```
|
||||
|
||||
Using specializations of generic implicit type aliases in other implicit type aliases works as
|
||||
@@ -490,43 +451,107 @@ expected:
|
||||
IntsOrNone = MyList[int] | None
|
||||
IntsOrStrs = Pair[int] | Pair[str]
|
||||
ListOfPairs = MyList[Pair[str]]
|
||||
ListOrTupleOfInts = ListOrTuple[int]
|
||||
AnnotatedInt = AnnotatedType[int]
|
||||
SubclassOfInt = MyType[int]
|
||||
CallableIntToStr = MyCallable[[int], str]
|
||||
|
||||
reveal_type(IntsOrNone) # revealed: UnionType
|
||||
reveal_type(IntsOrStrs) # revealed: UnionType
|
||||
reveal_type(ListOfPairs) # revealed: GenericAlias
|
||||
reveal_type(IntsOrNone) # revealed: types.UnionType
|
||||
reveal_type(IntsOrStrs) # revealed: types.UnionType
|
||||
reveal_type(ListOfPairs) # revealed: <class 'list[tuple[str, str]]'>
|
||||
reveal_type(ListOrTupleOfInts) # revealed: types.UnionType
|
||||
reveal_type(AnnotatedInt) # revealed: <typing.Annotated special form>
|
||||
reveal_type(SubclassOfInt) # revealed: GenericAlias
|
||||
reveal_type(CallableIntToStr) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||
|
||||
def _(
|
||||
# TODO: This should not be an error
|
||||
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
|
||||
ints_or_none: IntsOrNone,
|
||||
# TODO: This should not be an error
|
||||
# error: [invalid-type-form] "Variable of type `UnionType` is not allowed in a type expression"
|
||||
ints_or_strs: IntsOrStrs,
|
||||
list_of_pairs: ListOfPairs,
|
||||
list_or_tuple_of_ints: ListOrTupleOfInts,
|
||||
annotated_int: AnnotatedInt,
|
||||
subclass_of_int: SubclassOfInt,
|
||||
callable_int_to_str: CallableIntToStr,
|
||||
):
|
||||
# TODO: This should be `list[int] | None`
|
||||
reveal_type(ints_or_none) # revealed: Unknown
|
||||
# TODO: This should be `tuple[int, int] | tuple[str, str]`
|
||||
reveal_type(ints_or_strs) # revealed: Unknown
|
||||
# TODO: This should be `list[tuple[str, str]]`
|
||||
reveal_type(list_of_pairs) # revealed: @Todo(Support for `typing.GenericAlias` instances in type expressions)
|
||||
reveal_type(ints_or_none) # revealed: list[int] | None
|
||||
reveal_type(ints_or_strs) # revealed: tuple[int, int] | tuple[str, str]
|
||||
reveal_type(list_of_pairs) # revealed: list[tuple[str, str]]
|
||||
reveal_type(list_or_tuple_of_ints) # revealed: list[int] | tuple[int, ...]
|
||||
reveal_type(annotated_int) # revealed: int
|
||||
reveal_type(subclass_of_int) # revealed: type[int]
|
||||
# TODO: This should be `(int, /) -> str`
|
||||
reveal_type(callable_int_to_str) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||
```
|
||||
|
||||
If a generic implicit type alias is used unspecialized in a type expression, we treat it as an
|
||||
`Unknown` specialization:
|
||||
A generic implicit type alias can also be used in another generic implicit type alias:
|
||||
|
||||
```py
|
||||
from typing_extensions import Any
|
||||
|
||||
B = TypeVar("B", bound=int)
|
||||
|
||||
MyOtherList = MyList[T]
|
||||
MyOtherType = MyType[T]
|
||||
TypeOrList = MyType[B] | MyList[B]
|
||||
|
||||
reveal_type(MyOtherList) # revealed: <class 'list[T@MyOtherList]'>
|
||||
reveal_type(MyOtherType) # revealed: GenericAlias
|
||||
reveal_type(TypeOrList) # revealed: types.UnionType
|
||||
|
||||
def _(
|
||||
list_of_ints: MyOtherList[int],
|
||||
subclass_of_int: MyOtherType[int],
|
||||
type_or_list: TypeOrList[Any],
|
||||
):
|
||||
reveal_type(list_of_ints) # revealed: list[int]
|
||||
reveal_type(subclass_of_int) # revealed: type[int]
|
||||
reveal_type(type_or_list) # revealed: type[Any] | list[Any]
|
||||
```
|
||||
|
||||
If a generic implicit type alias is used unspecialized in a type expression, we use the default
|
||||
specialization. For type variables without defaults, this is `Unknown`:
|
||||
|
||||
```py
|
||||
def _(
|
||||
my_list: MyList,
|
||||
my_dict: MyDict,
|
||||
list_unknown: MyList,
|
||||
dict_unknown: MyDict,
|
||||
subclass_of_unknown: MyType,
|
||||
int_and_unknown: IntAndType,
|
||||
pair_of_unknown: Pair,
|
||||
unknown_and_unknown: Sum,
|
||||
list_or_tuple: ListOrTuple,
|
||||
list_or_tuple_legacy: ListOrTupleLegacy,
|
||||
my_callable: MyCallable,
|
||||
annotated_unknown: AnnotatedType,
|
||||
optional_unknown: MyOptional,
|
||||
):
|
||||
# TODO: Should be `list[Unknown]`
|
||||
reveal_type(my_list) # revealed: list[typing.TypeVar]
|
||||
# TODO: Should be `dict[Unknown, Unknown]`
|
||||
reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar]
|
||||
reveal_type(list_unknown) # revealed: list[Unknown]
|
||||
reveal_type(dict_unknown) # revealed: dict[Unknown, Unknown]
|
||||
reveal_type(subclass_of_unknown) # revealed: type[Unknown]
|
||||
reveal_type(int_and_unknown) # revealed: tuple[int, Unknown]
|
||||
reveal_type(pair_of_unknown) # revealed: tuple[Unknown, Unknown]
|
||||
reveal_type(unknown_and_unknown) # revealed: tuple[Unknown, Unknown]
|
||||
reveal_type(list_or_tuple) # revealed: list[Unknown] | tuple[Unknown, ...]
|
||||
reveal_type(list_or_tuple_legacy) # revealed: list[Unknown] | tuple[Unknown, ...]
|
||||
# TODO: Should be `(...) -> Unknown`
|
||||
reveal_type(my_callable) # revealed: (...) -> typing.TypeVar
|
||||
reveal_type(my_callable) # revealed: @Todo(Callable[..] specialized with ParamSpec)
|
||||
reveal_type(annotated_unknown) # revealed: Unknown
|
||||
reveal_type(optional_unknown) # revealed: Unknown | None
|
||||
```
|
||||
|
||||
For a type variable with a default, we use the default type:
|
||||
|
||||
```py
|
||||
T_default = TypeVar("T_default", default=int)
|
||||
|
||||
MyListWithDefault = list[T_default]
|
||||
|
||||
def _(
|
||||
list_of_str: MyListWithDefault[str],
|
||||
list_of_int: MyListWithDefault,
|
||||
):
|
||||
reveal_type(list_of_str) # revealed: list[str]
|
||||
reveal_type(list_of_int) # revealed: list[int]
|
||||
```
|
||||
|
||||
(Generic) implicit type aliases can be used as base classes:
|
||||
@@ -548,37 +573,209 @@ reveal_mro(Derived1)
|
||||
|
||||
GenericBaseAlias = GenericBase[T]
|
||||
|
||||
# TODO: No error here
|
||||
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[typing.TypeVar]'>` with no `__class_getitem__` method"
|
||||
class Derived2(GenericBaseAlias[int]):
|
||||
pass
|
||||
```
|
||||
|
||||
### Imported aliases
|
||||
|
||||
Generic implicit type aliases can be imported from other modules and specialized:
|
||||
|
||||
`my_types.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
MyList = list[T]
|
||||
```
|
||||
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
from my_types import MyList
|
||||
import my_types as mt
|
||||
|
||||
def _(
|
||||
list_of_ints1: MyList[int],
|
||||
list_of_ints2: mt.MyList[int],
|
||||
):
|
||||
reveal_type(list_of_ints1) # revealed: list[int]
|
||||
reveal_type(list_of_ints2) # revealed: list[int]
|
||||
```
|
||||
|
||||
### In stringified annotations
|
||||
|
||||
Generic implicit type aliases can be specialized in stringified annotations:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
MyList = list[T]
|
||||
|
||||
def _(
|
||||
list_of_ints: "MyList[int]",
|
||||
):
|
||||
reveal_type(list_of_ints) # revealed: list[int]
|
||||
```
|
||||
|
||||
### Tuple unpacking
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
V = TypeVar("V")
|
||||
|
||||
X = tuple[T, *tuple[U, ...], V]
|
||||
Y = X[T, tuple[int, str, U], bytes]
|
||||
|
||||
def g(obj: Y[bool, range]):
|
||||
reveal_type(obj) # revealed: tuple[bool, *tuple[tuple[int, str, range], ...], bytes]
|
||||
```
|
||||
|
||||
### Error cases
|
||||
|
||||
A generic alias that is already fully specialized cannot be specialized again:
|
||||
|
||||
```py
|
||||
ListOfInts = list[int]
|
||||
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
|
||||
def _(doubly_specialized: ListOfInts[int]):
|
||||
# TODO: this should be `Unknown`
|
||||
reveal_type(doubly_specialized) # revealed: @Todo(specialized generic alias in type expression)
|
||||
# TODO: This should ideally be `list[Unknown]` or `Unknown`
|
||||
reveal_type(doubly_specialized) # revealed: list[int]
|
||||
```
|
||||
|
||||
Specializing a generic implicit type alias with an incorrect number of type arguments also results
|
||||
in an error:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
MyList = list[T]
|
||||
MyDict = dict[T, U]
|
||||
|
||||
def _(
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
|
||||
list_too_many_args: MyList[int, str],
|
||||
# TODO: this should be an error
|
||||
# error: [invalid-type-arguments] "No type argument provided for required type variable `U`"
|
||||
dict_too_few_args: MyDict[int],
|
||||
):
|
||||
# TODO: this should be `Unknown`
|
||||
reveal_type(list_too_many_args) # revealed: @Todo(specialized generic alias in type expression)
|
||||
# TODO: this should be `Unknown`
|
||||
reveal_type(dict_too_few_args) # revealed: @Todo(specialized generic alias in type expression)
|
||||
reveal_type(list_too_many_args) # revealed: list[Unknown]
|
||||
reveal_type(dict_too_few_args) # revealed: dict[Unknown, Unknown]
|
||||
```
|
||||
|
||||
Trying to specialize a non-name node results in an error:
|
||||
|
||||
```py
|
||||
from ty_extensions import TypeOf
|
||||
|
||||
IntOrStr = int | str
|
||||
|
||||
def this_does_not_work() -> TypeOf[IntOrStr]:
|
||||
raise NotImplementedError()
|
||||
|
||||
def _(
|
||||
# TODO: Better error message (of kind `invalid-type-form`)?
|
||||
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
|
||||
specialized: this_does_not_work()[int],
|
||||
):
|
||||
reveal_type(specialized) # revealed: int | str
|
||||
```
|
||||
|
||||
Similarly, if you try to specialize a union type without a binding context, we emit an error:
|
||||
|
||||
```py
|
||||
# TODO: Better error message (of kind `invalid-type-form`)?
|
||||
# error: [invalid-type-arguments] "Too many type arguments: expected 0, got 1"
|
||||
x: (list[T] | set[T])[int]
|
||||
|
||||
def _():
|
||||
# TODO: `list[Unknown] | set[Unknown]` might be better
|
||||
reveal_type(x) # revealed: list[typing.TypeVar] | set[typing.TypeVar]
|
||||
```
|
||||
|
||||
### Multiple definitions
|
||||
|
||||
#### Shadowed definitions
|
||||
|
||||
When a generic type alias shadows a definition from an outer scope, the inner definition is used:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
MyAlias = list[T]
|
||||
|
||||
def outer():
|
||||
MyAlias = set[T]
|
||||
|
||||
def _(x: MyAlias[int]):
|
||||
reveal_type(x) # revealed: set[int]
|
||||
```
|
||||
|
||||
#### Statically known conditions
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
if True:
|
||||
MyAlias1 = list[T]
|
||||
else:
|
||||
MyAlias1 = set[T]
|
||||
|
||||
if False:
|
||||
MyAlias2 = list[T]
|
||||
else:
|
||||
MyAlias2 = set[T]
|
||||
|
||||
def _(
|
||||
x1: MyAlias1[int],
|
||||
x2: MyAlias2[int],
|
||||
):
|
||||
reveal_type(x1) # revealed: list[int]
|
||||
reveal_type(x2) # revealed: set[int]
|
||||
```
|
||||
|
||||
#### Statically unknown conditions
|
||||
|
||||
If several definitions are visible, we emit an error:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
if flag():
|
||||
MyAlias = list[T]
|
||||
else:
|
||||
MyAlias = set[T]
|
||||
|
||||
# It is questionable whether this should be supported or not. It might also be reasonable to
|
||||
# emit an error here (e.g. "Invalid subscript of object of type `<class 'list[T@MyAlias]'> |
|
||||
# <class 'set[T@MyAlias]'>` in type expression"). If we ever choose to do so, the revealed
|
||||
# type should probably be `Unknown`.
|
||||
def _(x: MyAlias[int]):
|
||||
reveal_type(x) # revealed: list[int] | set[int]
|
||||
```
|
||||
|
||||
## `Literal`s
|
||||
@@ -645,7 +842,7 @@ def _(weird: IntLiteral1[int]):
|
||||
|
||||
## `Annotated`
|
||||
|
||||
### Basic usage
|
||||
Basic usage:
|
||||
|
||||
```py
|
||||
from typing import Annotated
|
||||
@@ -668,65 +865,13 @@ Deprecated = Annotated[T, "deprecated attribute"]
|
||||
class C:
|
||||
old: Deprecated[int]
|
||||
|
||||
# TODO: Should be `int`
|
||||
reveal_type(C().old) # revealed: @Todo(Generic specialization of typing.Annotated)
|
||||
reveal_type(C().old) # revealed: int
|
||||
```
|
||||
|
||||
### Special members
|
||||
|
||||
The `__origin__` attribute on an instance of `Annotated` can be used to access the underlying type.
|
||||
We currently do not model this precisely:
|
||||
|
||||
```py
|
||||
from typing import Annotated
|
||||
|
||||
StrWithMetadata = Annotated[str, "metadata", 1, 2, 3]
|
||||
|
||||
reveal_type(StrWithMetadata.__origin__) # revealed: type | TypeAliasType
|
||||
```
|
||||
|
||||
At runtime, the `__metadata__` attribute contains the metadata elements `('metadata', 1, 2, 3)`, but
|
||||
we do not model the type of this attribute precisely:
|
||||
|
||||
```py
|
||||
reveal_type(StrWithMetadata.__metadata__) # revealed: Any
|
||||
```
|
||||
|
||||
### Attribute access and instantiation
|
||||
|
||||
A class can be instantiated through an `Annotated` alias, and attributes can be accessed on the
|
||||
created instance:
|
||||
|
||||
```py
|
||||
from __future__ import annotations
|
||||
from typing import Annotated, Union
|
||||
|
||||
class Foo:
|
||||
attribute: int = 1
|
||||
|
||||
FooWithMetadata = Annotated[Foo, "metadata"]
|
||||
|
||||
c = FooWithMetadata()
|
||||
|
||||
reveal_type(c) # revealed: Foo
|
||||
reveal_type(c.attribute) # revealed: int
|
||||
```
|
||||
|
||||
However, accessing attributes on the alias itself currently yields `Any`, instead of the attribute's
|
||||
type:
|
||||
|
||||
```py
|
||||
reveal_type(FooWithMetadata.attribute) # revealed: Any
|
||||
```
|
||||
|
||||
### Error cases
|
||||
|
||||
If the metadata argument is missing, we emit an error (because this code fails at runtime), but
|
||||
still use the first element as the type, when used in annotations:
|
||||
|
||||
```py
|
||||
from typing import Annotated
|
||||
|
||||
# error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)"
|
||||
WronglyAnnotatedInt = Annotated[int]
|
||||
|
||||
@@ -1375,3 +1520,21 @@ def _(
|
||||
reveal_type(recursive_dict3) # revealed: dict[Divergent, int]
|
||||
reveal_type(recursive_dict4) # revealed: dict[Divergent, int]
|
||||
```
|
||||
|
||||
### Self-referential generic implicit type aliases
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
NestedDict = dict[str, "NestedDict[T] | T"]
|
||||
NestedList = list["NestedList[T] | None"]
|
||||
|
||||
def _(
|
||||
nested_dict_int: NestedDict[int],
|
||||
nested_list_str: NestedList[str],
|
||||
):
|
||||
reveal_type(nested_dict_int) # revealed: dict[str, Divergent]
|
||||
reveal_type(nested_list_str) # revealed: list[Divergent]
|
||||
```
|
||||
|
||||
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"
|
||||
```
|
||||
@@ -0,0 +1,64 @@
|
||||
# numpy
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.14"
|
||||
```
|
||||
|
||||
## numpy's `dtype`
|
||||
|
||||
numpy functions often accept a `dtype` parameter. For example, one of `np.array`'s overloads accepts
|
||||
a `dtype` parameter of type `DTypeLike | None`. Here, we build up something that resembles numpy's
|
||||
internals in order to model the type `DTypeLike`. Many details have been left out.
|
||||
|
||||
`mini_numpy.py`:
|
||||
|
||||
```py
|
||||
from typing import TypeVar, Generic, Any, Protocol, TypeAlias, runtime_checkable, final
|
||||
import builtins
|
||||
|
||||
_ItemT_co = TypeVar("_ItemT_co", default=Any, covariant=True)
|
||||
|
||||
class generic(Generic[_ItemT_co]):
|
||||
@property
|
||||
def dtype(self) -> _DTypeT_co:
|
||||
raise NotImplementedError
|
||||
|
||||
_BoolItemT_co = TypeVar("_BoolItemT_co", bound=builtins.bool, default=builtins.bool, covariant=True)
|
||||
|
||||
class bool(generic[_BoolItemT_co], Generic[_BoolItemT_co]): ...
|
||||
|
||||
@final
|
||||
class object_(generic): ...
|
||||
|
||||
_ScalarT = TypeVar("_ScalarT", bound=generic)
|
||||
_ScalarT_co = TypeVar("_ScalarT_co", bound=generic, default=Any, covariant=True)
|
||||
|
||||
@final
|
||||
class dtype(Generic[_ScalarT_co]): ...
|
||||
|
||||
_DTypeT_co = TypeVar("_DTypeT_co", bound=dtype, default=dtype, covariant=True)
|
||||
|
||||
@runtime_checkable
|
||||
class _SupportsDType(Protocol[_DTypeT_co]):
|
||||
@property
|
||||
def dtype(self) -> _DTypeT_co: ...
|
||||
|
||||
_DTypeLike: TypeAlias = type[_ScalarT] | dtype[_ScalarT] | _SupportsDType[dtype[_ScalarT]]
|
||||
|
||||
DTypeLike: TypeAlias = _DTypeLike[Any] | str | None
|
||||
```
|
||||
|
||||
Now we can make sure that a function which accepts `DTypeLike | None` works as expected:
|
||||
|
||||
```py
|
||||
import mini_numpy as np
|
||||
|
||||
def accepts_dtype(dtype: np.DTypeLike | None) -> None: ...
|
||||
|
||||
accepts_dtype(dtype=np.bool)
|
||||
accepts_dtype(dtype=np.dtype[np.bool])
|
||||
accepts_dtype(dtype=object)
|
||||
accepts_dtype(dtype=np.object_)
|
||||
accepts_dtype(dtype="U")
|
||||
```
|
||||
@@ -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):
|
||||
|
||||
@@ -96,6 +96,45 @@ def _(x: MyAlias):
|
||||
reveal_type(x) # revealed: int | ((str, /) -> int)
|
||||
```
|
||||
|
||||
## Generic aliases
|
||||
|
||||
A more comprehensive set of tests can be found in
|
||||
[`implicit_type_aliases.md`](./implicit_type_aliases.md). If the implementations ever diverge, we
|
||||
may need to duplicate more tests here.
|
||||
|
||||
### Basic
|
||||
|
||||
```py
|
||||
from typing import TypeAlias, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
MyList: TypeAlias = list[T]
|
||||
ListOrSet: TypeAlias = list[T] | set[T]
|
||||
|
||||
reveal_type(MyList) # revealed: <class 'list[T]'>
|
||||
reveal_type(ListOrSet) # revealed: types.UnionType
|
||||
|
||||
def _(list_of_int: MyList[int], list_or_set_of_str: ListOrSet[str]):
|
||||
reveal_type(list_of_int) # revealed: list[int]
|
||||
reveal_type(list_or_set_of_str) # revealed: list[str] | set[str]
|
||||
```
|
||||
|
||||
### Stringified generic alias
|
||||
|
||||
```py
|
||||
from typing import TypeAlias, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
TotallyStringifiedPEP613: TypeAlias = "dict[T, U]"
|
||||
TotallyStringifiedPartiallySpecialized: TypeAlias = "TotallyStringifiedPEP613[U, int]"
|
||||
|
||||
def f(x: "TotallyStringifiedPartiallySpecialized[str]"):
|
||||
reveal_type(x) # revealed: @Todo(Generic stringified PEP-613 type alias)
|
||||
```
|
||||
|
||||
## Subscripted generic alias in union
|
||||
|
||||
```py
|
||||
@@ -107,8 +146,7 @@ Alias1: TypeAlias = list[T] | set[T]
|
||||
MyAlias: TypeAlias = int | Alias1[str]
|
||||
|
||||
def _(x: MyAlias):
|
||||
# TODO: int | list[str] | set[str]
|
||||
reveal_type(x) # revealed: int | @Todo(Specialization of union type alias)
|
||||
reveal_type(x) # revealed: int | list[str] | set[str]
|
||||
```
|
||||
|
||||
## Imported
|
||||
@@ -173,7 +211,7 @@ NestedDict: TypeAlias = dict[K, Union[V, "NestedDict[K, V]"]]
|
||||
|
||||
def _(nested: NestedDict[str, int]):
|
||||
# TODO should be `dict[str, int | NestedDict[str, int]]`
|
||||
reveal_type(nested) # revealed: @Todo(specialized generic alias in type expression)
|
||||
reveal_type(nested) # revealed: dict[@Todo(specialized recursive generic type alias), Divergent]
|
||||
|
||||
my_isinstance(1, int)
|
||||
my_isinstance(1, int | str)
|
||||
|
||||
@@ -255,12 +255,10 @@ And it is also an error to use `Protocol` in type expressions:
|
||||
|
||||
def f(
|
||||
x: Protocol, # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
|
||||
y: type[Protocol], # TODO: should emit `[invalid-type-form]` here too
|
||||
y: type[Protocol], # error: [invalid-type-form] "`typing.Protocol` is not allowed in type expressions"
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown
|
||||
|
||||
# TODO: should be `type[Unknown]`
|
||||
reveal_type(y) # revealed: @Todo(unsupported type[X] special form)
|
||||
reveal_type(y) # revealed: type[Unknown]
|
||||
|
||||
# fmt: on
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -0,0 +1,292 @@
|
||||
---
|
||||
source: crates/ty_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: final.md - Tests for the `@typing(_extensions).final` decorator - A possibly-undefined `@final` method is overridden
|
||||
mdtest path: crates/ty_python_semantic/resources/mdtest/final.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from typing import final
|
||||
2 |
|
||||
3 | def coinflip() -> bool:
|
||||
4 | return False
|
||||
5 |
|
||||
6 | class A:
|
||||
7 | if coinflip():
|
||||
8 | @final
|
||||
9 | def method1(self) -> None: ...
|
||||
10 | else:
|
||||
11 | def method1(self) -> None: ...
|
||||
12 |
|
||||
13 | if coinflip():
|
||||
14 | def method2(self) -> None: ...
|
||||
15 | else:
|
||||
16 | @final
|
||||
17 | def method2(self) -> None: ...
|
||||
18 |
|
||||
19 | if coinflip():
|
||||
20 | @final
|
||||
21 | def method3(self) -> None: ...
|
||||
22 | else:
|
||||
23 | @final
|
||||
24 | def method3(self) -> None: ...
|
||||
25 |
|
||||
26 | if coinflip():
|
||||
27 | def method4(self) -> None: ...
|
||||
28 | elif coinflip():
|
||||
29 | @final
|
||||
30 | def method4(self) -> None: ...
|
||||
31 | else:
|
||||
32 | def method4(self) -> None: ...
|
||||
33 |
|
||||
34 | class B(A):
|
||||
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 |
|
||||
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
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method1`
|
||||
--> src/mdtest_snippet.py:35:9
|
||||
|
|
||||
34 | class B(A):
|
||||
35 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
36 | def method2(self) -> None: ... # error: [override-of-final-method]
|
||||
37 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
|
|
||||
info: `A.method1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:8:9
|
||||
|
|
||||
6 | class A:
|
||||
7 | if coinflip():
|
||||
8 | @final
|
||||
| ------
|
||||
9 | def method1(self) -> None: ...
|
||||
| ------- `A.method1` defined here
|
||||
10 | else:
|
||||
11 | def method1(self) -> None: ...
|
||||
|
|
||||
help: Remove the override of `method1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
32 | def method4(self) -> None: ...
|
||||
33 |
|
||||
34 | class B(A):
|
||||
- def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
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 |
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method2`
|
||||
--> src/mdtest_snippet.py:36:9
|
||||
|
|
||||
34 | class B(A):
|
||||
35 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
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]
|
||||
|
|
||||
info: `A.method2` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:16:9
|
||||
|
|
||||
14 | def method2(self) -> None: ...
|
||||
15 | else:
|
||||
16 | @final
|
||||
| ------
|
||||
17 | def method2(self) -> None: ...
|
||||
| ------- `A.method2` defined here
|
||||
18 |
|
||||
19 | if coinflip():
|
||||
|
|
||||
help: Remove the override of `method2`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
33 |
|
||||
34 | class B(A):
|
||||
35 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
- 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 |
|
||||
39 | # check that autofixes don't introduce invalid syntax
|
||||
note: This is an unsafe fix and may change runtime behavior
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method3`
|
||||
--> src/mdtest_snippet.py:37:9
|
||||
|
|
||||
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]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
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
|
||||
|
|
||||
19 | if coinflip():
|
||||
20 | @final
|
||||
| ------
|
||||
21 | def method3(self) -> None: ...
|
||||
| ------- `A.method3` defined here
|
||||
22 | else:
|
||||
23 | @final
|
||||
|
|
||||
help: Remove the override of `method3`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
34 | class B(A):
|
||||
35 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
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 |
|
||||
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:44:5
|
||||
|
|
||||
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
|
||||
|
|
||||
27 | def method4(self) -> None: ...
|
||||
28 | elif coinflip():
|
||||
29 | @final
|
||||
| ------
|
||||
30 | def method4(self) -> None: ...
|
||||
| ------- `A.method4` defined here
|
||||
31 | else:
|
||||
32 | def method4(self) -> None: ...
|
||||
|
|
||||
help: Remove the override of `method4`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method1`
|
||||
--> src/mdtest_snippet.py:49:13
|
||||
|
|
||||
47 | class C(A):
|
||||
48 | if coinflip():
|
||||
49 | def method1(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
50 | else:
|
||||
51 | pass
|
||||
|
|
||||
info: `A.method1` is decorated with `@final`, forbidding overrides
|
||||
--> src/mdtest_snippet.py:8:9
|
||||
|
|
||||
6 | class A:
|
||||
7 | if coinflip():
|
||||
8 | @final
|
||||
| ------
|
||||
9 | def method1(self) -> None: ...
|
||||
| ------- `A.method1` defined here
|
||||
10 | else:
|
||||
11 | def method1(self) -> None: ...
|
||||
|
|
||||
help: Remove the override of `method1`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method3`
|
||||
--> src/mdtest_snippet.py:59:13
|
||||
|
|
||||
58 | if coinflip():
|
||||
59 | def method3(self) -> None: ... # error: [override-of-final-method]
|
||||
| ^^^^^^^ Overrides a definition from superclass `A`
|
||||
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
|
||||
|
|
||||
19 | if coinflip():
|
||||
20 | @final
|
||||
| ------
|
||||
21 | def method3(self) -> None: ...
|
||||
| ------- `A.method3` defined here
|
||||
22 | else:
|
||||
23 | @final
|
||||
|
|
||||
help: Remove the override of `method3`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error[override-of-final-method]: Cannot override `A.method4`
|
||||
--> src/mdtest_snippet.py:60:13
|
||||
|
|
||||
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
|
||||
--> src/mdtest_snippet.py:29:9
|
||||
|
|
||||
27 | def method4(self) -> None: ...
|
||||
28 | elif coinflip():
|
||||
29 | @final
|
||||
| ------
|
||||
30 | def method4(self) -> None: ...
|
||||
| ------- `A.method4` defined here
|
||||
31 | else:
|
||||
32 | def method4(self) -> None: ...
|
||||
|
|
||||
help: Remove the override of `method4`
|
||||
info: rule `override-of-final-method` is enabled by default
|
||||
|
||||
```
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user