Compare commits
341 Commits
0.5.7
...
charlie/zs
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3baec49df | ||
|
|
e6fde89e26 | ||
|
|
7579a792c7 | ||
|
|
0bbc138037 | ||
|
|
ff11db61b4 | ||
|
|
2823487bf8 | ||
|
|
910fac781d | ||
|
|
149fb2090e | ||
|
|
40c65dcfa7 | ||
|
|
03f3a4e855 | ||
|
|
531ebf6dff | ||
|
|
7c2011599f | ||
|
|
17e90823da | ||
|
|
d01cbf7f8f | ||
|
|
770b276c21 | ||
|
|
4e935f7d7d | ||
|
|
260c2ecd15 | ||
|
|
f110d80279 | ||
|
|
a6d3d2fccd | ||
|
|
afdb659111 | ||
|
|
a8d9104fa3 | ||
|
|
d3530ab997 | ||
|
|
cf1e91bb59 | ||
|
|
125eaafae0 | ||
|
|
7aae80903c | ||
|
|
4aca9b91ba | ||
|
|
8b3da1867e | ||
|
|
c173ec5bc7 | ||
|
|
44d916fb4e | ||
|
|
4eb849aed3 | ||
|
|
6ac61d7b89 | ||
|
|
c7b2e336f0 | ||
|
|
70748950ae | ||
|
|
dcfebaa4a8 | ||
|
|
d86e5ad031 | ||
|
|
bb12fe9d0c | ||
|
|
3b57faf19b | ||
|
|
c9f7c3d652 | ||
|
|
489dbbaadc | ||
|
|
47e9ea2d5d | ||
|
|
7919a7122a | ||
|
|
a70d693b1c | ||
|
|
1365b0806d | ||
|
|
f4de49ab37 | ||
|
|
8b49845537 | ||
|
|
d988204b1b | ||
|
|
8558126df1 | ||
|
|
9bd9981e70 | ||
|
|
21bfab9b69 | ||
|
|
43a5922f6f | ||
|
|
175d067250 | ||
|
|
4dc2c257ef | ||
|
|
b72d49be16 | ||
|
|
eded78a39b | ||
|
|
a7b8cc08f0 | ||
|
|
b93d0ab57c | ||
|
|
e6b927a583 | ||
|
|
acab1f4fd8 | ||
|
|
2ca78721e6 | ||
|
|
a528edad35 | ||
|
|
1d5bd89987 | ||
|
|
b7cef6c999 | ||
|
|
110193af57 | ||
|
|
d6bd841512 | ||
|
|
210a9e6068 | ||
|
|
7c872e639b | ||
|
|
5ef6979d9a | ||
|
|
62c7d8f6ba | ||
|
|
6f53aaf931 | ||
|
|
ac720cd705 | ||
|
|
312bd86e48 | ||
|
|
b04948fb72 | ||
|
|
a98dbcee78 | ||
|
|
1eb3e4057f | ||
|
|
346dbf45b5 | ||
|
|
f427a7a5a3 | ||
|
|
955dc8804a | ||
|
|
e1603e3dca | ||
|
|
35d45c1e4b | ||
|
|
e4aa479515 | ||
|
|
a7c936878d | ||
|
|
c3bcd5c842 | ||
|
|
594dee1b0b | ||
|
|
a4ebe7d344 | ||
|
|
2a3775e525 | ||
|
|
65cc6ec41d | ||
|
|
66fe226608 | ||
|
|
e965f9cc0e | ||
|
|
0512428a6f | ||
|
|
46a457318d | ||
|
|
57289099bb | ||
|
|
9d1bd7a8a7 | ||
|
|
e37bde458e | ||
|
|
862bd0c429 | ||
|
|
e1e9143c47 | ||
|
|
3c4ec82aee | ||
|
|
29c36a56b2 | ||
|
|
dfee65882b | ||
|
|
50c8ee5175 | ||
|
|
c2aac5f826 | ||
|
|
387af831f9 | ||
|
|
9d517061f2 | ||
|
|
facf6febf0 | ||
|
|
46e687e8d1 | ||
|
|
599103c933 | ||
|
|
54df960a4a | ||
|
|
3463683632 | ||
|
|
6b973b2556 | ||
|
|
c0e2c13d0d | ||
|
|
591a7a152c | ||
|
|
b7c7b4b387 | ||
|
|
ea0246c51a | ||
|
|
0f85769976 | ||
|
|
47f0b45be3 | ||
|
|
f4bed22b05 | ||
|
|
17eb65b26f | ||
|
|
9986397d56 | ||
|
|
58c641c92f | ||
|
|
227fa4e035 | ||
|
|
2b21b77ee6 | ||
|
|
ba272b093c | ||
|
|
8972e5d175 | ||
|
|
9ac2e61bad | ||
|
|
6deb056117 | ||
|
|
c4aad4b161 | ||
|
|
3abd5c08a5 | ||
|
|
2014cba87f | ||
|
|
5661353334 | ||
|
|
dd5d0d523c | ||
|
|
1be8c2e340 | ||
|
|
52d8847b60 | ||
|
|
d3b6e8f58b | ||
|
|
bf620dcb38 | ||
|
|
fae0573817 | ||
|
|
0c23b868dc | ||
|
|
3ceedf76b8 | ||
|
|
828871dc5c | ||
|
|
ee21fc7fd8 | ||
|
|
28ab5f4065 | ||
|
|
a73bebcf15 | ||
|
|
34dafb67a2 | ||
|
|
f8656ff35e | ||
|
|
34b4732c46 | ||
|
|
ce68f1cc1b | ||
|
|
281e6d9791 | ||
|
|
ee258caed7 | ||
|
|
b4d9d26020 | ||
|
|
a99832088a | ||
|
|
770ef2ab27 | ||
|
|
c6023c03a2 | ||
|
|
df694ca1c1 | ||
|
|
2e75cfbfe7 | ||
|
|
cfafaa7637 | ||
|
|
3e9c7adeee | ||
|
|
81cd438d88 | ||
|
|
483748c188 | ||
|
|
eb3dc37faa | ||
|
|
aba1802828 | ||
|
|
96b42b0c8f | ||
|
|
e6d0c4a65d | ||
|
|
4e1b289a67 | ||
|
|
a5ef124201 | ||
|
|
390bb43276 | ||
|
|
fe8b15291f | ||
|
|
c8e01d7c53 | ||
|
|
c4d628cc4c | ||
|
|
ab3648c4c5 | ||
|
|
a822fd6642 | ||
|
|
f8f2e2a442 | ||
|
|
0b5828a1e8 | ||
|
|
5af48337a5 | ||
|
|
39ad6b9472 | ||
|
|
41dec93cd2 | ||
|
|
aee2caa733 | ||
|
|
fe5544e137 | ||
|
|
14c014a48b | ||
|
|
ecd0597d6b | ||
|
|
202271fba6 | ||
|
|
4bdb0b4f86 | ||
|
|
2286f916c1 | ||
|
|
1e4c944251 | ||
|
|
f50f8732e9 | ||
|
|
ecab04e338 | ||
|
|
8c09496b07 | ||
|
|
d19fd1b91c | ||
|
|
99df859e20 | ||
|
|
2d5fe9a6d3 | ||
|
|
1f2cb09853 | ||
|
|
cfe25ab465 | ||
|
|
551ed2706b | ||
|
|
21c5606793 | ||
|
|
c73a7bb929 | ||
|
|
4f6accb5c6 | ||
|
|
1ca14e4335 | ||
|
|
b9c8113a8a | ||
|
|
2edd32aa31 | ||
|
|
02c4373a49 | ||
|
|
d37e2e5d33 | ||
|
|
d1d067896c | ||
|
|
93f9023ea3 | ||
|
|
8144a11f98 | ||
|
|
dce87c21fd | ||
|
|
f873d2ac12 | ||
|
|
ecd9e6a650 | ||
|
|
785c39927b | ||
|
|
a35cdbb275 | ||
|
|
0c98b5949c | ||
|
|
e5f37a8254 | ||
|
|
5c5dfc11f0 | ||
|
|
678045e1aa | ||
|
|
dedefd73da | ||
|
|
37a60460ed | ||
|
|
0bd258a370 | ||
|
|
9baab8672a | ||
|
|
c65e3310d5 | ||
|
|
38c19fb96e | ||
|
|
abb4cdbf3d | ||
|
|
fc811f5168 | ||
|
|
1a8f29ea41 | ||
|
|
aefaddeae7 | ||
|
|
df09045176 | ||
|
|
049cda2ff3 | ||
|
|
358792f2c9 | ||
|
|
e6d5a7af37 | ||
|
|
f5bff82e70 | ||
|
|
ab44152eb5 | ||
|
|
f4c8c7eb70 | ||
|
|
65de8f2c9b | ||
|
|
e6226436fd | ||
|
|
0345d46759 | ||
|
|
4d0d3b00cb | ||
|
|
2be1c4ff04 | ||
|
|
edd86d5603 | ||
|
|
78ad7959ca | ||
|
|
d72ecd6ded | ||
|
|
8617a508bd | ||
|
|
c88bd4e884 | ||
|
|
fbcda90316 | ||
|
|
169d4390cb | ||
|
|
80ade591df | ||
|
|
4881d32c80 | ||
|
|
81a2220ce1 | ||
|
|
900e98b584 | ||
|
|
f9d8189670 | ||
|
|
52ba94191a | ||
|
|
96802d6a7f | ||
|
|
dd0a7ec73e | ||
|
|
25f5ae44c4 | ||
|
|
251efe5c41 | ||
|
|
6359e55383 | ||
|
|
a9847af6e8 | ||
|
|
d61d75d4fa | ||
|
|
499c0bd875 | ||
|
|
4cb30b598f | ||
|
|
aba0d83c11 | ||
|
|
c319414e54 | ||
|
|
ef1f6d98a0 | ||
|
|
b850b812de | ||
|
|
a87b27c075 | ||
|
|
9b73532b11 | ||
|
|
d8debb7a36 | ||
|
|
bd4a947b29 | ||
|
|
f121f8b31b | ||
|
|
80efb865e9 | ||
|
|
52d27befe8 | ||
|
|
6ed06afd28 | ||
|
|
b9da31610a | ||
|
|
ac7b1770e2 | ||
|
|
e4c2859c0f | ||
|
|
6dcd743111 | ||
|
|
73160dc8b6 | ||
|
|
15aa5a6d57 | ||
|
|
33512a4249 | ||
|
|
d8ebb03591 | ||
|
|
2e211c5c22 | ||
|
|
9fd8aaaf29 | ||
|
|
d110bd4e60 | ||
|
|
eb9c7ae869 | ||
|
|
7defc0d136 | ||
|
|
45f459bafd | ||
|
|
99e946a005 | ||
|
|
78a7ac0722 | ||
|
|
fa2f3f9f2f | ||
|
|
3898d737d8 | ||
|
|
c487149b7d | ||
|
|
bebed67bf1 | ||
|
|
3ddcad64f5 | ||
|
|
05c35b6975 | ||
|
|
7fc39ad624 | ||
|
|
2520ebb145 | ||
|
|
89c8b49027 | ||
|
|
e05953a991 | ||
|
|
d0ac38f9d3 | ||
|
|
ff53db3d99 | ||
|
|
899a52390b | ||
|
|
82a3e69b8a | ||
|
|
7027344dfc | ||
|
|
fb9f0c448f | ||
|
|
75131c6f4a | ||
|
|
4b9ddc4a06 | ||
|
|
99dc208b00 | ||
|
|
540023262e | ||
|
|
2ea79572ae | ||
|
|
aa0db338d9 | ||
|
|
a99a45868c | ||
|
|
fabf19fdc9 | ||
|
|
59f712a566 | ||
|
|
1d080465de | ||
|
|
3481e16cdf | ||
|
|
d7e9280e1e | ||
|
|
f237d36d2f | ||
|
|
12f22b1fdd | ||
|
|
47d05ee9ea | ||
|
|
9caec36b59 | ||
|
|
cb364780b3 | ||
|
|
71b8bf211f | ||
|
|
109b9cc4f9 | ||
|
|
5d02627794 | ||
|
|
65444bb00e | ||
|
|
8822a79b4d | ||
|
|
2df4d23113 | ||
|
|
603b62607a | ||
|
|
2b71fc4510 | ||
|
|
1b78d872ec | ||
|
|
feba5031dc | ||
|
|
0c2b88f224 | ||
|
|
cf1a57df5a | ||
|
|
597c5f9124 | ||
|
|
69e1c567d4 | ||
|
|
37b9bac403 | ||
|
|
83db48d316 | ||
|
|
c4e651921b | ||
|
|
b595346213 | ||
|
|
253474b312 | ||
|
|
a176679b24 | ||
|
|
1f51048fa4 | ||
|
|
2abfab0f9b | ||
|
|
64f1f3468d | ||
|
|
ffaa35eafe | ||
|
|
c906b0183b | ||
|
|
bc5b9b81dd |
@@ -20,7 +20,7 @@
|
||||
"extensions": [
|
||||
"ms-python.python",
|
||||
"rust-lang.rust-analyzer",
|
||||
"serayuzgur.crates",
|
||||
"fill-labs.dependi",
|
||||
"tamasfe.even-better-toml",
|
||||
"Swellaby.vscode-rust-test-adapter",
|
||||
"charliermarsh.ruff"
|
||||
|
||||
16
.github/renovate.json5
vendored
16
.github/renovate.json5
vendored
@@ -14,12 +14,26 @@
|
||||
rangeStrategy: "update-lockfile",
|
||||
},
|
||||
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$"],
|
||||
},
|
||||
pip_requirements: {
|
||||
fileMatch: ["^docs/requirements.*\\.txt$"],
|
||||
// The default for this package manager is to run on all requirements.txt files:
|
||||
// https://docs.renovatebot.com/modules/manager/pip_requirements/#file-matching
|
||||
// `fileMatch` doesn't work for excluding files; to exclude `requirements.txt` files
|
||||
// outside the `doc/` directory, we instead have to use `ignorePaths`. Unlike `fileMatch`,
|
||||
// which takes a regex string, `ignorePaths` takes a glob string, so we have to use
|
||||
// a "negative glob pattern".
|
||||
// See:
|
||||
// - https://docs.renovatebot.com/modules/manager/#ignoring-files-that-match-the-default-filematch
|
||||
// - https://docs.renovatebot.com/configuration-options/#ignorepaths
|
||||
// - https://docs.renovatebot.com/string-pattern-matching/#negative-matching
|
||||
ignorePaths: ["!docs/requirements*.txt"]
|
||||
},
|
||||
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$"],
|
||||
},
|
||||
"pre-commit": {
|
||||
|
||||
9
.github/workflows/ci.yaml
vendored
9
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: tj-actions/changed-files@v44
|
||||
- uses: tj-actions/changed-files@v45
|
||||
id: changed
|
||||
with:
|
||||
files_yaml: |
|
||||
@@ -142,6 +142,13 @@ jobs:
|
||||
|
||||
# Check for broken links in the documentation.
|
||||
- run: cargo doc --all --no-deps
|
||||
env:
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
# Use --document-private-items so that all our doc comments are kept in
|
||||
# sync, not just public items. Eventually we should do this for all
|
||||
# crates; for now add crates here as they are warning-clean to prevent
|
||||
# regression.
|
||||
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p ruff_db --document-private-items
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
|
||||
9
.github/workflows/publish-docs.yml
vendored
9
.github/workflows/publish-docs.yml
vendored
@@ -34,10 +34,10 @@ jobs:
|
||||
- name: "Set docs version"
|
||||
run: |
|
||||
version="${{ (inputs.plan != '' && fromJson(inputs.plan).announcement_tag) || inputs.ref }}"
|
||||
# if version is missing, exit with error
|
||||
if [[ -z "$version" ]]; then
|
||||
echo "Can't build docs without a version."
|
||||
exit 1
|
||||
# if version is missing, use 'latest'
|
||||
if [ -z "$version" ]; then
|
||||
echo "Using 'latest' as version"
|
||||
version="latest"
|
||||
fi
|
||||
|
||||
# Use version as display name for now
|
||||
@@ -145,6 +145,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.ASTRAL_DOCS_PAT }}
|
||||
run: |
|
||||
branch_name="${{ env.branch_name }}"
|
||||
|
||||
# auto-merge the PR if the build was triggered by a release. Manual builds should be reviewed by a human.
|
||||
# give the PR a few seconds to be created before trying to auto-merge it
|
||||
sleep 10
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -1,3 +1,5 @@
|
||||
# This file was autogenerated by cargo-dist: https://opensource.axo.dev/cargo-dist/
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
@@ -64,7 +66,7 @@ jobs:
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.18.0/cargo-dist-installer.sh | sh"
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.22.1/cargo-dist-installer.sh | sh"
|
||||
- name: Cache cargo-dist
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
|
||||
@@ -14,6 +14,9 @@ MD041: false
|
||||
# MD013/line-length
|
||||
MD013: false
|
||||
|
||||
# MD014/commands-show-output
|
||||
MD014: false
|
||||
|
||||
# MD024/no-duplicate-heading
|
||||
MD024:
|
||||
# Allow when nested under different parents e.g. CHANGELOG.md
|
||||
|
||||
@@ -6,6 +6,8 @@ exclude: |
|
||||
crates/red_knot_workspace/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
crates/ruff_notebook/resources/.*|
|
||||
crates/ruff_server/resources/.*|
|
||||
crates/ruff/resources/.*|
|
||||
crates/ruff_python_formatter/resources/.*|
|
||||
crates/ruff_python_formatter/tests/snapshots/.*|
|
||||
@@ -15,7 +17,7 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.18
|
||||
rev: v0.19
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -43,7 +45,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.23.6
|
||||
rev: v1.24.5
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -57,7 +59,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.6
|
||||
rev: v0.6.5
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
@@ -1,5 +1,43 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.6.0
|
||||
|
||||
- Detect imports in `src` layouts by default for `isort` rules ([#12848](https://github.com/astral-sh/ruff/pull/12848))
|
||||
|
||||
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments ([#12838](https://github.com/astral-sh/ruff/pull/12838)).
|
||||
|
||||
- Lint and format Jupyter Notebook by default ([#12878](https://github.com/astral-sh/ruff/pull/12878)).
|
||||
|
||||
You can disable specific rules for notebooks using [`per-file-ignores`](https://docs.astral.sh/ruff/settings/#lint_per-file-ignores):
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"*.ipynb" = ["E501"] # disable line-too-long in notebooks
|
||||
```
|
||||
|
||||
If you'd prefer to either only lint or only format Jupyter Notebook files, you can use the
|
||||
section-specific `exclude` option to do so. For example, the following would only lint Jupyter
|
||||
Notebook files and not format them:
|
||||
|
||||
```toml
|
||||
[tool.ruff.format]
|
||||
exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
And, conversely, the following would only format Jupyter Notebook files and not lint them:
|
||||
|
||||
```toml
|
||||
[tool.ruff.lint]
|
||||
exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
You can completely disable Jupyter Notebook support by updating the [`extend-exclude`](https://docs.astral.sh/ruff/settings/#extend-exclude) setting:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
extend-exclude = ["*.ipynb"]
|
||||
```
|
||||
|
||||
## 0.5.0
|
||||
|
||||
- Follow the XDG specification to discover user-level configurations on macOS (same as on other Unix platforms)
|
||||
|
||||
256
CHANGELOG.md
256
CHANGELOG.md
@@ -1,5 +1,261 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.6
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`refurb`\] Skip `slice-to-remove-prefix-or-suffix` (`FURB188`) when non-trivial slice steps are present ([#13405](https://github.com/astral-sh/ruff/pull/13405))
|
||||
- Add a subcommand to generate dependency graphs ([#13402](https://github.com/astral-sh/ruff/pull/13402))
|
||||
|
||||
### Formatter
|
||||
|
||||
- Fix placement of inline parameter comments ([#13379](https://github.com/astral-sh/ruff/pull/13379))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix off-by one error in the `LineIndex::offset` calculation ([#13407](https://github.com/astral-sh/ruff/pull/13407))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`fastapi`\] Respect FastAPI aliases in route definitions ([#13394](https://github.com/astral-sh/ruff/pull/13394))
|
||||
- \[`pydocstyle`\] Respect word boundaries when detecting function signature in docs ([#13388](https://github.com/astral-sh/ruff/pull/13388))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add backlinks to rule overview linter ([#13368](https://github.com/astral-sh/ruff/pull/13368))
|
||||
- Fix documentation for editor vim plugin ALE ([#13348](https://github.com/astral-sh/ruff/pull/13348))
|
||||
- Fix rendering of `FURB188` docs ([#13406](https://github.com/astral-sh/ruff/pull/13406))
|
||||
|
||||
## 0.6.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`pydoclint`\] Ignore `DOC201` when function name is "**new**" ([#13300](https://github.com/astral-sh/ruff/pull/13300))
|
||||
- \[`refurb`\] Implement `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#13256](https://github.com/astral-sh/ruff/pull/13256))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`eradicate`\] Ignore script-comments with multiple end-tags (`ERA001`) ([#13283](https://github.com/astral-sh/ruff/pull/13283))
|
||||
- \[`pyflakes`\] Improve error message for `UndefinedName` when a builtin was added in a newer version than specified in Ruff config (`F821`) ([#13293](https://github.com/astral-sh/ruff/pull/13293))
|
||||
|
||||
### Server
|
||||
|
||||
- Add support for extensionless Python files for server ([#13326](https://github.com/astral-sh/ruff/pull/13326))
|
||||
- Fix configuration inheritance for configurations specified in the LSP settings ([#13285](https://github.com/astral-sh/ruff/pull/13285))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`ruff`\] Handle unary operators in `decimal-from-float-literal` (`RUF032`) ([#13275](https://github.com/astral-sh/ruff/pull/13275))
|
||||
|
||||
### CLI
|
||||
|
||||
- Only include rules with diagnostics in SARIF metadata ([#13268](https://github.com/astral-sh/ruff/pull/13268))
|
||||
|
||||
### Playground
|
||||
|
||||
- Add "Copy as pyproject.toml/ruff.toml" and "Paste from TOML" ([#13328](https://github.com/astral-sh/ruff/pull/13328))
|
||||
- Fix errors not shown for restored snippet on page load ([#13262](https://github.com/astral-sh/ruff/pull/13262))
|
||||
|
||||
## 0.6.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-builtins`\] Use dynamic builtins list based on Python version ([#13172](https://github.com/astral-sh/ruff/pull/13172))
|
||||
- \[`pydoclint`\] Permit yielding `None` in `DOC402` and `DOC403` ([#13148](https://github.com/astral-sh/ruff/pull/13148))
|
||||
- \[`pylint`\] Update diagnostic message for `PLW3201` ([#13194](https://github.com/astral-sh/ruff/pull/13194))
|
||||
- \[`ruff`\] Implement `post-init-default` (`RUF033`) ([#13192](https://github.com/astral-sh/ruff/pull/13192))
|
||||
- \[`ruff`\] Implement useless if-else (`RUF034`) ([#13218](https://github.com/astral-sh/ruff/pull/13218))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-pyi`\] Respect `pep8_naming.classmethod-decorators` settings when determining if a method is a classmethod in `custom-type-var-return-type` (`PYI019`) ([#13162](https://github.com/astral-sh/ruff/pull/13162))
|
||||
- \[`flake8-pyi`\] Teach various rules that annotations might be stringized ([#12951](https://github.com/astral-sh/ruff/pull/12951))
|
||||
- \[`pylint`\] Avoid `no-self-use` for `attrs`-style validators ([#13166](https://github.com/astral-sh/ruff/pull/13166))
|
||||
- \[`pylint`\] Recurse into subscript subexpressions when searching for list/dict lookups (`PLR1733`, `PLR1736`) ([#13186](https://github.com/astral-sh/ruff/pull/13186))
|
||||
- \[`pyupgrade`\] Detect `aiofiles.open` calls in `UP015` ([#13173](https://github.com/astral-sh/ruff/pull/13173))
|
||||
- \[`pyupgrade`\] Mark `sys.version_info[0] < 3` and similar comparisons as outdated (`UP036`) ([#13175](https://github.com/astral-sh/ruff/pull/13175))
|
||||
|
||||
### CLI
|
||||
|
||||
- Enrich messages of SARIF results ([#13180](https://github.com/astral-sh/ruff/pull/13180))
|
||||
- Handle singular case for incompatible rules warning in `ruff format` output ([#13212](https://github.com/astral-sh/ruff/pull/13212))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`pydocstyle`\] Improve heuristics for detecting Google-style docstrings ([#13142](https://github.com/astral-sh/ruff/pull/13142))
|
||||
- \[`refurb`\] Treat `sep` arguments with effects as unsafe removals (`FURB105`) ([#13165](https://github.com/astral-sh/ruff/pull/13165))
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with `dbm.sqlite3` (`SIM115`) ([#13104](https://github.com/astral-sh/ruff/pull/13104))
|
||||
- \[`pycodestyle`\] Disable `E741` in stub files (`.pyi`) ([#13119](https://github.com/astral-sh/ruff/pull/13119))
|
||||
- \[`pydoclint`\] Avoid `DOC201` on explicit returns in functions that only return `None` ([#13064](https://github.com/astral-sh/ruff/pull/13064))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-async`\] Disable check for `asyncio` before Python 3.11 (`ASYNC109`) ([#13023](https://github.com/astral-sh/ruff/pull/13023))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`FastAPI`\] Avoid introducing invalid syntax in fix for `fast-api-non-annotated-dependency` (`FAST002`) ([#13133](https://github.com/astral-sh/ruff/pull/13133))
|
||||
- \[`flake8-implicit-str-concat`\] Normalize octals before merging concatenated strings in `single-line-implicit-string-concatenation` (`ISC001`) ([#13118](https://github.com/astral-sh/ruff/pull/13118))
|
||||
- \[`flake8-pytest-style`\] Improve help message for `pytest-incorrect-mark-parentheses-style` (`PT023`) ([#13092](https://github.com/astral-sh/ruff/pull/13092))
|
||||
- \[`pylint`\] Avoid autofix for calls that aren't `min` or `max` as starred expression (`PLW3301`) ([#13089](https://github.com/astral-sh/ruff/pull/13089))
|
||||
- \[`ruff`\] Add `datetime.time`, `datetime.tzinfo`, and `datetime.timezone` as immutable function calls (`RUF009`) ([#13109](https://github.com/astral-sh/ruff/pull/13109))
|
||||
- \[`ruff`\] Extend comment deletion for `RUF100` to include trailing text from `noqa` directives while preserving any following comments on the same line, if any ([#13105](https://github.com/astral-sh/ruff/pull/13105))
|
||||
- Fix dark theme on initial page load for the Ruff playground ([#13077](https://github.com/astral-sh/ruff/pull/13077))
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Extend `open-file-with-context-handler` to work with other standard-library IO modules (`SIM115`) ([#12959](https://github.com/astral-sh/ruff/pull/12959))
|
||||
- \[`ruff`\] Avoid `unused-async` for functions with FastAPI route decorator (`RUF029`) ([#12938](https://github.com/astral-sh/ruff/pull/12938))
|
||||
- \[`ruff`\] Ignore `fstring-missing-syntax` (`RUF027`) for `fastAPI` paths ([#12939](https://github.com/astral-sh/ruff/pull/12939))
|
||||
- \[`ruff`\] Implement check for Decimal called with a float literal (RUF032) ([#12909](https://github.com/astral-sh/ruff/pull/12909))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-bugbear`\] Update diagnostic message when expression is at the end of function (`B015`) ([#12944](https://github.com/astral-sh/ruff/pull/12944))
|
||||
- \[`flake8-pyi`\] Skip type annotations in `string-or-bytes-too-long` (`PYI053`) ([#13002](https://github.com/astral-sh/ruff/pull/13002))
|
||||
- \[`flake8-type-checking`\] Always recognise relative imports as first-party ([#12994](https://github.com/astral-sh/ruff/pull/12994))
|
||||
- \[`flake8-unused-arguments`\] Ignore unused arguments on stub functions (`ARG001`) ([#12966](https://github.com/astral-sh/ruff/pull/12966))
|
||||
- \[`pylint`\] Ignore augmented assignment for `self-cls-assignment` (`PLW0642`) ([#12957](https://github.com/astral-sh/ruff/pull/12957))
|
||||
|
||||
### Server
|
||||
|
||||
- Show full context in error log messages ([#13029](https://github.com/astral-sh/ruff/pull/13029))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`pep8-naming`\] Don't flag `from` imports following conventional import names (`N817`) ([#12946](https://github.com/astral-sh/ruff/pull/12946))
|
||||
- \[`pylint`\] - Allow `__new__` methods to have `cls` as their first argument even if decorated with `@staticmethod` for `bad-staticmethod-argument` (`PLW0211`) ([#12958](https://github.com/astral-sh/ruff/pull/12958))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add `hyperfine` installation instructions; update `hyperfine` code samples ([#13034](https://github.com/astral-sh/ruff/pull/13034))
|
||||
- Expand note to use Ruff with other language server in Kate ([#12806](https://github.com/astral-sh/ruff/pull/12806))
|
||||
- Update example for `PT001` as per the new default behavior ([#13019](https://github.com/astral-sh/ruff/pull/13019))
|
||||
- \[`perflint`\] Improve docs for `try-except-in-loop` (`PERF203`) ([#12947](https://github.com/astral-sh/ruff/pull/12947))
|
||||
- \[`pydocstyle`\] Add reference to `lint.pydocstyle.ignore-decorators` setting to rule docs ([#12996](https://github.com/astral-sh/ruff/pull/12996))
|
||||
|
||||
## 0.6.1
|
||||
|
||||
This is a hotfix release to address an issue with `ruff-pre-commit`. In v0.6,
|
||||
Ruff changed its behavior to lint and format Jupyter notebooks by default;
|
||||
however, due to an oversight, these files were still excluded by default if
|
||||
Ruff was run via pre-commit, leading to inconsistent behavior.
|
||||
This has [now been fixed](https://github.com/astral-sh/ruff-pre-commit/pull/96).
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`fastapi`\] Implement `fast-api-unused-path-parameter` (`FAST003`) ([#12638](https://github.com/astral-sh/ruff/pull/12638))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`pylint`\] Rename `too-many-positional` to `too-many-positional-arguments` (`R0917`) ([#12905](https://github.com/astral-sh/ruff/pull/12905))
|
||||
|
||||
### Server
|
||||
|
||||
- Fix crash when applying "fix-all" code-action to notebook cells ([#12929](https://github.com/astral-sh/ruff/pull/12929))
|
||||
|
||||
### Other changes
|
||||
|
||||
- \[`flake8-naming`\]: Respect import conventions (`N817`) ([#12922](https://github.com/astral-sh/ruff/pull/12922))
|
||||
|
||||
## 0.6.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.6.0) for a migration guide and overview of the changes!
|
||||
|
||||
### Breaking changes
|
||||
|
||||
See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
- Lint and format Jupyter Notebook by default ([#12878](https://github.com/astral-sh/ruff/pull/12878)).
|
||||
- Detect imports in `src` layouts by default for `isort` rules ([#12848](https://github.com/astral-sh/ruff/pull/12848))
|
||||
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments ([#12838](https://github.com/astral-sh/ruff/pull/12838)).
|
||||
|
||||
### Deprecations
|
||||
|
||||
The following rules are now deprecated:
|
||||
|
||||
- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`)
|
||||
- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`)
|
||||
- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`)
|
||||
|
||||
### Remapped rules
|
||||
|
||||
The following rules have been remapped to new rule codes:
|
||||
|
||||
- [`unnecessary-dict-comprehension-for-iterable`](https://docs.astral.sh/ruff/rules/unnecessary-dict-comprehension-for-iterable/): `RUF025` to `C420`
|
||||
|
||||
### Stabilization
|
||||
|
||||
The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
- [`singledispatch-method`](https://docs.astral.sh/ruff/rules/singledispatch-method/) (`PLE1519`)
|
||||
- [`singledispatchmethod-function`](https://docs.astral.sh/ruff/rules/singledispatchmethod-function/) (`PLE1520`)
|
||||
- [`bad-staticmethod-argument`](https://docs.astral.sh/ruff/rules/bad-staticmethod-argument/) (`PLW0211`)
|
||||
- [`if-stmt-min-max`](https://docs.astral.sh/ruff/rules/if-stmt-min-max/) (`PLR1730`)
|
||||
- [`invalid-bytes-return-type`](https://docs.astral.sh/ruff/rules/invalid-bytes-return-type/) (`PLE0308`)
|
||||
- [`invalid-hash-return-type`](https://docs.astral.sh/ruff/rules/invalid-hash-return-type/) (`PLE0309`)
|
||||
- [`invalid-index-return-type`](https://docs.astral.sh/ruff/rules/invalid-index-return-type/) (`PLE0305`)
|
||||
- [`invalid-length-return-type`](https://docs.astral.sh/ruff/rules/invalid-length-return-type/) (`PLEE303`)
|
||||
- [`self-or-cls-assignment`](https://docs.astral.sh/ruff/rules/self-or-cls-assignment/) (`PLW0642`)
|
||||
- [`byte-string-usage`](https://docs.astral.sh/ruff/rules/byte-string-usage/) (`PYI057`)
|
||||
- [`duplicate-literal-member`](https://docs.astral.sh/ruff/rules/duplicate-literal-member/) (`PYI062`)
|
||||
- [`redirected-noqa`](https://docs.astral.sh/ruff/rules/redirected-noqa/) (`RUF101`)
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`cancel-scope-no-checkpoint`](https://docs.astral.sh/ruff/rules/cancel-scope-no-checkpoint/) (`ASYNC100`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-function-with-timeout`](https://docs.astral.sh/ruff/rules/async-function-with-timeout/) (`ASYNC109`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-busy-wait`](https://docs.astral.sh/ruff/rules/async-busy-wait/) (`ASYNC110`): Support `asyncio` and `anyio` context mangers.
|
||||
- [`async-zero-sleep`](https://docs.astral.sh/ruff/rules/async-zero-sleep/) (`ASYNC115`): Support `anyio` context mangers.
|
||||
- [`long-sleep-not-forever`](https://docs.astral.sh/ruff/rules/long-sleep-not-forever/) (`ASYNC116`): Support `anyio` context mangers.
|
||||
|
||||
The following fixes have been stabilized:
|
||||
|
||||
- [`superfluous-else-return`](https://docs.astral.sh/ruff/rules/superfluous-else-return/) (`RET505`)
|
||||
- [`superfluous-else-raise`](https://docs.astral.sh/ruff/rules/superfluous-else-raise/) (`RET506`)
|
||||
- [`superfluous-else-continue`](https://docs.astral.sh/ruff/rules/superfluous-else-continue/) (`RET507`)
|
||||
- [`superfluous-else-break`](https://docs.astral.sh/ruff/rules/superfluous-else-break/) (`RET508`)
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-simplify`\] Further simplify to binary in preview for (`SIM108`) ([#12796](https://github.com/astral-sh/ruff/pull/12796))
|
||||
- \[`pyupgrade`\] Show violations without auto-fix (`UP031`) ([#11229](https://github.com/astral-sh/ruff/pull/11229))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-import-conventions`\] Add `xml.etree.ElementTree` to default conventions ([#12455](https://github.com/astral-sh/ruff/pull/12455))
|
||||
- \[`flake8-pytest-style`\] Add a space after comma in CSV output (`PT006`) ([#12853](https://github.com/astral-sh/ruff/pull/12853))
|
||||
|
||||
### Server
|
||||
|
||||
- Show a message for incorrect settings ([#12781](https://github.com/astral-sh/ruff/pull/12781))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-async`\] Do not lint yield in context manager (`ASYNC100`) ([#12896](https://github.com/astral-sh/ruff/pull/12896))
|
||||
- \[`flake8-comprehensions`\] Do not lint `async for` comprehensions (`C419`) ([#12895](https://github.com/astral-sh/ruff/pull/12895))
|
||||
- \[`flake8-return`\] Only add return `None` at end of a function (`RET503`) ([#11074](https://github.com/astral-sh/ruff/pull/11074))
|
||||
- \[`flake8-type-checking`\] Avoid treating `dataclasses.KW_ONLY` as typing-only (`TCH003`) ([#12863](https://github.com/astral-sh/ruff/pull/12863))
|
||||
- \[`pep8-naming`\] Treat `type(Protocol)` et al as metaclass base (`N805`) ([#12770](https://github.com/astral-sh/ruff/pull/12770))
|
||||
- \[`pydoclint`\] Don't enforce returns and yields in abstract methods (`DOC201`, `DOC202`) ([#12771](https://github.com/astral-sh/ruff/pull/12771))
|
||||
- \[`ruff`\] Skip tuples with slice expressions in (`RUF031`) ([#12768](https://github.com/astral-sh/ruff/pull/12768))
|
||||
- \[`ruff`\] Ignore unparenthesized tuples in subscripts when the subscript is a type annotation or type alias (`RUF031`) ([#12762](https://github.com/astral-sh/ruff/pull/12762))
|
||||
- \[`ruff`\] Ignore template strings passed to logging and `builtins._()` calls (`RUF027`) ([#12889](https://github.com/astral-sh/ruff/pull/12889))
|
||||
- \[`ruff`\] Do not remove parens for tuples with starred expressions in Python \<=3.10 (`RUF031`) ([#12784](https://github.com/astral-sh/ruff/pull/12784))
|
||||
- Evaluate default parameter values for a function in that function's enclosing scope ([#12852](https://github.com/astral-sh/ruff/pull/12852))
|
||||
|
||||
### Other changes
|
||||
|
||||
- Respect VS Code cell metadata when detecting the language of Jupyter Notebook cells ([#12864](https://github.com/astral-sh/ruff/pull/12864))
|
||||
- Respect `kernelspec` notebook metadata when detecting the preferred language for a Jupyter Notebook ([#12875](https://github.com/astral-sh/ruff/pull/12875))
|
||||
|
||||
## 0.5.7
|
||||
|
||||
### Preview features
|
||||
|
||||
@@ -2,35 +2,6 @@
|
||||
|
||||
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
|
||||
|
||||
- [The Basics](#the-basics)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Development](#development)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Example: Adding a new lint rule](#example-adding-a-new-lint-rule)
|
||||
- [Rule naming convention](#rule-naming-convention)
|
||||
- [Rule testing: fixtures and snapshots](#rule-testing-fixtures-and-snapshots)
|
||||
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
|
||||
- [MkDocs](#mkdocs)
|
||||
- [Release Process](#release-process)
|
||||
- [Creating a new release](#creating-a-new-release)
|
||||
- [Ecosystem CI](#ecosystem-ci)
|
||||
- [Benchmarking and Profiling](#benchmarking-and-profiling)
|
||||
- [CPython Benchmark](#cpython-benchmark)
|
||||
- [Microbenchmarks](#microbenchmarks)
|
||||
- [Benchmark-driven Development](#benchmark-driven-development)
|
||||
- [PR Summary](#pr-summary)
|
||||
- [Tips](#tips)
|
||||
- [Profiling Projects](#profiling-projects)
|
||||
- [Linux](#linux)
|
||||
- [Mac](#mac)
|
||||
- [`cargo dev`](#cargo-dev)
|
||||
- [Subsystems](#subsystems)
|
||||
- [Compilation Pipeline](#compilation-pipeline)
|
||||
- [Import Categorization](#import-categorization)
|
||||
- [Project root](#project-root)
|
||||
- [Package root](#package-root)
|
||||
- [Import categorization](#import-categorization-1)
|
||||
|
||||
## The Basics
|
||||
|
||||
Ruff welcomes contributions in the form of pull requests.
|
||||
@@ -333,22 +304,34 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
|
||||
### Creating a new release
|
||||
|
||||
1. Install `uv`: `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
||||
|
||||
1. Run `./scripts/release.sh`; this command will:
|
||||
|
||||
- Generate a temporary virtual environment with `rooster`
|
||||
- Generate a changelog entry in `CHANGELOG.md`
|
||||
- Update versions in `pyproject.toml` and `Cargo.toml`
|
||||
- Update references to versions in the `README.md` and documentation
|
||||
- Display contributors for the release
|
||||
|
||||
1. The changelog should then be editorialized for consistency
|
||||
|
||||
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
|
||||
- Changes should be edited to be user-facing descriptions, avoiding internal details
|
||||
|
||||
1. Highlight any breaking changes in `BREAKING_CHANGES.md`
|
||||
|
||||
1. Run `cargo check`. This should update the lock file with new versions.
|
||||
|
||||
1. Create a pull request with the changelog and version updates
|
||||
|
||||
1. Merge the PR
|
||||
|
||||
1. Run the [release workflow](https://github.com/astral-sh/ruff/actions/workflows/release.yml) with:
|
||||
|
||||
- The new version number (without starting `v`)
|
||||
|
||||
1. The release workflow will do the following:
|
||||
|
||||
1. Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or
|
||||
uploaded anything, you can restart after pushing a fix. If you just need to rerun the build,
|
||||
make sure you're [re-running all the failed
|
||||
@@ -359,14 +342,25 @@ even patch releases may contain [non-backwards-compatible changes](https://semve
|
||||
1. Attach artifacts to draft GitHub release
|
||||
1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any
|
||||
downstream jobs manually if needed.
|
||||
|
||||
1. Verify the GitHub release:
|
||||
|
||||
1. The Changelog should match the content of `CHANGELOG.md`
|
||||
1. Append the contributors from the `bump.sh` script
|
||||
1. Append the contributors from the `scripts/release.sh` script
|
||||
|
||||
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
|
||||
|
||||
1. One can determine if an update is needed when
|
||||
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
|
||||
1. Once run successfully, you should follow the link in the output to create a PR.
|
||||
1. If needed, update the `ruff-lsp` and `ruff-vscode` repositories.
|
||||
|
||||
1. If needed, update the [`ruff-lsp`](https://github.com/astral-sh/ruff-lsp) and
|
||||
[`ruff-vscode`](https://github.com/astral-sh/ruff-vscode) repositories and follow
|
||||
the release instructions in those repositories. `ruff-lsp` should always be updated
|
||||
before `ruff-vscode`.
|
||||
|
||||
This step is generally not required for a patch release, but should always be done
|
||||
for a minor release.
|
||||
|
||||
## Ecosystem CI
|
||||
|
||||
@@ -389,7 +383,7 @@ We have several ways of benchmarking and profiling Ruff:
|
||||
- Microbenchmarks which run the linter or the formatter on individual files. These run on pull requests.
|
||||
- Profiling the linter on either the microbenchmarks or entire projects
|
||||
|
||||
> \[!NOTE\]
|
||||
> **Note**
|
||||
> When running benchmarks, ensure that your CPU is otherwise idle (e.g., close any background
|
||||
> applications, like web browsers). You may also want to switch your CPU to a "performance"
|
||||
> mode, if it exists, especially when benchmarking short-lived processes.
|
||||
@@ -403,12 +397,18 @@ which makes it a good target for benchmarking.
|
||||
git clone --branch 3.10 https://github.com/python/cpython.git crates/ruff_linter/resources/test/cpython
|
||||
```
|
||||
|
||||
Install `hyperfine`:
|
||||
|
||||
```shell
|
||||
cargo install hyperfine
|
||||
```
|
||||
|
||||
To benchmark the release build:
|
||||
|
||||
```shell
|
||||
cargo build --release && hyperfine --warmup 10 \
|
||||
"./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache -e" \
|
||||
"./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ -e"
|
||||
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e" \
|
||||
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ -e"
|
||||
|
||||
Benchmark 1: ./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache
|
||||
Time (mean ± σ): 293.8 ms ± 3.2 ms [User: 2384.6 ms, System: 90.3 ms]
|
||||
@@ -427,7 +427,7 @@ To benchmark against the ecosystem's existing tools:
|
||||
|
||||
```shell
|
||||
hyperfine --ignore-failure --warmup 5 \
|
||||
"./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache" \
|
||||
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache" \
|
||||
"pyflakes crates/ruff_linter/resources/test/cpython" \
|
||||
"autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
|
||||
"pycodestyle crates/ruff_linter/resources/test/cpython" \
|
||||
@@ -473,7 +473,7 @@ To benchmark a subset of rules, e.g. `LineTooLong` and `DocLineTooLong`:
|
||||
|
||||
```shell
|
||||
cargo build --release && hyperfine --warmup 10 \
|
||||
"./target/release/ruff ./crates/ruff_linter/resources/test/cpython/ --no-cache -e --select W505,E501"
|
||||
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e --select W505,E501"
|
||||
```
|
||||
|
||||
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
|
||||
@@ -530,6 +530,8 @@ You can run the benchmarks with
|
||||
cargo benchmark
|
||||
```
|
||||
|
||||
`cargo benchmark` is an alias for `cargo bench -p ruff_benchmark --bench linter --bench formatter --`
|
||||
|
||||
#### Benchmark-driven Development
|
||||
|
||||
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
|
||||
@@ -568,7 +570,7 @@ cargo install critcmp
|
||||
|
||||
#### Tips
|
||||
|
||||
- Use `cargo bench -p ruff_benchmark <filter>` to only run specific benchmarks. For example: `cargo benchmark lexer`
|
||||
- Use `cargo bench -p ruff_benchmark <filter>` to only run specific benchmarks. For example: `cargo bench -p ruff_benchmark lexer`
|
||||
to only run the lexer benchmarks.
|
||||
- Use `cargo bench -p ruff_benchmark -- --quiet` for a more cleaned up output (without statistical relevance)
|
||||
- Use `cargo bench -p ruff_benchmark -- --quick` to get faster results (more prone to noise)
|
||||
@@ -911,9 +913,5 @@ There are three ways in which an import can be categorized as "first-party":
|
||||
the `src` setting and, for each directory, check for the existence of a subdirectory `foo` or a
|
||||
file `foo.py`.
|
||||
|
||||
By default, `src` is set to the project root. In the above example, we'd want to set
|
||||
`src = ["./src"]` to ensure that we locate `./my_project/src/foo` and thus categorize `import foo`
|
||||
as first-party in `baz.py`. In practice, for this limited example, setting `src = ["./src"]` is
|
||||
unnecessary, as all imports within `./my_project/src/foo` would be categorized as first-party via
|
||||
the same-package heuristic; but if your project contains multiple packages, you'll want to set `src`
|
||||
explicitly.
|
||||
By default, `src` is set to the project root, along with `"src"` subdirectory in the project root.
|
||||
This ensures that Ruff supports both flat and "src" layouts out of the box.
|
||||
|
||||
484
Cargo.lock
generated
484
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
19
Cargo.toml
19
Cargo.toml
@@ -17,6 +17,7 @@ ruff_cache = { path = "crates/ruff_cache" }
|
||||
ruff_db = { path = "crates/ruff_db" }
|
||||
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
|
||||
ruff_formatter = { path = "crates/ruff_formatter" }
|
||||
ruff_graph = { path = "crates/ruff_graph" }
|
||||
ruff_index = { path = "crates/ruff_index" }
|
||||
ruff_linter = { path = "crates/ruff_linter" }
|
||||
ruff_macros = { path = "crates/ruff_macros" }
|
||||
@@ -42,6 +43,7 @@ red_knot_workspace = { path = "crates/red_knot_workspace" }
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
annotate-snippets = { version = "0.9.2", features = ["color"] }
|
||||
anyhow = { version = "1.0.80" }
|
||||
assert_fs = { version = "1.1.0" }
|
||||
argfile = { version = "0.2.0" }
|
||||
bincode = { version = "1.3.3" }
|
||||
bitflags = { version = "2.5.0" }
|
||||
@@ -68,6 +70,7 @@ fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
globwalk = { version = "0.9.1" }
|
||||
hashbrown = "0.14.3"
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
@@ -102,13 +105,13 @@ pep440_rs = { version = "0.6.0", features = ["serde"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.79" }
|
||||
pyproject-toml = { version = "0.9.0" }
|
||||
quick-junit = { version = "0.4.0" }
|
||||
quick-junit = { version = "0.5.0" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/MichaReiser/salsa.git", tag = "red-knot-0.0.1" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "f608ff8b24f07706492027199f51132244034f29" }
|
||||
schemars = { version = "0.8.16" }
|
||||
seahash = { version = "4.1.0" }
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
@@ -230,9 +233,9 @@ inherits = "release"
|
||||
# Config for 'cargo dist'
|
||||
[workspace.metadata.dist]
|
||||
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.18.0"
|
||||
cargo-dist-version = "0.22.1"
|
||||
# CI backends to support
|
||||
ci = ["github"]
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
installers = ["shell", "powershell"]
|
||||
# The archive format to use for windows builds (defaults .zip)
|
||||
@@ -263,11 +266,11 @@ targets = [
|
||||
auto-includes = false
|
||||
# Whether cargo-dist should create a GitHub Release or use an existing draft
|
||||
create-release = true
|
||||
# Publish jobs to run in CI
|
||||
# Which actions to run on pull requests
|
||||
pr-run-mode = "skip"
|
||||
# Whether CI should trigger releases with dispatches instead of tag pushes
|
||||
dispatch-releases = true
|
||||
# The stage during which the GitHub Release should be created
|
||||
# Which phase cargo-dist should use to create the GitHub release
|
||||
github-release = "announce"
|
||||
# Whether CI should include auto-generated code to build local artifacts
|
||||
build-local-artifacts = false
|
||||
@@ -275,9 +278,11 @@ build-local-artifacts = false
|
||||
local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Announcement jobs to run in CI
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
|
||||
# Custom permissions for GitHub Jobs
|
||||
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
# Path that installers should place binaries in
|
||||
install-path = "CARGO_HOME"
|
||||
|
||||
22
README.md
22
README.md
@@ -110,7 +110,7 @@ For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
1. [Who's Using Ruff?](#whos-using-ruff)
|
||||
1. [License](#license)
|
||||
|
||||
## Getting Started
|
||||
## Getting Started<a id="getting-started"></a>
|
||||
|
||||
For more, see the [documentation](https://docs.astral.sh/ruff/).
|
||||
|
||||
@@ -136,8 +136,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.5.7/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.5.7/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.6.6/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.6/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,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.5.7
|
||||
rev: v0.6.6
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
- uses: chartboost/ruff-action@v1
|
||||
```
|
||||
|
||||
### Configuration
|
||||
### Configuration<a id="configuration"></a>
|
||||
|
||||
Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file (see:
|
||||
[_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/)
|
||||
@@ -291,7 +291,7 @@ features that may change prior to stabilization.
|
||||
See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format`
|
||||
for more on the linting and formatting commands, respectively.
|
||||
|
||||
## Rules
|
||||
## Rules<a id="rules"></a>
|
||||
|
||||
<!-- Begin section: Rules -->
|
||||
|
||||
@@ -367,21 +367,21 @@ quality tools, including:
|
||||
|
||||
For a complete enumeration of the supported rules, see [_Rules_](https://docs.astral.sh/ruff/rules/).
|
||||
|
||||
## Contributing
|
||||
## Contributing<a id="contributing"></a>
|
||||
|
||||
Contributions are welcome and highly appreciated. To get started, check out the
|
||||
[**contributing guidelines**](https://docs.astral.sh/ruff/contributing/).
|
||||
|
||||
You can also join us on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Support
|
||||
## Support<a id="support"></a>
|
||||
|
||||
Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues),
|
||||
or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new).
|
||||
|
||||
You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh).
|
||||
|
||||
## Acknowledgements
|
||||
## Acknowledgements<a id="acknowledgements"></a>
|
||||
|
||||
Ruff's linter draws on both the APIs and implementation details of many other
|
||||
tools in the Python ecosystem, especially [Flake8](https://github.com/PyCQA/flake8), [Pyflakes](https://github.com/PyCQA/pyflakes),
|
||||
@@ -405,7 +405,7 @@ Ruff is the beneficiary of a large number of [contributors](https://github.com/a
|
||||
|
||||
Ruff is released under the MIT license.
|
||||
|
||||
## Who's Using Ruff?
|
||||
## Who's Using Ruff?<a id="whos-using-ruff"></a>
|
||||
|
||||
Ruff is used by a number of major open-source projects and companies, including:
|
||||
|
||||
@@ -524,7 +524,7 @@ If you're using Ruff, consider adding the Ruff badge to your project's `README.m
|
||||
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff" style="max-width:100%;"></a>
|
||||
```
|
||||
|
||||
## License
|
||||
## License<a id="license"></a>
|
||||
|
||||
This repository is licensed under the [MIT License](https://github.com/astral-sh/ruff/blob/main/LICENSE)
|
||||
|
||||
|
||||
@@ -13,12 +13,17 @@ The CLI supports different verbosity levels.
|
||||
- `-vv` activates `debug!` and timestamps: This should be enough information to get to the bottom of bug reports. When you're processing many packages or files, you'll get pages and pages of output, but each line is link to a specific action or state change.
|
||||
- `-vvv` activates `trace!` (only in debug builds) and shows tracing-spans: At this level, you're logging everything. Most of this is wasted, it's really slow, we dump e.g. the entire resolution graph. Only useful to developers, and you almost certainly want to use `RED_KNOT_LOG` to filter it down to the area your investigating.
|
||||
|
||||
## `RED_KNOT_LOG`
|
||||
## Better logging with `RED_KNOT_LOG` and `RAYON_NUM_THREADS`
|
||||
|
||||
By default, the CLI shows messages from the `ruff` and `red_knot` crates. Tracing messages from other crates are not shown.
|
||||
The `RED_KNOT_LOG` environment variable allows you to customize which messages are shown by specifying one
|
||||
or more [filter directives](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives).
|
||||
|
||||
The `RAYON_NUM_THREADS` environment variable, meanwhile, can be used to control the level of concurrency red-knot uses.
|
||||
By default, red-knot will attempt to parallelize its work so that multiple files are checked simultaneously,
|
||||
but this can result in a confused logging output where messages from different threads are intertwined.
|
||||
To switch off concurrency entirely and have more readable logs, use `RAYON_NUM_THREADS=1`.
|
||||
|
||||
### Examples
|
||||
|
||||
#### Show all debug messages
|
||||
@@ -72,6 +77,26 @@ runs or when restoring from a persistent cache. This can be confusing for users
|
||||
don't understand why a specific lint violation isn't raised. Instead, change your
|
||||
query to return the failure as part of the query's result or use a Salsa accumulator.
|
||||
|
||||
## Tracing in tests
|
||||
|
||||
You can use `ruff_db::testing::setup_logging` or `ruff_db::testing::setup_logging_with_filter` to set up logging in tests.
|
||||
|
||||
```rust
|
||||
use ruff_db::testing::setup_logging;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
let _logging = setup_logging();
|
||||
|
||||
tracing::info!("This message will be printed to stderr");
|
||||
}
|
||||
```
|
||||
|
||||
Note: Most test runners capture stderr and only show its output when a test fails.
|
||||
|
||||
Note also that `setup_logging` only sets up logging for the current thread because [`set_global_default`](https://docs.rs/tracing/latest/tracing/subscriber/fn.set_global_default.html) can only be
|
||||
called **once**.
|
||||
|
||||
## Release builds
|
||||
|
||||
`trace!` events are removed in release builds.
|
||||
|
||||
@@ -5,8 +5,8 @@ use colored::Colorize;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::io::BufWriter;
|
||||
use tracing::log::LevelFilter;
|
||||
use tracing::{Event, Subscriber};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::fmt::format::Writer;
|
||||
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
|
||||
use tracing_subscriber::registry::LookupSpan;
|
||||
@@ -60,10 +60,10 @@ pub(crate) enum VerbosityLevel {
|
||||
impl VerbosityLevel {
|
||||
const fn level_filter(self) -> LevelFilter {
|
||||
match self {
|
||||
VerbosityLevel::Default => LevelFilter::Warn,
|
||||
VerbosityLevel::Verbose => LevelFilter::Info,
|
||||
VerbosityLevel::ExtraVerbose => LevelFilter::Debug,
|
||||
VerbosityLevel::Trace => LevelFilter::Trace,
|
||||
VerbosityLevel::Default => LevelFilter::WARN,
|
||||
VerbosityLevel::Verbose => LevelFilter::INFO,
|
||||
VerbosityLevel::ExtraVerbose => LevelFilter::DEBUG,
|
||||
VerbosityLevel::Trace => LevelFilter::TRACE,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ pub(crate) fn setup_tracing(level: VerbosityLevel) -> anyhow::Result<TracingGuar
|
||||
match level {
|
||||
VerbosityLevel::Default => {
|
||||
// Show warning traces
|
||||
EnvFilter::default().add_directive(tracing::level_filters::LevelFilter::WARN.into())
|
||||
EnvFilter::default().add_directive(LevelFilter::WARN.into())
|
||||
}
|
||||
level => {
|
||||
let level_filter = level.level_filter();
|
||||
|
||||
@@ -7,13 +7,13 @@ use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::site_packages::site_packages_dirs_of_venv;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::program::{ProgramSettings, SearchPathSettings};
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use target_version::TargetVersion;
|
||||
|
||||
@@ -65,15 +65,14 @@ to resolve type information for the project's third-party dependencies.",
|
||||
value_name = "PATH",
|
||||
help = "Additional path to use as a module-resolution source (can be passed multiple times)"
|
||||
)]
|
||||
extra_search_path: Vec<SystemPathBuf>,
|
||||
extra_search_path: Option<Vec<SystemPathBuf>>,
|
||||
|
||||
#[arg(
|
||||
long,
|
||||
help = "Python version to assume when resolving types",
|
||||
default_value_t = TargetVersion::default(),
|
||||
value_name="VERSION")
|
||||
]
|
||||
target_version: TargetVersion,
|
||||
value_name = "VERSION"
|
||||
)]
|
||||
target_version: Option<TargetVersion>,
|
||||
|
||||
#[clap(flatten)]
|
||||
verbosity: Verbosity,
|
||||
@@ -86,6 +85,36 @@ to resolve type information for the project's third-party dependencies.",
|
||||
watch: bool,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn to_configuration(&self, cli_cwd: &SystemPath) -> Configuration {
|
||||
let mut configuration = Configuration::default();
|
||||
|
||||
if let Some(target_version) = self.target_version {
|
||||
configuration.target_version = Some(target_version.into());
|
||||
}
|
||||
|
||||
if let Some(venv_path) = &self.venv_path {
|
||||
configuration.search_paths.site_packages = Some(SitePackages::Derived {
|
||||
venv_path: SystemPath::absolute(venv_path, cli_cwd),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(custom_typeshed_dir) = &self.custom_typeshed_dir {
|
||||
configuration.search_paths.custom_typeshed =
|
||||
Some(SystemPath::absolute(custom_typeshed_dir, cli_cwd));
|
||||
}
|
||||
|
||||
if let Some(extra_search_paths) = &self.extra_search_path {
|
||||
configuration.search_paths.extra_paths = extra_search_paths
|
||||
.iter()
|
||||
.map(|path| Some(SystemPath::absolute(path, cli_cwd)))
|
||||
.collect();
|
||||
}
|
||||
|
||||
configuration
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum Command {
|
||||
/// Start the language server
|
||||
@@ -115,22 +144,13 @@ pub fn main() -> ExitStatus {
|
||||
}
|
||||
|
||||
fn run() -> anyhow::Result<ExitStatus> {
|
||||
let Args {
|
||||
command,
|
||||
current_directory,
|
||||
custom_typeshed_dir,
|
||||
extra_search_path: extra_paths,
|
||||
venv_path,
|
||||
target_version,
|
||||
verbosity,
|
||||
watch,
|
||||
} = Args::parse_from(std::env::args().collect::<Vec<_>>());
|
||||
let args = Args::parse_from(std::env::args().collect::<Vec<_>>());
|
||||
|
||||
if matches!(command, Some(Command::Server)) {
|
||||
if matches!(args.command, Some(Command::Server)) {
|
||||
return run_server().map(|()| ExitStatus::Success);
|
||||
}
|
||||
|
||||
let verbosity = verbosity.level();
|
||||
let verbosity = args.verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
|
||||
@@ -146,10 +166,12 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
})?
|
||||
};
|
||||
|
||||
let cwd = current_directory
|
||||
let cwd = args
|
||||
.current_directory
|
||||
.as_ref()
|
||||
.map(|cwd| {
|
||||
if cwd.as_std_path().is_dir() {
|
||||
Ok(SystemPath::absolute(&cwd, &cli_base_path))
|
||||
Ok(SystemPath::absolute(cwd, &cli_base_path))
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided current-directory path '{cwd}' is not a directory."
|
||||
@@ -160,40 +182,18 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
.unwrap_or_else(|| cli_base_path.clone());
|
||||
|
||||
let system = OsSystem::new(cwd.clone());
|
||||
let workspace_metadata = WorkspaceMetadata::from_path(system.current_directory(), &system)?;
|
||||
|
||||
// TODO: Verify the remaining search path settings eagerly.
|
||||
let site_packages = venv_path
|
||||
.map(|venv_path| {
|
||||
let venv_path = SystemPath::absolute(venv_path, &cli_base_path);
|
||||
|
||||
if system.is_directory(&venv_path) {
|
||||
Ok(site_packages_dirs_of_venv(&venv_path, &system)?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Provided venv-path {venv_path} is not a directory!"
|
||||
))
|
||||
}
|
||||
})
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
|
||||
// TODO: Respect the settings from the workspace metadata. when resolving the program settings.
|
||||
let program_settings = ProgramSettings {
|
||||
target_version: target_version.into(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths,
|
||||
src_root: workspace_metadata.root().to_path_buf(),
|
||||
custom_typeshed: custom_typeshed_dir,
|
||||
site_packages,
|
||||
},
|
||||
};
|
||||
let cli_configuration = args.to_configuration(&cwd);
|
||||
let workspace_metadata = WorkspaceMetadata::from_path(
|
||||
system.current_directory(),
|
||||
&system,
|
||||
Some(cli_configuration.clone()),
|
||||
)?;
|
||||
|
||||
// TODO: Use the `program_settings` to compute the key for the database's persistent
|
||||
// cache and load the cache if it exists.
|
||||
let mut db = RootDatabase::new(workspace_metadata, program_settings, system);
|
||||
let mut db = RootDatabase::new(workspace_metadata, system)?;
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new();
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_configuration);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
@@ -205,7 +205,7 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
}
|
||||
})?;
|
||||
|
||||
let exit_status = if watch {
|
||||
let exit_status = if args.watch {
|
||||
main_loop.watch(&mut db)?
|
||||
} else {
|
||||
main_loop.run(&mut db)
|
||||
@@ -245,10 +245,12 @@ struct MainLoop {
|
||||
|
||||
/// The file system watcher, if running in watch mode.
|
||||
watcher: Option<WorkspaceWatcher>,
|
||||
|
||||
cli_configuration: Configuration,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new() -> (Self, MainLoopCancellationToken) {
|
||||
fn new(cli_configuration: Configuration) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
@@ -256,6 +258,7 @@ impl MainLoop {
|
||||
sender: sender.clone(),
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_configuration,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
@@ -338,7 +341,7 @@ impl MainLoop {
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
revision += 1;
|
||||
// Automatically cancels any pending queries and waits for them to complete.
|
||||
db.apply_changes(changes);
|
||||
db.apply_changes(changes, Some(&self.cli_configuration));
|
||||
if let Some(watcher) = self.watcher.as_mut() {
|
||||
watcher.update(db);
|
||||
}
|
||||
|
||||
@@ -13,22 +13,36 @@ pub enum TargetVersion {
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
ruff_db::program::TargetVersion::from(*self).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for ruff_db::program::TargetVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::Py37,
|
||||
TargetVersion::Py38 => Self::Py38,
|
||||
TargetVersion::Py39 => Self::Py39,
|
||||
TargetVersion::Py310 => Self::Py310,
|
||||
TargetVersion::Py311 => Self::Py311,
|
||||
TargetVersion::Py312 => Self::Py312,
|
||||
TargetVersion::Py313 => Self::Py313,
|
||||
impl TargetVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "py37",
|
||||
Self::Py38 => "py38",
|
||||
Self::Py39 => "py39",
|
||||
Self::Py310 => "py310",
|
||||
Self::Py311 => "py311",
|
||||
Self::Py312 => "py312",
|
||||
Self::Py313 => "py313",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for red_knot_python_semantic::PythonVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => Self::PY37,
|
||||
TargetVersion::Py38 => Self::PY38,
|
||||
TargetVersion::Py39 => Self::PY39,
|
||||
TargetVersion::Py310 => Self::PY310,
|
||||
TargetVersion::Py311 => Self::PY311,
|
||||
TargetVersion::Py312 => Self::PY312,
|
||||
TargetVersion::Py313 => Self::PY313,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,14 @@ use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use salsa::Setter;
|
||||
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName};
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::{directory_watcher, WorkspaceWatcher};
|
||||
use red_knot_workspace::workspace::settings::{Configuration, SearchPathConfiguration};
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::program::{Program, ProgramSettings, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::Upcast;
|
||||
@@ -25,6 +24,7 @@ struct TestCase {
|
||||
/// We need to hold on to it in the test case or the temp files get deleted.
|
||||
_temp_dir: tempfile::TempDir,
|
||||
root_dir: SystemPathBuf,
|
||||
configuration: Configuration,
|
||||
}
|
||||
|
||||
impl TestCase {
|
||||
@@ -40,10 +40,6 @@ impl TestCase {
|
||||
&self.db
|
||||
}
|
||||
|
||||
fn db_mut(&mut self) -> &mut RootDatabase {
|
||||
&mut self.db
|
||||
}
|
||||
|
||||
fn stop_watch(&mut self) -> Vec<watch::ChangeEvent> {
|
||||
self.try_stop_watch(Duration::from_secs(10))
|
||||
.expect("Expected watch changes but observed none.")
|
||||
@@ -104,27 +100,32 @@ impl TestCase {
|
||||
Some(all_events)
|
||||
}
|
||||
|
||||
fn apply_changes(&mut self, changes: Vec<watch::ChangeEvent>) {
|
||||
self.db.apply_changes(changes, Some(&self.configuration));
|
||||
}
|
||||
|
||||
fn update_search_path_settings(
|
||||
&mut self,
|
||||
f: impl FnOnce(&SearchPathSettings) -> SearchPathSettings,
|
||||
) {
|
||||
configuration: SearchPathConfiguration,
|
||||
) -> anyhow::Result<()> {
|
||||
let program = Program::get(self.db());
|
||||
let search_path_settings = program.search_paths(self.db());
|
||||
|
||||
let new_settings = f(search_path_settings);
|
||||
self.configuration.search_paths = configuration.clone();
|
||||
let new_settings = configuration.into_settings(self.db.workspace().root(&self.db));
|
||||
|
||||
program.set_search_paths(&mut self.db).to(new_settings);
|
||||
program.update_search_paths(&mut self.db, &new_settings)?;
|
||||
|
||||
if let Some(watcher) = &mut self.watcher {
|
||||
watcher.update(&self.db);
|
||||
assert!(!watcher.has_errored_paths());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_package_files(&self, path: &SystemPath) -> Vec<File> {
|
||||
let package = self.db().workspace().package(self.db(), path).unwrap();
|
||||
let files = package.files(self.db());
|
||||
let files = files.read();
|
||||
let mut collected: Vec<_> = files.into_iter().collect();
|
||||
collected.sort_unstable_by_key(|file| file.path(self.db()).as_system_path().unwrap());
|
||||
collected
|
||||
@@ -177,17 +178,14 @@ fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
{
|
||||
setup_with_search_paths(setup_files, |_root, workspace_path| SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![],
|
||||
setup_with_search_paths(setup_files, |_root, _workspace_path| {
|
||||
SearchPathConfiguration::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn setup_with_search_paths<F>(
|
||||
setup_files: F,
|
||||
create_search_paths: impl FnOnce(&SystemPath, &SystemPath) -> SearchPathSettings,
|
||||
create_search_paths: impl FnOnce(&SystemPath, &SystemPath) -> SearchPathConfiguration,
|
||||
) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
@@ -219,25 +217,34 @@ where
|
||||
|
||||
let system = OsSystem::new(&workspace_path);
|
||||
|
||||
let workspace = WorkspaceMetadata::from_path(&workspace_path, &system)?;
|
||||
let search_paths = create_search_paths(&root_path, workspace.root());
|
||||
let search_paths = create_search_paths(&root_path, &workspace_path);
|
||||
|
||||
for path in search_paths
|
||||
.extra_paths
|
||||
.iter()
|
||||
.chain(search_paths.site_packages.iter())
|
||||
.flatten()
|
||||
.chain(search_paths.custom_typeshed.iter())
|
||||
.chain(search_paths.site_packages.iter().flat_map(|site_packages| {
|
||||
if let SitePackages::Known(path) = site_packages {
|
||||
path.as_slice()
|
||||
} else {
|
||||
&[]
|
||||
}
|
||||
}))
|
||||
{
|
||||
std::fs::create_dir_all(path.as_std_path())
|
||||
.with_context(|| format!("Failed to create search path '{path}'"))?;
|
||||
}
|
||||
|
||||
let settings = ProgramSettings {
|
||||
target_version: TargetVersion::default(),
|
||||
let configuration = Configuration {
|
||||
target_version: Some(PythonVersion::PY312),
|
||||
search_paths,
|
||||
};
|
||||
|
||||
let db = RootDatabase::new(workspace, settings, system);
|
||||
let workspace =
|
||||
WorkspaceMetadata::from_path(&workspace_path, &system, Some(configuration.clone()))?;
|
||||
|
||||
let db = RootDatabase::new(workspace, system)?;
|
||||
|
||||
let (sender, receiver) = crossbeam::channel::unbounded();
|
||||
let watcher = directory_watcher(move |events| sender.send(events).unwrap())
|
||||
@@ -252,6 +259,7 @@ where
|
||||
watcher: Some(watcher),
|
||||
_temp_dir: temp_dir,
|
||||
root_dir: root_path,
|
||||
configuration,
|
||||
};
|
||||
|
||||
// Sometimes the file watcher reports changes for events that happened before the watcher was started.
|
||||
@@ -304,7 +312,7 @@ fn new_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let foo = case.system_file(&foo_path).expect("foo.py to exist.");
|
||||
|
||||
@@ -327,7 +335,7 @@ fn new_ignored_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(case.system_file(&foo_path).is_ok());
|
||||
assert_eq!(&case.collect_package_files(&bar_path), &[bar_file]);
|
||||
@@ -351,7 +359,7 @@ fn changed_file() -> anyhow::Result<()> {
|
||||
|
||||
assert!(!changes.is_empty());
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
assert_eq!(&case.collect_package_files(&foo_path), &[foo]);
|
||||
@@ -374,7 +382,7 @@ fn deleted_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_package_files(&foo_path), &[] as &[File]);
|
||||
@@ -406,7 +414,7 @@ fn move_file_to_trash() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
assert_eq!(&case.collect_package_files(&foo_path), &[] as &[File]);
|
||||
@@ -438,7 +446,7 @@ fn move_file_to_workspace() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let foo_in_workspace = case.system_file(&foo_in_workspace_path)?;
|
||||
|
||||
@@ -466,7 +474,7 @@ fn rename_file() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(!foo.exists(case.db()));
|
||||
|
||||
@@ -507,7 +515,7 @@ fn directory_moved_to_workspace() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
let init_file = case
|
||||
.system_file(sub_new_path.join("__init__.py"))
|
||||
@@ -558,7 +566,7 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
@@ -612,7 +620,7 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
@@ -677,7 +685,7 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("sub.a").unwrap()).is_none());
|
||||
@@ -691,15 +699,13 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn search_path() -> anyhow::Result<()> {
|
||||
let mut case =
|
||||
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, workspace_path| {
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![root_path.join("site_packages")],
|
||||
}
|
||||
})?;
|
||||
let mut case = setup_with_search_paths(
|
||||
[("bar.py", "import sub.a")],
|
||||
|root_path, _workspace_path| SearchPathConfiguration {
|
||||
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
|
||||
..SearchPathConfiguration::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
let site_packages = case.root_path().join("site_packages");
|
||||
|
||||
@@ -712,7 +718,7 @@ fn search_path() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("a").unwrap()).is_some());
|
||||
assert_eq!(
|
||||
@@ -733,16 +739,17 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("a").unwrap()).is_none());
|
||||
|
||||
// Register site-packages as a search path.
|
||||
case.update_search_path_settings(|settings| SearchPathSettings {
|
||||
site_packages: vec![site_packages.clone()],
|
||||
..settings.clone()
|
||||
});
|
||||
case.update_search_path_settings(SearchPathConfiguration {
|
||||
site_packages: Some(SitePackages::Known(vec![site_packages.clone()])),
|
||||
..SearchPathConfiguration::default()
|
||||
})
|
||||
.expect("Search path settings to be valid");
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db().upcast(), ModuleName::new_static("a").unwrap()).is_some());
|
||||
|
||||
@@ -751,22 +758,21 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn remove_search_path() -> anyhow::Result<()> {
|
||||
let mut case =
|
||||
setup_with_search_paths([("bar.py", "import sub.a")], |root_path, workspace_path| {
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: workspace_path.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![root_path.join("site_packages")],
|
||||
}
|
||||
})?;
|
||||
let mut case = setup_with_search_paths(
|
||||
[("bar.py", "import sub.a")],
|
||||
|root_path, _workspace_path| SearchPathConfiguration {
|
||||
site_packages: Some(SitePackages::Known(vec![root_path.join("site_packages")])),
|
||||
..SearchPathConfiguration::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
// Remove site packages from the search path settings.
|
||||
let site_packages = case.root_path().join("site_packages");
|
||||
case.update_search_path_settings(|settings| SearchPathSettings {
|
||||
site_packages: vec![],
|
||||
..settings.clone()
|
||||
});
|
||||
case.update_search_path_settings(SearchPathConfiguration {
|
||||
site_packages: None,
|
||||
..SearchPathConfiguration::default()
|
||||
})
|
||||
.expect("Search path settings to be valid");
|
||||
|
||||
std::fs::write(site_packages.join("a.py").as_std_path(), "class A: ...")?;
|
||||
|
||||
@@ -777,6 +783,48 @@ fn remove_search_path() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn changed_versions_file() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_search_paths(
|
||||
|root_path: &SystemPath, workspace_path: &SystemPath| {
|
||||
std::fs::write(workspace_path.join("bar.py").as_std_path(), "import sub.a")?;
|
||||
std::fs::create_dir_all(root_path.join("typeshed/stdlib").as_std_path())?;
|
||||
std::fs::write(root_path.join("typeshed/stdlib/VERSIONS").as_std_path(), "")?;
|
||||
std::fs::write(
|
||||
root_path.join("typeshed/stdlib/os.pyi").as_std_path(),
|
||||
"# not important",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|root_path, _workspace_path| SearchPathConfiguration {
|
||||
custom_typeshed: Some(root_path.join("typeshed")),
|
||||
..SearchPathConfiguration::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
// Unset the custom typeshed directory.
|
||||
assert_eq!(
|
||||
resolve_module(case.db(), ModuleName::new("os").unwrap()),
|
||||
None
|
||||
);
|
||||
|
||||
std::fs::write(
|
||||
case.root_path()
|
||||
.join("typeshed/stdlib/VERSIONS")
|
||||
.as_std_path(),
|
||||
"os: 3.0-",
|
||||
)?;
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert!(resolve_module(case.db(), ModuleName::new("os").unwrap()).is_some());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Watch a workspace that contains two files where one file is a hardlink to another.
|
||||
///
|
||||
/// Setup:
|
||||
@@ -823,7 +871,7 @@ fn hard_links_in_workspace() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), foo).as_str(), "print('Version 2')");
|
||||
|
||||
@@ -894,7 +942,7 @@ fn hard_links_to_target_outside_workspace() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(source_text(case.db(), bar).as_str(), "print('Version 2')");
|
||||
|
||||
@@ -933,7 +981,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
foo.permissions(case.db()),
|
||||
@@ -1018,7 +1066,7 @@ mod unix {
|
||||
|
||||
let changes = case.take_watch_changes();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
@@ -1031,7 +1079,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz.file()).as_str(),
|
||||
@@ -1102,7 +1150,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
// The file watcher is guaranteed to emit one event for the changed file, but it isn't specified
|
||||
// if the event is emitted for the "original" or linked path because both paths are watched.
|
||||
@@ -1171,11 +1219,11 @@ mod unix {
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|_root, workspace| SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: workspace.to_path_buf(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![workspace.join(".venv/lib/python3.12/site-packages")],
|
||||
|_root, workspace| SearchPathConfiguration {
|
||||
site_packages: Some(SitePackages::Known(vec![
|
||||
workspace.join(".venv/lib/python3.12/site-packages")
|
||||
])),
|
||||
..SearchPathConfiguration::default()
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -1210,7 +1258,7 @@ mod unix {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
case.db_mut().apply_changes(changes);
|
||||
case.apply_changes(changes);
|
||||
|
||||
assert_eq!(
|
||||
source_text(case.db(), baz_original_file).as_str(),
|
||||
|
||||
@@ -15,8 +15,11 @@ ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
ruff_python_literal = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
@@ -24,22 +27,33 @@ countme = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
path-slash = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true, features = ["zstd", "deflate"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "powerpc64"))'.build-dependencies]
|
||||
zip = { workspace = true, features = ["deflate", "zstd"] }
|
||||
|
||||
[target.'cfg(target_arch = "powerpc64")'.build-dependencies]
|
||||
zip = { workspace = true, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["os", "testing"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
walkdir = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -30,10 +30,17 @@ fn zip_dir(directory_path: &str, writer: File) -> ZipResult<File> {
|
||||
// We can't use `#[cfg(...)]` here because the target-arch in a build script is the
|
||||
// architecture of the system running the build script and not the architecture of the build-target.
|
||||
// That's why we use the `TARGET` environment variable here.
|
||||
let method = if std::env::var("TARGET").unwrap().contains("wasm32") {
|
||||
CompressionMethod::Deflated
|
||||
} else {
|
||||
CompressionMethod::Zstd
|
||||
#[cfg(target_arch = "powerpc64")]
|
||||
let method = CompressionMethod::Deflated;
|
||||
|
||||
#[cfg(not(target_arch = "powerpc64"))]
|
||||
let method = {
|
||||
let target = std::env::var("TARGET").unwrap();
|
||||
if target.contains("wasm32") || target.contains("powerpc64") {
|
||||
CompressionMethod::Deflated
|
||||
} else {
|
||||
CompressionMethod::Zstd
|
||||
}
|
||||
};
|
||||
|
||||
let options = FileOptions::default()
|
||||
|
||||
@@ -31,10 +31,10 @@ impl<T> AstNodeRef<T> {
|
||||
/// which the `AstNodeRef` belongs.
|
||||
///
|
||||
/// ## Safety
|
||||
///
|
||||
/// Dereferencing the `node` can result in undefined behavior if `parsed` isn't the
|
||||
/// [`ParsedModule`] to which `node` belongs. It's the caller's responsibility to ensure that
|
||||
/// the invariant `node belongs to parsed` is upheld.
|
||||
|
||||
pub(super) unsafe fn new(parsed: ParsedModule, node: &T) -> Self {
|
||||
Self {
|
||||
_parsed: parsed,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::Db;
|
||||
|
||||
/// Salsa query to get the builtins scope.
|
||||
///
|
||||
/// Can return None if a custom typeshed is used that is missing `builtins.pyi`.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn builtins_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
let builtins_name =
|
||||
ModuleName::new_static("builtins").expect("Expected 'builtins' to be a valid module name");
|
||||
let builtins_file = resolve_module(db, builtins_name)?.file();
|
||||
Some(global_scope(db, builtins_file))
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
/// Database giving access to semantic information about a Python program.
|
||||
#[salsa::db]
|
||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {}
|
||||
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
|
||||
fn is_file_open(&self, file: File) -> bool;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::module_resolver::vendored_typeshed_stubs;
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
@@ -91,7 +94,11 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl Db for TestDb {}
|
||||
impl Db for TestDb {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for TestDb {
|
||||
|
||||
@@ -4,17 +4,24 @@ use rustc_hash::FxHasher;
|
||||
|
||||
pub use db::Db;
|
||||
pub use module_name::ModuleName;
|
||||
pub use module_resolver::{resolve_module, system_module_search_paths, vendored_typeshed_stubs};
|
||||
pub use module_resolver::{
|
||||
resolve_module, system_module_search_paths, vendored_typeshed_stubs, Module,
|
||||
};
|
||||
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
|
||||
pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod builtins;
|
||||
mod db;
|
||||
mod module_name;
|
||||
mod module_resolver;
|
||||
mod node_key;
|
||||
mod program;
|
||||
mod python_version;
|
||||
pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
pub mod types;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
@@ -168,6 +168,24 @@ impl ModuleName {
|
||||
};
|
||||
Some(Self(name))
|
||||
}
|
||||
|
||||
/// Extend `self` with the components of `other`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use red_knot_python_semantic::ModuleName;
|
||||
///
|
||||
/// let mut module_name = ModuleName::new_static("foo").unwrap();
|
||||
/// module_name.extend(&ModuleName::new_static("bar").unwrap());
|
||||
/// assert_eq!(&module_name, "foo.bar");
|
||||
/// module_name.extend(&ModuleName::new_static("baz.eggs.ham").unwrap());
|
||||
/// assert_eq!(&module_name, "foo.bar.baz.eggs.ham");
|
||||
/// ```
|
||||
pub fn extend(&mut self, other: &ModuleName) {
|
||||
self.0.push('.');
|
||||
self.0.push_str(other);
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for ModuleName {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
pub(crate) use module::Module;
|
||||
pub use module::Module;
|
||||
pub use resolver::resolve_module;
|
||||
pub(crate) use resolver::{file_to_module, SearchPaths};
|
||||
use ruff_db::system::SystemPath;
|
||||
pub use typeshed::vendored_typeshed_stubs;
|
||||
|
||||
use crate::module_resolver::resolver::search_paths;
|
||||
use crate::Db;
|
||||
use resolver::{module_resolution_settings, SearchPathIterator};
|
||||
use resolver::SearchPathIterator;
|
||||
|
||||
mod module;
|
||||
mod path;
|
||||
mod resolver;
|
||||
mod state;
|
||||
mod typeshed;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -20,7 +21,7 @@ mod testing;
|
||||
/// Returns an iterator over all search paths pointing to a system path
|
||||
pub fn system_module_search_paths(db: &dyn Db) -> SystemModuleSearchPathsIter {
|
||||
SystemModuleSearchPathsIter {
|
||||
inner: module_resolution_settings(db).search_paths(db),
|
||||
inner: search_paths(db),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,3 +77,9 @@ pub enum ModuleKind {
|
||||
/// A python package (`foo/__init__.py` or `foo/__init__.pyi`)
|
||||
Package,
|
||||
}
|
||||
|
||||
impl ModuleKind {
|
||||
pub const fn is_package(self) -> bool {
|
||||
matches!(self, ModuleKind::Package)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,11 @@ use ruff_db::files::{system_path_to_file, vendored_path_to_file, File, FileError
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::{VendoredPath, VendoredPathBuf};
|
||||
|
||||
use super::typeshed::{typeshed_versions, TypeshedVersionsParseError, TypeshedVersionsQueryResult};
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
use super::state::ResolverState;
|
||||
use super::typeshed::{TypeshedVersionsParseError, TypeshedVersionsQueryResult};
|
||||
use crate::module_resolver::resolver::ResolverContext;
|
||||
use crate::site_packages::SitePackagesDiscoveryError;
|
||||
|
||||
/// A path that points to a Python module.
|
||||
///
|
||||
@@ -59,8 +59,12 @@ impl ModulePath {
|
||||
self.relative_path.push(component);
|
||||
}
|
||||
|
||||
pub(crate) fn pop(&mut self) -> bool {
|
||||
self.relative_path.pop()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn is_directory(&self, resolver: &ResolverState) -> bool {
|
||||
pub(super) fn is_directory(&self, resolver: &ResolverContext) -> bool {
|
||||
let ModulePath {
|
||||
search_path,
|
||||
relative_path,
|
||||
@@ -74,7 +78,7 @@ impl ModulePath {
|
||||
== Err(FileError::IsADirectory)
|
||||
}
|
||||
SearchPathInner::StandardLibraryCustom(stdlib_root) => {
|
||||
match query_stdlib_version(Some(stdlib_root), relative_path, resolver) {
|
||||
match query_stdlib_version(relative_path, resolver) {
|
||||
TypeshedVersionsQueryResult::DoesNotExist => false,
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
| TypeshedVersionsQueryResult::MaybeExists => {
|
||||
@@ -84,7 +88,7 @@ impl ModulePath {
|
||||
}
|
||||
}
|
||||
SearchPathInner::StandardLibraryVendored(stdlib_root) => {
|
||||
match query_stdlib_version(None, relative_path, resolver) {
|
||||
match query_stdlib_version(relative_path, resolver) {
|
||||
TypeshedVersionsQueryResult::DoesNotExist => false,
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
| TypeshedVersionsQueryResult::MaybeExists => resolver
|
||||
@@ -96,7 +100,7 @@ impl ModulePath {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn is_regular_package(&self, resolver: &ResolverState) -> bool {
|
||||
pub(super) fn is_regular_package(&self, resolver: &ResolverContext) -> bool {
|
||||
let ModulePath {
|
||||
search_path,
|
||||
relative_path,
|
||||
@@ -113,7 +117,7 @@ impl ModulePath {
|
||||
.is_ok()
|
||||
}
|
||||
SearchPathInner::StandardLibraryCustom(search_path) => {
|
||||
match query_stdlib_version(Some(search_path), relative_path, resolver) {
|
||||
match query_stdlib_version(relative_path, resolver) {
|
||||
TypeshedVersionsQueryResult::DoesNotExist => false,
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
| TypeshedVersionsQueryResult::MaybeExists => system_path_to_file(
|
||||
@@ -124,7 +128,7 @@ impl ModulePath {
|
||||
}
|
||||
}
|
||||
SearchPathInner::StandardLibraryVendored(search_path) => {
|
||||
match query_stdlib_version(None, relative_path, resolver) {
|
||||
match query_stdlib_version(relative_path, resolver) {
|
||||
TypeshedVersionsQueryResult::DoesNotExist => false,
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
| TypeshedVersionsQueryResult::MaybeExists => resolver
|
||||
@@ -136,7 +140,7 @@ impl ModulePath {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn to_file(&self, resolver: &ResolverState) -> Option<File> {
|
||||
pub(super) fn to_file(&self, resolver: &ResolverContext) -> Option<File> {
|
||||
let db = resolver.db.upcast();
|
||||
let ModulePath {
|
||||
search_path,
|
||||
@@ -150,7 +154,7 @@ impl ModulePath {
|
||||
system_path_to_file(db, search_path.join(relative_path)).ok()
|
||||
}
|
||||
SearchPathInner::StandardLibraryCustom(stdlib_root) => {
|
||||
match query_stdlib_version(Some(stdlib_root), relative_path, resolver) {
|
||||
match query_stdlib_version(relative_path, resolver) {
|
||||
TypeshedVersionsQueryResult::DoesNotExist => None,
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
| TypeshedVersionsQueryResult::MaybeExists => {
|
||||
@@ -159,7 +163,7 @@ impl ModulePath {
|
||||
}
|
||||
}
|
||||
SearchPathInner::StandardLibraryVendored(stdlib_root) => {
|
||||
match query_stdlib_version(None, relative_path, resolver) {
|
||||
match query_stdlib_version(relative_path, resolver) {
|
||||
TypeshedVersionsQueryResult::DoesNotExist => None,
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
| TypeshedVersionsQueryResult::MaybeExists => {
|
||||
@@ -273,19 +277,15 @@ fn stdlib_path_to_module_name(relative_path: &Utf8Path) -> Option<ModuleName> {
|
||||
|
||||
#[must_use]
|
||||
fn query_stdlib_version(
|
||||
custom_stdlib_root: Option<&SystemPath>,
|
||||
relative_path: &Utf8Path,
|
||||
resolver: &ResolverState,
|
||||
context: &ResolverContext,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
let Some(module_name) = stdlib_path_to_module_name(relative_path) else {
|
||||
return TypeshedVersionsQueryResult::DoesNotExist;
|
||||
};
|
||||
let ResolverState {
|
||||
db,
|
||||
typeshed_versions,
|
||||
target_version,
|
||||
} = resolver;
|
||||
typeshed_versions.query_module(*db, &module_name, custom_stdlib_root, *target_version)
|
||||
let ResolverContext { db, target_version } = context;
|
||||
|
||||
typeshed_versions(*db).query_module(&module_name, *target_version)
|
||||
}
|
||||
|
||||
/// Enumeration describing the various ways in which validation of a search path might fail.
|
||||
@@ -293,7 +293,7 @@ fn query_stdlib_version(
|
||||
/// If validation fails for a search path derived from the user settings,
|
||||
/// a message must be displayed to the user,
|
||||
/// as type checking cannot be done reliably in these circumstances.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SearchPathValidationError {
|
||||
/// The path provided by the user was not a directory
|
||||
NotADirectory(SystemPathBuf),
|
||||
@@ -304,18 +304,20 @@ pub(crate) enum SearchPathValidationError {
|
||||
NoStdlibSubdirectory(SystemPathBuf),
|
||||
|
||||
/// The typeshed path provided by the user is a directory,
|
||||
/// but no `stdlib/VERSIONS` file exists.
|
||||
/// but `stdlib/VERSIONS` could not be read.
|
||||
/// (This is only relevant for stdlib search paths.)
|
||||
NoVersionsFile(SystemPathBuf),
|
||||
|
||||
/// `stdlib/VERSIONS` is a directory.
|
||||
/// (This is only relevant for stdlib search paths.)
|
||||
VersionsIsADirectory(SystemPathBuf),
|
||||
FailedToReadVersionsFile {
|
||||
path: SystemPathBuf,
|
||||
error: std::io::Error,
|
||||
},
|
||||
|
||||
/// The path provided by the user is a directory,
|
||||
/// and a `stdlib/VERSIONS` file exists, but it fails to parse.
|
||||
/// (This is only relevant for stdlib search paths.)
|
||||
VersionsParseError(TypeshedVersionsParseError),
|
||||
|
||||
/// Failed to discover the site-packages for the configured virtual environment.
|
||||
SitePackagesDiscovery(SitePackagesDiscoveryError),
|
||||
}
|
||||
|
||||
impl fmt::Display for SearchPathValidationError {
|
||||
@@ -325,9 +327,16 @@ impl fmt::Display for SearchPathValidationError {
|
||||
Self::NoStdlibSubdirectory(path) => {
|
||||
write!(f, "The directory at {path} has no `stdlib/` subdirectory")
|
||||
}
|
||||
Self::NoVersionsFile(path) => write!(f, "Expected a file at {path}/stdlib/VERSIONS"),
|
||||
Self::VersionsIsADirectory(path) => write!(f, "{path}/stdlib/VERSIONS is a directory."),
|
||||
Self::FailedToReadVersionsFile { path, error } => {
|
||||
write!(
|
||||
f,
|
||||
"Failed to read the custom typeshed versions file '{path}': {error}"
|
||||
)
|
||||
}
|
||||
Self::VersionsParseError(underlying_error) => underlying_error.fmt(f),
|
||||
SearchPathValidationError::SitePackagesDiscovery(error) => {
|
||||
write!(f, "Failed to discover the site-packages directory: {error}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,6 +351,18 @@ impl std::error::Error for SearchPathValidationError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TypeshedVersionsParseError> for SearchPathValidationError {
|
||||
fn from(value: TypeshedVersionsParseError) -> Self {
|
||||
Self::VersionsParseError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SitePackagesDiscoveryError> for SearchPathValidationError {
|
||||
fn from(value: SitePackagesDiscoveryError) -> Self {
|
||||
Self::SitePackagesDiscovery(value)
|
||||
}
|
||||
}
|
||||
|
||||
type SearchPathResult<T> = Result<T, SearchPathValidationError>;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
@@ -384,11 +405,10 @@ pub(crate) struct SearchPath(Arc<SearchPathInner>);
|
||||
|
||||
impl SearchPath {
|
||||
fn directory_path(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<SystemPathBuf> {
|
||||
let canonicalized = system.canonicalize_path(&root).unwrap_or(root);
|
||||
if system.is_directory(&canonicalized) {
|
||||
Ok(canonicalized)
|
||||
if system.is_directory(&root) {
|
||||
Ok(root)
|
||||
} else {
|
||||
Err(SearchPathValidationError::NotADirectory(canonicalized))
|
||||
Err(SearchPathValidationError::NotADirectory(root))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -407,32 +427,22 @@ impl SearchPath {
|
||||
}
|
||||
|
||||
/// Create a new standard-library search path pointing to a custom directory on disk
|
||||
pub(crate) fn custom_stdlib(db: &dyn Db, typeshed: SystemPathBuf) -> SearchPathResult<Self> {
|
||||
pub(crate) fn custom_stdlib(db: &dyn Db, typeshed: &SystemPath) -> SearchPathResult<Self> {
|
||||
let system = db.system();
|
||||
if !system.is_directory(&typeshed) {
|
||||
if !system.is_directory(typeshed) {
|
||||
return Err(SearchPathValidationError::NotADirectory(
|
||||
typeshed.to_path_buf(),
|
||||
));
|
||||
}
|
||||
|
||||
let stdlib =
|
||||
Self::directory_path(system, typeshed.join("stdlib")).map_err(|err| match err {
|
||||
SearchPathValidationError::NotADirectory(path) => {
|
||||
SearchPathValidationError::NoStdlibSubdirectory(path)
|
||||
SearchPathValidationError::NotADirectory(_) => {
|
||||
SearchPathValidationError::NoStdlibSubdirectory(typeshed.to_path_buf())
|
||||
}
|
||||
err => err,
|
||||
})?;
|
||||
let typeshed_versions =
|
||||
system_path_to_file(db.upcast(), stdlib.join("VERSIONS")).map_err(|err| match err {
|
||||
FileError::NotFound => SearchPathValidationError::NoVersionsFile(typeshed),
|
||||
FileError::IsADirectory => {
|
||||
SearchPathValidationError::VersionsIsADirectory(typeshed)
|
||||
}
|
||||
})?;
|
||||
super::typeshed::parse_typeshed_versions(db, typeshed_versions)
|
||||
.as_ref()
|
||||
.map_err(|validation_error| {
|
||||
SearchPathValidationError::VersionsParseError(validation_error.clone())
|
||||
})?;
|
||||
|
||||
Ok(Self(Arc::new(SearchPathInner::StandardLibraryCustom(
|
||||
stdlib,
|
||||
))))
|
||||
@@ -620,12 +630,11 @@ impl PartialEq<SearchPath> for VendoredPathBuf {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::Db;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
use crate::python_version::PythonVersion;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -639,15 +648,6 @@ mod tests {
|
||||
}
|
||||
|
||||
impl SearchPath {
|
||||
#[must_use]
|
||||
pub(crate) fn is_stdlib_search_path(&self) -> bool {
|
||||
matches!(
|
||||
&*self.0,
|
||||
SearchPathInner::StandardLibraryCustom(_)
|
||||
| SearchPathInner::StandardLibraryVendored(_)
|
||||
)
|
||||
}
|
||||
|
||||
fn join(&self, component: &str) -> ModulePath {
|
||||
self.to_module_path().join(component)
|
||||
}
|
||||
@@ -662,7 +662,7 @@ mod tests {
|
||||
.build();
|
||||
|
||||
assert_eq!(
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap().to_path_buf())
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
.to_module_path()
|
||||
.with_py_extension(),
|
||||
@@ -670,7 +670,7 @@ mod tests {
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
&SearchPath::custom_stdlib(&db, stdlib.parent().unwrap().to_path_buf())
|
||||
&SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
.join("foo")
|
||||
.with_pyi_extension(),
|
||||
@@ -781,7 +781,7 @@ mod tests {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap().to_path_buf())
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
.to_module_path()
|
||||
.push("bar.py");
|
||||
@@ -793,7 +793,7 @@ mod tests {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap().to_path_buf())
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap())
|
||||
.unwrap()
|
||||
.to_module_path()
|
||||
.push("bar.rs");
|
||||
@@ -825,7 +825,7 @@ mod tests {
|
||||
.with_custom_typeshed(MockedTypeshed::default())
|
||||
.build();
|
||||
|
||||
let root = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap().to_path_buf()).unwrap();
|
||||
let root = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap();
|
||||
|
||||
// Must have a `.pyi` extension or no extension:
|
||||
let bad_absolute_path = SystemPath::new("foo/stdlib/x.py");
|
||||
@@ -867,23 +867,22 @@ mod tests {
|
||||
|
||||
fn typeshed_test_case(
|
||||
typeshed: MockedTypeshed,
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> (TestDb, SearchPath) {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(typeshed)
|
||||
.with_target_version(target_version)
|
||||
.build();
|
||||
let stdlib =
|
||||
SearchPath::custom_stdlib(&db, stdlib.parent().unwrap().to_path_buf()).unwrap();
|
||||
let stdlib = SearchPath::custom_stdlib(&db, stdlib.parent().unwrap()).unwrap();
|
||||
(db, stdlib)
|
||||
}
|
||||
|
||||
fn py38_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, SearchPath) {
|
||||
typeshed_test_case(typeshed, TargetVersion::Py38)
|
||||
typeshed_test_case(typeshed, PythonVersion::PY38)
|
||||
}
|
||||
|
||||
fn py39_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, SearchPath) {
|
||||
typeshed_test_case(typeshed, TargetVersion::Py39)
|
||||
typeshed_test_case(typeshed, PythonVersion::PY39)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -899,7 +898,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY38);
|
||||
|
||||
let asyncio_regular_package = stdlib_path.join("asyncio");
|
||||
assert!(asyncio_regular_package.is_directory(&resolver));
|
||||
@@ -927,7 +926,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY38);
|
||||
|
||||
let xml_namespace_package = stdlib_path.join("xml");
|
||||
assert!(xml_namespace_package.is_directory(&resolver));
|
||||
@@ -949,7 +948,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY38);
|
||||
|
||||
let functools_module = stdlib_path.join("functools.pyi");
|
||||
assert!(functools_module.to_file(&resolver).is_some());
|
||||
@@ -965,7 +964,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY38);
|
||||
|
||||
let collections_regular_package = stdlib_path.join("collections");
|
||||
assert_eq!(collections_regular_package.to_file(&resolver), None);
|
||||
@@ -981,7 +980,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY38);
|
||||
|
||||
let importlib_namespace_package = stdlib_path.join("importlib");
|
||||
assert_eq!(importlib_namespace_package.to_file(&resolver), None);
|
||||
@@ -1002,7 +1001,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py38);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY38);
|
||||
|
||||
let non_existent = stdlib_path.join("doesnt_even_exist");
|
||||
assert_eq!(non_existent.to_file(&resolver), None);
|
||||
@@ -1030,7 +1029,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py39);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY39);
|
||||
|
||||
// Since we've set the target version to Py39,
|
||||
// `collections` should now exist as a directory, according to VERSIONS...
|
||||
@@ -1059,7 +1058,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py39);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY39);
|
||||
|
||||
// The `importlib` directory now also exists
|
||||
let importlib_namespace_package = stdlib_path.join("importlib");
|
||||
@@ -1083,7 +1082,7 @@ mod tests {
|
||||
};
|
||||
|
||||
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
|
||||
let resolver = ResolverState::new(&db, TargetVersion::Py39);
|
||||
let resolver = ResolverContext::new(&db, PythonVersion::PY39);
|
||||
|
||||
// The `xml` package no longer exists on py39:
|
||||
let xml_namespace_package = stdlib_path.join("xml");
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use std::borrow::Cow;
|
||||
use std::iter::FusedIterator;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::files::{File, FilePath, FileRootKind};
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use ruff_db::vendored::{VendoredFileSystem, VendoredPath};
|
||||
|
||||
use super::module::{Module, ModuleKind};
|
||||
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
|
||||
use super::state::ResolverState;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
|
||||
use crate::site_packages::VirtualEnvironment;
|
||||
use crate::{Program, PythonVersion, SearchPathSettings, SitePackages};
|
||||
|
||||
/// Resolves a module name to a module.
|
||||
pub fn resolve_module(db: &dyn Db, module_name: ModuleName) -> Option<Module> {
|
||||
@@ -40,7 +41,7 @@ pub(crate) fn resolve_module_query<'db>(
|
||||
|
||||
let module = Module::new(name.clone(), kind, search_path, module_file);
|
||||
|
||||
tracing::debug!(
|
||||
tracing::trace!(
|
||||
"Resolved module '{name}' to '{path}'.",
|
||||
path = module_file.path(db)
|
||||
);
|
||||
@@ -84,9 +85,7 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
FilePath::SystemVirtual(_) => return None,
|
||||
};
|
||||
|
||||
let settings = module_resolution_settings(db);
|
||||
|
||||
let mut search_paths = settings.search_paths(db);
|
||||
let mut search_paths = search_paths(db);
|
||||
|
||||
let module_name = loop {
|
||||
let candidate = search_paths.next()?;
|
||||
@@ -119,92 +118,182 @@ pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate and normalize the raw settings given by the user
|
||||
/// into settings we can use for module resolution
|
||||
///
|
||||
/// This method also implements the typing spec's [module resolution order].
|
||||
///
|
||||
/// [module resolution order]: https://typing.readthedocs.io/en/latest/spec/distributing.html#import-resolution-ordering
|
||||
fn try_resolve_module_resolution_settings(
|
||||
db: &dyn Db,
|
||||
) -> Result<ModuleResolutionSettings, SearchPathValidationError> {
|
||||
let program = Program::get(db.upcast());
|
||||
|
||||
let SearchPathSettings {
|
||||
extra_paths,
|
||||
src_root,
|
||||
custom_typeshed,
|
||||
site_packages,
|
||||
} = program.search_paths(db.upcast());
|
||||
|
||||
if !extra_paths.is_empty() {
|
||||
tracing::info!("Extra search paths: {extra_paths:?}");
|
||||
}
|
||||
|
||||
if let Some(custom_typeshed) = custom_typeshed {
|
||||
tracing::info!("Custom typeshed directory: {custom_typeshed}");
|
||||
}
|
||||
|
||||
if !site_packages.is_empty() {
|
||||
tracing::info!("Site-packages directories: {site_packages:?}");
|
||||
}
|
||||
|
||||
let system = db.system();
|
||||
let files = db.files();
|
||||
|
||||
let mut static_search_paths = vec![];
|
||||
|
||||
for path in extra_paths {
|
||||
files.try_add_root(db.upcast(), path, FileRootKind::LibrarySearchPath);
|
||||
static_search_paths.push(SearchPath::extra(system, path.clone())?);
|
||||
}
|
||||
|
||||
static_search_paths.push(SearchPath::first_party(system, src_root.clone())?);
|
||||
|
||||
static_search_paths.push(if let Some(custom_typeshed) = custom_typeshed.as_ref() {
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
custom_typeshed,
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
SearchPath::custom_stdlib(db, custom_typeshed.clone())?
|
||||
} else {
|
||||
SearchPath::vendored_stdlib()
|
||||
});
|
||||
|
||||
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
|
||||
|
||||
let target_version = program.target_version(db.upcast());
|
||||
tracing::info!("Target version: {target_version}");
|
||||
|
||||
// Filter out module resolution paths that point to the same directory on disk (the same invariant maintained by [`sys.path` at runtime]).
|
||||
// (Paths may, however, *overlap* -- e.g. you could have both `src/` and `src/foo`
|
||||
// as module resolution paths simultaneously.)
|
||||
//
|
||||
// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
|
||||
// This code doesn't use an `IndexSet` because the key is the system path and not the search root.
|
||||
let mut seen_paths =
|
||||
FxHashSet::with_capacity_and_hasher(static_search_paths.len(), FxBuildHasher);
|
||||
|
||||
static_search_paths.retain(|path| {
|
||||
if let Some(path) = path.as_system_path() {
|
||||
seen_paths.insert(path.to_path_buf())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
Ok(ModuleResolutionSettings {
|
||||
target_version,
|
||||
static_search_paths,
|
||||
site_packages_paths: site_packages.to_owned(),
|
||||
})
|
||||
pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
|
||||
Program::get(db).search_paths(db).iter(db)
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn module_resolution_settings(db: &dyn Db) -> ModuleResolutionSettings {
|
||||
// TODO proper error handling if this returns an error:
|
||||
try_resolve_module_resolution_settings(db).unwrap()
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SearchPaths {
|
||||
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
|
||||
/// These shouldn't ever change unless the config settings themselves change.
|
||||
static_paths: Vec<SearchPath>,
|
||||
|
||||
/// site-packages paths are not included in the above field:
|
||||
/// if there are multiple site-packages paths, editable installations can appear
|
||||
/// *between* the site-packages paths on `sys.path` at runtime.
|
||||
/// That means we can't know where a second or third `site-packages` path should sit
|
||||
/// in terms of module-resolution priority until we've discovered the editable installs
|
||||
/// for the first `site-packages` path
|
||||
site_packages: Vec<SearchPath>,
|
||||
|
||||
typeshed_versions: ResolvedTypeshedVersions,
|
||||
}
|
||||
|
||||
impl SearchPaths {
|
||||
/// Validate and normalize the raw settings given by the user
|
||||
/// into settings we can use for module resolution
|
||||
///
|
||||
/// This method also implements the typing spec's [module resolution order].
|
||||
///
|
||||
/// [module resolution order]: https://typing.readthedocs.io/en/latest/spec/distributing.html#import-resolution-ordering
|
||||
pub(crate) fn from_settings(
|
||||
db: &dyn Db,
|
||||
settings: &SearchPathSettings,
|
||||
) -> Result<Self, SearchPathValidationError> {
|
||||
fn canonicalize(path: &SystemPath, system: &dyn System) -> SystemPathBuf {
|
||||
system
|
||||
.canonicalize_path(path)
|
||||
.unwrap_or_else(|_| path.to_path_buf())
|
||||
}
|
||||
|
||||
let SearchPathSettings {
|
||||
extra_paths,
|
||||
src_root,
|
||||
custom_typeshed,
|
||||
site_packages: site_packages_paths,
|
||||
} = settings;
|
||||
|
||||
let system = db.system();
|
||||
let files = db.files();
|
||||
|
||||
let mut static_paths = vec![];
|
||||
|
||||
for path in extra_paths {
|
||||
let path = canonicalize(path, system);
|
||||
files.try_add_root(db.upcast(), &path, FileRootKind::LibrarySearchPath);
|
||||
tracing::debug!("Adding extra search-path '{path}'");
|
||||
|
||||
static_paths.push(SearchPath::extra(system, path)?);
|
||||
}
|
||||
|
||||
tracing::debug!("Adding first-party search path '{src_root}'");
|
||||
static_paths.push(SearchPath::first_party(system, src_root.to_path_buf())?);
|
||||
|
||||
let (typeshed_versions, stdlib_path) = if let Some(custom_typeshed) = custom_typeshed {
|
||||
let custom_typeshed = canonicalize(custom_typeshed, system);
|
||||
tracing::debug!("Adding custom-stdlib search path '{custom_typeshed}'");
|
||||
|
||||
files.try_add_root(
|
||||
db.upcast(),
|
||||
&custom_typeshed,
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
|
||||
let versions_path = custom_typeshed.join("stdlib/VERSIONS");
|
||||
|
||||
let versions_content = system.read_to_string(&versions_path).map_err(|error| {
|
||||
SearchPathValidationError::FailedToReadVersionsFile {
|
||||
path: versions_path,
|
||||
error,
|
||||
}
|
||||
})?;
|
||||
|
||||
let parsed: TypeshedVersions = versions_content.parse()?;
|
||||
|
||||
let search_path = SearchPath::custom_stdlib(db, &custom_typeshed)?;
|
||||
|
||||
(ResolvedTypeshedVersions::Custom(parsed), search_path)
|
||||
} else {
|
||||
tracing::debug!("Using vendored stdlib");
|
||||
(
|
||||
ResolvedTypeshedVersions::Vendored(vendored_typeshed_versions()),
|
||||
SearchPath::vendored_stdlib(),
|
||||
)
|
||||
};
|
||||
|
||||
static_paths.push(stdlib_path);
|
||||
|
||||
let site_packages_paths = match site_packages_paths {
|
||||
SitePackages::Derived { venv_path } => VirtualEnvironment::new(venv_path, system)
|
||||
.and_then(|venv| venv.site_packages_directories(system))?,
|
||||
SitePackages::Known(paths) => paths
|
||||
.iter()
|
||||
.map(|path| canonicalize(path, system))
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len());
|
||||
|
||||
for path in site_packages_paths {
|
||||
tracing::debug!("Adding site-packages search path '{path}'");
|
||||
files.try_add_root(db.upcast(), &path, FileRootKind::LibrarySearchPath);
|
||||
site_packages.push(SearchPath::site_packages(system, path)?);
|
||||
}
|
||||
|
||||
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
|
||||
|
||||
// Filter out module resolution paths that point to the same directory on disk (the same invariant maintained by [`sys.path` at runtime]).
|
||||
// (Paths may, however, *overlap* -- e.g. you could have both `src/` and `src/foo`
|
||||
// as module resolution paths simultaneously.)
|
||||
//
|
||||
// This code doesn't use an `IndexSet` because the key is the system path and not the search root.
|
||||
//
|
||||
// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
|
||||
let mut seen_paths = FxHashSet::with_capacity_and_hasher(static_paths.len(), FxBuildHasher);
|
||||
|
||||
static_paths.retain(|path| {
|
||||
if let Some(path) = path.as_system_path() {
|
||||
seen_paths.insert(path.to_path_buf())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
Ok(SearchPaths {
|
||||
static_paths,
|
||||
site_packages,
|
||||
typeshed_versions,
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn iter<'a>(&'a self, db: &'a dyn Db) -> SearchPathIterator<'a> {
|
||||
SearchPathIterator {
|
||||
db,
|
||||
static_paths: self.static_paths.iter(),
|
||||
dynamic_paths: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn custom_stdlib(&self) -> Option<&SystemPath> {
|
||||
self.static_paths.iter().find_map(|search_path| {
|
||||
if search_path.is_standard_library() {
|
||||
search_path.as_system_path()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn typeshed_versions(&self) -> &TypeshedVersions {
|
||||
&self.typeshed_versions
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum ResolvedTypeshedVersions {
|
||||
Vendored(&'static TypeshedVersions),
|
||||
Custom(TypeshedVersions),
|
||||
}
|
||||
|
||||
impl Deref for ResolvedTypeshedVersions {
|
||||
type Target = TypeshedVersions;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
ResolvedTypeshedVersions::Vendored(versions) => versions,
|
||||
ResolvedTypeshedVersions::Custom(versions) => versions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect all dynamic search paths. For each `site-packages` path:
|
||||
@@ -217,19 +306,21 @@ pub(crate) fn module_resolution_settings(db: &dyn Db) -> ModuleResolutionSetting
|
||||
/// module-resolution priority.
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
let ModuleResolutionSettings {
|
||||
target_version: _,
|
||||
static_search_paths,
|
||||
site_packages_paths,
|
||||
} = module_resolution_settings(db);
|
||||
tracing::debug!("Resolving dynamic module resolution paths");
|
||||
|
||||
let SearchPaths {
|
||||
static_paths,
|
||||
site_packages,
|
||||
typeshed_versions: _,
|
||||
} = Program::get(db).search_paths(db);
|
||||
|
||||
let mut dynamic_paths = Vec::new();
|
||||
|
||||
if site_packages_paths.is_empty() {
|
||||
if site_packages.is_empty() {
|
||||
return dynamic_paths;
|
||||
}
|
||||
|
||||
let mut existing_paths: FxHashSet<_> = static_search_paths
|
||||
let mut existing_paths: FxHashSet<_> = static_paths
|
||||
.iter()
|
||||
.filter_map(|path| path.as_system_path())
|
||||
.map(Cow::Borrowed)
|
||||
@@ -238,15 +329,19 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
let files = db.files();
|
||||
let system = db.system();
|
||||
|
||||
for site_packages_dir in site_packages_paths {
|
||||
for site_packages_search_path in site_packages {
|
||||
let site_packages_dir = site_packages_search_path
|
||||
.as_system_path()
|
||||
.expect("Expected site package path to be a system path");
|
||||
|
||||
if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) {
|
||||
continue;
|
||||
}
|
||||
let site_packages_root = files.try_add_root(
|
||||
db.upcast(),
|
||||
site_packages_dir,
|
||||
FileRootKind::LibrarySearchPath,
|
||||
);
|
||||
|
||||
let site_packages_root = files
|
||||
.root(db.upcast(), site_packages_dir)
|
||||
.expect("Site-package root to have been created.");
|
||||
|
||||
// This query needs to be re-executed each time a `.pth` file
|
||||
// is added, modified or removed from the `site-packages` directory.
|
||||
// However, we don't use Salsa queries to read the source text of `.pth` files;
|
||||
@@ -254,8 +349,7 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
// site-package directory's revision.
|
||||
site_packages_root.revision(db.upcast());
|
||||
|
||||
dynamic_paths
|
||||
.push(SearchPath::site_packages(system, site_packages_dir.to_owned()).unwrap());
|
||||
dynamic_paths.push(site_packages_search_path.clone());
|
||||
|
||||
// As well as modules installed directly into `site-packages`,
|
||||
// the directory may also contain `.pth` files.
|
||||
@@ -263,22 +357,42 @@ pub(crate) fn dynamic_resolution_paths(db: &dyn Db) -> Vec<SearchPath> {
|
||||
// containing a (relative or absolute) path.
|
||||
// Each of these paths may point to an editable install of a package,
|
||||
// so should be considered an additional search path.
|
||||
let Ok(pth_file_iterator) = PthFileIterator::new(db, site_packages_dir) else {
|
||||
continue;
|
||||
let pth_file_iterator = match PthFileIterator::new(db, site_packages_dir) {
|
||||
Ok(iterator) => iterator,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"Failed to search for editable installation in {site_packages_dir}: {error}"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// The Python documentation specifies that `.pth` files in `site-packages`
|
||||
// are processed in alphabetical order, so collecting and then sorting is necessary.
|
||||
// https://docs.python.org/3/library/site.html#module-site
|
||||
let mut all_pth_files: Vec<PthFile> = pth_file_iterator.collect();
|
||||
all_pth_files.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
all_pth_files.sort_unstable_by(|a, b| a.path.cmp(&b.path));
|
||||
|
||||
for pth_file in &all_pth_files {
|
||||
for installation in pth_file.editable_installations() {
|
||||
if existing_paths.insert(Cow::Owned(
|
||||
installation.as_system_path().unwrap().to_path_buf(),
|
||||
)) {
|
||||
dynamic_paths.push(installation);
|
||||
let installations = all_pth_files.iter().flat_map(PthFile::items);
|
||||
|
||||
for installation in installations {
|
||||
let installation = system
|
||||
.canonicalize_path(&installation)
|
||||
.unwrap_or(installation);
|
||||
|
||||
if existing_paths.insert(Cow::Owned(installation.clone())) {
|
||||
match SearchPath::editable(system, installation.clone()) {
|
||||
Ok(search_path) => {
|
||||
tracing::debug!(
|
||||
"Adding editable installation to module resolution path {path}",
|
||||
path = installation
|
||||
);
|
||||
dynamic_paths.push(search_path);
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
tracing::debug!("Skipping editable installation: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -324,7 +438,6 @@ impl<'db> FusedIterator for SearchPathIterator<'db> {}
|
||||
/// One or more lines in a `.pth` file may be a (relative or absolute)
|
||||
/// path that represents an editable installation of a package.
|
||||
struct PthFile<'db> {
|
||||
system: &'db dyn System,
|
||||
path: SystemPathBuf,
|
||||
contents: String,
|
||||
site_packages: &'db SystemPath,
|
||||
@@ -333,9 +446,8 @@ struct PthFile<'db> {
|
||||
impl<'db> PthFile<'db> {
|
||||
/// Yield paths in this `.pth` file that appear to represent editable installations,
|
||||
/// and should therefore be added as module-resolution search paths.
|
||||
fn editable_installations(&'db self) -> impl Iterator<Item = SearchPath> + 'db {
|
||||
fn items(&'db self) -> impl Iterator<Item = SystemPathBuf> + 'db {
|
||||
let PthFile {
|
||||
system,
|
||||
path: _,
|
||||
contents,
|
||||
site_packages,
|
||||
@@ -354,8 +466,8 @@ impl<'db> PthFile<'db> {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let possible_editable_install = SystemPath::absolute(line, site_packages);
|
||||
SearchPath::editable(*system, possible_editable_install).ok()
|
||||
|
||||
Some(SystemPath::absolute(line, site_packages))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -404,12 +516,15 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(contents) = db.system().read_to_string(&path) else {
|
||||
continue;
|
||||
let contents = match system.read_to_string(&path) {
|
||||
Ok(contents) => contents,
|
||||
Err(error) => {
|
||||
tracing::warn!("Failed to read .pth file '{path}': {error}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
return Some(PthFile {
|
||||
system,
|
||||
path,
|
||||
contents,
|
||||
site_packages,
|
||||
@@ -418,38 +533,6 @@ impl<'db> Iterator for PthFileIterator<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validated and normalized module-resolution settings.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ModuleResolutionSettings {
|
||||
target_version: TargetVersion,
|
||||
|
||||
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
|
||||
/// These shouldn't ever change unless the config settings themselves change.
|
||||
static_search_paths: Vec<SearchPath>,
|
||||
|
||||
/// site-packages paths are not included in the above field:
|
||||
/// if there are multiple site-packages paths, editable installations can appear
|
||||
/// *between* the site-packages paths on `sys.path` at runtime.
|
||||
/// That means we can't know where a second or third `site-packages` path should sit
|
||||
/// in terms of module-resolution priority until we've discovered the editable installs
|
||||
/// for the first `site-packages` path
|
||||
site_packages_paths: Vec<SystemPathBuf>,
|
||||
}
|
||||
|
||||
impl ModuleResolutionSettings {
|
||||
fn target_version(&self) -> TargetVersion {
|
||||
self.target_version
|
||||
}
|
||||
|
||||
pub(crate) fn search_paths<'db>(&'db self, db: &'db dyn Db) -> SearchPathIterator<'db> {
|
||||
SearchPathIterator {
|
||||
db,
|
||||
static_paths: self.static_search_paths.iter(),
|
||||
dynamic_paths: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A thin wrapper around `ModuleName` to make it a Salsa ingredient.
|
||||
///
|
||||
/// This is needed because Salsa requires that all query arguments are salsa ingredients.
|
||||
@@ -462,14 +545,13 @@ struct ModuleNameIngredient<'db> {
|
||||
/// Given a module name and a list of search paths in which to lookup modules,
|
||||
/// attempt to resolve the module name
|
||||
fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, ModuleKind)> {
|
||||
let resolver_settings = module_resolution_settings(db);
|
||||
let target_version = resolver_settings.target_version();
|
||||
let resolver_state = ResolverState::new(db, target_version);
|
||||
let (_, minor_version) = target_version.as_tuple();
|
||||
let program = Program::get(db);
|
||||
let target_version = program.target_version(db);
|
||||
let resolver_state = ResolverContext::new(db, target_version);
|
||||
let is_builtin_module =
|
||||
ruff_python_stdlib::sys::is_builtin_module(minor_version, name.as_str());
|
||||
ruff_python_stdlib::sys::is_builtin_module(target_version.minor, name.as_str());
|
||||
|
||||
for search_path in resolver_settings.search_paths(db) {
|
||||
for search_path in search_paths(db) {
|
||||
// When a builtin module is imported, standard module resolution is bypassed:
|
||||
// the module name always resolves to the stdlib module,
|
||||
// even if there's a module of the same name in the first-party root
|
||||
@@ -487,24 +569,16 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod
|
||||
|
||||
package_path.push(module_name);
|
||||
|
||||
// Must be a `__init__.pyi` or `__init__.py` or it isn't a package.
|
||||
let kind = if package_path.is_directory(&resolver_state) {
|
||||
package_path.push("__init__");
|
||||
ModuleKind::Package
|
||||
} else {
|
||||
ModuleKind::Module
|
||||
};
|
||||
|
||||
// TODO Implement full https://peps.python.org/pep-0561/#type-checker-module-resolution-order resolution
|
||||
if let Some(stub) = package_path.with_pyi_extension().to_file(&resolver_state) {
|
||||
return Some((search_path.clone(), stub, kind));
|
||||
// Check for a regular package first (highest priority)
|
||||
package_path.push("__init__");
|
||||
if let Some(regular_package) = resolve_file_module(&package_path, &resolver_state) {
|
||||
return Some((search_path.clone(), regular_package, ModuleKind::Package));
|
||||
}
|
||||
|
||||
if let Some(module) = package_path
|
||||
.with_py_extension()
|
||||
.and_then(|path| path.to_file(&resolver_state))
|
||||
{
|
||||
return Some((search_path.clone(), module, kind));
|
||||
// Check for a file module next
|
||||
package_path.pop();
|
||||
if let Some(file_module) = resolve_file_module(&package_path, &resolver_state) {
|
||||
return Some((search_path.clone(), file_module, ModuleKind::Module));
|
||||
}
|
||||
|
||||
// For regular packages, don't search the next search path. All files of that
|
||||
@@ -525,10 +599,27 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod
|
||||
None
|
||||
}
|
||||
|
||||
/// If `module` exists on disk with either a `.pyi` or `.py` extension,
|
||||
/// return the [`File`] corresponding to that path.
|
||||
///
|
||||
/// `.pyi` files take priority, as they always have priority when
|
||||
/// resolving modules.
|
||||
fn resolve_file_module(module: &ModulePath, resolver_state: &ResolverContext) -> Option<File> {
|
||||
// Stubs have precedence over source files
|
||||
module
|
||||
.with_pyi_extension()
|
||||
.to_file(resolver_state)
|
||||
.or_else(|| {
|
||||
module
|
||||
.with_py_extension()
|
||||
.and_then(|path| path.to_file(resolver_state))
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_package<'a, 'db, I>(
|
||||
module_search_path: &SearchPath,
|
||||
components: I,
|
||||
resolver_state: &ResolverState<'db>,
|
||||
resolver_state: &ResolverContext<'db>,
|
||||
) -> Result<ResolvedPackage, PackageKind>
|
||||
where
|
||||
I: Iterator<Item = &'a str>,
|
||||
@@ -551,7 +642,10 @@ where
|
||||
|
||||
if is_regular_package {
|
||||
in_namespace_package = false;
|
||||
} else if package_path.is_directory(resolver_state) {
|
||||
} else if package_path.is_directory(resolver_state)
|
||||
// Pure modules hide namespace packages with the same name
|
||||
&& resolve_file_module(&package_path, resolver_state).is_none()
|
||||
{
|
||||
// A directory without an `__init__.py` is a namespace package, continue with the next folder.
|
||||
in_namespace_package = true;
|
||||
} else if in_namespace_package {
|
||||
@@ -610,6 +704,21 @@ impl PackageKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct ResolverContext<'db> {
|
||||
pub(super) db: &'db dyn Db,
|
||||
pub(super) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl<'db> ResolverContext<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, target_version: PythonVersion) -> Self {
|
||||
Self { db, target_version }
|
||||
}
|
||||
|
||||
pub(super) fn vendored(&self) -> &VendoredFileSystem {
|
||||
self.db.vendored()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::{system_path_to_file, File, FilePath};
|
||||
@@ -623,6 +732,8 @@ mod tests {
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::module::ModuleKind;
|
||||
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use crate::PythonVersion;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -677,7 +788,7 @@ mod tests {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let builtins_module_name = ModuleName::new_static("builtins").unwrap();
|
||||
@@ -695,7 +806,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -748,7 +859,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
@@ -762,7 +873,7 @@ mod tests {
|
||||
"Search path for {module_name} was unexpectedly {search_path:?}"
|
||||
);
|
||||
assert!(
|
||||
search_path.is_stdlib_search_path(),
|
||||
search_path.is_standard_library(),
|
||||
"Expected a stdlib search path, but got {search_path:?}"
|
||||
);
|
||||
}
|
||||
@@ -793,7 +904,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&[
|
||||
@@ -837,7 +948,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let existing_modules = create_module_names(&[
|
||||
@@ -858,7 +969,7 @@ mod tests {
|
||||
"Search path for {module_name} was unexpectedly {search_path:?}"
|
||||
);
|
||||
assert!(
|
||||
search_path.is_stdlib_search_path(),
|
||||
search_path.is_standard_library(),
|
||||
"Expected a stdlib search path, but got {search_path:?}"
|
||||
);
|
||||
}
|
||||
@@ -879,7 +990,7 @@ mod tests {
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.with_target_version(PythonVersion::PY39)
|
||||
.build();
|
||||
|
||||
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
|
||||
@@ -903,7 +1014,7 @@ mod tests {
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -927,7 +1038,7 @@ mod tests {
|
||||
fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_vendored_typeshed()
|
||||
.with_target_version(TargetVersion::default())
|
||||
.with_target_version(PythonVersion::default())
|
||||
.build();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
@@ -992,6 +1103,25 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_file_takes_priority_over_namespace_package() {
|
||||
//const SRC: &[FileSpec] = &[("foo.py", "x = 1")];
|
||||
const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/bar.py", "x = 2")];
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
|
||||
|
||||
// `foo.py` takes priority over the `foo` namespace package
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
assert_eq!(foo_module.file().path(&db), &src.join("foo.py"));
|
||||
|
||||
// `foo.bar` isn't recognised as a module
|
||||
let foo_bar_module = resolve_module(&db, foo_bar_module_name.clone());
|
||||
assert_eq!(foo_bar_module, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_stub_over_module() {
|
||||
const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")];
|
||||
@@ -1143,7 +1273,7 @@ mod tests {
|
||||
fn symlink() -> anyhow::Result<()> {
|
||||
use anyhow::Context;
|
||||
|
||||
use ruff_db::program::Program;
|
||||
use crate::program::Program;
|
||||
use ruff_db::system::{OsSystem, SystemPath};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
@@ -1173,14 +1303,19 @@ mod tests {
|
||||
std::fs::write(foo.as_std_path(), "")?;
|
||||
std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?;
|
||||
|
||||
let search_paths = SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(custom_typeshed.clone()),
|
||||
site_packages: vec![site_packages],
|
||||
};
|
||||
|
||||
Program::new(&db, TargetVersion::Py38, search_paths);
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::PY38,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(custom_typeshed.clone()),
|
||||
site_packages: SitePackages::Known(vec![site_packages]),
|
||||
},
|
||||
},
|
||||
)
|
||||
.context("Invalid program settings")?;
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let bar_module = resolve_module(&db, ModuleName::new_static("bar").unwrap()).unwrap();
|
||||
@@ -1214,7 +1349,7 @@ mod tests {
|
||||
fn deleting_an_unrelated_file_doesnt_change_module_resolution() {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")])
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
@@ -1302,7 +1437,7 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1350,7 +1485,7 @@ mod tests {
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1390,7 +1525,7 @@ mod tests {
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.with_target_version(PythonVersion::PY38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
@@ -1644,8 +1779,7 @@ not_a_directory
|
||||
.with_site_packages_files(&[("_foo.pth", "/src")])
|
||||
.build();
|
||||
|
||||
let search_paths: Vec<&SearchPath> =
|
||||
module_resolution_settings(&db).search_paths(&db).collect();
|
||||
let search_paths: Vec<&SearchPath> = search_paths(&db).collect();
|
||||
|
||||
assert!(search_paths.contains(
|
||||
&&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap()
|
||||
@@ -1674,16 +1808,22 @@ not_a_directory
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
Program::new(
|
||||
Program::from_settings(
|
||||
&db,
|
||||
TargetVersion::default(),
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![venv_site_packages, system_site_packages],
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![
|
||||
venv_site_packages,
|
||||
system_site_packages,
|
||||
]),
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
.expect("Valid program settings");
|
||||
|
||||
// The editable installs discovered from the `.pth` file in the first `site-packages` directory
|
||||
// take precedence over the second `site-packages` directory...
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
|
||||
use super::typeshed::LazyTypeshedVersions;
|
||||
use crate::db::Db;
|
||||
|
||||
pub(crate) struct ResolverState<'db> {
|
||||
pub(crate) db: &'db dyn Db,
|
||||
pub(crate) typeshed_versions: LazyTypeshedVersions<'db>,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
}
|
||||
|
||||
impl<'db> ResolverState<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, target_version: TargetVersion) -> Self {
|
||||
Self {
|
||||
db,
|
||||
typeshed_versions: LazyTypeshedVersions::new(),
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn vendored(&self) -> &VendoredFileSystem {
|
||||
self.db.vendored()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPathBuf;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{ProgramSettings, SitePackages};
|
||||
|
||||
/// A test case for the module resolver.
|
||||
///
|
||||
@@ -16,7 +18,7 @@ pub(crate) struct TestCase<T> {
|
||||
// so this is a single directory instead of a `Vec` of directories,
|
||||
// like it is in `ruff_db::Program`.
|
||||
pub(crate) site_packages: SystemPathBuf,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
pub(crate) target_version: PythonVersion,
|
||||
}
|
||||
|
||||
/// A `(file_name, file_contents)` tuple
|
||||
@@ -98,7 +100,7 @@ pub(crate) struct UnspecifiedTypeshed;
|
||||
/// to `()`.
|
||||
pub(crate) struct TestCaseBuilder<T> {
|
||||
typeshed_option: T,
|
||||
target_version: TargetVersion,
|
||||
target_version: PythonVersion,
|
||||
first_party_files: Vec<FileSpec>,
|
||||
site_packages_files: Vec<FileSpec>,
|
||||
}
|
||||
@@ -117,7 +119,7 @@ impl<T> TestCaseBuilder<T> {
|
||||
}
|
||||
|
||||
/// Specify the target Python version the module resolver should assume
|
||||
pub(crate) fn with_target_version(mut self, target_version: TargetVersion) -> Self {
|
||||
pub(crate) fn with_target_version(mut self, target_version: PythonVersion) -> Self {
|
||||
self.target_version = target_version;
|
||||
self
|
||||
}
|
||||
@@ -144,7 +146,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn new() -> TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
Self {
|
||||
typeshed_option: UnspecifiedTypeshed,
|
||||
target_version: TargetVersion::default(),
|
||||
target_version: PythonVersion::default(),
|
||||
first_party_files: vec![],
|
||||
site_packages_files: vec![],
|
||||
}
|
||||
@@ -177,6 +179,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
|
||||
TestCaseBuilder {
|
||||
typeshed_option: typeshed,
|
||||
target_version,
|
||||
@@ -193,6 +196,7 @@ impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
site_packages,
|
||||
target_version,
|
||||
} = self.with_custom_typeshed(MockedTypeshed::default()).build();
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
src,
|
||||
@@ -219,16 +223,19 @@ impl TestCaseBuilder<MockedTypeshed> {
|
||||
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
|
||||
let typeshed = Self::build_typeshed_mock(&mut db, &typeshed_option);
|
||||
|
||||
Program::new(
|
||||
Program::from_settings(
|
||||
&db,
|
||||
target_version,
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: vec![site_packages.clone()],
|
||||
&ProgramSettings {
|
||||
target_version,
|
||||
search_paths: SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
.expect("Valid program settings");
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
@@ -272,16 +279,17 @@ impl TestCaseBuilder<VendoredTypeshed> {
|
||||
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
|
||||
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
|
||||
|
||||
Program::new(
|
||||
Program::from_settings(
|
||||
&db,
|
||||
target_version,
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: src.clone(),
|
||||
custom_typeshed: None,
|
||||
site_packages: vec![site_packages.clone()],
|
||||
&ProgramSettings {
|
||||
target_version,
|
||||
search_paths: SearchPathSettings {
|
||||
site_packages: SitePackages::Known(vec![site_packages.clone()]),
|
||||
..SearchPathSettings::new(src.clone())
|
||||
},
|
||||
},
|
||||
);
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub use self::vendored::vendored_typeshed_stubs;
|
||||
pub(super) use self::versions::{
|
||||
parse_typeshed_versions, LazyTypeshedVersions, TypeshedVersionsParseError,
|
||||
typeshed_versions, vendored_typeshed_versions, TypeshedVersions, TypeshedVersionsParseError,
|
||||
TypeshedVersionsQueryResult,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use std::cell::OnceCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::num::{NonZeroU16, NonZeroUsize};
|
||||
@@ -6,79 +5,12 @@ use std::ops::{RangeFrom, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use ruff_db::program::TargetVersion;
|
||||
use ruff_db::system::SystemPath;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LazyTypeshedVersions<'db>(OnceCell<&'db TypeshedVersions>);
|
||||
|
||||
impl<'db> LazyTypeshedVersions<'db> {
|
||||
#[must_use]
|
||||
pub(crate) fn new() -> Self {
|
||||
Self(OnceCell::new())
|
||||
}
|
||||
|
||||
/// Query whether a module exists at runtime in the stdlib on a certain Python version.
|
||||
///
|
||||
/// Simply probing whether a file exists in typeshed is insufficient for this question,
|
||||
/// as a module in the stdlib may have been added in Python 3.10, but the typeshed stub
|
||||
/// will still be available (either in a custom typeshed dir or in our vendored copy)
|
||||
/// even if the user specified Python 3.8 as the target version.
|
||||
///
|
||||
/// For top-level modules and packages, the VERSIONS file can always provide an unambiguous answer
|
||||
/// as to whether the module exists on the specified target version. However, VERSIONS does not
|
||||
/// provide comprehensive information on all submodules, meaning that this method sometimes
|
||||
/// returns [`TypeshedVersionsQueryResult::MaybeExists`].
|
||||
/// See [`TypeshedVersionsQueryResult`] for more details.
|
||||
#[must_use]
|
||||
pub(crate) fn query_module(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
module: &ModuleName,
|
||||
stdlib_root: Option<&SystemPath>,
|
||||
target_version: TargetVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
let versions = self.0.get_or_init(|| {
|
||||
let versions_path = if let Some(system_path) = stdlib_root {
|
||||
system_path.join("VERSIONS")
|
||||
} else {
|
||||
return &VENDORED_VERSIONS;
|
||||
};
|
||||
let Ok(versions_file) = system_path_to_file(db.upcast(), &versions_path) else {
|
||||
todo!(
|
||||
"Still need to figure out how to handle VERSIONS files being deleted \
|
||||
from custom typeshed directories! Expected a file to exist at {versions_path}"
|
||||
)
|
||||
};
|
||||
// TODO(Alex/Micha): If VERSIONS is invalid,
|
||||
// this should invalidate not just the specific module resolution we're currently attempting,
|
||||
// but all type inference that depends on any standard-library types.
|
||||
// Unwrapping here is not correct...
|
||||
parse_typeshed_versions(db, versions_file).as_ref().unwrap()
|
||||
});
|
||||
versions.query_module(module, PyVersion::from(target_version))
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn parse_typeshed_versions(
|
||||
db: &dyn Db,
|
||||
versions_file: File,
|
||||
) -> Result<TypeshedVersions, TypeshedVersionsParseError> {
|
||||
// TODO: Handle IO errors
|
||||
let file_content = versions_file
|
||||
.read_to_string(db.upcast())
|
||||
.unwrap_or_default();
|
||||
file_content.parse()
|
||||
}
|
||||
use crate::{Program, PythonVersion};
|
||||
|
||||
static VENDORED_VERSIONS: Lazy<TypeshedVersions> = Lazy::new(|| {
|
||||
TypeshedVersions::from_str(
|
||||
@@ -89,6 +21,14 @@ static VENDORED_VERSIONS: Lazy<TypeshedVersions> = Lazy::new(|| {
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
pub(crate) fn vendored_typeshed_versions() -> &'static TypeshedVersions {
|
||||
&VENDORED_VERSIONS
|
||||
}
|
||||
|
||||
pub(crate) fn typeshed_versions(db: &dyn Db) -> &TypeshedVersions {
|
||||
Program::get(db).search_paths(db).typeshed_versions()
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub(crate) struct TypeshedVersionsParseError {
|
||||
line_number: Option<NonZeroU16>,
|
||||
@@ -175,10 +115,10 @@ impl TypeshedVersions {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn query_module(
|
||||
pub(in crate::module_resolver) fn query_module(
|
||||
&self,
|
||||
module: &ModuleName,
|
||||
target_version: PyVersion,
|
||||
target_version: PythonVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
if let Some(range) = self.exact(module) {
|
||||
if range.contains(target_version) {
|
||||
@@ -205,7 +145,7 @@ impl TypeshedVersions {
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible answers [`LazyTypeshedVersions::query_module()`] could give to the question:
|
||||
/// Possible answers [`TypeshedVersions::query_module()`] could give to the question:
|
||||
/// "Does this module exist in the stdlib at runtime on a certain target version?"
|
||||
#[derive(Debug, Copy, PartialEq, Eq, Clone, Hash)]
|
||||
pub(crate) enum TypeshedVersionsQueryResult {
|
||||
@@ -323,13 +263,13 @@ impl fmt::Display for TypeshedVersions {
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
enum PyVersionRange {
|
||||
AvailableFrom(RangeFrom<PyVersion>),
|
||||
AvailableWithin(RangeInclusive<PyVersion>),
|
||||
AvailableFrom(RangeFrom<PythonVersion>),
|
||||
AvailableWithin(RangeInclusive<PythonVersion>),
|
||||
}
|
||||
|
||||
impl PyVersionRange {
|
||||
#[must_use]
|
||||
fn contains(&self, version: PyVersion) -> bool {
|
||||
fn contains(&self, version: PythonVersion) -> bool {
|
||||
match self {
|
||||
Self::AvailableFrom(inner) => inner.contains(&version),
|
||||
Self::AvailableWithin(inner) => inner.contains(&version),
|
||||
@@ -343,9 +283,14 @@ impl FromStr for PyVersionRange {
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = s.split('-').map(str::trim);
|
||||
match (parts.next(), parts.next(), parts.next()) {
|
||||
(Some(lower), Some(""), None) => Ok(Self::AvailableFrom((lower.parse()?)..)),
|
||||
(Some(lower), Some(""), None) => {
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
Ok(Self::AvailableFrom(lower..))
|
||||
}
|
||||
(Some(lower), Some(upper), None) => {
|
||||
Ok(Self::AvailableWithin((lower.parse()?)..=(upper.parse()?)))
|
||||
let lower = PythonVersion::from_versions_file_string(lower)?;
|
||||
let upper = PythonVersion::from_versions_file_string(upper)?;
|
||||
Ok(Self::AvailableWithin(lower..=upper))
|
||||
}
|
||||
_ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens),
|
||||
}
|
||||
@@ -363,74 +308,20 @@ impl fmt::Display for PyVersionRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
struct PyVersion {
|
||||
major: u8,
|
||||
minor: u8,
|
||||
}
|
||||
|
||||
impl FromStr for PyVersion {
|
||||
type Err = TypeshedVersionsParseErrorKind;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
impl PythonVersion {
|
||||
fn from_versions_file_string(s: &str) -> Result<Self, TypeshedVersionsParseErrorKind> {
|
||||
let mut parts = s.split('.').map(str::trim);
|
||||
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
|
||||
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
|
||||
s.to_string(),
|
||||
));
|
||||
};
|
||||
let major = match u8::from_str(major) {
|
||||
Ok(major) => major,
|
||||
Err(err) => {
|
||||
return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err,
|
||||
})
|
||||
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
|
||||
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err: int_parse_error,
|
||||
}
|
||||
};
|
||||
let minor = match u8::from_str(minor) {
|
||||
Ok(minor) => minor,
|
||||
Err(err) => {
|
||||
return Err(TypeshedVersionsParseErrorKind::IntegerParsingFailure {
|
||||
version: s.to_string(),
|
||||
err,
|
||||
})
|
||||
}
|
||||
};
|
||||
Ok(Self { major, minor })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PyVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let PyVersion { major, minor } = self;
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for PyVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => PyVersion { major: 3, minor: 7 },
|
||||
TargetVersion::Py38 => PyVersion { major: 3, minor: 8 },
|
||||
TargetVersion::Py39 => PyVersion { major: 3, minor: 9 },
|
||||
TargetVersion::Py310 => PyVersion {
|
||||
major: 3,
|
||||
minor: 10,
|
||||
},
|
||||
TargetVersion::Py311 => PyVersion {
|
||||
major: 3,
|
||||
minor: 11,
|
||||
},
|
||||
TargetVersion::Py312 => PyVersion {
|
||||
major: 3,
|
||||
minor: 12,
|
||||
},
|
||||
TargetVersion::Py313 => PyVersion {
|
||||
major: 3,
|
||||
minor: 13,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,7 +331,6 @@ mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use ruff_db::program::TargetVersion;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -478,27 +368,27 @@ mod tests {
|
||||
|
||||
assert!(versions.contains_exact(&asyncio));
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio, TargetVersion::Py310.into()),
|
||||
versions.query_module(&asyncio, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
|
||||
assert!(versions.contains_exact(&asyncio_staggered));
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio_staggered, TargetVersion::Py38.into()),
|
||||
versions.query_module(&asyncio_staggered, PythonVersion::PY38),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
versions.query_module(&asyncio_staggered, TargetVersion::Py37.into()),
|
||||
versions.query_module(&asyncio_staggered, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
|
||||
assert!(versions.contains_exact(&audioop));
|
||||
assert_eq!(
|
||||
versions.query_module(&audioop, TargetVersion::Py312.into()),
|
||||
versions.query_module(&audioop, PythonVersion::PY312),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
versions.query_module(&audioop, TargetVersion::Py313.into()),
|
||||
versions.query_module(&audioop, PythonVersion::PY313),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -590,15 +480,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&bar));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&bar, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -610,15 +500,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&foo));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py38.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY38),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&foo, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&foo, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
}
|
||||
@@ -630,15 +520,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(parsed_versions.contains_exact(&bar_baz));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py39.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY39),
|
||||
TypeshedVersionsQueryResult::Exists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_baz, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar_baz, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -650,15 +540,15 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(!parsed_versions.contains_exact(&bar_eggs));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py310.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY310),
|
||||
TypeshedVersionsQueryResult::MaybeExists
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&bar_eggs, TargetVersion::Py311.into()),
|
||||
parsed_versions.query_module(&bar_eggs, PythonVersion::PY311),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
@@ -670,11 +560,11 @@ foo: 3.8- # trailing comment
|
||||
|
||||
assert!(!parsed_versions.contains_exact(&spam));
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&spam, TargetVersion::Py37.into()),
|
||||
parsed_versions.query_module(&spam, PythonVersion::PY37),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
assert_eq!(
|
||||
parsed_versions.query_module(&spam, TargetVersion::Py313.into()),
|
||||
parsed_versions.query_module(&spam, PythonVersion::PY313),
|
||||
TypeshedVersionsQueryResult::DoesNotExist
|
||||
);
|
||||
}
|
||||
|
||||
100
crates/red_knot_python_semantic/src/program.rs
Normal file
100
crates/red_knot_python_semantic/src/program.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use crate::python_version::PythonVersion;
|
||||
use anyhow::Context;
|
||||
use salsa::Durability;
|
||||
use salsa::Setter;
|
||||
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
|
||||
use crate::module_resolver::SearchPaths;
|
||||
use crate::Db;
|
||||
|
||||
#[salsa::input(singleton)]
|
||||
pub struct Program {
|
||||
pub target_version: PythonVersion,
|
||||
|
||||
#[return_ref]
|
||||
pub(crate) search_paths: SearchPaths,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn from_settings(db: &dyn Db, settings: &ProgramSettings) -> anyhow::Result<Self> {
|
||||
let ProgramSettings {
|
||||
target_version,
|
||||
search_paths,
|
||||
} = settings;
|
||||
|
||||
tracing::info!("Target version: Python {target_version}");
|
||||
|
||||
let search_paths = SearchPaths::from_settings(db, search_paths)
|
||||
.with_context(|| "Invalid search path settings")?;
|
||||
|
||||
Ok(Program::builder(settings.target_version, search_paths)
|
||||
.durability(Durability::HIGH)
|
||||
.new(db))
|
||||
}
|
||||
|
||||
pub fn update_search_paths(
|
||||
self,
|
||||
db: &mut dyn Db,
|
||||
search_path_settings: &SearchPathSettings,
|
||||
) -> anyhow::Result<()> {
|
||||
let search_paths = SearchPaths::from_settings(db, search_path_settings)?;
|
||||
|
||||
if self.search_paths(db) != &search_paths {
|
||||
tracing::debug!("Update search paths");
|
||||
self.set_search_paths(db).to(search_paths);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn custom_stdlib_search_path(self, db: &dyn Db) -> Option<&SystemPath> {
|
||||
self.search_paths(db).custom_stdlib()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ProgramSettings {
|
||||
pub target_version: PythonVersion,
|
||||
pub search_paths: SearchPathSettings,
|
||||
}
|
||||
|
||||
/// Configures the search paths for module resolution.
|
||||
#[derive(Eq, PartialEq, Debug, Clone)]
|
||||
pub struct SearchPathSettings {
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||
/// or pyright's stubPath configuration setting.
|
||||
pub extra_paths: Vec<SystemPathBuf>,
|
||||
|
||||
/// The root of the workspace, used for finding first-party modules.
|
||||
pub src_root: SystemPathBuf,
|
||||
|
||||
/// Optional path to a "custom typeshed" directory on disk for us to use for standard-library types.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs for the stdlib,
|
||||
/// bundled as a zip file in the binary
|
||||
pub custom_typeshed: Option<SystemPathBuf>,
|
||||
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
pub site_packages: SitePackages,
|
||||
}
|
||||
|
||||
impl SearchPathSettings {
|
||||
pub fn new(src_root: SystemPathBuf) -> Self {
|
||||
Self {
|
||||
src_root,
|
||||
extra_paths: vec![],
|
||||
custom_typeshed: None,
|
||||
site_packages: SitePackages::Known(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum SitePackages {
|
||||
Derived {
|
||||
venv_path: SystemPathBuf,
|
||||
},
|
||||
/// Resolved site packages paths
|
||||
Known(Vec<SystemPathBuf>),
|
||||
}
|
||||
69
crates/red_knot_python_semantic/src/python_version.rs
Normal file
69
crates/red_knot_python_semantic/src/python_version.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::fmt;
|
||||
|
||||
/// Representation of a Python version.
|
||||
///
|
||||
/// Unlike the `TargetVersion` enums in the CLI crates,
|
||||
/// this does not necessarily represent a Python version that we actually support.
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct PythonVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
impl PythonVersion {
|
||||
pub const PY37: PythonVersion = PythonVersion { major: 3, minor: 7 };
|
||||
pub const PY38: PythonVersion = PythonVersion { major: 3, minor: 8 };
|
||||
pub const PY39: PythonVersion = PythonVersion { major: 3, minor: 9 };
|
||||
pub const PY310: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 10,
|
||||
};
|
||||
pub const PY311: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 11,
|
||||
};
|
||||
pub const PY312: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 12,
|
||||
};
|
||||
pub const PY313: PythonVersion = PythonVersion {
|
||||
major: 3,
|
||||
minor: 13,
|
||||
};
|
||||
|
||||
pub fn free_threaded_build_available(self) -> bool {
|
||||
self >= PythonVersion::PY313
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PythonVersion {
|
||||
fn default() -> Self {
|
||||
Self::PY38
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(&str, &str)> for PythonVersion {
|
||||
type Error = std::num::ParseIntError;
|
||||
|
||||
fn try_from(value: (&str, &str)) -> Result<Self, Self::Error> {
|
||||
let (major, minor) = value;
|
||||
Ok(Self {
|
||||
major: major.parse()?,
|
||||
minor: minor.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u8, u8)> for PythonVersion {
|
||||
fn from(value: (u8, u8)) -> Self {
|
||||
let (major, minor) = value;
|
||||
Self { major, minor }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let PythonVersion { major, minor } = self;
|
||||
write!(f, "{major}.{minor}")
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,21 @@ use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolTable,
|
||||
};
|
||||
use crate::semantic_index::use_def::UseDefMap;
|
||||
use crate::Db;
|
||||
|
||||
pub(crate) use self::use_def::UseDefMap;
|
||||
|
||||
pub mod ast_ids;
|
||||
mod builder;
|
||||
pub(crate) mod constraint;
|
||||
pub mod definition;
|
||||
pub mod expression;
|
||||
pub mod symbol;
|
||||
mod use_def;
|
||||
|
||||
pub(crate) use self::use_def::{
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
};
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
|
||||
|
||||
/// Returns the semantic index for `file`.
|
||||
@@ -89,8 +93,6 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
scopes: IndexVec<FileScopeId, Scope>,
|
||||
|
||||
/// Map expressions to their corresponding scope.
|
||||
/// We can't use [`ExpressionId`] here, because the challenge is how to get from
|
||||
/// an [`ast::Expr`] to an [`ExpressionId`] (which requires knowing the scope).
|
||||
scopes_by_expression: FxHashMap<ExpressionNodeKey, FileScopeId>,
|
||||
|
||||
/// Map from a node creating a definition to its definition.
|
||||
@@ -113,12 +115,15 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
/// Note: We should not depend on this map when analysing other files or
|
||||
/// changing a file invalidates all dependents.
|
||||
ast_ids: IndexVec<FileScopeId, AstIds>,
|
||||
|
||||
/// Flags about the global scope (code usage impacting inference)
|
||||
has_future_annotations: bool,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndex<'db> {
|
||||
/// Returns the symbol table for a specific scope.
|
||||
///
|
||||
/// Use the Salsa cached [`symbol_table`] query if you only need the
|
||||
/// Use the Salsa cached [`symbol_table()`] query if you only need the
|
||||
/// symbol table for a single scope.
|
||||
pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc<SymbolTable> {
|
||||
self.symbol_tables[scope_id].clone()
|
||||
@@ -126,7 +131,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
|
||||
/// Returns the use-def map for a specific scope.
|
||||
///
|
||||
/// Use the Salsa cached [`use_def_map`] query if you only need the
|
||||
/// Use the Salsa cached [`use_def_map()`] query if you only need the
|
||||
/// use-def map for a single scope.
|
||||
pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> Arc<UseDefMap> {
|
||||
self.use_def_maps[scope_id].clone()
|
||||
@@ -155,6 +160,10 @@ impl<'db> SemanticIndex<'db> {
|
||||
&self.scopes[id]
|
||||
}
|
||||
|
||||
pub(crate) fn scope_ids(&self) -> impl Iterator<Item = ScopeId> {
|
||||
self.scope_ids_by_scope.iter().copied()
|
||||
}
|
||||
|
||||
/// Returns the id of the parent scope.
|
||||
pub(crate) fn parent_scope_id(&self, scope_id: FileScopeId) -> Option<FileScopeId> {
|
||||
let scope = self.scope(scope_id);
|
||||
@@ -209,6 +218,12 @@ impl<'db> SemanticIndex<'db> {
|
||||
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
||||
self.scopes_by_node[&node.node_key()]
|
||||
}
|
||||
|
||||
/// Checks if there is an import of `__future__.annotations` in the global scope, which affects
|
||||
/// the logic for type inference.
|
||||
pub(super) fn has_future_annotations(&self) -> bool {
|
||||
self.has_future_annotations
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AncestorsIter<'a> {
|
||||
@@ -309,14 +324,32 @@ mod tests {
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::semantic_index::ast_ids::HasScopedUseId;
|
||||
use crate::semantic_index::definition::DefinitionKind;
|
||||
use crate::semantic_index::symbol::{FileScopeId, Scope, ScopeKind, SymbolTable};
|
||||
use crate::semantic_index::ast_ids::{HasScopedUseId, ScopedUseId};
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, Scope, ScopeKind, ScopedSymbolId, SymbolTable,
|
||||
};
|
||||
use crate::semantic_index::use_def::UseDefMap;
|
||||
use crate::semantic_index::{global_scope, semantic_index, symbol_table, use_def_map};
|
||||
use crate::Db;
|
||||
|
||||
impl UseDefMap<'_> {
|
||||
fn first_public_binding(&self, symbol: ScopedSymbolId) -> Option<Definition<'_>> {
|
||||
self.public_bindings(symbol)
|
||||
.next()
|
||||
.map(|constrained_binding| constrained_binding.binding)
|
||||
}
|
||||
|
||||
fn first_binding_at_use(&self, use_id: ScopedUseId) -> Option<Definition<'_>> {
|
||||
self.bindings_at_use(use_id)
|
||||
.next()
|
||||
.map(|constrained_binding| constrained_binding.binding)
|
||||
}
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
db: TestDb,
|
||||
file: File,
|
||||
@@ -375,10 +408,8 @@ mod tests {
|
||||
let foo = global_table.symbol_id_by_name("foo").unwrap();
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let [definition] = use_def.public_definitions(foo) else {
|
||||
panic!("expected one definition");
|
||||
};
|
||||
assert!(matches!(definition.node(&db), DefinitionKind::Import(_)));
|
||||
let binding = use_def.first_public_binding(foo).unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Import(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -407,22 +438,19 @@ mod tests {
|
||||
assert!(
|
||||
global_table
|
||||
.symbol_by_name("foo")
|
||||
.is_some_and(|symbol| { symbol.is_defined() && !symbol.is_used() }),
|
||||
.is_some_and(|symbol| { symbol.is_bound() && !symbol.is_used() }),
|
||||
"symbols that are defined get the defined flag"
|
||||
);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let [definition] = use_def.public_definitions(
|
||||
global_table
|
||||
.symbol_id_by_name("foo")
|
||||
.expect("symbol to exist"),
|
||||
) else {
|
||||
panic!("expected one definition");
|
||||
};
|
||||
assert!(matches!(
|
||||
definition.node(&db),
|
||||
DefinitionKind::ImportFrom(_)
|
||||
));
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
global_table
|
||||
.symbol_id_by_name("foo")
|
||||
.expect("symbol to exist"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::ImportFrom(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -435,18 +463,32 @@ mod tests {
|
||||
assert!(
|
||||
global_table
|
||||
.symbol_by_name("foo")
|
||||
.is_some_and(|symbol| { !symbol.is_defined() && symbol.is_used() }),
|
||||
"a symbol used but not defined in a scope should have only the used flag"
|
||||
.is_some_and(|symbol| { !symbol.is_bound() && symbol.is_used() }),
|
||||
"a symbol used but not bound in a scope should have only the used flag"
|
||||
);
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let [definition] =
|
||||
use_def.public_definitions(global_table.symbol_id_by_name("x").expect("symbol exists"))
|
||||
else {
|
||||
panic!("expected one definition");
|
||||
};
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name("x").expect("symbol exists"))
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn augmented_assignment() {
|
||||
let TestCase { db, file } = test_case("x += 1");
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(names(&global_table), vec!["x"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name("x").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
definition.node(&db),
|
||||
DefinitionKind::Assignment(_)
|
||||
binding.kind(&db),
|
||||
DefinitionKind::AugmentedAssignment(_)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -478,15 +520,10 @@ y = 2
|
||||
assert_eq!(names(&class_table), vec!["x"]);
|
||||
|
||||
let use_def = index.use_def_map(class_scope_id);
|
||||
let [definition] =
|
||||
use_def.public_definitions(class_table.symbol_id_by_name("x").expect("symbol exists"))
|
||||
else {
|
||||
panic!("expected one definition");
|
||||
};
|
||||
assert!(matches!(
|
||||
definition.node(&db),
|
||||
DefinitionKind::Assignment(_)
|
||||
));
|
||||
let binding = use_def
|
||||
.first_public_binding(class_table.symbol_id_by_name("x").expect("symbol exists"))
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -516,17 +553,298 @@ y = 2
|
||||
assert_eq!(names(&function_table), vec!["x"]);
|
||||
|
||||
let use_def = index.use_def_map(function_scope_id);
|
||||
let [definition] = use_def.public_definitions(
|
||||
function_table
|
||||
.symbol_id_by_name("x")
|
||||
.expect("symbol exists"),
|
||||
) else {
|
||||
panic!("expected one definition");
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
function_table
|
||||
.symbol_id_by_name("x")
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Assignment(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_parameter_symbols() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
def f(a: str, /, b: str, c: int = 1, *args, d: int = 2, **kwargs):
|
||||
pass
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert_eq!(names(&global_table), vec!["str", "int", "f"]);
|
||||
|
||||
let [(function_scope_id, _function_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("Expected a function scope")
|
||||
};
|
||||
assert!(matches!(
|
||||
definition.node(&db),
|
||||
DefinitionKind::Assignment(_)
|
||||
));
|
||||
|
||||
let function_table = index.symbol_table(function_scope_id);
|
||||
assert_eq!(
|
||||
names(&function_table),
|
||||
vec!["a", "b", "c", "args", "d", "kwargs"],
|
||||
);
|
||||
|
||||
let use_def = index.use_def_map(function_scope_id);
|
||||
for name in ["a", "b", "c", "d"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
function_table
|
||||
.symbol_id_by_name(name)
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
binding.kind(&db),
|
||||
DefinitionKind::ParameterWithDefault(_)
|
||||
));
|
||||
}
|
||||
for name in ["args", "kwargs"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
function_table
|
||||
.symbol_id_by_name(name)
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lambda_parameter_symbols() {
|
||||
let TestCase { db, file } = test_case("lambda a, b, c=1, *args, d=2, **kwargs: None");
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope(&db, file));
|
||||
|
||||
assert!(names(&global_table).is_empty());
|
||||
|
||||
let [(lambda_scope_id, _lambda_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("Expected a lambda scope")
|
||||
};
|
||||
|
||||
let lambda_table = index.symbol_table(lambda_scope_id);
|
||||
assert_eq!(
|
||||
names(&lambda_table),
|
||||
vec!["a", "b", "c", "args", "d", "kwargs"],
|
||||
);
|
||||
|
||||
let use_def = index.use_def_map(lambda_scope_id);
|
||||
for name in ["a", "b", "c", "d"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
binding.kind(&db),
|
||||
DefinitionKind::ParameterWithDefault(_)
|
||||
));
|
||||
}
|
||||
for name in ["args", "kwargs"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(lambda_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Parameter(_)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test case to validate that the comprehension scope is correctly identified and that the target
|
||||
/// variable is defined only in the comprehension scope and not in the global scope.
|
||||
#[test]
|
||||
fn comprehension_scope() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
[x for x, y in iter1]
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = index.symbol_table(FileScopeId::global());
|
||||
|
||||
assert_eq!(names(&global_table), vec!["iter1"]);
|
||||
|
||||
let [(comprehension_scope_id, comprehension_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("expected one child scope")
|
||||
};
|
||||
|
||||
assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension);
|
||||
assert_eq!(
|
||||
comprehension_scope_id.to_scope_id(&db, file).name(&db),
|
||||
"<listcomp>"
|
||||
);
|
||||
|
||||
let comprehension_symbol_table = index.symbol_table(comprehension_scope_id);
|
||||
|
||||
assert_eq!(names(&comprehension_symbol_table), vec!["x", "y"]);
|
||||
|
||||
let use_def = index.use_def_map(comprehension_scope_id);
|
||||
for name in ["x", "y"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
comprehension_symbol_table
|
||||
.symbol_id_by_name(name)
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
binding.kind(&db),
|
||||
DefinitionKind::Comprehension(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Test case to validate that the `x` variable used in the comprehension is referencing the
|
||||
/// `x` variable defined by the inner generator (`for x in iter2`) and not the outer one.
|
||||
#[test]
|
||||
fn multiple_generators() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
[x for x in iter1 for x in iter2]
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let [(comprehension_scope_id, _)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("expected one child scope")
|
||||
};
|
||||
|
||||
let use_def = index.use_def_map(comprehension_scope_id);
|
||||
|
||||
let module = parsed_module(&db, file).syntax();
|
||||
let element = module.body[0]
|
||||
.as_expr_stmt()
|
||||
.unwrap()
|
||||
.value
|
||||
.as_list_comp_expr()
|
||||
.unwrap()
|
||||
.elt
|
||||
.as_name_expr()
|
||||
.unwrap();
|
||||
let element_use_id =
|
||||
element.scoped_use_id(&db, comprehension_scope_id.to_scope_id(&db, file));
|
||||
|
||||
let binding = use_def.first_binding_at_use(element_use_id).unwrap();
|
||||
let DefinitionKind::Comprehension(comprehension) = binding.kind(&db) else {
|
||||
panic!("expected generator definition")
|
||||
};
|
||||
let target = comprehension.target();
|
||||
let name = target.id().as_str();
|
||||
|
||||
assert_eq!(name, "x");
|
||||
assert_eq!(target.range(), TextRange::new(23.into(), 24.into()));
|
||||
}
|
||||
|
||||
/// Test case to validate that the nested comprehension creates a new scope which is a child of
|
||||
/// the outer comprehension scope and the variables are correctly defined in the respective
|
||||
/// scopes.
|
||||
#[test]
|
||||
fn nested_generators() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
[{x for x in iter2} for y in iter1]
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = index.symbol_table(FileScopeId::global());
|
||||
|
||||
assert_eq!(names(&global_table), vec!["iter1"]);
|
||||
|
||||
let [(comprehension_scope_id, comprehension_scope)] = index
|
||||
.child_scopes(FileScopeId::global())
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("expected one child scope")
|
||||
};
|
||||
|
||||
assert_eq!(comprehension_scope.kind(), ScopeKind::Comprehension);
|
||||
assert_eq!(
|
||||
comprehension_scope_id.to_scope_id(&db, file).name(&db),
|
||||
"<listcomp>"
|
||||
);
|
||||
|
||||
let comprehension_symbol_table = index.symbol_table(comprehension_scope_id);
|
||||
|
||||
assert_eq!(names(&comprehension_symbol_table), vec!["y", "iter2"]);
|
||||
|
||||
let [(inner_comprehension_scope_id, inner_comprehension_scope)] = index
|
||||
.child_scopes(comprehension_scope_id)
|
||||
.collect::<Vec<_>>()[..]
|
||||
else {
|
||||
panic!("expected one inner generator scope")
|
||||
};
|
||||
|
||||
assert_eq!(inner_comprehension_scope.kind(), ScopeKind::Comprehension);
|
||||
assert_eq!(
|
||||
inner_comprehension_scope_id
|
||||
.to_scope_id(&db, file)
|
||||
.name(&db),
|
||||
"<setcomp>"
|
||||
);
|
||||
|
||||
let inner_comprehension_symbol_table = index.symbol_table(inner_comprehension_scope_id);
|
||||
|
||||
assert_eq!(names(&inner_comprehension_symbol_table), vec!["x"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_item_definition() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
with item1 as x, item2 as y:
|
||||
pass
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = index.symbol_table(FileScopeId::global());
|
||||
|
||||
assert_eq!(names(&global_table), vec!["item1", "x", "item2", "y"]);
|
||||
|
||||
let use_def = index.use_def_map(FileScopeId::global());
|
||||
for name in ["x", "y"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.expect("Expected with item definition for {name}");
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_item_unpacked_definition() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
with context() as (x, y):
|
||||
pass
|
||||
",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
let global_table = index.symbol_table(FileScopeId::global());
|
||||
|
||||
assert_eq!(names(&global_table), vec!["context", "x", "y"]);
|
||||
|
||||
let use_def = index.use_def_map(FileScopeId::global());
|
||||
for name in ["x", "y"] {
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.expect("Expected with item definition for {name}");
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::WithItem(_)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -562,14 +880,14 @@ def func():
|
||||
assert_eq!(names(&func2_table), vec!["y"]);
|
||||
|
||||
let use_def = index.use_def_map(FileScopeId::global());
|
||||
let [definition] = use_def.public_definitions(
|
||||
global_table
|
||||
.symbol_id_by_name("func")
|
||||
.expect("symbol exists"),
|
||||
) else {
|
||||
panic!("expected one definition");
|
||||
};
|
||||
assert!(matches!(definition.node(&db), DefinitionKind::Function(_)));
|
||||
let binding = use_def
|
||||
.first_public_binding(
|
||||
global_table
|
||||
.symbol_id_by_name("func")
|
||||
.expect("symbol exists"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::Function(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -637,7 +955,7 @@ class C[T]:
|
||||
assert!(
|
||||
ann_table
|
||||
.symbol_by_name("T")
|
||||
.is_some_and(|s| s.is_defined() && !s.is_used()),
|
||||
.is_some_and(|s| s.is_bound() && !s.is_used()),
|
||||
"type parameters are defined by the scope that introduces them"
|
||||
);
|
||||
|
||||
@@ -669,10 +987,8 @@ class C[T]:
|
||||
};
|
||||
let x_use_id = x_use_expr_name.scoped_use_id(&db, scope);
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let [definition] = use_def.use_definitions(x_use_id) else {
|
||||
panic!("expected one definition");
|
||||
};
|
||||
let DefinitionKind::Assignment(assignment) = definition.node(&db) else {
|
||||
let binding = use_def.first_binding_at_use(x_use_id).unwrap();
|
||||
let DefinitionKind::Assignment(assignment) = binding.kind(&db) else {
|
||||
panic!("should be an assignment definition")
|
||||
};
|
||||
let ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
@@ -720,7 +1036,7 @@ class C[T]:
|
||||
}
|
||||
|
||||
let TestCase { db, file } = test_case(
|
||||
r#"
|
||||
r"
|
||||
class Test:
|
||||
def foo():
|
||||
def bar():
|
||||
@@ -729,7 +1045,7 @@ class Test:
|
||||
pass
|
||||
|
||||
def x():
|
||||
pass"#,
|
||||
pass",
|
||||
);
|
||||
|
||||
let index = semantic_index(&db, file);
|
||||
@@ -762,4 +1078,136 @@ def x():
|
||||
vec!["bar", "foo", "Test", "<module>"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_stmt() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
match subject:
|
||||
case a: ...
|
||||
case [b, c, *d]: ...
|
||||
case e as f: ...
|
||||
case {'x': g, **h}: ...
|
||||
case Foo(i, z=j): ...
|
||||
case k | l: ...
|
||||
case _: ...
|
||||
",
|
||||
);
|
||||
|
||||
let global_scope_id = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope_id);
|
||||
|
||||
assert!(global_table.symbol_by_name("Foo").unwrap().is_used());
|
||||
assert_eq!(
|
||||
names(&global_table),
|
||||
vec!["subject", "a", "b", "c", "d", "e", "f", "g", "h", "Foo", "i", "j", "k", "l"]
|
||||
);
|
||||
|
||||
let use_def = use_def_map(&db, global_scope_id);
|
||||
for (name, expected_index) in [
|
||||
("a", 0),
|
||||
("b", 0),
|
||||
("c", 1),
|
||||
("d", 2),
|
||||
("e", 0),
|
||||
("f", 1),
|
||||
("g", 0),
|
||||
("h", 1),
|
||||
("i", 0),
|
||||
("j", 1),
|
||||
("k", 0),
|
||||
("l", 1),
|
||||
] {
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.expect("Expected with item definition for {name}");
|
||||
if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) {
|
||||
assert_eq!(pattern.index(), expected_index);
|
||||
} else {
|
||||
panic!("Expected match pattern definition for {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_match_case() {
|
||||
let TestCase { db, file } = test_case(
|
||||
"
|
||||
match 1:
|
||||
case first:
|
||||
match 2:
|
||||
case second:
|
||||
pass
|
||||
",
|
||||
);
|
||||
|
||||
let global_scope_id = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, global_scope_id);
|
||||
|
||||
assert_eq!(names(&global_table), vec!["first", "second"]);
|
||||
|
||||
let use_def = use_def_map(&db, global_scope_id);
|
||||
for (name, expected_index) in [("first", 0), ("second", 0)] {
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name(name).expect("symbol exists"))
|
||||
.expect("Expected with item definition for {name}");
|
||||
if let DefinitionKind::MatchPattern(pattern) = binding.kind(&db) {
|
||||
assert_eq!(pattern.index(), expected_index);
|
||||
} else {
|
||||
panic!("Expected match pattern definition for {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loops_single_assignment() {
|
||||
let TestCase { db, file } = test_case("for x in a: pass");
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(&global_table), &["a", "x"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name("x").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loops_simple_unpacking() {
|
||||
let TestCase { db, file } = test_case("for (x, y) in a: pass");
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(&global_table), &["a", "x", "y"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let x_binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name("x").unwrap())
|
||||
.unwrap();
|
||||
let y_binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name("y").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(x_binding.kind(&db), DefinitionKind::For(_)));
|
||||
assert!(matches!(y_binding.kind(&db), DefinitionKind::For(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_loops_complex_unpacking() {
|
||||
let TestCase { db, file } = test_case("for [((a,) b), (c, d)] in e: pass");
|
||||
let scope = global_scope(&db, file);
|
||||
let global_table = symbol_table(&db, scope);
|
||||
|
||||
assert_eq!(&names(&global_table), &["e", "a", "b", "c", "d"]);
|
||||
|
||||
let use_def = use_def_map(&db, scope);
|
||||
let binding = use_def
|
||||
.first_public_binding(global_table.symbol_id_by_name("a").unwrap())
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(binding.kind(&db), DefinitionKind::For(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,9 @@ use crate::Db;
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct AstIds {
|
||||
/// Maps expressions to their expression id. Uses `NodeKey` because it avoids cloning [`Parsed`].
|
||||
/// Maps expressions to their expression id.
|
||||
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
|
||||
/// Maps expressions which "use" a symbol (that is, [`ExprName`]) to a use id.
|
||||
/// Maps expressions which "use" a symbol (that is, [`ast::ExprName`]) to a use id.
|
||||
uses_map: FxHashMap<ExpressionNodeKey, ScopedUseId>,
|
||||
}
|
||||
|
||||
|
||||
@@ -7,24 +7,31 @@ use ruff_db::parsed::ParsedModule;
|
||||
use ruff_index::IndexVec;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_stmt, Visitor};
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
|
||||
use ruff_python_ast::AnyParameterRef;
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionNodeRef, Definition, DefinitionNodeKey, DefinitionNodeRef,
|
||||
ImportFromDefinitionNodeRef,
|
||||
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
|
||||
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
};
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId, SymbolFlags,
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
|
||||
SymbolTableBuilder,
|
||||
};
|
||||
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, PatternConstraint};
|
||||
use super::definition::{
|
||||
DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
||||
WithItemDefinitionNodeRef,
|
||||
};
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db> {
|
||||
// Builder state
|
||||
db: &'db dyn Db,
|
||||
@@ -33,9 +40,14 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
scope_stack: Vec<FileScopeId>,
|
||||
/// The assignment we're currently visiting.
|
||||
current_assignment: Option<CurrentAssignment<'db>>,
|
||||
/// The match case we're currently visiting.
|
||||
current_match_case: Option<CurrentMatchCase<'db>>,
|
||||
/// Flow states at each `break` in the current loop.
|
||||
loop_break_states: Vec<FlowSnapshot>,
|
||||
|
||||
/// Flags about the file's global scope
|
||||
has_future_annotations: bool,
|
||||
|
||||
// Semantic Index fields
|
||||
scopes: IndexVec<FileScopeId, Scope>,
|
||||
scope_ids_by_scope: IndexVec<FileScopeId, ScopeId<'db>>,
|
||||
@@ -56,8 +68,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
module: parsed,
|
||||
scope_stack: Vec::new(),
|
||||
current_assignment: None,
|
||||
current_match_case: None,
|
||||
loop_break_states: vec![],
|
||||
|
||||
has_future_annotations: false,
|
||||
|
||||
scopes: IndexVec::new(),
|
||||
symbol_tables: IndexVec::new(),
|
||||
ast_ids: IndexVec::new(),
|
||||
@@ -155,49 +170,102 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.current_use_def_map_mut().restore(state);
|
||||
}
|
||||
|
||||
fn flow_merge(&mut self, state: &FlowSnapshot) {
|
||||
fn flow_merge(&mut self, state: FlowSnapshot) {
|
||||
self.current_use_def_map_mut().merge(state);
|
||||
}
|
||||
|
||||
fn add_or_update_symbol(&mut self, name: Name, flags: SymbolFlags) -> ScopedSymbolId {
|
||||
let symbol_table = self.current_symbol_table();
|
||||
let (symbol_id, added) = symbol_table.add_or_update_symbol(name, flags);
|
||||
fn add_symbol(&mut self, name: Name) -> ScopedSymbolId {
|
||||
let (symbol_id, added) = self.current_symbol_table().add_symbol(name);
|
||||
if added {
|
||||
let use_def_map = self.current_use_def_map_mut();
|
||||
use_def_map.add_symbol(symbol_id);
|
||||
self.current_use_def_map_mut().add_symbol(symbol_id);
|
||||
}
|
||||
symbol_id
|
||||
}
|
||||
|
||||
fn mark_symbol_bound(&mut self, id: ScopedSymbolId) {
|
||||
self.current_symbol_table().mark_symbol_bound(id);
|
||||
}
|
||||
|
||||
fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
|
||||
self.current_symbol_table().mark_symbol_used(id);
|
||||
}
|
||||
|
||||
fn add_definition<'a>(
|
||||
&mut self,
|
||||
symbol: ScopedSymbolId,
|
||||
definition_node: impl Into<DefinitionNodeRef<'a>>,
|
||||
) -> Definition<'db> {
|
||||
let definition_node = definition_node.into();
|
||||
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
|
||||
#[allow(unsafe_code)]
|
||||
// SAFETY: `definition_node` is guaranteed to be a child of `self.module`
|
||||
let kind = unsafe { definition_node.into_owned(self.module.clone()) };
|
||||
let category = kind.category();
|
||||
let definition = Definition::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
symbol,
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
definition_node.into_owned(self.module.clone())
|
||||
},
|
||||
kind,
|
||||
countme::Count::default(),
|
||||
);
|
||||
|
||||
self.definitions_by_node
|
||||
let existing_definition = self
|
||||
.definitions_by_node
|
||||
.insert(definition_node.key(), definition);
|
||||
self.current_use_def_map_mut()
|
||||
.record_definition(symbol, definition);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
|
||||
if category.is_binding() {
|
||||
self.mark_symbol_bound(symbol);
|
||||
}
|
||||
|
||||
let use_def = self.current_use_def_map_mut();
|
||||
match category {
|
||||
DefinitionCategory::DeclarationAndBinding => {
|
||||
use_def.record_declaration_and_binding(symbol, definition);
|
||||
}
|
||||
DefinitionCategory::Declaration => use_def.record_declaration(symbol, definition),
|
||||
DefinitionCategory::Binding => use_def.record_binding(symbol, definition),
|
||||
}
|
||||
|
||||
definition
|
||||
}
|
||||
|
||||
fn add_expression_constraint(&mut self, constraint_node: &ast::Expr) -> Expression<'db> {
|
||||
let expression = self.add_standalone_expression(constraint_node);
|
||||
self.current_use_def_map_mut()
|
||||
.record_constraint(Constraint::Expression(expression));
|
||||
|
||||
expression
|
||||
}
|
||||
|
||||
fn add_pattern_constraint(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
pattern: &ast::Pattern,
|
||||
) -> PatternConstraint<'db> {
|
||||
#[allow(unsafe_code)]
|
||||
let (subject, pattern) = unsafe {
|
||||
(
|
||||
AstNodeRef::new(self.module.clone(), subject),
|
||||
AstNodeRef::new(self.module.clone(), pattern),
|
||||
)
|
||||
};
|
||||
let pattern_constraint = PatternConstraint::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
subject,
|
||||
pattern,
|
||||
countme::Count::default(),
|
||||
);
|
||||
self.current_use_def_map_mut()
|
||||
.record_constraint(Constraint::Pattern(pattern_constraint));
|
||||
pattern_constraint
|
||||
}
|
||||
|
||||
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
|
||||
/// standalone (type narrowing tests, RHS of an assignment.)
|
||||
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) {
|
||||
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
|
||||
let expression = Expression::new(
|
||||
self.db,
|
||||
self.file,
|
||||
@@ -210,6 +278,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
);
|
||||
self.expressions_by_node
|
||||
.insert(expression_node.into(), expression);
|
||||
expression
|
||||
}
|
||||
|
||||
fn with_type_params(
|
||||
@@ -238,10 +307,13 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
..
|
||||
}) => (name, &None, default),
|
||||
};
|
||||
// TODO create Definition for typevars
|
||||
self.add_or_update_symbol(name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
if let Some(bound) = bound {
|
||||
self.visit_expr(bound);
|
||||
let symbol = self.add_symbol(name.id.clone());
|
||||
// TODO create Definition for PEP 695 typevars
|
||||
// note that the "bound" on the typevar is a totally different thing than whether
|
||||
// or not a name is "bound" by a typevar declaration; the latter is always true.
|
||||
self.mark_symbol_bound(symbol);
|
||||
if let Some(bounds) = bound {
|
||||
self.visit_expr(bounds);
|
||||
}
|
||||
if let Some(default) = default {
|
||||
self.visit_expr(default);
|
||||
@@ -258,6 +330,83 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
nested_scope
|
||||
}
|
||||
|
||||
/// This method does several things:
|
||||
/// - It pushes a new scope onto the stack for visiting
|
||||
/// a list/dict/set comprehension or generator expression
|
||||
/// - Inside that scope, it visits a list of [`Comprehension`] nodes,
|
||||
/// assumed to be the "generators" that compose a comprehension
|
||||
/// (that is, the `for x in y` and `for y in z` parts of `x for x in y for y in z`).
|
||||
/// - Inside that scope, it also calls a closure for visiting the outer `elt`
|
||||
/// of a list/dict/set comprehension or generator expression
|
||||
/// - It then pops the new scope off the stack
|
||||
///
|
||||
/// [`Comprehension`]: ast::Comprehension
|
||||
fn with_generators_scope(
|
||||
&mut self,
|
||||
scope: NodeWithScopeRef,
|
||||
generators: &'db [ast::Comprehension],
|
||||
visit_outer_elt: impl FnOnce(&mut Self),
|
||||
) {
|
||||
let mut generators_iter = generators.iter();
|
||||
|
||||
let Some(generator) = generators_iter.next() else {
|
||||
unreachable!("Expression must contain at least one generator");
|
||||
};
|
||||
|
||||
// The `iter` of the first generator is evaluated in the outer scope, while all subsequent
|
||||
// nodes are evaluated in the inner scope.
|
||||
self.add_standalone_expression(&generator.iter);
|
||||
self.visit_expr(&generator.iter);
|
||||
self.push_scope(scope);
|
||||
|
||||
self.current_assignment = Some(CurrentAssignment::Comprehension {
|
||||
node: generator,
|
||||
first: true,
|
||||
});
|
||||
self.visit_expr(&generator.target);
|
||||
self.current_assignment = None;
|
||||
|
||||
for expr in &generator.ifs {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
|
||||
for generator in generators_iter {
|
||||
self.add_standalone_expression(&generator.iter);
|
||||
self.visit_expr(&generator.iter);
|
||||
|
||||
self.current_assignment = Some(CurrentAssignment::Comprehension {
|
||||
node: generator,
|
||||
first: false,
|
||||
});
|
||||
self.visit_expr(&generator.target);
|
||||
self.current_assignment = None;
|
||||
|
||||
for expr in &generator.ifs {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
visit_outer_elt(self);
|
||||
self.pop_scope();
|
||||
}
|
||||
|
||||
fn declare_parameter(&mut self, parameter: AnyParameterRef) {
|
||||
let symbol = self.add_symbol(parameter.name().id().clone());
|
||||
|
||||
let definition = self.add_definition(symbol, parameter);
|
||||
|
||||
if let AnyParameterRef::NonVariadic(with_default) = parameter {
|
||||
// Insert a mapping from the parameter to the same definition.
|
||||
// This ensures that calling `HasTy::ty` on the inner parameter returns
|
||||
// a valid type (and doesn't panic)
|
||||
let existing_definition = self.definitions_by_node.insert(
|
||||
DefinitionNodeRef::from(AnyParameterRef::Variadic(&with_default.parameter)).key(),
|
||||
definition,
|
||||
);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn build(mut self) -> SemanticIndex<'db> {
|
||||
let module = self.module;
|
||||
self.visit_body(module.suite());
|
||||
@@ -306,6 +455,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
scopes_by_expression: self.scopes_by_expression,
|
||||
scopes_by_node: self.scopes_by_node,
|
||||
use_def_maps,
|
||||
has_future_annotations: self.has_future_annotations,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -321,10 +471,6 @@ where
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
|
||||
let symbol = self
|
||||
.add_or_update_symbol(function_def.name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
self.add_definition(symbol, function_def);
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::FunctionTypeParameters(function_def),
|
||||
function_def.type_params.as_deref(),
|
||||
@@ -335,18 +481,37 @@ where
|
||||
}
|
||||
|
||||
builder.push_scope(NodeWithScopeRef::Function(function_def));
|
||||
|
||||
// Add symbols and definitions for the parameters to the function scope.
|
||||
for parameter in &*function_def.parameters {
|
||||
builder.declare_parameter(parameter);
|
||||
}
|
||||
|
||||
builder.visit_body(&function_def.body);
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in function_def
|
||||
.parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.visit_expr(default);
|
||||
}
|
||||
// The symbol for the function name itself has to be evaluated
|
||||
// at the end to match the runtime evaluation of parameter defaults
|
||||
// and return-type annotations.
|
||||
let symbol = self.add_symbol(function_def.name.id.clone());
|
||||
self.add_definition(symbol, function_def);
|
||||
}
|
||||
ast::Stmt::ClassDef(class) => {
|
||||
for decorator in &class.decorator_list {
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
|
||||
let symbol =
|
||||
self.add_or_update_symbol(class.name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
let symbol = self.add_symbol(class.name.id.clone());
|
||||
self.add_definition(symbol, class);
|
||||
|
||||
self.with_type_params(
|
||||
@@ -372,7 +537,7 @@ where
|
||||
Name::new(alias.name.id.split('.').next().unwrap())
|
||||
};
|
||||
|
||||
let symbol = self.add_or_update_symbol(symbol_name, SymbolFlags::IS_DEFINED);
|
||||
let symbol = self.add_symbol(symbol_name);
|
||||
self.add_definition(symbol, alias);
|
||||
}
|
||||
}
|
||||
@@ -384,8 +549,16 @@ where
|
||||
&alias.name.id
|
||||
};
|
||||
|
||||
let symbol =
|
||||
self.add_or_update_symbol(symbol_name.clone(), SymbolFlags::IS_DEFINED);
|
||||
// Look for imports `from __future__ import annotations`, ignore `as ...`
|
||||
// We intentionally don't enforce the rules about location of `__future__`
|
||||
// imports here, we assume the user's intent was to apply the `__future__`
|
||||
// import, so we still check using it (and will also emit a diagnostic about a
|
||||
// miss-placed `__future__` import.)
|
||||
self.has_future_annotations |= alias.name.id == "annotations"
|
||||
&& node.module.as_deref() == Some("__future__");
|
||||
|
||||
let symbol = self.add_symbol(symbol_name.clone());
|
||||
|
||||
self.add_definition(symbol, ImportFromDefinitionNodeRef { node, alias_index });
|
||||
}
|
||||
}
|
||||
@@ -401,7 +574,6 @@ where
|
||||
}
|
||||
ast::Stmt::AnnAssign(node) => {
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
// TODO deferred annotation visiting
|
||||
self.visit_expr(&node.annotation);
|
||||
if let Some(value) = &node.value {
|
||||
self.visit_expr(value);
|
||||
@@ -410,9 +582,24 @@ where
|
||||
self.visit_expr(&node.target);
|
||||
self.current_assignment = None;
|
||||
}
|
||||
ast::Stmt::AugAssign(
|
||||
aug_assign @ ast::StmtAugAssign {
|
||||
range: _,
|
||||
target,
|
||||
op: _,
|
||||
value,
|
||||
},
|
||||
) => {
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
self.visit_expr(value);
|
||||
self.current_assignment = Some(aug_assign.into());
|
||||
self.visit_expr(target);
|
||||
self.current_assignment = None;
|
||||
}
|
||||
ast::Stmt::If(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
let pre_if = self.flow_snapshot();
|
||||
self.add_expression_constraint(&node.test);
|
||||
self.visit_body(&node.body);
|
||||
let mut post_clauses: Vec<FlowSnapshot> = vec![];
|
||||
for clause in &node.elif_else_clauses {
|
||||
@@ -425,7 +612,7 @@ where
|
||||
self.visit_elif_else_clause(clause);
|
||||
}
|
||||
for post_clause_state in post_clauses {
|
||||
self.flow_merge(&post_clause_state);
|
||||
self.flow_merge(post_clause_state);
|
||||
}
|
||||
let has_else = node
|
||||
.elif_else_clauses
|
||||
@@ -434,17 +621,26 @@ where
|
||||
if !has_else {
|
||||
// if there's no else clause, then it's possible we took none of the branches,
|
||||
// and the pre_if state can reach here
|
||||
self.flow_merge(&pre_if);
|
||||
self.flow_merge(pre_if);
|
||||
}
|
||||
}
|
||||
ast::Stmt::While(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
ast::Stmt::While(ast::StmtWhile {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_expr(test);
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
|
||||
// Save aside any break states from an outer loop
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
self.visit_body(&node.body);
|
||||
|
||||
// TODO: definitions created inside the body should be fully visible
|
||||
// to other statements/expressions inside the body --Alex/Carl
|
||||
self.visit_body(body);
|
||||
|
||||
// Get the break states from the body of this loop, and restore the saved outer
|
||||
// ones.
|
||||
let break_states =
|
||||
@@ -452,18 +648,147 @@ where
|
||||
|
||||
// We may execute the `else` clause without ever executing the body, so merge in
|
||||
// the pre-loop state before visiting `else`.
|
||||
self.flow_merge(&pre_loop);
|
||||
self.visit_body(&node.orelse);
|
||||
self.flow_merge(pre_loop);
|
||||
self.visit_body(orelse);
|
||||
|
||||
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
|
||||
// states after visiting `else`.
|
||||
for break_state in break_states {
|
||||
self.flow_merge(&break_state);
|
||||
self.flow_merge(break_state);
|
||||
}
|
||||
}
|
||||
ast::Stmt::With(ast::StmtWith { items, body, .. }) => {
|
||||
for item in items {
|
||||
self.visit_expr(&item.context_expr);
|
||||
if let Some(optional_vars) = item.optional_vars.as_deref() {
|
||||
self.add_standalone_expression(&item.context_expr);
|
||||
self.current_assignment = Some(item.into());
|
||||
self.visit_expr(optional_vars);
|
||||
self.current_assignment = None;
|
||||
}
|
||||
}
|
||||
self.visit_body(body);
|
||||
}
|
||||
ast::Stmt::Break(_) => {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
|
||||
ast::Stmt::For(
|
||||
for_stmt @ ast::StmtFor {
|
||||
range: _,
|
||||
is_async: _,
|
||||
target,
|
||||
iter,
|
||||
body,
|
||||
orelse,
|
||||
},
|
||||
) => {
|
||||
self.add_standalone_expression(iter);
|
||||
self.visit_expr(iter);
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
self.current_assignment = Some(for_stmt.into());
|
||||
self.visit_expr(target);
|
||||
self.current_assignment = None;
|
||||
|
||||
// TODO: Definitions created by loop variables
|
||||
// (and definitions created inside the body)
|
||||
// are fully visible to other statements/expressions inside the body --Alex/Carl
|
||||
self.visit_body(body);
|
||||
|
||||
let break_states =
|
||||
std::mem::replace(&mut self.loop_break_states, saved_break_states);
|
||||
|
||||
// We may execute the `else` clause without ever executing the body, so merge in
|
||||
// the pre-loop state before visiting `else`.
|
||||
self.flow_merge(pre_loop);
|
||||
self.visit_body(orelse);
|
||||
|
||||
// Breaking out of a `for` loop bypasses the `else` clause, so merge in the break
|
||||
// states after visiting `else`.
|
||||
for break_state in break_states {
|
||||
self.flow_merge(break_state);
|
||||
}
|
||||
}
|
||||
ast::Stmt::Match(ast::StmtMatch {
|
||||
subject,
|
||||
cases,
|
||||
range: _,
|
||||
}) => {
|
||||
self.add_standalone_expression(subject);
|
||||
self.visit_expr(subject);
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let Some((first, remaining)) = cases.split_first() else {
|
||||
return;
|
||||
};
|
||||
self.add_pattern_constraint(subject, &first.pattern);
|
||||
self.visit_match_case(first);
|
||||
|
||||
let mut post_case_snapshots = vec![];
|
||||
for case in remaining {
|
||||
post_case_snapshots.push(self.flow_snapshot());
|
||||
self.flow_restore(after_subject.clone());
|
||||
self.add_pattern_constraint(subject, &case.pattern);
|
||||
self.visit_match_case(case);
|
||||
}
|
||||
for post_clause_state in post_case_snapshots {
|
||||
self.flow_merge(post_clause_state);
|
||||
}
|
||||
if !cases
|
||||
.last()
|
||||
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard())
|
||||
{
|
||||
self.flow_merge(after_subject);
|
||||
}
|
||||
}
|
||||
ast::Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
is_star,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_body(body);
|
||||
|
||||
for except_handler in handlers {
|
||||
let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler;
|
||||
let ast::ExceptHandlerExceptHandler {
|
||||
name: symbol_name,
|
||||
type_: handled_exceptions,
|
||||
body: handler_body,
|
||||
range: _,
|
||||
} = except_handler;
|
||||
|
||||
if let Some(handled_exceptions) = handled_exceptions {
|
||||
self.visit_expr(handled_exceptions);
|
||||
}
|
||||
|
||||
// If `handled_exceptions` above was `None`, it's something like `except as e:`,
|
||||
// which is invalid syntax. However, it's still pretty obvious here that the user
|
||||
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
|
||||
if let Some(symbol_name) = symbol_name {
|
||||
let symbol = self.add_symbol(symbol_name.id.clone());
|
||||
|
||||
self.add_definition(
|
||||
symbol,
|
||||
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
|
||||
handler: except_handler,
|
||||
is_star: *is_star,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
self.visit_body(handler_body);
|
||||
}
|
||||
|
||||
self.visit_body(orelse);
|
||||
self.visit_body(finalbody);
|
||||
}
|
||||
_ => {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
@@ -476,16 +801,19 @@ where
|
||||
self.current_ast_ids().record_expression(expr);
|
||||
|
||||
match expr {
|
||||
ast::Expr::Name(name_node) => {
|
||||
let ast::ExprName { id, ctx, .. } = name_node;
|
||||
let flags = match ctx {
|
||||
ast::ExprContext::Load => SymbolFlags::IS_USED,
|
||||
ast::ExprContext::Store => SymbolFlags::IS_DEFINED,
|
||||
ast::ExprContext::Del => SymbolFlags::IS_DEFINED,
|
||||
ast::ExprContext::Invalid => SymbolFlags::empty(),
|
||||
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
|
||||
let (is_use, is_definition) = match (ctx, self.current_assignment) {
|
||||
(ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => {
|
||||
// For augmented assignment, the target expression is also used.
|
||||
(true, true)
|
||||
}
|
||||
(ast::ExprContext::Load, _) => (true, false),
|
||||
(ast::ExprContext::Store, _) => (false, true),
|
||||
(ast::ExprContext::Del, _) => (false, true),
|
||||
(ast::ExprContext::Invalid, _) => (false, false),
|
||||
};
|
||||
let symbol = self.add_or_update_symbol(id.clone(), flags);
|
||||
if flags.contains(SymbolFlags::IS_DEFINED) {
|
||||
let symbol = self.add_symbol(id.clone());
|
||||
if is_definition {
|
||||
match self.current_assignment {
|
||||
Some(CurrentAssignment::Assign(assignment)) => {
|
||||
self.add_definition(
|
||||
@@ -499,14 +827,51 @@ where
|
||||
Some(CurrentAssignment::AnnAssign(ann_assign)) => {
|
||||
self.add_definition(symbol, ann_assign);
|
||||
}
|
||||
Some(CurrentAssignment::AugAssign(aug_assign)) => {
|
||||
self.add_definition(symbol, aug_assign);
|
||||
}
|
||||
Some(CurrentAssignment::For(node)) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ForStmtDefinitionNodeRef {
|
||||
iterable: &node.iter,
|
||||
target: name_node,
|
||||
is_async: node.is_async,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(CurrentAssignment::Named(named)) => {
|
||||
// TODO(dhruvmanila): If the current scope is a comprehension, then the
|
||||
// named expression is implicitly nonlocal. This is yet to be
|
||||
// implemented.
|
||||
self.add_definition(symbol, named);
|
||||
}
|
||||
Some(CurrentAssignment::Comprehension { node, first }) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
ComprehensionDefinitionNodeRef {
|
||||
iterable: &node.iter,
|
||||
target: name_node,
|
||||
first,
|
||||
is_async: node.is_async,
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(CurrentAssignment::WithItem(with_item)) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
WithItemDefinitionNodeRef {
|
||||
node: with_item,
|
||||
target: name_node,
|
||||
},
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
if flags.contains(SymbolFlags::IS_USED) {
|
||||
if is_use {
|
||||
self.mark_symbol_used(symbol);
|
||||
let use_id = self.current_ast_ids().record_use(expr);
|
||||
self.current_use_def_map_mut().record_use(symbol, use_id);
|
||||
}
|
||||
@@ -515,17 +880,33 @@ where
|
||||
}
|
||||
ast::Expr::Named(node) => {
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
self.current_assignment = Some(node.into());
|
||||
// TODO walrus in comprehensions is implicitly nonlocal
|
||||
self.visit_expr(&node.value);
|
||||
self.current_assignment = Some(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
self.current_assignment = None;
|
||||
self.visit_expr(&node.value);
|
||||
}
|
||||
ast::Expr::Lambda(lambda) => {
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
// The default value of the parameters needs to be evaluated in the
|
||||
// enclosing scope.
|
||||
for default in parameters
|
||||
.iter_non_variadic_params()
|
||||
.filter_map(|param| param.default.as_deref())
|
||||
{
|
||||
self.visit_expr(default);
|
||||
}
|
||||
self.visit_parameters(parameters);
|
||||
}
|
||||
self.push_scope(NodeWithScopeRef::Lambda(lambda));
|
||||
|
||||
// Add symbols and definitions for the parameters to the lambda scope.
|
||||
if let Some(parameters) = &lambda.parameters {
|
||||
for parameter in &**parameters {
|
||||
self.declare_parameter(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
self.visit_expr(lambda.body.as_ref());
|
||||
self.pop_scope();
|
||||
}
|
||||
@@ -541,20 +922,139 @@ where
|
||||
let post_body = self.flow_snapshot();
|
||||
self.flow_restore(pre_if);
|
||||
self.visit_expr(orelse);
|
||||
self.flow_merge(&post_body);
|
||||
self.flow_merge(post_body);
|
||||
}
|
||||
ast::Expr::ListComp(
|
||||
list_comprehension @ ast::ExprListComp {
|
||||
elt, generators, ..
|
||||
},
|
||||
) => {
|
||||
self.with_generators_scope(
|
||||
NodeWithScopeRef::ListComprehension(list_comprehension),
|
||||
generators,
|
||||
|builder| builder.visit_expr(elt),
|
||||
);
|
||||
}
|
||||
ast::Expr::SetComp(
|
||||
set_comprehension @ ast::ExprSetComp {
|
||||
elt, generators, ..
|
||||
},
|
||||
) => {
|
||||
self.with_generators_scope(
|
||||
NodeWithScopeRef::SetComprehension(set_comprehension),
|
||||
generators,
|
||||
|builder| builder.visit_expr(elt),
|
||||
);
|
||||
}
|
||||
ast::Expr::Generator(
|
||||
generator @ ast::ExprGenerator {
|
||||
elt, generators, ..
|
||||
},
|
||||
) => {
|
||||
self.with_generators_scope(
|
||||
NodeWithScopeRef::GeneratorExpression(generator),
|
||||
generators,
|
||||
|builder| builder.visit_expr(elt),
|
||||
);
|
||||
}
|
||||
ast::Expr::DictComp(
|
||||
dict_comprehension @ ast::ExprDictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
self.with_generators_scope(
|
||||
NodeWithScopeRef::DictComprehension(dict_comprehension),
|
||||
generators,
|
||||
|builder| {
|
||||
builder.visit_expr(key);
|
||||
builder.visit_expr(value);
|
||||
},
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_parameters(&mut self, parameters: &'ast ast::Parameters) {
|
||||
// Intentionally avoid walking default expressions, as we handle them in the enclosing
|
||||
// scope.
|
||||
for parameter in parameters.iter().map(ast::AnyParameterRef::as_parameter) {
|
||||
self.visit_parameter(parameter);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_match_case(&mut self, match_case: &'ast ast::MatchCase) {
|
||||
debug_assert!(self.current_match_case.is_none());
|
||||
self.current_match_case = Some(CurrentMatchCase::new(&match_case.pattern));
|
||||
self.visit_pattern(&match_case.pattern);
|
||||
self.current_match_case = None;
|
||||
|
||||
if let Some(expr) = &match_case.guard {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
self.visit_body(&match_case.body);
|
||||
}
|
||||
|
||||
fn visit_pattern(&mut self, pattern: &'ast ast::Pattern) {
|
||||
if let ast::Pattern::MatchStar(ast::PatternMatchStar {
|
||||
name: Some(name),
|
||||
range: _,
|
||||
}) = pattern
|
||||
{
|
||||
let symbol = self.add_symbol(name.id().clone());
|
||||
let state = self.current_match_case.as_ref().unwrap();
|
||||
self.add_definition(
|
||||
symbol,
|
||||
MatchPatternDefinitionNodeRef {
|
||||
pattern: state.pattern,
|
||||
identifier: name,
|
||||
index: state.index,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
walk_pattern(self, pattern);
|
||||
|
||||
if let ast::Pattern::MatchAs(ast::PatternMatchAs {
|
||||
name: Some(name), ..
|
||||
})
|
||||
| ast::Pattern::MatchMapping(ast::PatternMatchMapping {
|
||||
rest: Some(name), ..
|
||||
}) = pattern
|
||||
{
|
||||
let symbol = self.add_symbol(name.id().clone());
|
||||
let state = self.current_match_case.as_ref().unwrap();
|
||||
self.add_definition(
|
||||
symbol,
|
||||
MatchPatternDefinitionNodeRef {
|
||||
pattern: state.pattern,
|
||||
identifier: name,
|
||||
index: state.index,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
self.current_match_case.as_mut().unwrap().index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign(&'a ast::StmtAssign),
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
For(&'a ast::StmtFor),
|
||||
Named(&'a ast::ExprNamed),
|
||||
Comprehension {
|
||||
node: &'a ast::Comprehension,
|
||||
first: bool,
|
||||
},
|
||||
WithItem(&'a ast::WithItem),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAssign> for CurrentAssignment<'a> {
|
||||
@@ -569,8 +1069,50 @@ impl<'a> From<&'a ast::StmtAnnAssign> for CurrentAssignment<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAugAssign> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtAugAssign) -> Self {
|
||||
Self::AugAssign(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFor> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::StmtFor) -> Self {
|
||||
Self::For(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::ExprNamed) -> Self {
|
||||
Self::Named(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::WithItem> for CurrentAssignment<'a> {
|
||||
fn from(value: &'a ast::WithItem) -> Self {
|
||||
Self::WithItem(value)
|
||||
}
|
||||
}
|
||||
|
||||
struct CurrentMatchCase<'a> {
|
||||
/// The pattern that's part of the current match case.
|
||||
pattern: &'a ast::Pattern,
|
||||
|
||||
/// The index of the sub-pattern that's being currently visited within the pattern.
|
||||
///
|
||||
/// For example:
|
||||
/// ```py
|
||||
/// match subject:
|
||||
/// case a as b: ...
|
||||
/// case [a, b]: ...
|
||||
/// case a | b: ...
|
||||
/// ```
|
||||
///
|
||||
/// In all of the above cases, the index would be 0 for `a` and 1 for `b`.
|
||||
index: u32,
|
||||
}
|
||||
|
||||
impl<'a> CurrentMatchCase<'a> {
|
||||
fn new(pattern: &'a ast::Pattern) -> Self {
|
||||
Self { pattern, index: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Constraint<'db> {
|
||||
Expression(Expression<'db>),
|
||||
Pattern(PatternConstraint<'db>),
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
pub(crate) struct PatternConstraint<'db> {
|
||||
#[id]
|
||||
pub(crate) file: File,
|
||||
|
||||
#[id]
|
||||
pub(crate) file_scope: FileScopeId,
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) subject: AstNodeRef<ast::Expr>,
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) pattern: AstNodeRef<ast::Pattern>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<PatternConstraint<'static>>,
|
||||
}
|
||||
|
||||
impl<'db> PatternConstraint<'db> {
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
self.file_scope(db).to_scope_id(db, self.file(db))
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ pub struct Definition<'db> {
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) node: DefinitionKind,
|
||||
pub(crate) kind: DefinitionKind,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Definition<'static>>,
|
||||
@@ -33,17 +33,36 @@ impl<'db> Definition<'db> {
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
self.file_scope(db).to_scope_id(db, self.file(db))
|
||||
}
|
||||
|
||||
pub(crate) fn category(self, db: &'db dyn Db) -> DefinitionCategory {
|
||||
self.kind(db).category()
|
||||
}
|
||||
|
||||
pub(crate) fn is_declaration(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).category().is_declaration()
|
||||
}
|
||||
|
||||
pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool {
|
||||
self.kind(db).category().is_binding()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) enum DefinitionNodeRef<'a> {
|
||||
Import(&'a ast::Alias),
|
||||
ImportFrom(ImportFromDefinitionNodeRef<'a>),
|
||||
For(ForStmtDefinitionNodeRef<'a>),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
Class(&'a ast::StmtClassDef),
|
||||
NamedExpression(&'a ast::ExprNamed),
|
||||
Assignment(AssignmentDefinitionNodeRef<'a>),
|
||||
AnnotatedAssignment(&'a ast::StmtAnnAssign),
|
||||
AugmentedAssignment(&'a ast::StmtAugAssign),
|
||||
Comprehension(ComprehensionDefinitionNodeRef<'a>),
|
||||
Parameter(ast::AnyParameterRef<'a>),
|
||||
WithItem(WithItemDefinitionNodeRef<'a>),
|
||||
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
|
||||
ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> {
|
||||
@@ -70,6 +89,12 @@ impl<'a> From<&'a ast::StmtAnnAssign> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtAugAssign> for DefinitionNodeRef<'a> {
|
||||
fn from(node: &'a ast::StmtAugAssign) -> Self {
|
||||
Self::AugmentedAssignment(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::Alias> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: &'a ast::Alias) -> Self {
|
||||
Self::Import(node_ref)
|
||||
@@ -82,12 +107,42 @@ impl<'a> From<ImportFromDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ForStmtDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(value: ForStmtDefinitionNodeRef<'a>) -> Self {
|
||||
Self::For(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AssignmentDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: AssignmentDefinitionNodeRef<'a>) -> Self {
|
||||
Self::Assignment(node_ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<WithItemDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node_ref: WithItemDefinitionNodeRef<'a>) -> Self {
|
||||
Self::WithItem(node_ref)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ComprehensionDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node: ComprehensionDefinitionNodeRef<'a>) -> Self {
|
||||
Self::Comprehension(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<ast::AnyParameterRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node: ast::AnyParameterRef<'a>) -> Self {
|
||||
Self::Parameter(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<MatchPatternDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
|
||||
fn from(node: MatchPatternDefinitionNodeRef<'a>) -> Self {
|
||||
Self::MatchPattern(node)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::StmtImportFrom,
|
||||
@@ -100,6 +155,44 @@ pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
pub(crate) node: &'a ast::WithItem,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ExceptHandlerDefinitionNodeRef<'a> {
|
||||
pub(crate) handler: &'a ast::ExceptHandlerExceptHandler,
|
||||
pub(crate) is_star: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ComprehensionDefinitionNodeRef<'a> {
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct MatchPatternDefinitionNodeRef<'a> {
|
||||
/// The outermost pattern node in which the identifier being defined occurs.
|
||||
pub(crate) pattern: &'a ast::Pattern,
|
||||
/// The identifier being defined.
|
||||
pub(crate) identifier: &'a ast::Identifier,
|
||||
/// The index of the identifier in the pattern when visiting the `pattern` node in evaluation
|
||||
/// order.
|
||||
pub(crate) index: u32,
|
||||
}
|
||||
|
||||
impl DefinitionNodeRef<'_> {
|
||||
#[allow(unsafe_code)]
|
||||
pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind {
|
||||
@@ -131,6 +224,59 @@ impl DefinitionNodeRef<'_> {
|
||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||
}
|
||||
DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => {
|
||||
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
|
||||
}
|
||||
DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
|
||||
iterable,
|
||||
target,
|
||||
is_async,
|
||||
}) => DefinitionKind::For(ForStmtDefinitionKind {
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
|
||||
iterable,
|
||||
target,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::Comprehension(ComprehensionDefinitionKind {
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::Parameter(parameter) => match parameter {
|
||||
ast::AnyParameterRef::Variadic(parameter) => {
|
||||
DefinitionKind::Parameter(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
ast::AnyParameterRef::NonVariadic(parameter) => {
|
||||
DefinitionKind::ParameterWithDefault(AstNodeRef::new(parsed, parameter))
|
||||
}
|
||||
},
|
||||
DefinitionNodeRef::WithItem(WithItemDefinitionNodeRef { node, target }) => {
|
||||
DefinitionKind::WithItem(WithItemDefinitionKind {
|
||||
node: AstNodeRef::new(parsed.clone(), node),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::MatchPattern(MatchPatternDefinitionNodeRef {
|
||||
pattern,
|
||||
identifier,
|
||||
index,
|
||||
}) => DefinitionKind::MatchPattern(MatchPatternDefinitionKind {
|
||||
pattern: AstNodeRef::new(parsed.clone(), pattern),
|
||||
identifier: AstNodeRef::new(parsed, identifier),
|
||||
index,
|
||||
}),
|
||||
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
|
||||
handler,
|
||||
is_star,
|
||||
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
|
||||
handler: AstNodeRef::new(parsed.clone(), handler),
|
||||
is_star,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,10 +294,61 @@ impl DefinitionNodeRef<'_> {
|
||||
target,
|
||||
}) => target.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
Self::For(ForStmtDefinitionNodeRef {
|
||||
iterable: _,
|
||||
target,
|
||||
is_async: _,
|
||||
}) => target.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
|
||||
Self::Parameter(node) => match node {
|
||||
ast::AnyParameterRef::Variadic(parameter) => parameter.into(),
|
||||
ast::AnyParameterRef::NonVariadic(parameter) => parameter.into(),
|
||||
},
|
||||
Self::WithItem(WithItemDefinitionNodeRef { node: _, target }) => target.into(),
|
||||
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
|
||||
identifier.into()
|
||||
}
|
||||
Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum DefinitionCategory {
|
||||
/// A Definition which binds a value to a name (e.g. `x = 1`).
|
||||
Binding,
|
||||
/// A Definition which declares the upper-bound of acceptable types for this name (`x: int`).
|
||||
Declaration,
|
||||
/// A Definition which both declares a type and binds a value (e.g. `x: int = 1`).
|
||||
DeclarationAndBinding,
|
||||
}
|
||||
|
||||
impl DefinitionCategory {
|
||||
/// True if this definition establishes a "declared type" for the symbol.
|
||||
///
|
||||
/// If so, any assignments reached by this definition are in error if they assign a value of a
|
||||
/// type not assignable to the declared type.
|
||||
///
|
||||
/// Annotations establish a declared type. So do function and class definitions, and imports.
|
||||
pub(crate) fn is_declaration(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DefinitionCategory::Declaration | DefinitionCategory::DeclarationAndBinding
|
||||
)
|
||||
}
|
||||
|
||||
/// True if this definition assigns a value to the symbol.
|
||||
///
|
||||
/// False only for annotated assignments without a RHS.
|
||||
pub(crate) fn is_binding(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
DefinitionCategory::Binding | DefinitionCategory::DeclarationAndBinding
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DefinitionKind {
|
||||
Import(AstNodeRef<ast::Alias>),
|
||||
@@ -161,6 +358,103 @@ pub enum DefinitionKind {
|
||||
NamedExpression(AstNodeRef<ast::ExprNamed>),
|
||||
Assignment(AssignmentDefinitionKind),
|
||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||
For(ForStmtDefinitionKind),
|
||||
Comprehension(ComprehensionDefinitionKind),
|
||||
Parameter(AstNodeRef<ast::Parameter>),
|
||||
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
|
||||
WithItem(WithItemDefinitionKind),
|
||||
MatchPattern(MatchPatternDefinitionKind),
|
||||
ExceptHandler(ExceptHandlerDefinitionKind),
|
||||
}
|
||||
|
||||
impl DefinitionKind {
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
DefinitionKind::Function(_)
|
||||
| DefinitionKind::Class(_)
|
||||
| DefinitionKind::Import(_)
|
||||
| DefinitionKind::ImportFrom(_) => DefinitionCategory::DeclarationAndBinding,
|
||||
// a parameter always binds a value, but is only a declaration if annotated
|
||||
DefinitionKind::Parameter(parameter) => {
|
||||
if parameter.annotation.is_some() {
|
||||
DefinitionCategory::DeclarationAndBinding
|
||||
} else {
|
||||
DefinitionCategory::Binding
|
||||
}
|
||||
}
|
||||
// presence of a default is irrelevant, same logic as for a no-default parameter
|
||||
DefinitionKind::ParameterWithDefault(parameter_with_default) => {
|
||||
if parameter_with_default.parameter.annotation.is_some() {
|
||||
DefinitionCategory::DeclarationAndBinding
|
||||
} else {
|
||||
DefinitionCategory::Binding
|
||||
}
|
||||
}
|
||||
// annotated assignment is always a declaration, only a binding if there is a RHS
|
||||
DefinitionKind::AnnotatedAssignment(ann_assign) => {
|
||||
if ann_assign.value.is_some() {
|
||||
DefinitionCategory::DeclarationAndBinding
|
||||
} else {
|
||||
DefinitionCategory::Declaration
|
||||
}
|
||||
}
|
||||
// all of these bind values without declaring a type
|
||||
DefinitionKind::NamedExpression(_)
|
||||
| DefinitionKind::Assignment(_)
|
||||
| DefinitionKind::AugmentedAssignment(_)
|
||||
| DefinitionKind::For(_)
|
||||
| DefinitionKind::Comprehension(_)
|
||||
| DefinitionKind::WithItem(_)
|
||||
| DefinitionKind::MatchPattern(_)
|
||||
| DefinitionKind::ExceptHandler(_) => DefinitionCategory::Binding,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct MatchPatternDefinitionKind {
|
||||
pattern: AstNodeRef<ast::Pattern>,
|
||||
identifier: AstNodeRef<ast::Identifier>,
|
||||
index: u32,
|
||||
}
|
||||
|
||||
impl MatchPatternDefinitionKind {
|
||||
pub(crate) fn pattern(&self) -> &ast::Pattern {
|
||||
self.pattern.node()
|
||||
}
|
||||
|
||||
pub(crate) fn index(&self) -> u32 {
|
||||
self.index
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ComprehensionDefinitionKind {
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
impl ComprehensionDefinitionKind {
|
||||
pub(crate) fn iterable(&self) -> &ast::Expr {
|
||||
self.iterable.node()
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
}
|
||||
|
||||
pub(crate) fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -180,7 +474,6 @@ impl ImportFromDefinitionKind {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef<ast::StmtAssign>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
@@ -190,6 +483,67 @@ impl AssignmentDefinitionKind {
|
||||
pub(crate) fn assignment(&self) -> &ast::StmtAssign {
|
||||
self.assignment.node()
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WithItemDefinitionKind {
|
||||
node: AstNodeRef<ast::WithItem>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
}
|
||||
|
||||
impl WithItemDefinitionKind {
|
||||
pub(crate) fn node(&self) -> &ast::WithItem {
|
||||
self.node.node()
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ForStmtDefinitionKind {
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
impl ForStmtDefinitionKind {
|
||||
pub(crate) fn iterable(&self) -> &ast::Expr {
|
||||
self.iterable.node()
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
}
|
||||
|
||||
pub(crate) fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExceptHandlerDefinitionKind {
|
||||
handler: AstNodeRef<ast::ExceptHandlerExceptHandler>,
|
||||
is_star: bool,
|
||||
}
|
||||
|
||||
impl ExceptHandlerDefinitionKind {
|
||||
pub(crate) fn node(&self) -> &ast::ExceptHandlerExceptHandler {
|
||||
self.handler.node()
|
||||
}
|
||||
|
||||
pub(crate) fn handled_exceptions(&self) -> Option<&ast::Expr> {
|
||||
self.node().type_.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn is_star(&self) -> bool {
|
||||
self.is_star
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
@@ -230,3 +584,39 @@ impl From<&ast::StmtAnnAssign> for DefinitionNodeKey {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::StmtAugAssign> for DefinitionNodeKey {
|
||||
fn from(node: &ast::StmtAugAssign) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::StmtFor> for DefinitionNodeKey {
|
||||
fn from(value: &ast::StmtFor) -> Self {
|
||||
Self(NodeKey::from_node(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Parameter> for DefinitionNodeKey {
|
||||
fn from(node: &ast::Parameter) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::ParameterWithDefault> for DefinitionNodeKey {
|
||||
fn from(node: &ast::ParameterWithDefault) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::Identifier> for DefinitionNodeKey {
|
||||
fn from(identifier: &ast::Identifier) -> Self {
|
||||
Self(NodeKey::from_node(identifier))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey {
|
||||
fn from(handler: &ast::ExceptHandlerExceptHandler) -> Self {
|
||||
Self(NodeKey::from_node(handler))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(crate) struct Expression<'db> {
|
||||
/// The expression node.
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) node: AstNodeRef<ast::Expr>,
|
||||
pub(crate) node_ref: AstNodeRef<ast::Expr>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Expression<'static>>,
|
||||
|
||||
@@ -44,16 +44,16 @@ impl Symbol {
|
||||
}
|
||||
|
||||
/// Is the symbol defined in its containing scope?
|
||||
pub fn is_defined(&self) -> bool {
|
||||
self.flags.contains(SymbolFlags::IS_DEFINED)
|
||||
pub fn is_bound(&self) -> bool {
|
||||
self.flags.contains(SymbolFlags::IS_BOUND)
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub(super) struct SymbolFlags: u8 {
|
||||
struct SymbolFlags: u8 {
|
||||
const IS_USED = 1 << 0;
|
||||
const IS_DEFINED = 1 << 1;
|
||||
const IS_BOUND = 1 << 1;
|
||||
/// TODO: This flag is not yet set by anything
|
||||
const MARKED_GLOBAL = 1 << 2;
|
||||
/// TODO: This flag is not yet set by anything
|
||||
@@ -114,6 +114,10 @@ impl<'db> ScopeId<'db> {
|
||||
NodeWithScopeKind::ClassTypeParameters(_)
|
||||
| NodeWithScopeKind::FunctionTypeParameters(_)
|
||||
| NodeWithScopeKind::Function(_)
|
||||
| NodeWithScopeKind::ListComprehension(_)
|
||||
| NodeWithScopeKind::SetComprehension(_)
|
||||
| NodeWithScopeKind::DictComprehension(_)
|
||||
| NodeWithScopeKind::GeneratorExpression(_)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,6 +131,10 @@ impl<'db> ScopeId<'db> {
|
||||
NodeWithScopeKind::Function(function)
|
||||
| NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(),
|
||||
NodeWithScopeKind::Lambda(_) => "<lambda>",
|
||||
NodeWithScopeKind::ListComprehension(_) => "<listcomp>",
|
||||
NodeWithScopeKind::SetComprehension(_) => "<setcomp>",
|
||||
NodeWithScopeKind::DictComprehension(_) => "<dictcomp>",
|
||||
NodeWithScopeKind::GeneratorExpression(_) => "<generator>",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,6 +149,10 @@ impl FileScopeId {
|
||||
FileScopeId::from_u32(0)
|
||||
}
|
||||
|
||||
pub fn is_global(self) -> bool {
|
||||
self == FileScopeId::global()
|
||||
}
|
||||
|
||||
pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
let index = semantic_index(db, file);
|
||||
index.scope_ids_by_scope[self]
|
||||
@@ -170,6 +182,13 @@ pub enum ScopeKind {
|
||||
Annotation,
|
||||
Class,
|
||||
Function,
|
||||
Comprehension,
|
||||
}
|
||||
|
||||
impl ScopeKind {
|
||||
pub const fn is_comprehension(self) -> bool {
|
||||
matches!(self, ScopeKind::Comprehension)
|
||||
}
|
||||
}
|
||||
|
||||
/// Symbol table for a specific [`Scope`].
|
||||
@@ -253,11 +272,7 @@ impl SymbolTableBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn add_or_update_symbol(
|
||||
&mut self,
|
||||
name: Name,
|
||||
flags: SymbolFlags,
|
||||
) -> (ScopedSymbolId, bool) {
|
||||
pub(super) fn add_symbol(&mut self, name: Name) -> (ScopedSymbolId, bool) {
|
||||
let hash = SymbolTable::hash_name(&name);
|
||||
let entry = self
|
||||
.table
|
||||
@@ -266,15 +281,9 @@ impl SymbolTableBuilder {
|
||||
.from_hash(hash, |id| self.table.symbols[*id].name() == &name);
|
||||
|
||||
match entry {
|
||||
RawEntryMut::Occupied(entry) => {
|
||||
let symbol = &mut self.table.symbols[*entry.key()];
|
||||
symbol.insert_flags(flags);
|
||||
|
||||
(*entry.key(), false)
|
||||
}
|
||||
RawEntryMut::Occupied(entry) => (*entry.key(), false),
|
||||
RawEntryMut::Vacant(entry) => {
|
||||
let mut symbol = Symbol::new(name);
|
||||
symbol.insert_flags(flags);
|
||||
let symbol = Symbol::new(name);
|
||||
|
||||
let id = self.table.symbols.push(symbol);
|
||||
entry.insert_with_hasher(hash, id, (), |id| {
|
||||
@@ -285,6 +294,14 @@ impl SymbolTableBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn mark_symbol_bound(&mut self, id: ScopedSymbolId) {
|
||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_BOUND);
|
||||
}
|
||||
|
||||
pub(super) fn mark_symbol_used(&mut self, id: ScopedSymbolId) {
|
||||
self.table.symbols[id].insert_flags(SymbolFlags::IS_USED);
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> SymbolTable {
|
||||
self.table.shrink_to_fit();
|
||||
self.table
|
||||
@@ -300,6 +317,10 @@ pub(crate) enum NodeWithScopeRef<'a> {
|
||||
Lambda(&'a ast::ExprLambda),
|
||||
FunctionTypeParameters(&'a ast::StmtFunctionDef),
|
||||
ClassTypeParameters(&'a ast::StmtClassDef),
|
||||
ListComprehension(&'a ast::ExprListComp),
|
||||
SetComprehension(&'a ast::ExprSetComp),
|
||||
DictComprehension(&'a ast::ExprDictComp),
|
||||
GeneratorExpression(&'a ast::ExprGenerator),
|
||||
}
|
||||
|
||||
impl NodeWithScopeRef<'_> {
|
||||
@@ -326,6 +347,18 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::ClassTypeParameters(class) => {
|
||||
NodeWithScopeKind::ClassTypeParameters(AstNodeRef::new(module, class))
|
||||
}
|
||||
NodeWithScopeRef::ListComprehension(comprehension) => {
|
||||
NodeWithScopeKind::ListComprehension(AstNodeRef::new(module, comprehension))
|
||||
}
|
||||
NodeWithScopeRef::SetComprehension(comprehension) => {
|
||||
NodeWithScopeKind::SetComprehension(AstNodeRef::new(module, comprehension))
|
||||
}
|
||||
NodeWithScopeRef::DictComprehension(comprehension) => {
|
||||
NodeWithScopeKind::DictComprehension(AstNodeRef::new(module, comprehension))
|
||||
}
|
||||
NodeWithScopeRef::GeneratorExpression(generator) => {
|
||||
NodeWithScopeKind::GeneratorExpression(AstNodeRef::new(module, generator))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +370,10 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::Lambda(_) => ScopeKind::Function,
|
||||
NodeWithScopeRef::FunctionTypeParameters(_)
|
||||
| NodeWithScopeRef::ClassTypeParameters(_) => ScopeKind::Annotation,
|
||||
NodeWithScopeRef::ListComprehension(_)
|
||||
| NodeWithScopeRef::SetComprehension(_)
|
||||
| NodeWithScopeRef::DictComprehension(_)
|
||||
| NodeWithScopeRef::GeneratorExpression(_) => ScopeKind::Comprehension,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -356,6 +393,18 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::ClassTypeParameters(class) => {
|
||||
NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class))
|
||||
}
|
||||
NodeWithScopeRef::ListComprehension(comprehension) => {
|
||||
NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension))
|
||||
}
|
||||
NodeWithScopeRef::SetComprehension(comprehension) => {
|
||||
NodeWithScopeKey::SetComprehension(NodeKey::from_node(comprehension))
|
||||
}
|
||||
NodeWithScopeRef::DictComprehension(comprehension) => {
|
||||
NodeWithScopeKey::DictComprehension(NodeKey::from_node(comprehension))
|
||||
}
|
||||
NodeWithScopeRef::GeneratorExpression(generator) => {
|
||||
NodeWithScopeKey::GeneratorExpression(NodeKey::from_node(generator))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,6 +418,10 @@ pub enum NodeWithScopeKind {
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
FunctionTypeParameters(AstNodeRef<ast::StmtFunctionDef>),
|
||||
Lambda(AstNodeRef<ast::ExprLambda>),
|
||||
ListComprehension(AstNodeRef<ast::ExprListComp>),
|
||||
SetComprehension(AstNodeRef<ast::ExprSetComp>),
|
||||
DictComprehension(AstNodeRef<ast::ExprDictComp>),
|
||||
GeneratorExpression(AstNodeRef<ast::ExprGenerator>),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -379,4 +432,8 @@ pub(crate) enum NodeWithScopeKey {
|
||||
Function(NodeKey),
|
||||
FunctionTypeParameters(NodeKey),
|
||||
Lambda(NodeKey),
|
||||
ListComprehension(NodeKey),
|
||||
SetComprehension(NodeKey),
|
||||
DictComprehension(NodeKey),
|
||||
GeneratorExpression(NodeKey),
|
||||
}
|
||||
|
||||
@@ -1,4 +1,79 @@
|
||||
//! Build a map from each use of a symbol to the definitions visible from that use.
|
||||
//! First, some terminology:
|
||||
//!
|
||||
//! * A "binding" gives a new value to a variable. This includes many different Python statements
|
||||
//! (assignment statements of course, but also imports, `def` and `class` statements, `as`
|
||||
//! clauses in `with` and `except` statements, match patterns, and others) and even one
|
||||
//! expression kind (named expressions). It notably does not include annotated assignment
|
||||
//! statements without a right-hand side value; these do not assign any new value to the
|
||||
//! variable. We consider function parameters to be bindings as well, since (from the perspective
|
||||
//! of the function's internal scope), a function parameter begins the scope bound to a value.
|
||||
//!
|
||||
//! * A "declaration" establishes an upper bound type for the values that a variable may be
|
||||
//! permitted to take on. Annotated assignment statements (with or without an RHS value) are
|
||||
//! declarations; annotated function parameters are also declarations. We consider `def` and
|
||||
//! `class` statements to also be declarations, so as to prohibit accidentally shadowing them.
|
||||
//!
|
||||
//! Annotated assignments with a right-hand side, and annotated function parameters, are both
|
||||
//! bindings and declarations.
|
||||
//!
|
||||
//! We use [`Definition`] as the universal term (and Salsa tracked struct) encompassing both
|
||||
//! bindings and declarations. (This sacrifices a bit of type safety in exchange for improved
|
||||
//! performance via fewer Salsa tracked structs and queries, since most declarations -- typed
|
||||
//! parameters and annotated assignments with RHS -- are both bindings and declarations.)
|
||||
//!
|
||||
//! At any given use of a variable, we can ask about both its "declared type" and its "inferred
|
||||
//! type". These may be different, but the inferred type must always be assignable to the declared
|
||||
//! type; that is, the declared type is always wider, and the inferred type may be more precise. If
|
||||
//! we see an invalid assignment, we emit a diagnostic and abandon our inferred type, deferring to
|
||||
//! the declared type (this allows an explicit annotation to override bad inference, without a
|
||||
//! cast), maintaining the invariant.
|
||||
//!
|
||||
//! The **inferred type** represents the most precise type we believe encompasses all possible
|
||||
//! values for the variable at a given use. It is based on a union of the bindings which can reach
|
||||
//! that use through some control flow path, and the narrowing constraints that control flow must
|
||||
//! have passed through between the binding and the use. For example, in this code:
|
||||
//!
|
||||
//! ```python
|
||||
//! x = 1 if flag else None
|
||||
//! if x is not None:
|
||||
//! use(x)
|
||||
//! ```
|
||||
//!
|
||||
//! For the use of `x` on the third line, the inferred type should be `Literal[1]`. This is based
|
||||
//! on the binding on the first line, which assigns the type `Literal[1] | None`, and the narrowing
|
||||
//! constraint on the second line, which rules out the type `None`, since control flow must pass
|
||||
//! through this constraint to reach the use in question.
|
||||
//!
|
||||
//! The **declared type** represents the code author's declaration (usually through a type
|
||||
//! annotation) that a given variable should not be assigned any type outside the declared type. In
|
||||
//! our model, declared types are also control-flow-sensitive; we allow the code author to
|
||||
//! explicitly re-declare the same variable with a different type. So for a given binding of a
|
||||
//! variable, we will want to ask which declarations of that variable can reach that binding, in
|
||||
//! order to determine whether the binding is permitted, or should be a type error. For example:
|
||||
//!
|
||||
//! ```python
|
||||
//! from pathlib import Path
|
||||
//! def f(path: str):
|
||||
//! path: Path = Path(path)
|
||||
//! ```
|
||||
//!
|
||||
//! In this function, the initial declared type of `path` is `str`, meaning that the assignment
|
||||
//! `path = Path(path)` would be a type error, since it assigns to `path` a value whose type is not
|
||||
//! assignable to `str`. This is the purpose of declared types: they prevent accidental assignment
|
||||
//! of the wrong type to a variable.
|
||||
//!
|
||||
//! But in some cases it is useful to "shadow" or "re-declare" a variable with a new type, and we
|
||||
//! permit this, as long as it is done with an explicit re-annotation. So `path: Path =
|
||||
//! Path(path)`, with the explicit `: Path` annotation, is permitted.
|
||||
//!
|
||||
//! The general rule is that whatever declaration(s) can reach a given binding determine the
|
||||
//! validity of that binding. If there is a path in which the symbol is not declared, that is a
|
||||
//! declaration of `Unknown`. If multiple declarations can reach a binding, we union them, but by
|
||||
//! default we also issue a type error, since this implicit union of declared types may hide an
|
||||
//! error.
|
||||
//!
|
||||
//! To support type inference, we build a map from each use of a symbol to the bindings live at
|
||||
//! that use, and the type narrowing constraints that apply to each binding.
|
||||
//!
|
||||
//! Let's take this code sample:
|
||||
//!
|
||||
@@ -13,342 +88,488 @@
|
||||
//! z = x
|
||||
//! ```
|
||||
//!
|
||||
//! In this snippet, we have four definitions of `x` (the statements assigning `1`, `2`, `3`,
|
||||
//! and `4` to it), and two uses of `x` (the `y = x` and `z = x` assignments). The first
|
||||
//! [`Definition`] of `x` is never visible to any use, because it's immediately replaced by the
|
||||
//! second definition, before any use happens. (A linter could thus flag the statement `x = 1`
|
||||
//! as likely superfluous.)
|
||||
//! In this snippet, we have four bindings of `x` (the statements assigning `1`, `2`, `3`, and `4`
|
||||
//! to it), and two uses of `x` (the `y = x` and `z = x` assignments). The first binding of `x`
|
||||
//! does not reach any use, because it's immediately replaced by the second binding, before any use
|
||||
//! happens. (A linter could thus flag the statement `x = 1` as likely superfluous.)
|
||||
//!
|
||||
//! The first use of `x` has one definition visible to it: the assignment `x = 2`.
|
||||
//! The first use of `x` has one live binding: the assignment `x = 2`.
|
||||
//!
|
||||
//! Things get a bit more complex when we have branches. We will definitely take either the `if` or
|
||||
//! the `else` branch. Thus, the second use of `x` has two definitions visible to it: `x = 3` and
|
||||
//! `x = 4`. The `x = 2` definition is no longer visible, because it must be replaced by either `x
|
||||
//! = 3` or `x = 4`, no matter which branch was taken. We don't know which branch was taken, so we
|
||||
//! must consider both definitions as visible, which means eventually we would (in type inference)
|
||||
//! look at these two definitions and infer a type of `Literal[3, 4]` -- the union of `Literal[3]`
|
||||
//! and `Literal[4]` -- for the second use of `x`.
|
||||
//! the `else` branch. Thus, the second use of `x` has two live bindings: `x = 3` and `x = 4`. The
|
||||
//! `x = 2` assignment is no longer visible, because it must be replaced by either `x = 3` or `x =
|
||||
//! 4`, no matter which branch was taken. We don't know which branch was taken, so we must consider
|
||||
//! both bindings as live, which means eventually we would (in type inference) look at these two
|
||||
//! bindings and infer a type of `Literal[3, 4]` -- the union of `Literal[3]` and `Literal[4]` --
|
||||
//! for the second use of `x`.
|
||||
//!
|
||||
//! So that's one question our use-def map needs to answer: given a specific use of a symbol, which
|
||||
//! definition(s) is/are visible from that use. In
|
||||
//! [`AstIds`](crate::semantic_index::ast_ids::AstIds) we number all uses (that means a `Name` node
|
||||
//! with `Load` context) so we have a `ScopedUseId` to efficiently represent each use.
|
||||
//! binding(s) can reach that use. In [`AstIds`](crate::semantic_index::ast_ids::AstIds) we number
|
||||
//! all uses (that means a `Name` node with `Load` context) so we have a `ScopedUseId` to
|
||||
//! efficiently represent each use.
|
||||
//!
|
||||
//! The other case we need to handle is when a symbol is referenced from a different scope (the
|
||||
//! most obvious example of this is an import). We call this "public" use of a symbol. So the other
|
||||
//! question we need to be able to answer is, what are the publicly-visible definitions of each
|
||||
//! symbol?
|
||||
//! We also need to know, for a given definition of a symbol, what type narrowing constraints apply
|
||||
//! to it. For instance, in this code sample:
|
||||
//!
|
||||
//! Technically, public use of a symbol could also occur from any point in control flow of the
|
||||
//! scope where the symbol is defined (via inline imports and import cycles, in the case of an
|
||||
//! import, or via a function call partway through the local scope that ends up using a symbol from
|
||||
//! the scope via a global or nonlocal reference.) But modeling this fully accurately requires
|
||||
//! whole-program analysis that isn't tractable for an efficient incremental compiler, since it
|
||||
//! means a given symbol could have a different type every place it's referenced throughout the
|
||||
//! program, depending on the shape of arbitrarily-sized call/import graphs. So we follow other
|
||||
//! Python type-checkers in making the simplifying assumption that usually the scope will finish
|
||||
//! execution before its symbols are made visible to other scopes; for instance, most imports will
|
||||
//! import from a complete module, not a partially-executed module. (We may want to get a little
|
||||
//! smarter than this in the future, in particular for closures, but for now this is where we
|
||||
//! start.)
|
||||
//! ```python
|
||||
//! x = 1 if flag else None
|
||||
//! if x is not None:
|
||||
//! use(x)
|
||||
//! ```
|
||||
//!
|
||||
//! So this means that the publicly-visible definitions of a symbol are the definitions still
|
||||
//! visible at the end of the scope.
|
||||
//! At the use of `x`, the live binding of `x` is `1 if flag else None`, which would infer as the
|
||||
//! type `Literal[1] | None`. But the constraint `x is not None` dominates this use, which means we
|
||||
//! can rule out the possibility that `x` is `None` here, which should give us the type
|
||||
//! `Literal[1]` for this use.
|
||||
//!
|
||||
//! The data structure we build to answer these two questions is the `UseDefMap`. It has a
|
||||
//! `definitions_by_use` vector indexed by [`ScopedUseId`] and a `public_definitions` vector
|
||||
//! indexed by [`ScopedSymbolId`]. The values in each of these vectors are (in principle) a list of
|
||||
//! visible definitions at that use, or at the end of the scope for that symbol.
|
||||
//! For declared types, we need to be able to answer the question "given a binding to a symbol,
|
||||
//! which declarations of that symbol can reach the binding?" This allows us to emit a diagnostic
|
||||
//! if the binding is attempting to bind a value of a type that is not assignable to the declared
|
||||
//! type for that symbol, at that point in control flow.
|
||||
//!
|
||||
//! In order to avoid vectors-of-vectors and all the allocations that would entail, we don't
|
||||
//! actually store these "list of visible definitions" as a vector of [`Definition`] IDs. Instead,
|
||||
//! the values in `definitions_by_use` and `public_definitions` are a [`Definitions`] struct that
|
||||
//! keeps a [`Range`] into a third vector of [`Definition`] IDs, `all_definitions`. The trick with
|
||||
//! this representation is that it requires that the definitions visible at any given use of a
|
||||
//! symbol are stored sequentially in `all_definitions`.
|
||||
//! We also need to know, given a declaration of a symbol, what the inferred type of that symbol is
|
||||
//! at that point. This allows us to emit a diagnostic in a case like `x = "foo"; x: int`. The
|
||||
//! binding `x = "foo"` occurs before the declaration `x: int`, so according to our
|
||||
//! control-flow-sensitive interpretation of declarations, the assignment is not an error. But the
|
||||
//! declaration is an error, since it would violate the "inferred type must be assignable to
|
||||
//! declared type" rule.
|
||||
//!
|
||||
//! There is another special kind of possible "definition" for a symbol: it might be unbound in the
|
||||
//! scope. (This isn't equivalent to "zero visible definitions", since we may go through an `if`
|
||||
//! that has a definition for the symbol, leaving us with one visible definition, but still also
|
||||
//! the "unbound" possibility, since we might not have taken the `if` branch.)
|
||||
//! Another case we need to handle is when a symbol is referenced from a different scope (for
|
||||
//! example, an import or a nonlocal reference). We call this "public" use of a symbol. For public
|
||||
//! use of a symbol, we prefer the declared type, if there are any declarations of that symbol; if
|
||||
//! not, we fall back to the inferred type. So we also need to know which declarations and bindings
|
||||
//! can reach the end of the scope.
|
||||
//!
|
||||
//! The simplest way to model "unbound" would be as an actual [`Definition`] itself: the initial
|
||||
//! visible [`Definition`] for each symbol in a scope. But actually modeling it this way would
|
||||
//! dramatically increase the number of [`Definition`] that Salsa must track. Since "unbound" is a
|
||||
//! special definition in that all symbols share it, and it doesn't have any additional per-symbol
|
||||
//! state, we can represent it more efficiently: we use the `may_be_unbound` boolean on the
|
||||
//! [`Definitions`] struct. If this flag is `true`, it means the symbol/use really has one
|
||||
//! additional visible "definition", which is the unbound state. If this flag is `false`, it means
|
||||
//! we've eliminated the possibility of unbound: every path we've followed includes a definition
|
||||
//! Technically, public use of a symbol could occur from any point in control flow of the scope
|
||||
//! where the symbol is defined (via inline imports and import cycles, in the case of an import, or
|
||||
//! via a function call partway through the local scope that ends up using a symbol from the scope
|
||||
//! via a global or nonlocal reference.) But modeling this fully accurately requires whole-program
|
||||
//! analysis that isn't tractable for an efficient analysis, since it means a given symbol could
|
||||
//! have a different type every place it's referenced throughout the program, depending on the
|
||||
//! shape of arbitrarily-sized call/import graphs. So we follow other Python type checkers in
|
||||
//! making the simplifying assumption that usually the scope will finish execution before its
|
||||
//! symbols are made visible to other scopes; for instance, most imports will import from a
|
||||
//! complete module, not a partially-executed module. (We may want to get a little smarter than
|
||||
//! this in the future for some closures, but for now this is where we start.)
|
||||
//!
|
||||
//! The data structure we build to answer these questions is the `UseDefMap`. It has a
|
||||
//! `bindings_by_use` vector of [`SymbolBindings`] indexed by [`ScopedUseId`], a
|
||||
//! `declarations_by_binding` vector of [`SymbolDeclarations`] indexed by [`ScopedDefinitionId`], a
|
||||
//! `bindings_by_declaration` vector of [`SymbolBindings`] indexed by [`ScopedDefinitionId`], and
|
||||
//! `public_bindings` and `public_definitions` vectors indexed by [`ScopedSymbolId`]. The values in
|
||||
//! each of these vectors are (in principle) a list of live bindings at that use/definition, or at
|
||||
//! the end of the scope for that symbol, with a list of the dominating constraints for each
|
||||
//! binding.
|
||||
//!
|
||||
//! In order to avoid vectors-of-vectors-of-vectors and all the allocations that would entail, we
|
||||
//! don't actually store these "list of visible definitions" as a vector of [`Definition`].
|
||||
//! Instead, [`SymbolBindings`] and [`SymbolDeclarations`] are structs which use bit-sets to track
|
||||
//! definitions (and constraints, in the case of bindings) in terms of [`ScopedDefinitionId`] and
|
||||
//! [`ScopedConstraintId`], which are indices into the `all_definitions` and `all_constraints`
|
||||
//! indexvecs in the [`UseDefMap`].
|
||||
//!
|
||||
//! There is another special kind of possible "definition" for a symbol: there might be a path from
|
||||
//! the scope entry to a given use in which the symbol is never bound.
|
||||
//!
|
||||
//! The simplest way to model "unbound" would be as a "binding" itself: the initial "binding" for
|
||||
//! each symbol in a scope. But actually modeling it this way would unnecessarily increase the
|
||||
//! number of [`Definition`]s that Salsa must track. Since "unbound" is special in that all symbols
|
||||
//! share it, and it doesn't have any additional per-symbol state, and constraints are irrelevant
|
||||
//! to it, we can represent it more efficiently: we use the `may_be_unbound` boolean on the
|
||||
//! [`SymbolBindings`] struct. If this flag is `true` for a use of a symbol, it means the symbol
|
||||
//! has a path to the use in which it is never bound. If this flag is `false`, it means we've
|
||||
//! eliminated the possibility of unbound: every control flow path to the use includes a binding
|
||||
//! for this symbol.
|
||||
//!
|
||||
//! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use and definition
|
||||
//! as they are encountered by the
|
||||
//! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use, definition, and
|
||||
//! constraint as they are encountered by the
|
||||
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder) AST visit. For
|
||||
//! each symbol, the builder tracks the currently-visible definitions for that symbol. When we hit
|
||||
//! a use of a symbol, it records the currently-visible definitions for that symbol as the visible
|
||||
//! definitions for that use. When we reach the end of the scope, it records the currently-visible
|
||||
//! definitions for each symbol as the public definitions of that symbol.
|
||||
//! each symbol, the builder tracks the `SymbolState` (`SymbolBindings` and `SymbolDeclarations`)
|
||||
//! for that symbol. When we hit a use or definition of a symbol, we record the necessary parts of
|
||||
//! the current state for that symbol that we need for that use or definition. When we reach the
|
||||
//! end of the scope, it records the state for each symbol as the public definitions of that
|
||||
//! symbol.
|
||||
//!
|
||||
//! Let's walk through the above example. Initially we record for `x` that it has no visible
|
||||
//! definitions, and may be unbound. When we see `x = 1`, we record that as the sole visible
|
||||
//! definition of `x`, and flip `may_be_unbound` to `false`. Then we see `x = 2`, and it replaces
|
||||
//! `x = 1` as the sole visible definition of `x`. When we get to `y = x`, we record that the
|
||||
//! visible definitions for that use of `x` are just the `x = 2` definition.
|
||||
//! Let's walk through the above example. Initially we record for `x` that it has no bindings, and
|
||||
//! may be unbound. When we see `x = 1`, we record that as the sole live binding of `x`, and flip
|
||||
//! `may_be_unbound` to `false`. Then we see `x = 2`, and we replace `x = 1` as the sole live
|
||||
//! binding of `x`. When we get to `y = x`, we record that the live bindings for that use of `x`
|
||||
//! are just the `x = 2` definition.
|
||||
//!
|
||||
//! Then we hit the `if` branch. We visit the `test` node (`flag` in this case), since that will
|
||||
//! happen regardless. Then we take a pre-branch snapshot of the currently visible definitions for
|
||||
//! all symbols, which we'll need later. Then we go ahead and visit the `if` body. When we see `x =
|
||||
//! 3`, it replaces `x = 2` as the sole visible definition of `x`. At the end of the `if` body, we
|
||||
//! take another snapshot of the currently-visible definitions; we'll call this the post-if-body
|
||||
//! snapshot.
|
||||
//! happen regardless. Then we take a pre-branch snapshot of the current state for all symbols,
|
||||
//! which we'll need later. Then we record `flag` as a possible constraint on the current binding
|
||||
//! (`x = 2`), and go ahead and visit the `if` body. When we see `x = 3`, it replaces `x = 2`
|
||||
//! (constrained by `flag`) as the sole live binding of `x`. At the end of the `if` body, we take
|
||||
//! another snapshot of the current symbol state; we'll call this the post-if-body snapshot.
|
||||
//!
|
||||
//! Now we need to visit the `else` clause. The conditions when entering the `else` clause should
|
||||
//! be the pre-if conditions; if we are entering the `else` clause, we know that the `if` test
|
||||
//! failed and we didn't execute the `if` body. So we first reset the builder to the pre-if state,
|
||||
//! using the snapshot we took previously (meaning we now have `x = 2` as the sole visible
|
||||
//! definition for `x` again), then visit the `else` clause, where `x = 4` replaces `x = 2` as the
|
||||
//! sole visible definition of `x`.
|
||||
//! using the snapshot we took previously (meaning we now have `x = 2` as the sole binding for `x`
|
||||
//! again), then visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding
|
||||
//! of `x`.
|
||||
//!
|
||||
//! Now we reach the end of the if/else, and want to visit the following code. The state here needs
|
||||
//! to reflect that we might have gone through the `if` branch, or we might have gone through the
|
||||
//! `else` branch, and we don't know which. So we need to "merge" our current builder state
|
||||
//! (reflecting the end-of-else state, with `x = 4` as the only visible definition) with our
|
||||
//! post-if-body snapshot (which has `x = 3` as the only visible definition). The result of this
|
||||
//! merge is that we now have two visible definitions of `x`: `x = 3` and `x = 4`.
|
||||
//! (reflecting the end-of-else state, with `x = 4` as the only live binding) with our post-if-body
|
||||
//! snapshot (which has `x = 3` as the only live binding). The result of this merge is that we now
|
||||
//! have two live bindings of `x`: `x = 3` and `x = 4`.
|
||||
//!
|
||||
//! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a
|
||||
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
|
||||
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
|
||||
//! visits a `StmtIf` node.
|
||||
//!
|
||||
//! (In the future we may have some other questions we want to answer as well, such as "is this
|
||||
//! definition used?", which will require tracking a bit more info in our map, e.g. a "used" bit
|
||||
//! for each [`Definition`] which is flipped to true when we record that definition for a use.)
|
||||
use self::symbol_state::{
|
||||
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
|
||||
ScopedConstraintId, ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
|
||||
};
|
||||
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use ruff_index::IndexVec;
|
||||
use std::ops::Range;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
/// All definitions that can reach a given use of a name.
|
||||
use super::constraint::Constraint;
|
||||
|
||||
mod bitset;
|
||||
mod symbol_state;
|
||||
|
||||
/// Applicable definitions and constraints for every use of a name.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct UseDefMap<'db> {
|
||||
// TODO store constraints with definitions for type narrowing
|
||||
/// Definition IDs array for `definitions_by_use` and `public_definitions` to slice into.
|
||||
all_definitions: Vec<Definition<'db>>,
|
||||
/// Array of [`Definition`] in this scope.
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
|
||||
/// Definitions that can reach a [`ScopedUseId`].
|
||||
definitions_by_use: IndexVec<ScopedUseId, Definitions>,
|
||||
/// Array of [`Constraint`] in this scope.
|
||||
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
|
||||
/// Definitions of each symbol visible at end of scope.
|
||||
public_definitions: IndexVec<ScopedSymbolId, Definitions>,
|
||||
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
|
||||
/// [`SymbolBindings`] or [`SymbolDeclarations`] reaching a given [`Definition`].
|
||||
///
|
||||
/// If the definition is a binding (only) -- `x = 1` for example -- then we need
|
||||
/// [`SymbolDeclarations`] to know whether this binding is permitted by the live declarations.
|
||||
///
|
||||
/// If the definition is a declaration (only) -- `x: int` for example -- then we need
|
||||
/// [`SymbolBindings`] to know whether this declaration is consistent with the previously
|
||||
/// inferred type.
|
||||
///
|
||||
/// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then
|
||||
/// we don't actually need anything here, all we'll need to validate is that our own RHS is a
|
||||
/// valid assignment to our own annotation.
|
||||
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
|
||||
|
||||
/// [`SymbolState`] visible at end of scope for each symbol.
|
||||
public_symbols: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
}
|
||||
|
||||
impl<'db> UseDefMap<'db> {
|
||||
pub(crate) fn use_definitions(&self, use_id: ScopedUseId) -> &[Definition<'db>] {
|
||||
&self.all_definitions[self.definitions_by_use[use_id].definitions_range.clone()]
|
||||
pub(crate) fn bindings_at_use(
|
||||
&self,
|
||||
use_id: ScopedUseId,
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
self.bindings_iterator(&self.bindings_by_use[use_id])
|
||||
}
|
||||
|
||||
pub(crate) fn use_may_be_unbound(&self, use_id: ScopedUseId) -> bool {
|
||||
self.definitions_by_use[use_id].may_be_unbound
|
||||
self.bindings_by_use[use_id].may_be_unbound()
|
||||
}
|
||||
|
||||
pub(crate) fn public_definitions(&self, symbol: ScopedSymbolId) -> &[Definition<'db>] {
|
||||
&self.all_definitions[self.public_definitions[symbol].definitions_range.clone()]
|
||||
pub(crate) fn public_bindings(
|
||||
&self,
|
||||
symbol: ScopedSymbolId,
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
||||
}
|
||||
|
||||
pub(crate) fn public_may_be_unbound(&self, symbol: ScopedSymbolId) -> bool {
|
||||
self.public_definitions[symbol].may_be_unbound
|
||||
self.public_symbols[symbol].may_be_unbound()
|
||||
}
|
||||
}
|
||||
|
||||
/// Definitions visible for a symbol at a particular use (or end-of-scope).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct Definitions {
|
||||
/// [`Range`] in `all_definitions` of the visible definition IDs.
|
||||
definitions_range: Range<usize>,
|
||||
/// Is the symbol possibly unbound at this point?
|
||||
may_be_unbound: bool,
|
||||
}
|
||||
pub(crate) fn bindings_at_declaration(
|
||||
&self,
|
||||
declaration: Definition<'db>,
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
if let SymbolDefinitions::Bindings(bindings) = &self.definitions_by_definition[&declaration]
|
||||
{
|
||||
self.bindings_iterator(bindings)
|
||||
} else {
|
||||
unreachable!("Declaration has non-Bindings in definitions_by_definition");
|
||||
}
|
||||
}
|
||||
|
||||
impl Definitions {
|
||||
/// The default state of a symbol is "no definitions, may be unbound", aka definitely-unbound.
|
||||
fn unbound() -> Self {
|
||||
Self {
|
||||
definitions_range: Range::default(),
|
||||
may_be_unbound: true,
|
||||
pub(crate) fn declarations_at_binding(
|
||||
&self,
|
||||
binding: Definition<'db>,
|
||||
) -> DeclarationsIterator<'_, 'db> {
|
||||
if let SymbolDefinitions::Declarations(declarations) =
|
||||
&self.definitions_by_definition[&binding]
|
||||
{
|
||||
self.declarations_iterator(declarations)
|
||||
} else {
|
||||
unreachable!("Binding has non-Declarations in definitions_by_definition");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn public_declarations(
|
||||
&self,
|
||||
symbol: ScopedSymbolId,
|
||||
) -> DeclarationsIterator<'_, 'db> {
|
||||
let declarations = self.public_symbols[symbol].declarations();
|
||||
self.declarations_iterator(declarations)
|
||||
}
|
||||
|
||||
pub(crate) fn has_public_declarations(&self, symbol: ScopedSymbolId) -> bool {
|
||||
!self.public_symbols[symbol].declarations().is_empty()
|
||||
}
|
||||
|
||||
fn bindings_iterator<'a>(
|
||||
&'a self,
|
||||
bindings: &'a SymbolBindings,
|
||||
) -> BindingWithConstraintsIterator<'a, 'db> {
|
||||
BindingWithConstraintsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
all_constraints: &self.all_constraints,
|
||||
inner: bindings.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn declarations_iterator<'a>(
|
||||
&'a self,
|
||||
declarations: &'a SymbolDeclarations,
|
||||
) -> DeclarationsIterator<'a, 'db> {
|
||||
DeclarationsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
inner: declarations.iter(),
|
||||
may_be_undeclared: declarations.may_be_undeclared(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Definitions {
|
||||
fn default() -> Self {
|
||||
Definitions::unbound()
|
||||
}
|
||||
}
|
||||
|
||||
/// A snapshot of the visible definitions for each symbol at a particular point in control flow.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct FlowSnapshot {
|
||||
definitions_by_symbol: IndexVec<ScopedSymbolId, Definitions>,
|
||||
/// Either live bindings or live declarations for a symbol.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum SymbolDefinitions {
|
||||
Bindings(SymbolBindings),
|
||||
Declarations(SymbolDeclarations),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
inner: BindingIdWithConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
|
||||
type Item = BindingWithConstraints<'map, 'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner
|
||||
.next()
|
||||
.map(|def_id_with_constraints| BindingWithConstraints {
|
||||
binding: self.all_definitions[def_id_with_constraints.definition],
|
||||
constraints: ConstraintsIterator {
|
||||
all_constraints: self.all_constraints,
|
||||
constraint_ids: def_id_with_constraints.constraint_ids,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct BindingWithConstraints<'map, 'db> {
|
||||
pub(crate) binding: Definition<'db>,
|
||||
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
|
||||
}
|
||||
|
||||
pub(crate) struct ConstraintsIterator<'map, 'db> {
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
constraint_ids: ConstraintIdIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map, 'db> Iterator for ConstraintsIterator<'map, 'db> {
|
||||
type Item = Constraint<'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.constraint_ids
|
||||
.next()
|
||||
.map(|constraint_id| self.all_constraints[constraint_id])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct DeclarationsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
inner: DeclarationIdIterator<'map>,
|
||||
may_be_undeclared: bool,
|
||||
}
|
||||
|
||||
impl DeclarationsIterator<'_, '_> {
|
||||
pub(crate) fn may_be_undeclared(&self) -> bool {
|
||||
self.may_be_undeclared
|
||||
}
|
||||
}
|
||||
|
||||
impl<'map, 'db> Iterator for DeclarationsIterator<'map, 'db> {
|
||||
type Item = Definition<'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(|def_id| self.all_definitions[def_id])
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
|
||||
|
||||
/// A snapshot of the definitions and constraints state at a particular point in control flow.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) struct FlowSnapshot {
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Definition IDs array for `definitions_by_use` and `definitions_by_symbol` to slice into.
|
||||
all_definitions: Vec<Definition<'db>>,
|
||||
/// Append-only array of [`Definition`].
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
|
||||
/// Visible definitions at each so-far-recorded use.
|
||||
definitions_by_use: IndexVec<ScopedUseId, Definitions>,
|
||||
/// Append-only array of [`Constraint`].
|
||||
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
|
||||
/// Currently visible definitions for each symbol.
|
||||
definitions_by_symbol: IndexVec<ScopedSymbolId, Definitions>,
|
||||
/// Live bindings at each so-far-recorded use.
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
|
||||
/// Live bindings or declarations for each so-far-recorded definition.
|
||||
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
|
||||
|
||||
/// Currently live bindings and declarations for each symbol.
|
||||
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
|
||||
}
|
||||
|
||||
impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn new() -> Self {
|
||||
Self {
|
||||
all_definitions: Vec::new(),
|
||||
definitions_by_use: IndexVec::new(),
|
||||
definitions_by_symbol: IndexVec::new(),
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||
let new_symbol = self.definitions_by_symbol.push(Definitions::unbound());
|
||||
let new_symbol = self.symbol_states.push(SymbolState::undefined());
|
||||
debug_assert_eq!(symbol, new_symbol);
|
||||
}
|
||||
|
||||
pub(super) fn record_definition(
|
||||
pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) {
|
||||
let def_id = self.all_definitions.push(binding);
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
self.definitions_by_definition.insert(
|
||||
binding,
|
||||
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
|
||||
);
|
||||
symbol_state.record_binding(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) {
|
||||
let constraint_id = self.all_constraints.push(constraint);
|
||||
for state in &mut self.symbol_states {
|
||||
state.record_constraint(constraint_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn record_declaration(
|
||||
&mut self,
|
||||
symbol: ScopedSymbolId,
|
||||
declaration: Definition<'db>,
|
||||
) {
|
||||
let def_id = self.all_definitions.push(declaration);
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
self.definitions_by_definition.insert(
|
||||
declaration,
|
||||
SymbolDefinitions::Bindings(symbol_state.bindings().clone()),
|
||||
);
|
||||
symbol_state.record_declaration(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn record_declaration_and_binding(
|
||||
&mut self,
|
||||
symbol: ScopedSymbolId,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
// We have a new definition of a symbol; this replaces any previous definitions in this
|
||||
// path.
|
||||
let def_idx = self.all_definitions.len();
|
||||
self.all_definitions.push(definition);
|
||||
self.definitions_by_symbol[symbol] = Definitions {
|
||||
#[allow(clippy::range_plus_one)]
|
||||
definitions_range: def_idx..(def_idx + 1),
|
||||
may_be_unbound: false,
|
||||
};
|
||||
// We don't need to store anything in self.definitions_by_definition.
|
||||
let def_id = self.all_definitions.push(definition);
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
symbol_state.record_declaration(def_id);
|
||||
symbol_state.record_binding(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
|
||||
// We have a use of a symbol; clone the currently visible definitions for that symbol, and
|
||||
// record them as the visible definitions for this use.
|
||||
// We have a use of a symbol; clone the current bindings for that symbol, and record them
|
||||
// as the live bindings for this use.
|
||||
let new_use = self
|
||||
.definitions_by_use
|
||||
.push(self.definitions_by_symbol[symbol].clone());
|
||||
.bindings_by_use
|
||||
.push(self.symbol_states[symbol].bindings().clone());
|
||||
debug_assert_eq!(use_id, new_use);
|
||||
}
|
||||
|
||||
/// Take a snapshot of the current visible-symbols state.
|
||||
pub(super) fn snapshot(&self) -> FlowSnapshot {
|
||||
FlowSnapshot {
|
||||
definitions_by_symbol: self.definitions_by_symbol.clone(),
|
||||
symbol_states: self.symbol_states.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore the current builder visible-definitions state to the given snapshot.
|
||||
/// Restore the current builder symbols state to the given snapshot.
|
||||
pub(super) fn restore(&mut self, snapshot: FlowSnapshot) {
|
||||
// We never remove symbols from `definitions_by_symbol` (it's an IndexVec, and the symbol
|
||||
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
|
||||
// IDs must line up), so the current number of known symbols must always be equal to or
|
||||
// greater than the number of known symbols in a previously-taken snapshot.
|
||||
let num_symbols = self.definitions_by_symbol.len();
|
||||
debug_assert!(num_symbols >= snapshot.definitions_by_symbol.len());
|
||||
let num_symbols = self.symbol_states.len();
|
||||
debug_assert!(num_symbols >= snapshot.symbol_states.len());
|
||||
|
||||
// Restore the current visible-definitions state to the given snapshot.
|
||||
self.definitions_by_symbol = snapshot.definitions_by_symbol;
|
||||
self.symbol_states = snapshot.symbol_states;
|
||||
|
||||
// If the snapshot we are restoring is missing some symbols we've recorded since, we need
|
||||
// to fill them in so the symbol IDs continue to line up. Since they don't exist in the
|
||||
// snapshot, the correct state to fill them in with is "unbound", the default.
|
||||
self.definitions_by_symbol
|
||||
.resize(num_symbols, Definitions::unbound());
|
||||
// snapshot, the correct state to fill them in with is "undefined".
|
||||
self.symbol_states
|
||||
.resize(num_symbols, SymbolState::undefined());
|
||||
}
|
||||
|
||||
/// Merge the given snapshot into the current state, reflecting that we might have taken either
|
||||
/// path to get here. The new visible-definitions state for each symbol should include
|
||||
/// definitions from both the prior state and the snapshot.
|
||||
pub(super) fn merge(&mut self, snapshot: &FlowSnapshot) {
|
||||
// The tricky thing about merging two Ranges pointing into `all_definitions` is that if the
|
||||
// two Ranges aren't already adjacent in `all_definitions`, we will have to copy at least
|
||||
// one or the other of the ranges to the end of `all_definitions` so as to make them
|
||||
// adjacent. We can't ever move things around in `all_definitions` because previously
|
||||
// recorded uses may still have ranges pointing to any part of it; all we can do is append.
|
||||
// It's possible we may end up with some old entries in `all_definitions` that nobody is
|
||||
// pointing to, but that's OK.
|
||||
|
||||
// We never remove symbols from `definitions_by_symbol` (it's an IndexVec, and the symbol
|
||||
/// path to get here. The new state for each symbol should include definitions from both the
|
||||
/// prior state and the snapshot.
|
||||
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
|
||||
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
|
||||
// IDs must line up), so the current number of known symbols must always be equal to or
|
||||
// greater than the number of known symbols in a previously-taken snapshot.
|
||||
debug_assert!(self.definitions_by_symbol.len() >= snapshot.definitions_by_symbol.len());
|
||||
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
|
||||
|
||||
for (symbol_id, current) in self.definitions_by_symbol.iter_mut_enumerated() {
|
||||
let Some(snapshot) = snapshot.definitions_by_symbol.get(symbol_id) else {
|
||||
// Symbol not present in snapshot, so it's unbound from that path.
|
||||
current.may_be_unbound = true;
|
||||
continue;
|
||||
};
|
||||
|
||||
// If the symbol can be unbound in either predecessor, it can be unbound post-merge.
|
||||
current.may_be_unbound |= snapshot.may_be_unbound;
|
||||
|
||||
// Merge the definition ranges.
|
||||
let current = &mut current.definitions_range;
|
||||
let snapshot = &snapshot.definitions_range;
|
||||
|
||||
// We never create reversed ranges.
|
||||
debug_assert!(current.end >= current.start);
|
||||
debug_assert!(snapshot.end >= snapshot.start);
|
||||
|
||||
if current == snapshot {
|
||||
// Ranges already identical, nothing to do.
|
||||
} else if snapshot.is_empty() {
|
||||
// Merging from an empty range; nothing to do.
|
||||
} else if (*current).is_empty() {
|
||||
// Merging to an empty range; just use the incoming range.
|
||||
*current = snapshot.clone();
|
||||
} else if snapshot.end >= current.start && snapshot.start <= current.end {
|
||||
// Ranges are adjacent or overlapping, merge them in-place.
|
||||
*current = current.start.min(snapshot.start)..current.end.max(snapshot.end);
|
||||
} else if current.end == self.all_definitions.len() {
|
||||
// Ranges are not adjacent or overlapping, `current` is at the end of
|
||||
// `all_definitions`, we need to copy `snapshot` to the end so they are adjacent
|
||||
// and can be merged into one range.
|
||||
self.all_definitions.extend_from_within(snapshot.clone());
|
||||
current.end = self.all_definitions.len();
|
||||
} else if snapshot.end == self.all_definitions.len() {
|
||||
// Ranges are not adjacent or overlapping, `snapshot` is at the end of
|
||||
// `all_definitions`, we need to copy `current` to the end so they are adjacent and
|
||||
// can be merged into one range.
|
||||
self.all_definitions.extend_from_within(current.clone());
|
||||
current.start = snapshot.start;
|
||||
current.end = self.all_definitions.len();
|
||||
let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
|
||||
for current in &mut self.symbol_states {
|
||||
if let Some(snapshot) = snapshot_definitions_iter.next() {
|
||||
current.merge(snapshot);
|
||||
} else {
|
||||
// Ranges are not adjacent and neither one is at the end of `all_definitions`, we
|
||||
// have to copy both to the end so they are adjacent and we can merge them.
|
||||
let start = self.all_definitions.len();
|
||||
self.all_definitions.extend_from_within(current.clone());
|
||||
self.all_definitions.extend_from_within(snapshot.clone());
|
||||
current.start = start;
|
||||
current.end = self.all_definitions.len();
|
||||
// Symbol not present in snapshot, so it's unbound/undeclared from that path.
|
||||
current.set_may_be_unbound();
|
||||
current.set_may_be_undeclared();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn finish(mut self) -> UseDefMap<'db> {
|
||||
self.all_definitions.shrink_to_fit();
|
||||
self.definitions_by_symbol.shrink_to_fit();
|
||||
self.definitions_by_use.shrink_to_fit();
|
||||
self.all_constraints.shrink_to_fit();
|
||||
self.symbol_states.shrink_to_fit();
|
||||
self.bindings_by_use.shrink_to_fit();
|
||||
self.definitions_by_definition.shrink_to_fit();
|
||||
|
||||
UseDefMap {
|
||||
all_definitions: self.all_definitions,
|
||||
definitions_by_use: self.definitions_by_use,
|
||||
public_definitions: self.definitions_by_symbol,
|
||||
all_constraints: self.all_constraints,
|
||||
bindings_by_use: self.bindings_by_use,
|
||||
public_symbols: self.symbol_states,
|
||||
definitions_by_definition: self.definitions_by_definition,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
/// Ordered set of `u32`.
|
||||
///
|
||||
/// Uses an inline bit-set for small values (up to 64 * B), falls back to heap allocated vector of
|
||||
/// blocks for larger values.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) enum BitSet<const B: usize> {
|
||||
/// Bit-set (in 64-bit blocks) for the first 64 * B entries.
|
||||
Inline([u64; B]),
|
||||
|
||||
/// Overflow beyond 64 * B.
|
||||
Heap(Vec<u64>),
|
||||
}
|
||||
|
||||
impl<const B: usize> Default for BitSet<B> {
|
||||
fn default() -> Self {
|
||||
// B * 64 must fit in a u32, or else we have unusable bits; this assertion makes the
|
||||
// truncating casts to u32 below safe. This would be better as a const assertion, but
|
||||
// that's not possible on stable with const generic params. (B should never really be
|
||||
// anywhere close to this large.)
|
||||
assert!(B * 64 < (u32::MAX as usize));
|
||||
// This implementation requires usize >= 32 bits.
|
||||
static_assertions::const_assert!(usize::BITS >= 32);
|
||||
Self::Inline([0; B])
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> BitSet<B> {
|
||||
/// Create and return a new [`BitSet`] with a single `value` inserted.
|
||||
pub(super) fn with(value: u32) -> Self {
|
||||
let mut bitset = Self::default();
|
||||
bitset.insert(value);
|
||||
bitset
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.blocks().iter().all(|&b| b == 0)
|
||||
}
|
||||
|
||||
/// Convert from Inline to Heap, if needed, and resize the Heap vector, if needed.
|
||||
fn resize(&mut self, value: u32) {
|
||||
let num_blocks_needed = (value / 64) + 1;
|
||||
self.resize_blocks(num_blocks_needed as usize);
|
||||
}
|
||||
|
||||
fn resize_blocks(&mut self, num_blocks_needed: usize) {
|
||||
match self {
|
||||
Self::Inline(blocks) => {
|
||||
let mut vec = blocks.to_vec();
|
||||
vec.resize(num_blocks_needed, 0);
|
||||
*self = Self::Heap(vec);
|
||||
}
|
||||
Self::Heap(vec) => {
|
||||
vec.resize(num_blocks_needed, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blocks_mut(&mut self) -> &mut [u64] {
|
||||
match self {
|
||||
Self::Inline(blocks) => blocks.as_mut_slice(),
|
||||
Self::Heap(blocks) => blocks.as_mut_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
fn blocks(&self) -> &[u64] {
|
||||
match self {
|
||||
Self::Inline(blocks) => blocks.as_slice(),
|
||||
Self::Heap(blocks) => blocks.as_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a value into the [`BitSet`].
|
||||
///
|
||||
/// Return true if the value was newly inserted, false if already present.
|
||||
pub(super) fn insert(&mut self, value: u32) -> bool {
|
||||
let value_usize = value as usize;
|
||||
let (block, index) = (value_usize / 64, value_usize % 64);
|
||||
if block >= self.blocks().len() {
|
||||
self.resize(value);
|
||||
}
|
||||
let blocks = self.blocks_mut();
|
||||
let missing = blocks[block] & (1 << index) == 0;
|
||||
blocks[block] |= 1 << index;
|
||||
missing
|
||||
}
|
||||
|
||||
/// Intersect in-place with another [`BitSet`].
|
||||
pub(super) fn intersect(&mut self, other: &BitSet<B>) {
|
||||
let my_blocks = self.blocks_mut();
|
||||
let other_blocks = other.blocks();
|
||||
let min_len = my_blocks.len().min(other_blocks.len());
|
||||
for i in 0..min_len {
|
||||
my_blocks[i] &= other_blocks[i];
|
||||
}
|
||||
for block in my_blocks.iter_mut().skip(min_len) {
|
||||
*block = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Union in-place with another [`BitSet`].
|
||||
pub(super) fn union(&mut self, other: &BitSet<B>) {
|
||||
let mut max_len = self.blocks().len();
|
||||
let other_len = other.blocks().len();
|
||||
if other_len > max_len {
|
||||
max_len = other_len;
|
||||
self.resize_blocks(max_len);
|
||||
}
|
||||
for (my_block, other_block) in self.blocks_mut().iter_mut().zip(other.blocks()) {
|
||||
*my_block |= other_block;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
|
||||
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
|
||||
let blocks = self.blocks();
|
||||
BitSetIterator {
|
||||
blocks,
|
||||
current_block_index: 0,
|
||||
current_block: blocks[0],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over values in a [`BitSet`].
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BitSetIterator<'a, const B: usize> {
|
||||
/// The blocks we are iterating over.
|
||||
blocks: &'a [u64],
|
||||
|
||||
/// The index of the block we are currently iterating through.
|
||||
current_block_index: usize,
|
||||
|
||||
/// The block we are currently iterating through (and zeroing as we go.)
|
||||
current_block: u64,
|
||||
}
|
||||
|
||||
impl<const B: usize> Iterator for BitSetIterator<'_, B> {
|
||||
type Item = u32;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while self.current_block == 0 {
|
||||
if self.current_block_index + 1 >= self.blocks.len() {
|
||||
return None;
|
||||
}
|
||||
self.current_block_index += 1;
|
||||
self.current_block = self.blocks[self.current_block_index];
|
||||
}
|
||||
let lowest_bit_set = self.current_block.trailing_zeros();
|
||||
// reset the lowest set bit, without a data dependency on `lowest_bit_set`
|
||||
self.current_block &= self.current_block.wrapping_sub(1);
|
||||
// SAFETY: `lowest_bit_set` cannot be more than 64, `current_block_index` cannot be more
|
||||
// than `B - 1`, and we check above that `B * 64 < u32::MAX`. So both `64 *
|
||||
// current_block_index` and the final value here must fit in u32.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Some(lowest_bit_set + (64 * self.current_block_index) as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize> std::iter::FusedIterator for BitSetIterator<'_, B> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::BitSet;
|
||||
|
||||
fn assert_bitset<const B: usize>(bitset: &BitSet<B>, contents: &[u32]) {
|
||||
assert_eq!(bitset.iter().collect::<Vec<_>>(), contents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() {
|
||||
let mut b = BitSet::<1>::with(3);
|
||||
b.insert(27);
|
||||
b.insert(6);
|
||||
assert!(matches!(b, BitSet::Inline(_)));
|
||||
assert_bitset(&b, &[3, 6, 27]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter_overflow() {
|
||||
let mut b = BitSet::<1>::with(140);
|
||||
b.insert(100);
|
||||
b.insert(129);
|
||||
assert!(matches!(b, BitSet::Heap(_)));
|
||||
assert_bitset(&b, &[100, 129, 140]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(23);
|
||||
b2.insert(5);
|
||||
|
||||
b1.intersect(&b2);
|
||||
assert_bitset(&b1, &[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_mixed_1() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(5);
|
||||
|
||||
b1.intersect(&b2);
|
||||
assert_bitset(&b1, &[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_mixed_2() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(23);
|
||||
b2.insert(89);
|
||||
|
||||
b1.intersect(&b2);
|
||||
assert_bitset(&b1, &[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_heap() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(90);
|
||||
|
||||
b1.intersect(&b2);
|
||||
assert_bitset(&b1, &[4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_heap_2() {
|
||||
let mut b1 = BitSet::<1>::with(89);
|
||||
let mut b2 = BitSet::<1>::with(89);
|
||||
b1.insert(91);
|
||||
b2.insert(90);
|
||||
|
||||
b1.intersect(&b2);
|
||||
assert_bitset(&b1, &[89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
let mut b1 = BitSet::<1>::with(2);
|
||||
let b2 = BitSet::<1>::with(4);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[2, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_1() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(5);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 5, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_2() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(23);
|
||||
b2.insert(89);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 23, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 89, 90]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap_2() {
|
||||
let mut b1 = BitSet::<1>::with(89);
|
||||
let mut b2 = BitSet::<1>::with(89);
|
||||
b1.insert(91);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[89, 90, 91]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_blocks() {
|
||||
let mut b = BitSet::<2>::with(120);
|
||||
b.insert(45);
|
||||
assert!(matches!(b, BitSet::Inline(_)));
|
||||
assert_bitset(&b, &[45, 120]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let b = BitSet::<1>::default();
|
||||
|
||||
assert!(b.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,588 @@
|
||||
//! Track live bindings per symbol, applicable constraints per binding, and live declarations.
|
||||
//!
|
||||
//! These data structures operate entirely on scope-local newtype-indices for definitions and
|
||||
//! constraints, referring to their location in the `all_definitions` and `all_constraints`
|
||||
//! indexvecs in [`super::UseDefMapBuilder`].
|
||||
//!
|
||||
//! We need to track arbitrary associations between bindings and constraints, not just a single set
|
||||
//! of currently dominating constraints (where "dominating" means "control flow must have passed
|
||||
//! through it to reach this point"), because we can have dominating constraints that apply to some
|
||||
//! bindings but not others, as in this code:
|
||||
//!
|
||||
//! ```python
|
||||
//! x = 1 if flag else None
|
||||
//! if x is not None:
|
||||
//! if flag2:
|
||||
//! x = 2 if flag else None
|
||||
//! x
|
||||
//! ```
|
||||
//!
|
||||
//! The `x is not None` constraint dominates the final use of `x`, but it applies only to the first
|
||||
//! binding of `x`, not the second, so `None` is a possible value for `x`.
|
||||
//!
|
||||
//! And we can't just track, for each binding, an index into a list of dominating constraints,
|
||||
//! either, because we can have bindings which are still visible, but subject to constraints that
|
||||
//! are no longer dominating, as in this code:
|
||||
//!
|
||||
//! ```python
|
||||
//! x = 0
|
||||
//! if flag1:
|
||||
//! x = 1 if flag2 else None
|
||||
//! assert x is not None
|
||||
//! x
|
||||
//! ```
|
||||
//!
|
||||
//! From the point of view of the final use of `x`, the `x is not None` constraint no longer
|
||||
//! dominates, but it does dominate the `x = 1 if flag2 else None` binding, so we have to keep
|
||||
//! track of that.
|
||||
//!
|
||||
//! The data structures used here ([`BitSet`] and [`smallvec::SmallVec`]) optimize for keeping all
|
||||
//! data inline (avoiding lots of scattered allocations) in small-to-medium cases, and falling back
|
||||
//! to heap allocation to be able to scale to arbitrary numbers of live bindings and constraints
|
||||
//! when needed.
|
||||
//!
|
||||
//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
|
||||
//! similar to tracking live bindings.
|
||||
use super::bitset::{BitSet, BitSetIterator};
|
||||
use ruff_index::newtype_index;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A newtype-index for a definition in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(super) struct ScopedDefinitionId;
|
||||
|
||||
/// A newtype-index for a constraint expression in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(super) struct ScopedConstraintId;
|
||||
|
||||
/// Can reference this * 64 total definitions inline; more will fall back to the heap.
|
||||
const INLINE_BINDING_BLOCKS: usize = 3;
|
||||
|
||||
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live bindings of a symbol in a scope.
|
||||
type Bindings = BitSet<INLINE_BINDING_BLOCKS>;
|
||||
type BindingsIterator<'a> = BitSetIterator<'a, INLINE_BINDING_BLOCKS>;
|
||||
|
||||
/// Can reference this * 64 total declarations inline; more will fall back to the heap.
|
||||
const INLINE_DECLARATION_BLOCKS: usize = 3;
|
||||
|
||||
/// A [`BitSet`] of [`ScopedDefinitionId`], representing live declarations of a symbol in a scope.
|
||||
type Declarations = BitSet<INLINE_DECLARATION_BLOCKS>;
|
||||
type DeclarationsIterator<'a> = BitSetIterator<'a, INLINE_DECLARATION_BLOCKS>;
|
||||
|
||||
/// Can reference this * 64 total constraints inline; more will fall back to the heap.
|
||||
const INLINE_CONSTRAINT_BLOCKS: usize = 2;
|
||||
|
||||
/// Can keep inline this many live bindings per symbol at a given time; more will go to heap.
|
||||
const INLINE_BINDINGS_PER_SYMBOL: usize = 4;
|
||||
|
||||
/// One [`BitSet`] of applicable [`ScopedConstraintId`] per live binding.
|
||||
type InlineConstraintArray = [BitSet<INLINE_CONSTRAINT_BLOCKS>; INLINE_BINDINGS_PER_SYMBOL];
|
||||
type Constraints = SmallVec<InlineConstraintArray>;
|
||||
type ConstraintsIterator<'a> = std::slice::Iter<'a, BitSet<INLINE_CONSTRAINT_BLOCKS>>;
|
||||
type ConstraintsIntoIterator = smallvec::IntoIter<InlineConstraintArray>;
|
||||
|
||||
/// Live declarations for a single symbol at some point in control flow.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct SymbolDeclarations {
|
||||
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
live_declarations: Declarations,
|
||||
|
||||
/// Could the symbol be un-declared at this point?
|
||||
may_be_undeclared: bool,
|
||||
}
|
||||
|
||||
impl SymbolDeclarations {
|
||||
fn undeclared() -> Self {
|
||||
Self {
|
||||
live_declarations: Declarations::default(),
|
||||
may_be_undeclared: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration for this symbol.
|
||||
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
|
||||
self.live_declarations = Declarations::with(declaration_id.into());
|
||||
self.may_be_undeclared = false;
|
||||
}
|
||||
|
||||
/// Add undeclared as a possibility for this symbol.
|
||||
fn set_may_be_undeclared(&mut self) {
|
||||
self.may_be_undeclared = true;
|
||||
}
|
||||
|
||||
/// Return an iterator over live declarations for this symbol.
|
||||
pub(super) fn iter(&self) -> DeclarationIdIterator {
|
||||
DeclarationIdIterator {
|
||||
inner: self.live_declarations.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.live_declarations.is_empty()
|
||||
}
|
||||
|
||||
pub(super) fn may_be_undeclared(&self) -> bool {
|
||||
self.may_be_undeclared
|
||||
}
|
||||
}
|
||||
|
||||
/// Live bindings and narrowing constraints for a single symbol at some point in control flow.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct SymbolBindings {
|
||||
/// [`BitSet`]: which bindings (as [`ScopedDefinitionId`]) can reach the current location?
|
||||
live_bindings: Bindings,
|
||||
|
||||
/// For each live binding, which [`ScopedConstraintId`] apply?
|
||||
///
|
||||
/// This is a [`smallvec::SmallVec`] which should always have one [`BitSet`] of constraints per
|
||||
/// binding in `live_bindings`.
|
||||
constraints: Constraints,
|
||||
|
||||
/// Could the symbol be unbound at this point?
|
||||
may_be_unbound: bool,
|
||||
}
|
||||
|
||||
impl SymbolBindings {
|
||||
fn unbound() -> Self {
|
||||
Self {
|
||||
live_bindings: Bindings::default(),
|
||||
constraints: Constraints::default(),
|
||||
may_be_unbound: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Unbound as a possibility for this symbol.
|
||||
fn set_may_be_unbound(&mut self) {
|
||||
self.may_be_unbound = true;
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
// The new binding replaces all previous live bindings in this path, and has no
|
||||
// constraints.
|
||||
self.live_bindings = Bindings::with(binding_id.into());
|
||||
self.constraints = Constraints::with_capacity(1);
|
||||
self.constraints.push(BitSet::default());
|
||||
self.may_be_unbound = false;
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
|
||||
for bitset in &mut self.constraints {
|
||||
bitset.insert(constraint_id.into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over currently live bindings for this symbol.
|
||||
pub(super) fn iter(&self) -> BindingIdWithConstraintsIterator {
|
||||
BindingIdWithConstraintsIterator {
|
||||
definitions: self.live_bindings.iter(),
|
||||
constraints: self.constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn may_be_unbound(&self) -> bool {
|
||||
self.may_be_unbound
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(super) struct SymbolState {
|
||||
declarations: SymbolDeclarations,
|
||||
bindings: SymbolBindings,
|
||||
}
|
||||
|
||||
impl SymbolState {
|
||||
/// Return a new [`SymbolState`] representing an unbound, undeclared symbol.
|
||||
pub(super) fn undefined() -> Self {
|
||||
Self {
|
||||
declarations: SymbolDeclarations::undeclared(),
|
||||
bindings: SymbolBindings::unbound(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add Unbound as a possibility for this symbol.
|
||||
pub(super) fn set_may_be_unbound(&mut self) {
|
||||
self.bindings.set_may_be_unbound();
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
self.bindings.record_binding(binding_id);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
pub(super) fn record_constraint(&mut self, constraint_id: ScopedConstraintId) {
|
||||
self.bindings.record_constraint(constraint_id);
|
||||
}
|
||||
|
||||
/// Add undeclared as a possibility for this symbol.
|
||||
pub(super) fn set_may_be_undeclared(&mut self) {
|
||||
self.declarations.set_may_be_undeclared();
|
||||
}
|
||||
|
||||
/// Record a newly-encountered declaration of this symbol.
|
||||
pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
|
||||
self.declarations.record_declaration(declaration_id);
|
||||
}
|
||||
|
||||
/// Merge another [`SymbolState`] into this one.
|
||||
pub(super) fn merge(&mut self, b: SymbolState) {
|
||||
let mut a = Self {
|
||||
bindings: SymbolBindings {
|
||||
live_bindings: Bindings::default(),
|
||||
constraints: Constraints::default(),
|
||||
may_be_unbound: self.bindings.may_be_unbound || b.bindings.may_be_unbound,
|
||||
},
|
||||
declarations: SymbolDeclarations {
|
||||
live_declarations: self.declarations.live_declarations.clone(),
|
||||
may_be_undeclared: self.declarations.may_be_undeclared
|
||||
|| b.declarations.may_be_undeclared,
|
||||
},
|
||||
};
|
||||
|
||||
std::mem::swap(&mut a, self);
|
||||
self.declarations
|
||||
.live_declarations
|
||||
.union(&b.declarations.live_declarations);
|
||||
|
||||
let mut a_defs_iter = a.bindings.live_bindings.iter();
|
||||
let mut b_defs_iter = b.bindings.live_bindings.iter();
|
||||
let mut a_constraints_iter = a.bindings.constraints.into_iter();
|
||||
let mut b_constraints_iter = b.bindings.constraints.into_iter();
|
||||
|
||||
let mut opt_a_def: Option<u32> = a_defs_iter.next();
|
||||
let mut opt_b_def: Option<u32> = b_defs_iter.next();
|
||||
|
||||
// Iterate through the definitions from `a` and `b`, always processing the lower definition
|
||||
// ID first, and pushing each definition onto the merged `SymbolState` with its
|
||||
// constraints. If a definition is found in both `a` and `b`, push it with the intersection
|
||||
// of the constraints from the two paths; a constraint that applies from only one possible
|
||||
// path is irrelevant.
|
||||
|
||||
// Helper to push `def`, with constraints in `constraints_iter`, onto `self`.
|
||||
let push = |def, constraints_iter: &mut ConstraintsIntoIterator, merged: &mut Self| {
|
||||
merged.bindings.live_bindings.insert(def);
|
||||
// SAFETY: we only ever create SymbolState with either no definitions and no constraint
|
||||
// bitsets (`::unbound`) or one definition and one constraint bitset (`::with`), and
|
||||
// `::merge` always pushes one definition and one constraint bitset together (just
|
||||
// below), so the number of definitions and the number of constraint bitsets can never
|
||||
// get out of sync.
|
||||
let constraints = constraints_iter
|
||||
.next()
|
||||
.expect("definitions and constraints length mismatch");
|
||||
merged.bindings.constraints.push(constraints);
|
||||
};
|
||||
|
||||
loop {
|
||||
match (opt_a_def, opt_b_def) {
|
||||
(Some(a_def), Some(b_def)) => match a_def.cmp(&b_def) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// Next definition ID is only in `a`, push it to `self` and advance `a`.
|
||||
push(a_def, &mut a_constraints_iter, self);
|
||||
opt_a_def = a_defs_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
// Next definition ID is only in `b`, push it to `self` and advance `b`.
|
||||
push(b_def, &mut b_constraints_iter, self);
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
// Next definition is in both; push to `self` and intersect constraints.
|
||||
push(a_def, &mut b_constraints_iter, self);
|
||||
// SAFETY: we only ever create SymbolState with either no definitions and
|
||||
// no constraint bitsets (`::unbound`) or one definition and one constraint
|
||||
// bitset (`::with`), and `::merge` always pushes one definition and one
|
||||
// constraint bitset together (just below), so the number of definitions
|
||||
// and the number of constraint bitsets can never get out of sync.
|
||||
let a_constraints = a_constraints_iter
|
||||
.next()
|
||||
.expect("definitions and constraints length mismatch");
|
||||
// If the same definition is visible through both paths, any constraint
|
||||
// that applies on only one path is irrelevant to the resulting type from
|
||||
// unioning the two paths, so we intersect the constraints.
|
||||
self.bindings
|
||||
.constraints
|
||||
.last_mut()
|
||||
.unwrap()
|
||||
.intersect(&a_constraints);
|
||||
opt_a_def = a_defs_iter.next();
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
},
|
||||
(Some(a_def), None) => {
|
||||
// We've exhausted `b`, just push the def from `a` and move on to the next.
|
||||
push(a_def, &mut a_constraints_iter, self);
|
||||
opt_a_def = a_defs_iter.next();
|
||||
}
|
||||
(None, Some(b_def)) => {
|
||||
// We've exhausted `a`, just push the def from `b` and move on to the next.
|
||||
push(b_def, &mut b_constraints_iter, self);
|
||||
opt_b_def = b_defs_iter.next();
|
||||
}
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn bindings(&self) -> &SymbolBindings {
|
||||
&self.bindings
|
||||
}
|
||||
|
||||
pub(super) fn declarations(&self) -> &SymbolDeclarations {
|
||||
&self.declarations
|
||||
}
|
||||
|
||||
/// Could the symbol be unbound?
|
||||
pub(super) fn may_be_unbound(&self) -> bool {
|
||||
self.bindings.may_be_unbound()
|
||||
}
|
||||
}
|
||||
|
||||
/// The default state of a symbol, if we've seen no definitions of it, is undefined (that is,
|
||||
/// both unbound and undeclared).
|
||||
impl Default for SymbolState {
|
||||
fn default() -> Self {
|
||||
SymbolState::undefined()
|
||||
}
|
||||
}
|
||||
|
||||
/// A single binding (as [`ScopedDefinitionId`]) with an iterator of its applicable
|
||||
/// [`ScopedConstraintId`].
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraints<'a> {
|
||||
pub(super) definition: ScopedDefinitionId,
|
||||
pub(super) constraint_ids: ConstraintIdIterator<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraintsIterator<'a> {
|
||||
definitions: BindingsIterator<'a>,
|
||||
constraints: ConstraintsIterator<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for BindingIdWithConstraintsIterator<'a> {
|
||||
type Item = BindingIdWithConstraints<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.definitions.next(), self.constraints.next()) {
|
||||
(None, None) => None,
|
||||
(Some(def), Some(constraints)) => Some(BindingIdWithConstraints {
|
||||
definition: ScopedDefinitionId::from_u32(def),
|
||||
constraint_ids: ConstraintIdIterator {
|
||||
wrapped: constraints.iter(),
|
||||
},
|
||||
}),
|
||||
// SAFETY: see above.
|
||||
_ => unreachable!("definitions and constraints length mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ConstraintIdIterator<'a> {
|
||||
wrapped: BitSetIterator<'a, INLINE_CONSTRAINT_BLOCKS>,
|
||||
}
|
||||
|
||||
impl Iterator for ConstraintIdIterator<'_> {
|
||||
type Item = ScopedConstraintId;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.wrapped.next().map(ScopedConstraintId::from_u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct DeclarationIdIterator<'a> {
|
||||
inner: DeclarationsIterator<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for DeclarationIdIterator<'a> {
|
||||
type Item = ScopedDefinitionId;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(ScopedDefinitionId::from_u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ScopedConstraintId, ScopedDefinitionId, SymbolState};
|
||||
|
||||
fn assert_bindings(symbol: &SymbolState, may_be_unbound: bool, expected: &[&str]) {
|
||||
assert_eq!(symbol.may_be_unbound(), may_be_unbound);
|
||||
let actual = symbol
|
||||
.bindings()
|
||||
.iter()
|
||||
.map(|def_id_with_constraints| {
|
||||
format!(
|
||||
"{}<{}>",
|
||||
def_id_with_constraints.definition.as_u32(),
|
||||
def_id_with_constraints
|
||||
.constraint_ids
|
||||
.map(ScopedConstraintId::as_u32)
|
||||
.map(|idx| idx.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
pub(crate) fn assert_declarations(
|
||||
symbol: &SymbolState,
|
||||
may_be_undeclared: bool,
|
||||
expected: &[u32],
|
||||
) {
|
||||
assert_eq!(symbol.declarations.may_be_undeclared(), may_be_undeclared);
|
||||
let actual = symbol
|
||||
.declarations()
|
||||
.iter()
|
||||
.map(ScopedDefinitionId::as_u32)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unbound() {
|
||||
let sym = SymbolState::undefined();
|
||||
|
||||
assert_bindings(&sym, true, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
|
||||
assert_bindings(&sym, false, &["0<>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_may_be_unbound() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
sym.set_may_be_unbound();
|
||||
|
||||
assert_bindings(&sym, true, &["0<>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_constraint() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
sym.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
assert_bindings(&sym, false, &["0<0>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
// merging the same definition with the same constraint keeps the constraint
|
||||
let mut sym0a = SymbolState::undefined();
|
||||
sym0a.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
sym0a.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
let mut sym0b = SymbolState::undefined();
|
||||
sym0b.record_binding(ScopedDefinitionId::from_u32(0));
|
||||
sym0b.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
sym0a.merge(sym0b);
|
||||
let mut sym0 = sym0a;
|
||||
assert_bindings(&sym0, false, &["0<0>"]);
|
||||
|
||||
// merging the same definition with differing constraints drops all constraints
|
||||
let mut sym1a = SymbolState::undefined();
|
||||
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1a.record_constraint(ScopedConstraintId::from_u32(1));
|
||||
|
||||
let mut sym1b = SymbolState::undefined();
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
|
||||
|
||||
sym1a.merge(sym1b);
|
||||
let sym1 = sym1a;
|
||||
assert_bindings(&sym1, false, &["1<>"]);
|
||||
|
||||
// merging a constrained definition with unbound keeps both
|
||||
let mut sym2a = SymbolState::undefined();
|
||||
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym2a.record_constraint(ScopedConstraintId::from_u32(3));
|
||||
|
||||
let sym2b = SymbolState::undefined();
|
||||
|
||||
sym2a.merge(sym2b);
|
||||
let sym2 = sym2a;
|
||||
assert_bindings(&sym2, true, &["2<3>"]);
|
||||
|
||||
// merging different definitions keeps them each with their existing constraints
|
||||
sym0.merge(sym2);
|
||||
let sym = sym0;
|
||||
assert_bindings(&sym, true, &["0<0>", "2<3>"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_declaration() {
|
||||
let sym = SymbolState::undefined();
|
||||
|
||||
assert_declarations(&sym, true, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
assert_declarations(&sym, false, &[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration_override() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(2));
|
||||
|
||||
assert_declarations(&sym, false, &[2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration_merge() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
let mut sym2 = SymbolState::undefined();
|
||||
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
|
||||
|
||||
sym.merge(sym2);
|
||||
|
||||
assert_declarations(&sym, false, &[1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_declaration_merge_partial_undeclared() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
let sym2 = SymbolState::undefined();
|
||||
|
||||
sym.merge(sym2);
|
||||
|
||||
assert_declarations(&sym, true, &[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_may_be_undeclared() {
|
||||
let mut sym = SymbolState::undefined();
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(0));
|
||||
sym.set_may_be_undeclared();
|
||||
|
||||
assert_declarations(&sym, true, &[0]);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::files::{File, FilePath};
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Expr, ExpressionRef, StmtClassDef};
|
||||
use ruff_python_ast::{Expr, ExpressionRef};
|
||||
use ruff_source_file::LineIndex;
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{resolve_module, Module};
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::{definition_ty, global_symbol_ty_by_name, infer_scope_types, Type};
|
||||
use crate::types::{binding_ty, global_symbol_ty, infer_scope_types, Type};
|
||||
use crate::Db;
|
||||
|
||||
pub struct SemanticModel<'db> {
|
||||
@@ -25,12 +27,20 @@ impl<'db> SemanticModel<'db> {
|
||||
self.db
|
||||
}
|
||||
|
||||
pub fn file_path(&self) -> &FilePath {
|
||||
self.file.path(self.db)
|
||||
}
|
||||
|
||||
pub fn line_index(&self) -> LineIndex {
|
||||
line_index(self.db.upcast(), self.file)
|
||||
}
|
||||
|
||||
pub fn resolve_module(&self, module_name: ModuleName) -> Option<Module> {
|
||||
resolve_module(self.db, module_name)
|
||||
}
|
||||
|
||||
pub fn global_symbol_ty(&self, module: &Module, symbol_name: &str) -> Type<'db> {
|
||||
global_symbol_ty_by_name(self.db, module.file(), symbol_name)
|
||||
global_symbol_ty(self.db, module.file(), symbol_name)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,62 +147,56 @@ impl HasTy for ast::Expr {
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTy for ast::StmtFunctionDef {
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let definition = index.definition(self);
|
||||
definition_ty(model.db, definition)
|
||||
}
|
||||
macro_rules! impl_binding_has_ty {
|
||||
($ty: ty) => {
|
||||
impl HasTy for $ty {
|
||||
#[inline]
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let binding = index.definition(self);
|
||||
binding_ty(model.db, binding)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl HasTy for StmtClassDef {
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let definition = index.definition(self);
|
||||
definition_ty(model.db, definition)
|
||||
}
|
||||
}
|
||||
|
||||
impl HasTy for ast::Alias {
|
||||
fn ty<'db>(&self, model: &SemanticModel<'db>) -> Type<'db> {
|
||||
let index = semantic_index(model.db, model.file);
|
||||
let definition = index.definition(self);
|
||||
definition_ty(model.db, definition)
|
||||
}
|
||||
}
|
||||
impl_binding_has_ty!(ast::StmtFunctionDef);
|
||||
impl_binding_has_ty!(ast::StmtClassDef);
|
||||
impl_binding_has_ty!(ast::Alias);
|
||||
impl_binding_has_ty!(ast::Parameter);
|
||||
impl_binding_has_ty!(ast::ParameterWithDefault);
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::program::{Program, SearchPathSettings, TargetVersion};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::Type;
|
||||
use crate::{HasTy, SemanticModel};
|
||||
use crate::{HasTy, ProgramSettings, SemanticModel};
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
Program::new(
|
||||
fn setup_db<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<TestDb> {
|
||||
let mut db = TestDb::new();
|
||||
db.write_files(files)?;
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
TargetVersion::Py38,
|
||||
SearchPathSettings {
|
||||
extra_paths: vec![],
|
||||
src_root: SystemPathBuf::from("/src"),
|
||||
site_packages: vec![],
|
||||
custom_typeshed: None,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(SystemPathBuf::from("/src")),
|
||||
},
|
||||
);
|
||||
)?;
|
||||
|
||||
db
|
||||
Ok(db)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_ty() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db([("/src/foo.py", "def test(): pass")])?;
|
||||
|
||||
db.write_file("/src/foo.py", "def test(): pass")?;
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
let ast = parsed_module(&db, foo);
|
||||
@@ -208,9 +212,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn class_ty() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db([("/src/foo.py", "class Test: pass")])?;
|
||||
|
||||
db.write_file("/src/foo.py", "class Test: pass")?;
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
let ast = parsed_module(&db, foo);
|
||||
@@ -226,12 +229,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn alias_ty() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
let db = setup_db([
|
||||
("/src/foo.py", "class Test: pass"),
|
||||
("/src/bar.py", "from foo import Test"),
|
||||
])?;
|
||||
|
||||
let bar = system_path_to_file(&db, "/src/bar.py").unwrap();
|
||||
|
||||
let ast = parsed_module(&db, bar);
|
||||
|
||||
842
crates/red_knot_python_semantic/src/site_packages.rs
Normal file
842
crates/red_knot_python_semantic/src/site_packages.rs
Normal file
@@ -0,0 +1,842 @@
|
||||
//! Utilities for finding the `site-packages` directory,
|
||||
//! into which third-party packages are installed.
|
||||
//!
|
||||
//! The routines exposed by this module have different behaviour depending
|
||||
//! on the platform of the *host machine*, which may be
|
||||
//! different from the *target platform for type checking*. (A user
|
||||
//! might be running red-knot on a Windows machine, but might
|
||||
//! reasonably ask us to type-check code assuming that the code runs
|
||||
//! on Linux.)
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::Deref;
|
||||
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
|
||||
use crate::PythonVersion;
|
||||
|
||||
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;
|
||||
|
||||
/// Abstraction for a Python virtual environment.
|
||||
///
|
||||
/// Most of this information is derived from the virtual environment's `pyvenv.cfg` file.
|
||||
/// The format of this file is not defined anywhere, and exactly which keys are present
|
||||
/// depends on the tool that was used to create the virtual environment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct VirtualEnvironment {
|
||||
venv_path: SysPrefixPath,
|
||||
base_executable_home_path: PythonHomePath,
|
||||
include_system_site_packages: bool,
|
||||
|
||||
/// The version of the Python executable that was used to create this virtual environment.
|
||||
///
|
||||
/// The Python version is encoded under different keys and in different formats
|
||||
/// by different virtual-environment creation tools,
|
||||
/// and the key is never read by the standard-library `site.py` module,
|
||||
/// so it's possible that we might not be able to find this information
|
||||
/// in an acceptable format under any of the keys we expect.
|
||||
/// This field will be `None` if so.
|
||||
version: Option<PythonVersion>,
|
||||
}
|
||||
|
||||
impl VirtualEnvironment {
|
||||
pub(crate) fn new(
|
||||
path: impl AsRef<SystemPath>,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Self> {
|
||||
Self::new_impl(path.as_ref(), system)
|
||||
}
|
||||
|
||||
fn new_impl(path: &SystemPath, system: &dyn System) -> SitePackagesDiscoveryResult<Self> {
|
||||
fn pyvenv_cfg_line_number(index: usize) -> NonZeroUsize {
|
||||
index.checked_add(1).and_then(NonZeroUsize::new).unwrap()
|
||||
}
|
||||
|
||||
let venv_path = SysPrefixPath::new(path, system)?;
|
||||
let pyvenv_cfg_path = venv_path.join("pyvenv.cfg");
|
||||
tracing::debug!("Attempting to parse virtual environment metadata at '{pyvenv_cfg_path}'");
|
||||
|
||||
let pyvenv_cfg = system
|
||||
.read_to_string(&pyvenv_cfg_path)
|
||||
.map_err(SitePackagesDiscoveryError::NoPyvenvCfgFile)?;
|
||||
|
||||
let mut include_system_site_packages = false;
|
||||
let mut base_executable_home_path = None;
|
||||
let mut version_info_string = None;
|
||||
|
||||
// A `pyvenv.cfg` file *looks* like a `.ini` file, but actually isn't valid `.ini` syntax!
|
||||
// The Python standard-library's `site` module parses these files by splitting each line on
|
||||
// '=' characters, so that's what we should do as well.
|
||||
//
|
||||
// See also: https://snarky.ca/how-virtual-environments-work/
|
||||
for (index, line) in pyvenv_cfg.lines().enumerate() {
|
||||
if let Some((key, value)) = line.split_once('=') {
|
||||
let key = key.trim();
|
||||
if key.is_empty() {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair {
|
||||
line_number: pyvenv_cfg_line_number(index),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let value = value.trim();
|
||||
if value.is_empty() {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair {
|
||||
line_number: pyvenv_cfg_line_number(index),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if value.contains('=') {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::TooManyEquals {
|
||||
line_number: pyvenv_cfg_line_number(index),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
match key {
|
||||
"include-system-site-packages" => {
|
||||
include_system_site_packages = value.eq_ignore_ascii_case("true");
|
||||
}
|
||||
"home" => base_executable_home_path = Some(value),
|
||||
// `virtualenv` and `uv` call this key `version_info`,
|
||||
// but the stdlib venv module calls it `version`
|
||||
"version" | "version_info" => version_info_string = Some(value),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The `home` key is read by the standard library's `site.py` module,
|
||||
// so if it's missing from the `pyvenv.cfg` file
|
||||
// (or the provided value is invalid),
|
||||
// it's reasonable to consider the virtual environment irredeemably broken.
|
||||
let Some(base_executable_home_path) = base_executable_home_path else {
|
||||
return Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::NoHomeKey,
|
||||
));
|
||||
};
|
||||
let base_executable_home_path = PythonHomePath::new(base_executable_home_path, system)
|
||||
.map_err(|io_err| {
|
||||
SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
pyvenv_cfg_path,
|
||||
PyvenvCfgParseErrorKind::InvalidHomeValue(io_err),
|
||||
)
|
||||
})?;
|
||||
|
||||
// but the `version`/`version_info` key is not read by the standard library,
|
||||
// and is provided under different keys depending on which virtual-environment creation tool
|
||||
// created the `pyvenv.cfg` file. Lenient parsing is appropriate here:
|
||||
// the file isn't really *invalid* if it doesn't have this key,
|
||||
// or if the value doesn't parse according to our expectations.
|
||||
let version = version_info_string.and_then(|version_string| {
|
||||
let mut version_info_parts = version_string.split('.');
|
||||
let (major, minor) = (version_info_parts.next()?, version_info_parts.next()?);
|
||||
PythonVersion::try_from((major, minor)).ok()
|
||||
});
|
||||
|
||||
let metadata = Self {
|
||||
venv_path,
|
||||
base_executable_home_path,
|
||||
include_system_site_packages,
|
||||
version,
|
||||
};
|
||||
|
||||
tracing::trace!("Resolved metadata for virtual environment: {metadata:?}");
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Return a list of `site-packages` directories that are available from this virtual environment
|
||||
///
|
||||
/// See the documentation for `site_packages_dir_from_sys_prefix` for more details.
|
||||
pub(crate) fn site_packages_directories(
|
||||
&self,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Vec<SystemPathBuf>> {
|
||||
let VirtualEnvironment {
|
||||
venv_path,
|
||||
base_executable_home_path,
|
||||
include_system_site_packages,
|
||||
version,
|
||||
} = self;
|
||||
|
||||
let mut site_packages_directories = vec![site_packages_directory_from_sys_prefix(
|
||||
venv_path, *version, system,
|
||||
)?];
|
||||
|
||||
if *include_system_site_packages {
|
||||
let system_sys_prefix =
|
||||
SysPrefixPath::from_executable_home_path(base_executable_home_path);
|
||||
|
||||
// If we fail to resolve the `sys.prefix` path from the base executable home path,
|
||||
// or if we fail to resolve the `site-packages` from the `sys.prefix` path,
|
||||
// we should probably print a warning but *not* abort type checking
|
||||
if let Some(sys_prefix_path) = system_sys_prefix {
|
||||
match site_packages_directory_from_sys_prefix(&sys_prefix_path, *version, system) {
|
||||
Ok(site_packages_directory) => {
|
||||
site_packages_directories.push(site_packages_directory);
|
||||
}
|
||||
Err(error) => tracing::warn!(
|
||||
"{error}. System site-packages will not be used for module resolution."
|
||||
),
|
||||
}
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"Failed to resolve `sys.prefix` of the system Python installation \
|
||||
from the `home` value in the `pyvenv.cfg` file at '{}'. \
|
||||
System site-packages will not be used for module resolution.",
|
||||
venv_path.join("pyvenv.cfg")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!("Resolved site-packages directories for this virtual environment are: {site_packages_directories:?}");
|
||||
Ok(site_packages_directories)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub(crate) enum SitePackagesDiscoveryError {
|
||||
#[error("Invalid --venv-path argument: {0} could not be canonicalized")]
|
||||
VenvDirCanonicalizationError(SystemPathBuf, #[source] io::Error),
|
||||
#[error("Invalid --venv-path argument: {0} does not point to a directory on disk")]
|
||||
VenvDirIsNotADirectory(SystemPathBuf),
|
||||
#[error("--venv-path points to a broken venv with no pyvenv.cfg file")]
|
||||
NoPyvenvCfgFile(#[source] io::Error),
|
||||
#[error("Failed to parse the pyvenv.cfg file at {0} because {1}")]
|
||||
PyvenvCfgParseError(SystemPathBuf, PyvenvCfgParseErrorKind),
|
||||
#[error("Failed to search the `lib` directory of the Python installation at {1} for `site-packages`")]
|
||||
CouldNotReadLibDirectory(#[source] io::Error, SysPrefixPath),
|
||||
#[error("Could not find the `site-packages` directory for the Python installation at {0}")]
|
||||
NoSitePackagesDirFound(SysPrefixPath),
|
||||
}
|
||||
|
||||
/// The various ways in which parsing a `pyvenv.cfg` file could fail
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum PyvenvCfgParseErrorKind {
|
||||
TooManyEquals { line_number: NonZeroUsize },
|
||||
MalformedKeyValuePair { line_number: NonZeroUsize },
|
||||
NoHomeKey,
|
||||
InvalidHomeValue(io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for PyvenvCfgParseErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::TooManyEquals { line_number } => {
|
||||
write!(f, "line {line_number} has too many '=' characters")
|
||||
}
|
||||
Self::MalformedKeyValuePair { line_number } => write!(
|
||||
f,
|
||||
"line {line_number} has a malformed `<key> = <value>` pair"
|
||||
),
|
||||
Self::NoHomeKey => f.write_str("the file does not have a `home` key"),
|
||||
Self::InvalidHomeValue(io_err) => {
|
||||
write!(
|
||||
f,
|
||||
"the following error was encountered \
|
||||
when trying to resolve the `home` value to a directory on disk: {io_err}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to retrieve the `site-packages` directory
|
||||
/// associated with a given Python installation.
|
||||
///
|
||||
/// The location of the `site-packages` directory can vary according to the
|
||||
/// Python version that this installation represents. The Python version may
|
||||
/// or may not be known at this point, which is why the `python_version`
|
||||
/// parameter is an `Option`.
|
||||
fn site_packages_directory_from_sys_prefix(
|
||||
sys_prefix_path: &SysPrefixPath,
|
||||
python_version: Option<PythonVersion>,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<SystemPathBuf> {
|
||||
tracing::debug!("Searching for site-packages directory in {sys_prefix_path}");
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
let site_packages = sys_prefix_path.join(r"Lib\site-packages");
|
||||
return system
|
||||
.is_directory(&site_packages)
|
||||
.then_some(site_packages)
|
||||
.ok_or(SitePackagesDiscoveryError::NoSitePackagesDirFound(
|
||||
sys_prefix_path.to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// In the Python standard library's `site.py` module (used for finding `site-packages`
|
||||
// at runtime), we can find this in [the non-Windows branch]:
|
||||
//
|
||||
// ```py
|
||||
// libdirs = [sys.platlibdir]
|
||||
// if sys.platlibdir != "lib":
|
||||
// libdirs.append("lib")
|
||||
// ```
|
||||
//
|
||||
// Pyright therefore searches for both a `lib/python3.X/site-packages` directory
|
||||
// and a `lib64/python3.X/site-packages` directory on non-MacOS Unix systems,
|
||||
// since `sys.platlibdir` can sometimes be equal to `"lib64"`.
|
||||
//
|
||||
// However, we only care about the `site-packages` directory insofar as it allows
|
||||
// us to discover Python source code that can be used for inferring type
|
||||
// information regarding third-party dependencies. That means that we don't need
|
||||
// to care about any possible `lib64/site-packages` directories, since
|
||||
// [the `sys`-module documentation] states that `sys.platlibdir` is *only* ever
|
||||
// used for C extensions, never for pure-Python modules.
|
||||
//
|
||||
// [the non-Windows branch]: https://github.com/python/cpython/blob/a8be8fc6c4682089be45a87bd5ee1f686040116c/Lib/site.py#L401-L410
|
||||
// [the `sys`-module documentation]: https://docs.python.org/3/library/sys.html#sys.platlibdir
|
||||
|
||||
// If we were able to figure out what Python version this installation is,
|
||||
// we should be able to avoid iterating through all items in the `lib/` directory:
|
||||
if let Some(version) = python_version {
|
||||
let expected_path = sys_prefix_path.join(format!("lib/python{version}/site-packages"));
|
||||
if system.is_directory(&expected_path) {
|
||||
return Ok(expected_path);
|
||||
}
|
||||
if version.free_threaded_build_available() {
|
||||
// Nearly the same as `expected_path`, but with an additional `t` after {version}:
|
||||
let alternative_path =
|
||||
sys_prefix_path.join(format!("lib/python{version}t/site-packages"));
|
||||
if system.is_directory(&alternative_path) {
|
||||
return Ok(alternative_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Either we couldn't figure out the version before calling this function
|
||||
// (e.g., from a `pyvenv.cfg` file if this was a venv),
|
||||
// or we couldn't find a `site-packages` folder at the expected location given
|
||||
// the parsed version
|
||||
//
|
||||
// Note: the `python3.x` part of the `site-packages` path can't be computed from
|
||||
// the `--target-version` the user has passed, as they might be running Python 3.12 locally
|
||||
// even if they've requested that we type check their code "as if" they're running 3.8.
|
||||
for entry_result in system
|
||||
.read_directory(&sys_prefix_path.join("lib"))
|
||||
.map_err(|io_err| {
|
||||
SitePackagesDiscoveryError::CouldNotReadLibDirectory(io_err, sys_prefix_path.to_owned())
|
||||
})?
|
||||
{
|
||||
let Ok(entry) = entry_result else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !entry.file_type().is_directory() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut path = entry.into_path();
|
||||
|
||||
let name = path
|
||||
.file_name()
|
||||
.expect("File name to be non-null because path is guaranteed to be a child of `lib`");
|
||||
|
||||
if !name.starts_with("python3.") {
|
||||
continue;
|
||||
}
|
||||
|
||||
path.push("site-packages");
|
||||
if system.is_directory(&path) {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
Err(SitePackagesDiscoveryError::NoSitePackagesDirFound(
|
||||
sys_prefix_path.to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
/// A path that represents the value of [`sys.prefix`] at runtime in Python
|
||||
/// for a given Python executable.
|
||||
///
|
||||
/// For the case of a virtual environment, where a
|
||||
/// Python binary is at `/.venv/bin/python`, `sys.prefix` is the path to
|
||||
/// the virtual environment the Python binary lies inside, i.e. `/.venv`,
|
||||
/// and `site-packages` will be at `.venv/lib/python3.X/site-packages`.
|
||||
/// System Python installations generally work the same way: if a system
|
||||
/// Python installation lies at `/opt/homebrew/bin/python`, `sys.prefix`
|
||||
/// will be `/opt/homebrew`, and `site-packages` will be at
|
||||
/// `/opt/homebrew/lib/python3.X/site-packages`.
|
||||
///
|
||||
/// [`sys.prefix`]: https://docs.python.org/3/library/sys.html#sys.prefix
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub(crate) struct SysPrefixPath(SystemPathBuf);
|
||||
|
||||
impl SysPrefixPath {
|
||||
fn new(
|
||||
unvalidated_path: impl AsRef<SystemPath>,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Self> {
|
||||
Self::new_impl(unvalidated_path.as_ref(), system)
|
||||
}
|
||||
|
||||
fn new_impl(
|
||||
unvalidated_path: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> SitePackagesDiscoveryResult<Self> {
|
||||
// It's important to resolve symlinks here rather than simply making the path absolute,
|
||||
// since system Python installations often only put symlinks in the "expected"
|
||||
// locations for `home` and `site-packages`
|
||||
let canonicalized = system
|
||||
.canonicalize_path(unvalidated_path)
|
||||
.map_err(|io_err| {
|
||||
SitePackagesDiscoveryError::VenvDirCanonicalizationError(
|
||||
unvalidated_path.to_path_buf(),
|
||||
io_err,
|
||||
)
|
||||
})?;
|
||||
system
|
||||
.is_directory(&canonicalized)
|
||||
.then_some(Self(canonicalized))
|
||||
.ok_or_else(|| {
|
||||
SitePackagesDiscoveryError::VenvDirIsNotADirectory(unvalidated_path.to_path_buf())
|
||||
})
|
||||
}
|
||||
|
||||
fn from_executable_home_path(path: &PythonHomePath) -> Option<Self> {
|
||||
// No need to check whether `path.parent()` is a directory:
|
||||
// the parent of a canonicalised path that is known to exist
|
||||
// is guaranteed to be a directory.
|
||||
if cfg!(target_os = "windows") {
|
||||
Some(Self(path.to_path_buf()))
|
||||
} else {
|
||||
path.parent().map(|path| Self(path.to_path_buf()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SysPrefixPath {
|
||||
type Target = SystemPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SysPrefixPath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "`sys.prefix` path '{}'", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// The value given by the `home` key in `pyvenv.cfg` files.
|
||||
///
|
||||
/// This is equivalent to `{sys_prefix_path}/bin`, and points
|
||||
/// to a directory in which a Python executable can be found.
|
||||
/// Confusingly, it is *not* the same as the [`PYTHONHOME`]
|
||||
/// environment variable that Python provides! However, it's
|
||||
/// consistent among all mainstream creators of Python virtual
|
||||
/// environments (the stdlib Python `venv` module, the third-party
|
||||
/// `virtualenv` library, and `uv`), was specified by
|
||||
/// [the original PEP adding the `venv` module],
|
||||
/// and it's one of the few fields that's read by the Python
|
||||
/// standard library's `site.py` module.
|
||||
///
|
||||
/// Although it doesn't appear to be specified anywhere,
|
||||
/// all existing virtual environment tools always use an absolute path
|
||||
/// for the `home` value, and the Python standard library also assumes
|
||||
/// that the `home` value will be an absolute path.
|
||||
///
|
||||
/// Other values, such as the path to the Python executable or the
|
||||
/// base-executable `sys.prefix` value, are either only provided in
|
||||
/// `pyvenv.cfg` files by some virtual-environment creators,
|
||||
/// or are included under different keys depending on which
|
||||
/// virtual-environment creation tool you've used.
|
||||
///
|
||||
/// [`PYTHONHOME`]: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHOME
|
||||
/// [the original PEP adding the `venv` module]: https://peps.python.org/pep-0405/
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct PythonHomePath(SystemPathBuf);
|
||||
|
||||
impl PythonHomePath {
|
||||
fn new(path: impl AsRef<SystemPath>, system: &dyn System) -> io::Result<Self> {
|
||||
let path = path.as_ref();
|
||||
// It's important to resolve symlinks here rather than simply making the path absolute,
|
||||
// since system Python installations often only put symlinks in the "expected"
|
||||
// locations for `home` and `site-packages`
|
||||
let canonicalized = system.canonicalize_path(path)?;
|
||||
system
|
||||
.is_directory(&canonicalized)
|
||||
.then_some(Self(canonicalized))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "not a directory"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PythonHomePath {
|
||||
type Target = SystemPath;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonHomePath {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "`home` location '{}'", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SystemPath> for PythonHomePath {
|
||||
fn eq(&self, other: &SystemPath) -> bool {
|
||||
&*self.0 == other
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SystemPathBuf> for PythonHomePath {
|
||||
fn eq(&self, other: &SystemPathBuf) -> bool {
|
||||
self == &**other
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::system::TestSystem;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct VirtualEnvironmentTester {
|
||||
system: TestSystem,
|
||||
minor_version: u8,
|
||||
free_threaded: bool,
|
||||
system_site_packages: bool,
|
||||
pyvenv_cfg_version_field: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl VirtualEnvironmentTester {
|
||||
/// Builds a mock virtual environment, and returns the path to the venv
|
||||
fn build_mock_venv(&self) -> SystemPathBuf {
|
||||
let VirtualEnvironmentTester {
|
||||
system,
|
||||
minor_version,
|
||||
system_site_packages,
|
||||
free_threaded,
|
||||
pyvenv_cfg_version_field,
|
||||
} = self;
|
||||
let memory_fs = system.memory_file_system();
|
||||
let unix_site_packages = if *free_threaded {
|
||||
format!("lib/python3.{minor_version}t/site-packages")
|
||||
} else {
|
||||
format!("lib/python3.{minor_version}/site-packages")
|
||||
};
|
||||
|
||||
let system_install_sys_prefix =
|
||||
SystemPathBuf::from(&*format!("/Python3.{minor_version}"));
|
||||
let (system_home_path, system_exe_path, system_site_packages_path) =
|
||||
if cfg!(target_os = "windows") {
|
||||
let system_home_path = system_install_sys_prefix.clone();
|
||||
let system_exe_path = system_home_path.join("python.exe");
|
||||
let system_site_packages_path =
|
||||
system_install_sys_prefix.join(r"Lib\site-packages");
|
||||
(system_home_path, system_exe_path, system_site_packages_path)
|
||||
} else {
|
||||
let system_home_path = system_install_sys_prefix.join("bin");
|
||||
let system_exe_path = system_home_path.join("python");
|
||||
let system_site_packages_path =
|
||||
system_install_sys_prefix.join(&unix_site_packages);
|
||||
(system_home_path, system_exe_path, system_site_packages_path)
|
||||
};
|
||||
memory_fs.write_file(system_exe_path, "").unwrap();
|
||||
memory_fs
|
||||
.create_directory_all(&system_site_packages_path)
|
||||
.unwrap();
|
||||
|
||||
let venv_sys_prefix = SystemPathBuf::from("/.venv");
|
||||
let (venv_exe, site_packages_path) = if cfg!(target_os = "windows") {
|
||||
(
|
||||
venv_sys_prefix.join(r"Scripts\python.exe"),
|
||||
venv_sys_prefix.join(r"Lib\site-packages"),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
venv_sys_prefix.join("bin/python"),
|
||||
venv_sys_prefix.join(&unix_site_packages),
|
||||
)
|
||||
};
|
||||
memory_fs.write_file(&venv_exe, "").unwrap();
|
||||
memory_fs.create_directory_all(&site_packages_path).unwrap();
|
||||
|
||||
let pyvenv_cfg_path = venv_sys_prefix.join("pyvenv.cfg");
|
||||
let mut pyvenv_cfg_contents = format!("home = {system_home_path}\n");
|
||||
if let Some(version_field) = pyvenv_cfg_version_field {
|
||||
pyvenv_cfg_contents.push_str(version_field);
|
||||
pyvenv_cfg_contents.push('\n');
|
||||
}
|
||||
// Deliberately using weird casing here to test that our pyvenv.cfg parsing is case-insensitive:
|
||||
if *system_site_packages {
|
||||
pyvenv_cfg_contents.push_str("include-system-site-packages = TRuE\n");
|
||||
}
|
||||
memory_fs
|
||||
.write_file(pyvenv_cfg_path, &pyvenv_cfg_contents)
|
||||
.unwrap();
|
||||
|
||||
venv_sys_prefix
|
||||
}
|
||||
|
||||
fn test(self) {
|
||||
let venv_path = self.build_mock_venv();
|
||||
let venv = VirtualEnvironment::new(venv_path.clone(), &self.system).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
venv.venv_path,
|
||||
SysPrefixPath(self.system.canonicalize_path(&venv_path).unwrap())
|
||||
);
|
||||
assert_eq!(venv.include_system_site_packages, self.system_site_packages);
|
||||
|
||||
if self.pyvenv_cfg_version_field.is_some() {
|
||||
assert_eq!(
|
||||
venv.version,
|
||||
Some(PythonVersion {
|
||||
major: 3,
|
||||
minor: self.minor_version
|
||||
})
|
||||
);
|
||||
} else {
|
||||
assert_eq!(venv.version, None);
|
||||
}
|
||||
|
||||
let expected_home = if cfg!(target_os = "windows") {
|
||||
SystemPathBuf::from(&*format!(r"\Python3.{}", self.minor_version))
|
||||
} else {
|
||||
SystemPathBuf::from(&*format!("/Python3.{}/bin", self.minor_version))
|
||||
};
|
||||
assert_eq!(venv.base_executable_home_path, expected_home);
|
||||
|
||||
let site_packages_directories = venv.site_packages_directories(&self.system).unwrap();
|
||||
let expected_venv_site_packages = if cfg!(target_os = "windows") {
|
||||
SystemPathBuf::from(r"\.venv\Lib\site-packages")
|
||||
} else if self.free_threaded {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/.venv/lib/python3.{}t/site-packages",
|
||||
self.minor_version
|
||||
))
|
||||
} else {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/.venv/lib/python3.{}/site-packages",
|
||||
self.minor_version
|
||||
))
|
||||
};
|
||||
|
||||
let expected_system_site_packages = if cfg!(target_os = "windows") {
|
||||
SystemPathBuf::from(&*format!(
|
||||
r"\Python3.{}\Lib\site-packages",
|
||||
self.minor_version
|
||||
))
|
||||
} else if self.free_threaded {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/Python3.{minor_version}/lib/python3.{minor_version}t/site-packages",
|
||||
minor_version = self.minor_version
|
||||
))
|
||||
} else {
|
||||
SystemPathBuf::from(&*format!(
|
||||
"/Python3.{minor_version}/lib/python3.{minor_version}/site-packages",
|
||||
minor_version = self.minor_version
|
||||
))
|
||||
};
|
||||
|
||||
if self.system_site_packages {
|
||||
assert_eq!(
|
||||
&site_packages_directories,
|
||||
&[expected_venv_site_packages, expected_system_site_packages]
|
||||
);
|
||||
} else {
|
||||
assert_eq!(&site_packages_directories, &[expected_venv_site_packages]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_no_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: None,
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_venv_style_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version = 3.12"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_uv_style_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.12"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_virtualenv_style_version_field_in_pyvenv_cfg() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 12,
|
||||
free_threaded: false,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.12.0rc2"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_find_site_packages_directory_freethreaded_build() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 13,
|
||||
free_threaded: true,
|
||||
system_site_packages: false,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.13"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finds_system_site_packages() {
|
||||
let tester = VirtualEnvironmentTester {
|
||||
system: TestSystem::default(),
|
||||
minor_version: 13,
|
||||
free_threaded: true,
|
||||
system_site_packages: true,
|
||||
pyvenv_cfg_version_field: Some("version_info = 3.13"),
|
||||
};
|
||||
tester.test();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_venv_that_does_not_exist() {
|
||||
let system = TestSystem::default();
|
||||
assert!(matches!(
|
||||
VirtualEnvironment::new("/.venv", &system),
|
||||
Err(SitePackagesDiscoveryError::VenvDirIsNotADirectory(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reject_venv_with_no_pyvenv_cfg_file() {
|
||||
let system = TestSystem::default();
|
||||
system
|
||||
.memory_file_system()
|
||||
.create_directory_all("/.venv")
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
VirtualEnvironment::new("/.venv", &system),
|
||||
Err(SitePackagesDiscoveryError::NoPyvenvCfgFile(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_too_many_equals() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs
|
||||
.write_file(&pyvenv_cfg_path, "home = bar = /.venv/bin")
|
||||
.unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::TooManyEquals { line_number }
|
||||
))
|
||||
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_key_but_no_value_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs.write_file(&pyvenv_cfg_path, "home =").unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
|
||||
))
|
||||
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_value_but_no_key_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs
|
||||
.write_file(&pyvenv_cfg_path, "= whatever")
|
||||
.unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::MalformedKeyValuePair { line_number }
|
||||
))
|
||||
if path == pyvenv_cfg_path && Some(line_number) == NonZeroUsize::new(1)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_no_home_key_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs.write_file(&pyvenv_cfg_path, "").unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::NoHomeKey
|
||||
))
|
||||
if path == pyvenv_cfg_path
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsing_pyvenv_cfg_with_invalid_home_key_fails() {
|
||||
let system = TestSystem::default();
|
||||
let memory_fs = system.memory_file_system();
|
||||
let pyvenv_cfg_path = SystemPathBuf::from("/.venv/pyvenv.cfg");
|
||||
memory_fs
|
||||
.write_file(&pyvenv_cfg_path, "home = foo")
|
||||
.unwrap();
|
||||
let venv_result = VirtualEnvironment::new("/.venv", &system);
|
||||
assert!(matches!(
|
||||
venv_result,
|
||||
Err(SitePackagesDiscoveryError::PyvenvCfgParseError(
|
||||
path,
|
||||
PyvenvCfgParseErrorKind::InvalidHomeValue(_)
|
||||
))
|
||||
if path == pyvenv_cfg_path
|
||||
));
|
||||
}
|
||||
}
|
||||
87
crates/red_knot_python_semantic/src/stdlib.rs
Normal file
87
crates/red_knot_python_semantic/src/stdlib.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{global_symbol_ty, Type};
|
||||
use crate::Db;
|
||||
|
||||
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CoreStdlibModule {
|
||||
Builtins,
|
||||
Types,
|
||||
Typeshed,
|
||||
TypingExtensions,
|
||||
}
|
||||
|
||||
impl CoreStdlibModule {
|
||||
fn name(self) -> ModuleName {
|
||||
let module_name = match self {
|
||||
Self::Builtins => "builtins",
|
||||
Self::Types => "types",
|
||||
Self::Typeshed => "_typeshed",
|
||||
Self::TypingExtensions => "typing_extensions",
|
||||
};
|
||||
ModuleName::new_static(module_name)
|
||||
.unwrap_or_else(|| panic!("{module_name} should be a valid module name!"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in a given core module
|
||||
///
|
||||
/// Returns `Unbound` if the given core module cannot be resolved for some reason
|
||||
fn core_module_symbol_ty<'db>(
|
||||
db: &'db dyn Db,
|
||||
core_module: CoreStdlibModule,
|
||||
symbol: &str,
|
||||
) -> Type<'db> {
|
||||
resolve_module(db, core_module.name())
|
||||
.map(|module| global_symbol_ty(db, module.file(), symbol))
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the builtins namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `builtins` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn builtins_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Builtins, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `types` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `types` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn types_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Types, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `_typeshed` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `_typeshed` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typeshed_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Typeshed, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `typing_extensions` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `typing_extensions` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typing_extensions_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::TypingExtensions, symbol)
|
||||
}
|
||||
|
||||
/// Get the scope of a core stdlib module.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
fn core_module_scope(db: &dyn Db, core_module: CoreStdlibModule) -> Option<ScopeId<'_>> {
|
||||
resolve_module(db, core_module.name()).map(|module| global_scope(db, module.file()))
|
||||
}
|
||||
|
||||
/// Get the `builtins` module scope.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`.
|
||||
pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
core_module_scope(db, CoreStdlibModule::Builtins)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
575
crates/red_knot_python_semantic/src/types/builder.rs
Normal file
575
crates/red_knot_python_semantic/src/types/builder.rs
Normal file
@@ -0,0 +1,575 @@
|
||||
//! Smart builders for union and intersection types.
|
||||
//!
|
||||
//! Invariants we maintain here:
|
||||
//! * No single-element union types (should just be the contained type instead.)
|
||||
//! * No single-positive-element intersection types. Single-negative-element are OK, we don't
|
||||
//! have a standalone negation type so there's no other representation for this.
|
||||
//! * The same type should never appear more than once in a union or intersection. (This should
|
||||
//! be expanded to cover subtyping -- see below -- but for now we only implement it for type
|
||||
//! identity.)
|
||||
//! * Disjunctive normal form (DNF): the tree of unions and intersections can never be deeper
|
||||
//! than a union-of-intersections. Unions cannot contain other unions (the inner union just
|
||||
//! flattens into the outer one), intersections cannot contain other intersections (also
|
||||
//! flattens), and intersections cannot contain unions (the intersection distributes over the
|
||||
//! union, inverting it into a union-of-intersections).
|
||||
//!
|
||||
//! The implication of these invariants is that a [`UnionBuilder`] does not necessarily build a
|
||||
//! [`Type::Union`]. For example, if only one type is added to the [`UnionBuilder`], `build()` will
|
||||
//! just return that type directly. The same is true for [`IntersectionBuilder`]; for example, if a
|
||||
//! union type is added to the intersection, it will distribute and [`IntersectionBuilder::build`]
|
||||
//! may end up returning a [`Type::Union`] of intersections.
|
||||
//!
|
||||
//! In the future we should have these additional invariants, but they aren't implemented yet:
|
||||
//! * No type in a union can be a subtype of any other type in the union (just eliminate the
|
||||
//! subtype from the union).
|
||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
||||
//! eliminate the supertype from the intersection).
|
||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl<'db> UnionBuilder<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
db,
|
||||
elements: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a type to this union.
|
||||
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
|
||||
match ty {
|
||||
Type::Union(union) => {
|
||||
let new_elements = union.elements(self.db);
|
||||
self.elements.reserve(new_elements.len());
|
||||
for element in new_elements {
|
||||
self = self.add(*element);
|
||||
}
|
||||
}
|
||||
Type::Never => {}
|
||||
_ => {
|
||||
let bool_pair = if let Type::BooleanLiteral(b) = ty {
|
||||
Some(Type::BooleanLiteral(!b))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut to_add = ty;
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
for (index, element) in self.elements.iter().enumerate() {
|
||||
if Some(*element) == bool_pair {
|
||||
to_add = builtins_symbol_ty(self.db, "bool");
|
||||
to_remove.push(index);
|
||||
// The type we are adding is a BooleanLiteral, which doesn't have any
|
||||
// subtypes. And we just found that the union already contained our
|
||||
// mirror-image BooleanLiteral, so it can't also contain bool or any
|
||||
// supertype of bool. Therefore, we are done.
|
||||
break;
|
||||
}
|
||||
if ty.is_subtype_of(self.db, *element) {
|
||||
return self;
|
||||
} else if element.is_subtype_of(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
match to_remove[..] {
|
||||
[] => self.elements.push(to_add),
|
||||
[index] => self.elements[index] = to_add,
|
||||
_ => {
|
||||
let mut current_index = 0;
|
||||
let mut to_remove = to_remove.into_iter();
|
||||
let mut next_to_remove_index = to_remove.next();
|
||||
self.elements.retain(|_| {
|
||||
let retain = if Some(current_index) == next_to_remove_index {
|
||||
next_to_remove_index = to_remove.next();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
current_index += 1;
|
||||
retain
|
||||
});
|
||||
self.elements.push(to_add);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> Type<'db> {
|
||||
match self.elements.len() {
|
||||
0 => Type::Never,
|
||||
1 => self.elements[0],
|
||||
_ => Type::Union(UnionType::new(self.db, self.elements.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct IntersectionBuilder<'db> {
|
||||
// Really this builds a union-of-intersections, because we always keep our set-theoretic types
|
||||
// in disjunctive normal form (DNF), a union of intersections. In the simplest case there's
|
||||
// just a single intersection in this vector, and we are building a single intersection type,
|
||||
// but if a union is added to the intersection, we'll distribute ourselves over that union and
|
||||
// create a union of intersections.
|
||||
intersections: Vec<InnerIntersectionBuilder<'db>>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl<'db> IntersectionBuilder<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
db,
|
||||
intersections: vec![InnerIntersectionBuilder::new()],
|
||||
}
|
||||
}
|
||||
|
||||
fn empty(db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
db,
|
||||
intersections: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_positive(mut self, ty: Type<'db>) -> Self {
|
||||
if let Type::Union(union) = ty {
|
||||
// Distribute ourself over this union: for each union element, clone ourself and
|
||||
// intersect with that union element, then create a new union-of-intersections with all
|
||||
// of those sub-intersections in it. E.g. if `self` is a simple intersection `T1 & T2`
|
||||
// and we add `T3 | T4` to the intersection, we don't get `T1 & T2 & (T3 | T4)` (that's
|
||||
// not in DNF), we distribute the union and get `(T1 & T3) | (T2 & T3) | (T1 & T4) |
|
||||
// (T2 & T4)`. If `self` is already a union-of-intersections `(T1 & T2) | (T3 & T4)`
|
||||
// and we add `T5 | T6` to it, that flattens all the way out to `(T1 & T2 & T5) | (T1 &
|
||||
// T2 & T6) | (T3 & T4 & T5) ...` -- you get the idea.
|
||||
union
|
||||
.elements(self.db)
|
||||
.iter()
|
||||
.map(|elem| self.clone().add_positive(*elem))
|
||||
.fold(IntersectionBuilder::empty(self.db), |mut builder, sub| {
|
||||
builder.intersections.extend(sub.intersections);
|
||||
builder
|
||||
})
|
||||
} else {
|
||||
// If we are already a union-of-intersections, distribute the new intersected element
|
||||
// across all of those intersections.
|
||||
for inner in &mut self.intersections {
|
||||
inner.add_positive(self.db, ty);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_negative(mut self, ty: Type<'db>) -> Self {
|
||||
// See comments above in `add_positive`; this is just the negated version.
|
||||
if let Type::Union(union) = ty {
|
||||
union
|
||||
.elements(self.db)
|
||||
.iter()
|
||||
.map(|elem| self.clone().add_negative(*elem))
|
||||
.fold(IntersectionBuilder::empty(self.db), |mut builder, sub| {
|
||||
builder.intersections.extend(sub.intersections);
|
||||
builder
|
||||
})
|
||||
} else {
|
||||
for inner in &mut self.intersections {
|
||||
inner.add_negative(self.db, ty);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build(mut self) -> Type<'db> {
|
||||
// Avoid allocating the UnionBuilder unnecessarily if we have just one intersection:
|
||||
if self.intersections.len() == 1 {
|
||||
self.intersections.pop().unwrap().build(self.db)
|
||||
} else {
|
||||
UnionType::from_elements(
|
||||
self.db,
|
||||
self.intersections
|
||||
.into_iter()
|
||||
.map(|inner| inner.build(self.db)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct InnerIntersectionBuilder<'db> {
|
||||
positive: FxOrderSet<Type<'db>>,
|
||||
negative: FxOrderSet<Type<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> InnerIntersectionBuilder<'db> {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Adds a positive type to this intersection.
|
||||
fn add_positive(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
match ty {
|
||||
Type::Intersection(inter) => {
|
||||
let pos = inter.positive(db);
|
||||
let neg = inter.negative(db);
|
||||
self.positive.extend(pos.difference(&self.negative));
|
||||
self.negative.extend(neg.difference(&self.positive));
|
||||
self.positive.retain(|elem| !neg.contains(elem));
|
||||
self.negative.retain(|elem| !pos.contains(elem));
|
||||
}
|
||||
_ => {
|
||||
if !self.negative.remove(&ty) {
|
||||
self.positive.insert(ty);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a negative type to this intersection.
|
||||
fn add_negative(&mut self, db: &'db dyn Db, ty: Type<'db>) {
|
||||
// TODO Any/Unknown actually should not self-cancel
|
||||
match ty {
|
||||
Type::Intersection(intersection) => {
|
||||
let pos = intersection.negative(db);
|
||||
let neg = intersection.positive(db);
|
||||
self.positive.extend(pos.difference(&self.negative));
|
||||
self.negative.extend(neg.difference(&self.positive));
|
||||
self.positive.retain(|elem| !neg.contains(elem));
|
||||
self.negative.retain(|elem| !pos.contains(elem));
|
||||
}
|
||||
Type::Never => {}
|
||||
Type::Unbound => {}
|
||||
_ => {
|
||||
if !self.positive.remove(&ty) {
|
||||
self.negative.insert(ty);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simplify(&mut self) {
|
||||
// TODO this should be generalized based on subtyping, for now we just handle a few cases
|
||||
|
||||
// Never is a subtype of all types
|
||||
if self.positive.contains(&Type::Never) {
|
||||
self.positive.retain(Type::is_never);
|
||||
self.negative.clear();
|
||||
}
|
||||
|
||||
if self.positive.contains(&Type::Unbound) {
|
||||
self.positive.retain(Type::is_unbound);
|
||||
self.negative.clear();
|
||||
}
|
||||
|
||||
// None intersects only with object
|
||||
for pos in &self.positive {
|
||||
if let Type::Instance(_) = pos {
|
||||
// could be `object` type
|
||||
} else {
|
||||
self.negative.remove(&Type::None);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.simplify();
|
||||
match (self.positive.len(), self.negative.len()) {
|
||||
(0, 0) => Type::Never,
|
||||
(1, 0) => self.positive[0],
|
||||
_ => {
|
||||
self.positive.shrink_to_fit();
|
||||
self.negative.shrink_to_fit();
|
||||
Type::Intersection(IntersectionType::new(db, self.positive, self.negative))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::{builtins_symbol_ty, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
|
||||
assert_eq!(union.elements(&db), &[t0, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_single() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0]);
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_empty() {
|
||||
let db = setup_db();
|
||||
let ty = UnionBuilder::new(&db).build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_never() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0, Type::Never]);
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_bool() {
|
||||
let db = setup_db();
|
||||
let bool_ty = builtins_symbol_ty(&db, "bool");
|
||||
|
||||
let t0 = Type::BooleanLiteral(true);
|
||||
let t1 = Type::BooleanLiteral(true);
|
||||
let t2 = Type::BooleanLiteral(false);
|
||||
let t3 = Type::IntLiteral(17);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[t0, t3]);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[bool_ty, t3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_flatten() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let u1 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
|
||||
|
||||
assert_eq!(union.elements(&db), &[t0, t1, t2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_simplify_subtype() {
|
||||
let db = setup_db();
|
||||
let t0 = builtins_symbol_ty(&db, "str").to_instance(&db);
|
||||
let t1 = Type::LiteralString;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0, t0);
|
||||
assert_eq!(u1, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_no_simplify_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = builtins_symbol_ty(&db, "str").to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[t0, t1]);
|
||||
assert_eq!(u1.expect_union().elements(&db), &[t1, t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_subsume_multiple() {
|
||||
let db = setup_db();
|
||||
let str_ty = builtins_symbol_ty(&db, "str").to_instance(&db);
|
||||
let int_ty = builtins_symbol_ty(&db, "int").to_instance(&db);
|
||||
let object_ty = builtins_symbol_ty(&db, "object").to_instance(&db);
|
||||
let unknown_ty = Type::Unknown;
|
||||
|
||||
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
|
||||
|
||||
assert_eq!(u0.expect_union().elements(&db), &[unknown_ty, object_ty]);
|
||||
}
|
||||
|
||||
impl<'db> IntersectionType<'db> {
|
||||
fn pos_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.positive(db).into_iter().copied().collect()
|
||||
}
|
||||
|
||||
fn neg_vec(self, db: &'db TestDb) -> Vec<Type<'db>> {
|
||||
self.negative(db).into_iter().copied().collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ta = Type::Any;
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[t0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_positive() {
|
||||
let db = setup_db();
|
||||
let ta = Type::Any;
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_positive(i0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[t2, ta]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_flatten_negative() {
|
||||
let db = setup_db();
|
||||
let ta = Type::Any;
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let i0 = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_negative(t1)
|
||||
.build();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(t2)
|
||||
.add_negative(i0)
|
||||
.build()
|
||||
.expect_intersection();
|
||||
|
||||
assert_eq!(intersection.pos_vec(&db), &[t2, t1]);
|
||||
assert_eq!(intersection.neg_vec(&db), &[ta]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let ta = Type::Any;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
|
||||
let union = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
.add_positive(u0)
|
||||
.build()
|
||||
.expect_union();
|
||||
let [Type::Intersection(i0), Type::Intersection(i1)] = union.elements(&db)[..] else {
|
||||
panic!("expected a union of two intersections");
|
||||
};
|
||||
assert_eq!(i0.pos_vec(&db), &[ta, t0]);
|
||||
assert_eq!(i1.pos_vec(&db), &[ta, t1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_self_negation() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::None)
|
||||
.add_negative(Type::None)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::None)
|
||||
.add_negative(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::None)
|
||||
.add_positive(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_unbound() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Unbound)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Unbound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_unbound() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Unbound)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_none() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::None)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
}
|
||||
111
crates/red_knot_python_semantic/src/types/diagnostic.rs
Normal file
111
crates/red_knot_python_semantic/src/types/diagnostic.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
// TODO: Don't use string keys for rules
|
||||
pub(super) rule: String,
|
||||
pub(super) message: String,
|
||||
pub(super) range: TextRange,
|
||||
pub(super) file: File,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostic {
|
||||
pub fn rule(&self) -> &str {
|
||||
&self.rule
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &str {
|
||||
&self.message
|
||||
}
|
||||
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TypeCheckDiagnostic {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
/// A collection of type check diagnostics.
|
||||
///
|
||||
/// The diagnostics are wrapped in an `Arc` because they need to be cloned multiple times
|
||||
/// when going from `infer_expression` to `check_file`. We could consider
|
||||
/// making [`TypeCheckDiagnostic`] a Salsa struct to have them Arena-allocated (once the Tables refactor is done).
|
||||
/// Using Salsa struct does have the downside that it leaks the Salsa dependency into diagnostics and
|
||||
/// each Salsa-struct comes with an overhead.
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub struct TypeCheckDiagnostics {
|
||||
inner: Vec<std::sync::Arc<TypeCheckDiagnostic>>,
|
||||
}
|
||||
|
||||
impl TypeCheckDiagnostics {
|
||||
pub fn new() -> Self {
|
||||
Self { inner: Vec::new() }
|
||||
}
|
||||
|
||||
pub(super) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
|
||||
self.inner.push(Arc::new(diagnostic));
|
||||
}
|
||||
|
||||
pub(crate) fn shrink_to_fit(&mut self) {
|
||||
self.inner.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<TypeCheckDiagnostic> for TypeCheckDiagnostics {
|
||||
fn extend<T: IntoIterator<Item = TypeCheckDiagnostic>>(&mut self, iter: T) {
|
||||
self.inner.extend(iter.into_iter().map(std::sync::Arc::new));
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
|
||||
fn extend<T: IntoIterator<Item = Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
|
||||
self.inner.extend(iter);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Extend<&'a std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
|
||||
fn extend<T: IntoIterator<Item = &'a Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
|
||||
self.inner
|
||||
.extend(iter.into_iter().map(std::sync::Arc::clone));
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TypeCheckDiagnostics {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for TypeCheckDiagnostics {
|
||||
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for TypeCheckDiagnostics {
|
||||
type Item = Arc<TypeCheckDiagnostic>;
|
||||
type IntoIter = std::vec::IntoIter<std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
|
||||
type Item = &'a Arc<TypeCheckDiagnostic>;
|
||||
type IntoIter = std::slice::Iter<'a, std::sync::Arc<TypeCheckDiagnostic>>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner.iter()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
//! Display implementations for types.
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use ruff_db::display::FormatterJoinExtension;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
impl<'db> Type<'db> {
|
||||
pub fn display(&'db self, db: &'db dyn Db) -> DisplayType<'db> {
|
||||
pub fn display(&self, db: &'db dyn Db) -> DisplayType {
|
||||
DisplayType { ty: self, db }
|
||||
}
|
||||
fn representation(self, db: &'db dyn Db) -> DisplayRepresentation<'db> {
|
||||
DisplayRepresentation { db, ty: self }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -18,7 +26,41 @@ pub struct DisplayType<'db> {
|
||||
}
|
||||
|
||||
impl Display for DisplayType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let representation = self.ty.representation(self.db);
|
||||
if matches!(
|
||||
self.ty,
|
||||
Type::IntLiteral(_)
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::Class(_)
|
||||
| Type::Function(_)
|
||||
| Type::RevealTypeFunction(_)
|
||||
) {
|
||||
write!(f, "Literal[{representation}]",)
|
||||
} else {
|
||||
representation.fmt(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DisplayType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes the string representation of a type, which is the value displayed either as
|
||||
/// `Literal[<repr>]` or `Literal[<repr1>, <repr2>]` for literal types or as `<repr>` for
|
||||
/// non literals
|
||||
struct DisplayRepresentation<'db> {
|
||||
ty: Type<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayRepresentation<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.ty {
|
||||
Type::Any => f.write_str("Any"),
|
||||
Type::Never => f.write_str("Never"),
|
||||
@@ -29,25 +71,39 @@ impl Display for DisplayType<'_> {
|
||||
write!(f, "<module '{:?}'>", file.path(self.db))
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::Class(class) => write!(f, "Literal[{}]", class.name(self.db)),
|
||||
Type::Instance(class) => f.write_str(&class.name(self.db)),
|
||||
Type::Function(function) => write!(f, "Literal[{}]", function.name(self.db)),
|
||||
Type::Class(class) => f.write_str(class.name(self.db)),
|
||||
Type::Instance(class) => f.write_str(class.name(self.db)),
|
||||
Type::Function(function) | Type::RevealTypeFunction(function) => {
|
||||
f.write_str(function.name(self.db))
|
||||
}
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
Type::IntLiteral(n) => write!(f, "Literal[{n}]"),
|
||||
Type::BooleanLiteral(boolean) => {
|
||||
write!(f, "Literal[{}]", if *boolean { "True" } else { "False" })
|
||||
Type::IntLiteral(n) => n.fmt(f),
|
||||
Type::BooleanLiteral(boolean) => f.write_str(if boolean { "True" } else { "False" }),
|
||||
Type::StringLiteral(string) => {
|
||||
write!(f, r#""{}""#, string.value(self.db).replace('"', r#"\""#))
|
||||
}
|
||||
Type::LiteralString => f.write_str("LiteralString"),
|
||||
Type::BytesLiteral(bytes) => {
|
||||
let escape =
|
||||
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
|
||||
|
||||
escape.bytes_repr().write(f)
|
||||
}
|
||||
Type::Tuple(tuple) => {
|
||||
f.write_str("tuple[")?;
|
||||
let elements = tuple.elements(self.db);
|
||||
if elements.is_empty() {
|
||||
f.write_str("()")?;
|
||||
} else {
|
||||
elements.display(self.db).fmt(f)?;
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DisplayType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> UnionType<'db> {
|
||||
fn display(&'db self, db: &'db dyn Db) -> DisplayUnionType<'db> {
|
||||
DisplayUnionType { db, ty: self }
|
||||
@@ -60,54 +116,90 @@ struct DisplayUnionType<'db> {
|
||||
}
|
||||
|
||||
impl Display for DisplayUnionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let union = self.ty;
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let elements = self.ty.elements(self.db);
|
||||
|
||||
let (int_literals, other_types): (Vec<Type>, Vec<Type>) = union
|
||||
.elements(self.db)
|
||||
.iter()
|
||||
.copied()
|
||||
.partition(|ty| matches!(ty, Type::IntLiteral(_)));
|
||||
// Group literal types by kind.
|
||||
let mut grouped_literals = FxHashMap::default();
|
||||
|
||||
let mut first = true;
|
||||
if !int_literals.is_empty() {
|
||||
f.write_str("Literal[")?;
|
||||
let mut nums: Vec<_> = int_literals
|
||||
.into_iter()
|
||||
.filter_map(|ty| {
|
||||
if let Type::IntLiteral(n) = ty {
|
||||
Some(n)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
nums.sort_unstable();
|
||||
for num in nums {
|
||||
if !first {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
write!(f, "{num}")?;
|
||||
first = false;
|
||||
for element in elements {
|
||||
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
|
||||
grouped_literals
|
||||
.entry(literal_kind)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(*element);
|
||||
}
|
||||
f.write_str("]")?;
|
||||
}
|
||||
|
||||
for ty in other_types {
|
||||
if !first {
|
||||
f.write_str(" | ")?;
|
||||
};
|
||||
first = false;
|
||||
write!(f, "{}", ty.display(self.db))?;
|
||||
let mut join = f.join(" | ");
|
||||
|
||||
for element in elements {
|
||||
if let Ok(literal_kind) = LiteralTypeKind::try_from(*element) {
|
||||
let Some(mut literals) = grouped_literals.remove(&literal_kind) else {
|
||||
continue;
|
||||
};
|
||||
if literal_kind == LiteralTypeKind::IntLiteral {
|
||||
literals.sort_unstable_by_key(|ty| ty.expect_int_literal());
|
||||
}
|
||||
join.entry(&DisplayLiteralGroup {
|
||||
literals,
|
||||
db: self.db,
|
||||
});
|
||||
} else {
|
||||
join.entry(&element.display(self.db));
|
||||
}
|
||||
}
|
||||
|
||||
join.finish()?;
|
||||
|
||||
debug_assert!(grouped_literals.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DisplayUnionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
impl fmt::Debug for DisplayUnionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayLiteralGroup<'db> {
|
||||
literals: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl Display for DisplayLiteralGroup<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("Literal[")?;
|
||||
f.join(", ")
|
||||
.entries(self.literals.iter().map(|ty| ty.representation(self.db)))
|
||||
.finish()?;
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
enum LiteralTypeKind {
|
||||
Class,
|
||||
Function,
|
||||
IntLiteral,
|
||||
StringLiteral,
|
||||
BytesLiteral,
|
||||
}
|
||||
|
||||
impl TryFrom<Type<'_>> for LiteralTypeKind {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: Type<'_>) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
Type::Class(_) => Ok(Self::Class),
|
||||
Type::Function(_) | Type::RevealTypeFunction(_) => Ok(Self::Function),
|
||||
Type::IntLiteral(_) => Ok(Self::IntLiteral),
|
||||
Type::StringLiteral(_) => Ok(Self::StringLiteral),
|
||||
Type::BytesLiteral(_) => Ok(Self::BytesLiteral),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,30 +215,159 @@ struct DisplayIntersectionType<'db> {
|
||||
}
|
||||
|
||||
impl Display for DisplayIntersectionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut first = true;
|
||||
for (neg, ty) in self
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let tys = self
|
||||
.ty
|
||||
.positive(self.db)
|
||||
.iter()
|
||||
.map(|ty| (false, ty))
|
||||
.chain(self.ty.negative(self.db).iter().map(|ty| (true, ty)))
|
||||
{
|
||||
if !first {
|
||||
f.write_str(" & ")?;
|
||||
};
|
||||
first = false;
|
||||
if neg {
|
||||
f.write_str("~")?;
|
||||
};
|
||||
write!(f, "{}", ty.display(self.db))?;
|
||||
}
|
||||
Ok(())
|
||||
.map(|&ty| DisplayMaybeNegatedType {
|
||||
ty,
|
||||
db: self.db,
|
||||
negated: false,
|
||||
})
|
||||
.chain(
|
||||
self.ty
|
||||
.negative(self.db)
|
||||
.iter()
|
||||
.map(|&ty| DisplayMaybeNegatedType {
|
||||
ty,
|
||||
db: self.db,
|
||||
negated: true,
|
||||
}),
|
||||
);
|
||||
f.join(" & ").entries(tys).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for DisplayIntersectionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(self, f)
|
||||
impl fmt::Debug for DisplayIntersectionType<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
struct DisplayMaybeNegatedType<'db> {
|
||||
ty: Type<'db>,
|
||||
db: &'db dyn Db,
|
||||
negated: bool,
|
||||
}
|
||||
|
||||
impl<'db> Display for DisplayMaybeNegatedType<'db> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
if self.negated {
|
||||
f.write_str("~")?;
|
||||
}
|
||||
self.ty.display(self.db).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait TypeArrayDisplay<'db> {
|
||||
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray;
|
||||
}
|
||||
|
||||
impl<'db> TypeArrayDisplay<'db> for Box<[Type<'db>]> {
|
||||
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
|
||||
DisplayTypeArray { types: self, db }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> TypeArrayDisplay<'db> for Vec<Type<'db>> {
|
||||
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
|
||||
DisplayTypeArray { types: self, db }
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DisplayTypeArray<'b, 'db> {
|
||||
types: &'b [Type<'db>],
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl<'db> Display for DisplayTypeArray<'_, 'db> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.join(", ")
|
||||
.entries(self.types.iter().map(|ty| ty.display(self.db)))
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::types::{global_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType};
|
||||
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_condense_literal_display_by_type() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/main.py",
|
||||
"
|
||||
def foo(x: int) -> int:
|
||||
return x + 1
|
||||
|
||||
def bar(s: str) -> str:
|
||||
return s
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
",
|
||||
)?;
|
||||
let mod_file = system_path_to_file(&db, "src/main.py").expect("Expected file to exist.");
|
||||
|
||||
let union_elements = &[
|
||||
Type::Unknown,
|
||||
Type::IntLiteral(-1),
|
||||
global_symbol_ty(&db, mod_file, "A"),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, Box::from("A"))),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([0]))),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([7]))),
|
||||
Type::IntLiteral(0),
|
||||
Type::IntLiteral(1),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, Box::from("B"))),
|
||||
global_symbol_ty(&db, mod_file, "foo"),
|
||||
global_symbol_ty(&db, mod_file, "bar"),
|
||||
global_symbol_ty(&db, mod_file, "B"),
|
||||
Type::BooleanLiteral(true),
|
||||
Type::None,
|
||||
];
|
||||
let union = UnionType::from_elements(&db, union_elements).expect_union();
|
||||
let display = format!("{}", union.display(&db));
|
||||
assert_eq!(
|
||||
display,
|
||||
concat!(
|
||||
"Unknown | ",
|
||||
"Literal[-1, 0, 1] | ",
|
||||
"Literal[A, B] | ",
|
||||
"Literal[\"A\", \"B\"] | ",
|
||||
"Literal[b\"\\x00\", b\"\\x07\"] | ",
|
||||
"Literal[foo, bar] | ",
|
||||
"Literal[True] | ",
|
||||
"None"
|
||||
)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
186
crates/red_knot_python_semantic/src/types/narrow.rs
Normal file
186
crates/red_knot_python_semantic/src/types/narrow.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::constraint::{Constraint, PatternConstraint};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
|
||||
use crate::semantic_index::symbol_table;
|
||||
use crate::types::{infer_expression_types, IntersectionBuilder, Type};
|
||||
use crate::Db;
|
||||
use ruff_python_ast as ast;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Return the type constraint that `test` (if true) would place on `definition`, if any.
|
||||
///
|
||||
/// For example, if we have this code:
|
||||
///
|
||||
/// ```python
|
||||
/// y = 1 if flag else None
|
||||
/// x = 1 if flag else None
|
||||
/// if x is not None:
|
||||
/// ...
|
||||
/// ```
|
||||
///
|
||||
/// The `test` expression `x is not None` places the constraint "not None" on the definition of
|
||||
/// `x`, so in that case we'd return `Some(Type::Intersection(negative=[Type::None]))`.
|
||||
///
|
||||
/// But if we called this with the same `test` expression, but the `definition` of `y`, no
|
||||
/// constraint is applied to that definition, so we'd just return `None`.
|
||||
pub(crate) fn narrowing_constraint<'db>(
|
||||
db: &'db dyn Db,
|
||||
constraint: Constraint<'db>,
|
||||
definition: Definition<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
match constraint {
|
||||
Constraint::Expression(expression) => {
|
||||
all_narrowing_constraints_for_expression(db, expression)
|
||||
.get(&definition.symbol(db))
|
||||
.copied()
|
||||
}
|
||||
Constraint::Pattern(pattern) => all_narrowing_constraints_for_pattern(db, pattern)
|
||||
.get(&definition.symbol(db))
|
||||
.copied(),
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn all_narrowing_constraints_for_pattern<'db>(
|
||||
db: &'db dyn Db,
|
||||
pattern: PatternConstraint<'db>,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
NarrowingConstraintsBuilder::new(db, Constraint::Pattern(pattern)).finish()
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn all_narrowing_constraints_for_expression<'db>(
|
||||
db: &'db dyn Db,
|
||||
expression: Expression<'db>,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
NarrowingConstraintsBuilder::new(db, Constraint::Expression(expression)).finish()
|
||||
}
|
||||
|
||||
type NarrowingConstraints<'db> = FxHashMap<ScopedSymbolId, Type<'db>>;
|
||||
|
||||
struct NarrowingConstraintsBuilder<'db> {
|
||||
db: &'db dyn Db,
|
||||
constraint: Constraint<'db>,
|
||||
constraints: NarrowingConstraints<'db>,
|
||||
}
|
||||
|
||||
impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
fn new(db: &'db dyn Db, constraint: Constraint<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
constraint,
|
||||
constraints: NarrowingConstraints::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> NarrowingConstraints<'db> {
|
||||
match self.constraint {
|
||||
Constraint::Expression(expression) => self.evaluate_expression_constraint(expression),
|
||||
Constraint::Pattern(pattern) => self.evaluate_pattern_constraint(pattern),
|
||||
}
|
||||
|
||||
self.constraints.shrink_to_fit();
|
||||
self.constraints
|
||||
}
|
||||
|
||||
fn evaluate_expression_constraint(&mut self, expression: Expression<'db>) {
|
||||
if let ast::Expr::Compare(expr_compare) = expression.node_ref(self.db).node() {
|
||||
self.add_expr_compare(expr_compare, expression);
|
||||
}
|
||||
// TODO other test expression kinds
|
||||
}
|
||||
|
||||
fn evaluate_pattern_constraint(&mut self, pattern: PatternConstraint<'db>) {
|
||||
let subject = pattern.subject(self.db);
|
||||
|
||||
match pattern.pattern(self.db).node() {
|
||||
ast::Pattern::MatchValue(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton_pattern) => {
|
||||
self.add_match_pattern_singleton(subject, singleton_pattern);
|
||||
}
|
||||
ast::Pattern::MatchSequence(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchMapping(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchClass(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchStar(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchAs(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchOr(_) => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn symbols(&self) -> Arc<SymbolTable> {
|
||||
symbol_table(self.db, self.scope())
|
||||
}
|
||||
|
||||
fn scope(&self) -> ScopeId<'db> {
|
||||
match self.constraint {
|
||||
Constraint::Expression(expression) => expression.scope(self.db),
|
||||
Constraint::Pattern(pattern) => pattern.scope(self.db),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_expr_compare(&mut self, expr_compare: &ast::ExprCompare, expression: Expression<'db>) {
|
||||
let ast::ExprCompare {
|
||||
range: _,
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
} = expr_compare;
|
||||
|
||||
if let ast::Expr::Name(ast::ExprName {
|
||||
range: _,
|
||||
id,
|
||||
ctx: _,
|
||||
}) = left.as_ref()
|
||||
{
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
|
||||
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
|
||||
if matches!(op, ast::CmpOp::IsNot) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
};
|
||||
// TODO other comparison types
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_match_pattern_singleton(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
pattern: &ast::PatternMatchSingleton,
|
||||
) {
|
||||
if let Some(ast::ExprName { id, .. }) = subject.as_name_expr() {
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let ty = match pattern.value {
|
||||
ast::Singleton::None => Type::None,
|
||||
ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
};
|
||||
self.constraints.insert(symbol, ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
4ef2d66663fc080fefa379e6ae5fc45d4f8b54eb
|
||||
9e506eb5e8fc2823db8c60ad561b1145ff114947
|
||||
|
||||
@@ -41,7 +41,7 @@ _json: 3.0-
|
||||
_locale: 3.0-
|
||||
_lsprof: 3.0-
|
||||
_markupbase: 3.0-
|
||||
_msi: 3.0-
|
||||
_msi: 3.0-3.12
|
||||
_operator: 3.4-
|
||||
_osx_support: 3.0-
|
||||
_posixsubprocess: 3.2-
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import sys
|
||||
import typing_extensions
|
||||
from typing import Any, ClassVar, Generic, Literal, TypedDict, overload
|
||||
from typing_extensions import Unpack
|
||||
from typing_extensions import Self, Unpack
|
||||
|
||||
PyCF_ONLY_AST: Literal[1024]
|
||||
PyCF_TYPE_COMMENTS: Literal[4096]
|
||||
@@ -34,6 +34,9 @@ class AST:
|
||||
if sys.version_info >= (3, 13):
|
||||
_field_types: ClassVar[dict[str, Any]]
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self) -> Self: ...
|
||||
|
||||
class mod(AST): ...
|
||||
class type_ignore(AST): ...
|
||||
|
||||
@@ -44,6 +47,9 @@ class TypeIgnore(type_ignore):
|
||||
tag: str
|
||||
def __init__(self, lineno: int, tag: str) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, lineno: int = ..., tag: str = ...) -> Self: ...
|
||||
|
||||
class FunctionType(mod):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("argtypes", "returns")
|
||||
@@ -57,6 +63,9 @@ class FunctionType(mod):
|
||||
else:
|
||||
def __init__(self, argtypes: list[expr], returns: expr) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, argtypes: list[expr] = ..., returns: expr = ...) -> Self: ...
|
||||
|
||||
class Module(mod):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("body", "type_ignores")
|
||||
@@ -67,6 +76,9 @@ class Module(mod):
|
||||
else:
|
||||
def __init__(self, body: list[stmt], type_ignores: list[TypeIgnore]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, body: list[stmt] = ..., type_ignores: list[TypeIgnore] = ...) -> Self: ...
|
||||
|
||||
class Interactive(mod):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("body",)
|
||||
@@ -76,12 +88,18 @@ class Interactive(mod):
|
||||
else:
|
||||
def __init__(self, body: list[stmt]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, body: list[stmt] = ...) -> Self: ...
|
||||
|
||||
class Expression(mod):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("body",)
|
||||
body: expr
|
||||
def __init__(self, body: expr) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, body: expr = ...) -> Self: ...
|
||||
|
||||
class stmt(AST):
|
||||
lineno: int
|
||||
col_offset: int
|
||||
@@ -89,6 +107,9 @@ class stmt(AST):
|
||||
end_col_offset: int | None
|
||||
def __init__(self, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class FunctionDef(stmt):
|
||||
if sys.version_info >= (3, 12):
|
||||
__match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment", "type_params")
|
||||
@@ -152,6 +173,19 @@ class FunctionDef(stmt):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
name: _Identifier = ...,
|
||||
args: arguments = ...,
|
||||
body: list[stmt] = ...,
|
||||
decorator_list: list[expr] = ...,
|
||||
returns: expr | None = ...,
|
||||
type_comment: str | None = ...,
|
||||
type_params: list[type_param] = ...,
|
||||
) -> Self: ...
|
||||
|
||||
class AsyncFunctionDef(stmt):
|
||||
if sys.version_info >= (3, 12):
|
||||
__match_args__ = ("name", "args", "body", "decorator_list", "returns", "type_comment", "type_params")
|
||||
@@ -215,6 +249,19 @@ class AsyncFunctionDef(stmt):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
name: _Identifier = ...,
|
||||
args: arguments = ...,
|
||||
body: list[stmt],
|
||||
decorator_list: list[expr],
|
||||
returns: expr | None,
|
||||
type_comment: str | None,
|
||||
type_params: list[type_param],
|
||||
) -> Self: ...
|
||||
|
||||
class ClassDef(stmt):
|
||||
if sys.version_info >= (3, 12):
|
||||
__match_args__ = ("name", "bases", "keywords", "body", "decorator_list", "type_params")
|
||||
@@ -260,12 +307,28 @@ class ClassDef(stmt):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
name: _Identifier,
|
||||
bases: list[expr],
|
||||
keywords: list[keyword],
|
||||
body: list[stmt],
|
||||
decorator_list: list[expr],
|
||||
type_params: list[type_param],
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class Return(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value",)
|
||||
value: expr | None
|
||||
def __init__(self, value: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Delete(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("targets",)
|
||||
@@ -275,6 +338,9 @@ class Delete(stmt):
|
||||
else:
|
||||
def __init__(self, targets: list[expr], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, targets: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Assign(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("targets", "value", "type_comment")
|
||||
@@ -295,6 +361,11 @@ class Assign(stmt):
|
||||
self, targets: list[expr], value: expr, type_comment: str | None = None, **kwargs: Unpack[_Attributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, targets: list[expr] = ..., value: expr = ..., type_comment: str | None = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class AugAssign(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("target", "op", "value")
|
||||
@@ -305,6 +376,16 @@ class AugAssign(stmt):
|
||||
self, target: Name | Attribute | Subscript, op: operator, value: expr, **kwargs: Unpack[_Attributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
target: Name | Attribute | Subscript = ...,
|
||||
op: operator = ...,
|
||||
value: expr = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class AnnAssign(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("target", "annotation", "value", "simple")
|
||||
@@ -332,6 +413,17 @@ class AnnAssign(stmt):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
target: Name | Attribute | Subscript = ...,
|
||||
annotation: expr = ...,
|
||||
value: expr | None = ...,
|
||||
simple: int = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class For(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("target", "iter", "body", "orelse", "type_comment")
|
||||
@@ -361,6 +453,18 @@ class For(stmt):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
target: expr = ...,
|
||||
iter: expr = ...,
|
||||
body: list[stmt] = ...,
|
||||
orelse: list[stmt] = ...,
|
||||
type_comment: str | None = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class AsyncFor(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("target", "iter", "body", "orelse", "type_comment")
|
||||
@@ -390,6 +494,18 @@ class AsyncFor(stmt):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
target: expr = ...,
|
||||
iter: expr = ...,
|
||||
body: list[stmt] = ...,
|
||||
orelse: list[stmt] = ...,
|
||||
type_comment: str | None = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class While(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("test", "body", "orelse")
|
||||
@@ -403,6 +519,9 @@ class While(stmt):
|
||||
else:
|
||||
def __init__(self, test: expr, body: list[stmt], orelse: list[stmt], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, test: expr, body: list[stmt], orelse: list[stmt], **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class If(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("test", "body", "orelse")
|
||||
@@ -416,6 +535,11 @@ class If(stmt):
|
||||
else:
|
||||
def __init__(self, test: expr, body: list[stmt], orelse: list[stmt], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, test: expr = ..., body: list[stmt] = ..., orelse: list[stmt] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class With(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("items", "body", "type_comment")
|
||||
@@ -435,6 +559,16 @@ class With(stmt):
|
||||
self, items: list[withitem], body: list[stmt], type_comment: str | None = None, **kwargs: Unpack[_Attributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
items: list[withitem] = ...,
|
||||
body: list[stmt] = ...,
|
||||
type_comment: str | None = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class AsyncWith(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("items", "body", "type_comment")
|
||||
@@ -454,6 +588,16 @@ class AsyncWith(stmt):
|
||||
self, items: list[withitem], body: list[stmt], type_comment: str | None = None, **kwargs: Unpack[_Attributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
items: list[withitem] = ...,
|
||||
body: list[stmt] = ...,
|
||||
type_comment: str | None = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class Raise(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("exc", "cause")
|
||||
@@ -461,6 +605,9 @@ class Raise(stmt):
|
||||
cause: expr | None
|
||||
def __init__(self, exc: expr | None = None, cause: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, exc: expr | None = ..., cause: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Try(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("body", "handlers", "orelse", "finalbody")
|
||||
@@ -487,6 +634,17 @@ class Try(stmt):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
body: list[stmt] = ...,
|
||||
handlers: list[ExceptHandler] = ...,
|
||||
orelse: list[stmt] = ...,
|
||||
finalbody: list[stmt] = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
class TryStar(stmt):
|
||||
__match_args__ = ("body", "handlers", "orelse", "finalbody")
|
||||
@@ -513,6 +671,17 @@ if sys.version_info >= (3, 11):
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
body: list[stmt] = ...,
|
||||
handlers: list[ExceptHandler] = ...,
|
||||
orelse: list[stmt] = ...,
|
||||
finalbody: list[stmt] = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class Assert(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("test", "msg")
|
||||
@@ -520,6 +689,9 @@ class Assert(stmt):
|
||||
msg: expr | None
|
||||
def __init__(self, test: expr, msg: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, test: expr, msg: expr | None, **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Import(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("names",)
|
||||
@@ -529,6 +701,9 @@ class Import(stmt):
|
||||
else:
|
||||
def __init__(self, names: list[alias], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, names: list[alias] = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class ImportFrom(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("module", "names", "level")
|
||||
@@ -550,6 +725,11 @@ class ImportFrom(stmt):
|
||||
self, module: str | None = None, *, names: list[alias], level: int, **kwargs: Unpack[_Attributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, module: str | None = ..., names: list[alias] = ..., level: int = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class Global(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("names",)
|
||||
@@ -559,6 +739,9 @@ class Global(stmt):
|
||||
else:
|
||||
def __init__(self, names: list[_Identifier], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, names: list[_Identifier], **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Nonlocal(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("names",)
|
||||
@@ -568,12 +751,18 @@ class Nonlocal(stmt):
|
||||
else:
|
||||
def __init__(self, names: list[_Identifier], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, names: list[_Identifier] = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Expr(stmt):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value",)
|
||||
value: expr
|
||||
def __init__(self, value: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Pass(stmt): ...
|
||||
class Break(stmt): ...
|
||||
class Continue(stmt): ...
|
||||
@@ -585,6 +774,9 @@ class expr(AST):
|
||||
end_col_offset: int | None
|
||||
def __init__(self, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class BoolOp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("op", "values")
|
||||
@@ -595,6 +787,9 @@ class BoolOp(expr):
|
||||
else:
|
||||
def __init__(self, op: boolop, values: list[expr], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, op: boolop = ..., values: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class BinOp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("left", "op", "right")
|
||||
@@ -603,6 +798,11 @@ class BinOp(expr):
|
||||
right: expr
|
||||
def __init__(self, left: expr, op: operator, right: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, left: expr = ..., op: operator = ..., right: expr = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class UnaryOp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("op", "operand")
|
||||
@@ -610,6 +810,9 @@ class UnaryOp(expr):
|
||||
operand: expr
|
||||
def __init__(self, op: unaryop, operand: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, op: unaryop = ..., operand: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Lambda(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("args", "body")
|
||||
@@ -617,6 +820,9 @@ class Lambda(expr):
|
||||
body: expr
|
||||
def __init__(self, args: arguments, body: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, args: arguments = ..., body: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class IfExp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("test", "body", "orelse")
|
||||
@@ -625,6 +831,11 @@ class IfExp(expr):
|
||||
orelse: expr
|
||||
def __init__(self, test: expr, body: expr, orelse: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, test: expr = ..., body: expr = ..., orelse: expr = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class Dict(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("keys", "values")
|
||||
@@ -635,6 +846,11 @@ class Dict(expr):
|
||||
else:
|
||||
def __init__(self, keys: list[expr | None], values: list[expr], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, keys: list[expr | None] = ..., values: list[expr] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class Set(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("elts",)
|
||||
@@ -644,6 +860,9 @@ class Set(expr):
|
||||
else:
|
||||
def __init__(self, elts: list[expr], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, elts: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class ListComp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("elt", "generators")
|
||||
@@ -654,6 +873,11 @@ class ListComp(expr):
|
||||
else:
|
||||
def __init__(self, elt: expr, generators: list[comprehension], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, elt: expr = ..., generators: list[comprehension] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class SetComp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("elt", "generators")
|
||||
@@ -664,6 +888,11 @@ class SetComp(expr):
|
||||
else:
|
||||
def __init__(self, elt: expr, generators: list[comprehension], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, elt: expr = ..., generators: list[comprehension] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class DictComp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("key", "value", "generators")
|
||||
@@ -677,6 +906,11 @@ class DictComp(expr):
|
||||
else:
|
||||
def __init__(self, key: expr, value: expr, generators: list[comprehension], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, key: expr = ..., value: expr = ..., generators: list[comprehension] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class GeneratorExp(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("elt", "generators")
|
||||
@@ -687,24 +921,38 @@ class GeneratorExp(expr):
|
||||
else:
|
||||
def __init__(self, elt: expr, generators: list[comprehension], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, elt: expr = ..., generators: list[comprehension] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class Await(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value",)
|
||||
value: expr
|
||||
def __init__(self, value: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Yield(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value",)
|
||||
value: expr | None
|
||||
def __init__(self, value: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: expr | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class YieldFrom(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value",)
|
||||
value: expr
|
||||
def __init__(self, value: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Compare(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("left", "ops", "comparators")
|
||||
@@ -718,6 +966,11 @@ class Compare(expr):
|
||||
else:
|
||||
def __init__(self, left: expr, ops: list[cmpop], comparators: list[expr], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, left: expr = ..., ops: list[cmpop] = ..., comparators: list[expr] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class Call(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("func", "args", "keywords")
|
||||
@@ -731,6 +984,11 @@ class Call(expr):
|
||||
else:
|
||||
def __init__(self, func: expr, args: list[expr], keywords: list[keyword], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, func: expr = ..., args: list[expr] = ..., keywords: list[keyword] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class FormattedValue(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value", "conversion", "format_spec")
|
||||
@@ -739,6 +997,11 @@ class FormattedValue(expr):
|
||||
format_spec: expr | None
|
||||
def __init__(self, value: expr, conversion: int, format_spec: expr | None = None, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, value: expr = ..., conversion: int = ..., format_spec: expr | None = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class JoinedStr(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("values",)
|
||||
@@ -748,16 +1011,24 @@ class JoinedStr(expr):
|
||||
else:
|
||||
def __init__(self, values: list[expr], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, values: list[expr] = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Constant(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value", "kind")
|
||||
value: Any # None, str, bytes, bool, int, float, complex, Ellipsis
|
||||
kind: str | None
|
||||
# Aliases for value, for backwards compatibility
|
||||
s: Any
|
||||
n: int | float | complex
|
||||
if sys.version_info < (3, 14):
|
||||
# Aliases for value, for backwards compatibility
|
||||
s: Any
|
||||
n: int | float | complex
|
||||
|
||||
def __init__(self, value: Any, kind: str | None = None, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: Any = ..., kind: str | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class NamedExpr(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("target", "value")
|
||||
@@ -765,6 +1036,9 @@ class NamedExpr(expr):
|
||||
value: expr
|
||||
def __init__(self, target: Name, value: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, target: Name = ..., value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Attribute(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value", "attr", "ctx")
|
||||
@@ -773,6 +1047,11 @@ class Attribute(expr):
|
||||
ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__`
|
||||
def __init__(self, value: expr, attr: _Identifier, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, value: expr = ..., attr: _Identifier = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
_Slice: typing_extensions.TypeAlias = expr
|
||||
_SliceAttributes: typing_extensions.TypeAlias = _Attributes
|
||||
@@ -792,6 +1071,16 @@ class Slice(_Slice):
|
||||
self, lower: expr | None = None, upper: expr | None = None, step: expr | None = None, **kwargs: Unpack[_SliceAttributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
lower: expr | None = ...,
|
||||
upper: expr | None = ...,
|
||||
step: expr | None = ...,
|
||||
**kwargs: Unpack[_SliceAttributes],
|
||||
) -> Self: ...
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
class ExtSlice(slice):
|
||||
dims: list[slice]
|
||||
@@ -809,6 +1098,11 @@ class Subscript(expr):
|
||||
ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__`
|
||||
def __init__(self, value: expr, slice: _Slice, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, value: expr = ..., slice: _Slice = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class Starred(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("value", "ctx")
|
||||
@@ -816,6 +1110,9 @@ class Starred(expr):
|
||||
ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__`
|
||||
def __init__(self, value: expr, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: expr = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Name(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("id", "ctx")
|
||||
@@ -823,6 +1120,9 @@ class Name(expr):
|
||||
ctx: expr_context # Not present in Python < 3.13 if not passed to `__init__`
|
||||
def __init__(self, id: _Identifier, ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, id: _Identifier = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class List(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("elts", "ctx")
|
||||
@@ -833,6 +1133,9 @@ class List(expr):
|
||||
else:
|
||||
def __init__(self, elts: list[expr], ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, elts: list[expr] = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class Tuple(expr):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("elts", "ctx")
|
||||
@@ -845,6 +1148,9 @@ class Tuple(expr):
|
||||
else:
|
||||
def __init__(self, elts: list[expr], ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, elts: list[expr] = ..., ctx: expr_context = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class expr_context(AST): ...
|
||||
|
||||
if sys.version_info < (3, 9):
|
||||
@@ -908,6 +1214,9 @@ class comprehension(AST):
|
||||
else:
|
||||
def __init__(self, target: expr, iter: expr, ifs: list[expr], is_async: int) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, target: expr = ..., iter: expr = ..., ifs: list[expr] = ..., is_async: int = ...) -> Self: ...
|
||||
|
||||
class excepthandler(AST):
|
||||
lineno: int
|
||||
col_offset: int
|
||||
@@ -915,6 +1224,11 @@ class excepthandler(AST):
|
||||
end_col_offset: int | None
|
||||
def __init__(self, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, lineno: int = ..., col_offset: int = ..., end_lineno: int | None = ..., end_col_offset: int | None = ...
|
||||
) -> Self: ...
|
||||
|
||||
class ExceptHandler(excepthandler):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("type", "name", "body")
|
||||
@@ -935,6 +1249,16 @@ class ExceptHandler(excepthandler):
|
||||
self, type: expr | None = None, name: _Identifier | None = None, *, body: list[stmt], **kwargs: Unpack[_Attributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
type: expr | None = ...,
|
||||
name: _Identifier | None = ...,
|
||||
body: list[stmt] = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class arguments(AST):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("posonlyargs", "args", "vararg", "kwonlyargs", "kw_defaults", "kwarg", "defaults")
|
||||
@@ -993,6 +1317,19 @@ class arguments(AST):
|
||||
defaults: list[expr],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
posonlyargs: list[arg] = ...,
|
||||
args: list[arg] = ...,
|
||||
vararg: arg | None = ...,
|
||||
kwonlyargs: list[arg] = ...,
|
||||
kw_defaults: list[expr | None] = ...,
|
||||
kwarg: arg | None = ...,
|
||||
defaults: list[expr] = ...,
|
||||
) -> Self: ...
|
||||
|
||||
class arg(AST):
|
||||
lineno: int
|
||||
col_offset: int
|
||||
@@ -1007,6 +1344,16 @@ class arg(AST):
|
||||
self, arg: _Identifier, annotation: expr | None = None, type_comment: str | None = None, **kwargs: Unpack[_Attributes]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
arg: _Identifier = ...,
|
||||
annotation: expr | None = ...,
|
||||
type_comment: str | None = ...,
|
||||
**kwargs: Unpack[_Attributes],
|
||||
) -> Self: ...
|
||||
|
||||
class keyword(AST):
|
||||
lineno: int
|
||||
col_offset: int
|
||||
@@ -1021,6 +1368,9 @@ class keyword(AST):
|
||||
@overload
|
||||
def __init__(self, arg: _Identifier | None = None, *, value: expr, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, arg: _Identifier | None = ..., value: expr = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class alias(AST):
|
||||
lineno: int
|
||||
col_offset: int
|
||||
@@ -1032,6 +1382,9 @@ class alias(AST):
|
||||
asname: _Identifier | None
|
||||
def __init__(self, name: str, asname: _Identifier | None = None, **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, name: str = ..., asname: _Identifier | None = ..., **kwargs: Unpack[_Attributes]) -> Self: ...
|
||||
|
||||
class withitem(AST):
|
||||
if sys.version_info >= (3, 10):
|
||||
__match_args__ = ("context_expr", "optional_vars")
|
||||
@@ -1039,6 +1392,9 @@ class withitem(AST):
|
||||
optional_vars: expr | None
|
||||
def __init__(self, context_expr: expr, optional_vars: expr | None = None) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, context_expr: expr = ..., optional_vars: expr | None = ...) -> Self: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
class Match(stmt):
|
||||
__match_args__ = ("subject", "cases")
|
||||
@@ -1049,6 +1405,11 @@ if sys.version_info >= (3, 10):
|
||||
else:
|
||||
def __init__(self, subject: expr, cases: list[match_case], **kwargs: Unpack[_Attributes]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, subject: expr = ..., cases: list[match_case] = ..., **kwargs: Unpack[_Attributes]
|
||||
) -> Self: ...
|
||||
|
||||
class pattern(AST):
|
||||
lineno: int
|
||||
col_offset: int
|
||||
@@ -1056,6 +1417,11 @@ if sys.version_info >= (3, 10):
|
||||
end_col_offset: int
|
||||
def __init__(self, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, lineno: int = ..., col_offset: int = ..., end_lineno: int = ..., end_col_offset: int = ...
|
||||
) -> Self: ...
|
||||
|
||||
# Without the alias, Pyright complains variables named pattern are recursively defined
|
||||
_Pattern: typing_extensions.TypeAlias = pattern
|
||||
|
||||
@@ -1072,16 +1438,25 @@ if sys.version_info >= (3, 10):
|
||||
@overload
|
||||
def __init__(self, pattern: _Pattern, guard: expr | None = None, *, body: list[stmt]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, pattern: _Pattern = ..., guard: expr | None = ..., body: list[stmt] = ...) -> Self: ...
|
||||
|
||||
class MatchValue(pattern):
|
||||
__match_args__ = ("value",)
|
||||
value: expr
|
||||
def __init__(self, value: expr, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: expr = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ...
|
||||
|
||||
class MatchSingleton(pattern):
|
||||
__match_args__ = ("value",)
|
||||
value: Literal[True, False] | None
|
||||
def __init__(self, value: Literal[True, False] | None, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, value: Literal[True, False] | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ...
|
||||
|
||||
class MatchSequence(pattern):
|
||||
__match_args__ = ("patterns",)
|
||||
patterns: list[pattern]
|
||||
@@ -1090,11 +1465,17 @@ if sys.version_info >= (3, 10):
|
||||
else:
|
||||
def __init__(self, patterns: list[pattern], **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ...
|
||||
|
||||
class MatchStar(pattern):
|
||||
__match_args__ = ("name",)
|
||||
name: _Identifier | None
|
||||
def __init__(self, name: _Identifier | None, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, name: _Identifier | None = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ...
|
||||
|
||||
class MatchMapping(pattern):
|
||||
__match_args__ = ("keys", "patterns", "rest")
|
||||
keys: list[expr]
|
||||
@@ -1117,6 +1498,16 @@ if sys.version_info >= (3, 10):
|
||||
**kwargs: Unpack[_Attributes[int]],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
keys: list[expr] = ...,
|
||||
patterns: list[pattern] = ...,
|
||||
rest: _Identifier | None = ...,
|
||||
**kwargs: Unpack[_Attributes[int]],
|
||||
) -> Self: ...
|
||||
|
||||
class MatchClass(pattern):
|
||||
__match_args__ = ("cls", "patterns", "kwd_attrs", "kwd_patterns")
|
||||
cls: expr
|
||||
@@ -1142,6 +1533,17 @@ if sys.version_info >= (3, 10):
|
||||
**kwargs: Unpack[_Attributes[int]],
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
cls: expr = ...,
|
||||
patterns: list[pattern] = ...,
|
||||
kwd_attrs: list[_Identifier] = ...,
|
||||
kwd_patterns: list[pattern] = ...,
|
||||
**kwargs: Unpack[_Attributes[int]],
|
||||
) -> Self: ...
|
||||
|
||||
class MatchAs(pattern):
|
||||
__match_args__ = ("pattern", "name")
|
||||
pattern: _Pattern | None
|
||||
@@ -1150,6 +1552,11 @@ if sys.version_info >= (3, 10):
|
||||
self, pattern: _Pattern | None = None, name: _Identifier | None = None, **kwargs: Unpack[_Attributes[int]]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, pattern: _Pattern | None = ..., name: _Identifier | None = ..., **kwargs: Unpack[_Attributes[int]]
|
||||
) -> Self: ...
|
||||
|
||||
class MatchOr(pattern):
|
||||
__match_args__ = ("patterns",)
|
||||
patterns: list[pattern]
|
||||
@@ -1158,6 +1565,9 @@ if sys.version_info >= (3, 10):
|
||||
else:
|
||||
def __init__(self, patterns: list[pattern], **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, *, patterns: list[pattern] = ..., **kwargs: Unpack[_Attributes[int]]) -> Self: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class type_param(AST):
|
||||
lineno: int
|
||||
@@ -1166,6 +1576,9 @@ if sys.version_info >= (3, 12):
|
||||
end_col_offset: int
|
||||
def __init__(self, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(self, **kwargs: Unpack[_Attributes[int]]) -> Self: ...
|
||||
|
||||
class TypeVar(type_param):
|
||||
if sys.version_info >= (3, 13):
|
||||
__match_args__ = ("name", "bound", "default_value")
|
||||
@@ -1185,6 +1598,16 @@ if sys.version_info >= (3, 12):
|
||||
else:
|
||||
def __init__(self, name: _Identifier, bound: expr | None = None, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
name: _Identifier = ...,
|
||||
bound: expr | None = ...,
|
||||
default_value: expr | None = ...,
|
||||
**kwargs: Unpack[_Attributes[int]],
|
||||
) -> Self: ...
|
||||
|
||||
class ParamSpec(type_param):
|
||||
if sys.version_info >= (3, 13):
|
||||
__match_args__ = ("name", "default_value")
|
||||
@@ -1199,6 +1622,11 @@ if sys.version_info >= (3, 12):
|
||||
else:
|
||||
def __init__(self, name: _Identifier, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, name: _Identifier = ..., default_value: expr | None = ..., **kwargs: Unpack[_Attributes[int]]
|
||||
) -> Self: ...
|
||||
|
||||
class TypeVarTuple(type_param):
|
||||
if sys.version_info >= (3, 13):
|
||||
__match_args__ = ("name", "default_value")
|
||||
@@ -1213,6 +1641,11 @@ if sys.version_info >= (3, 12):
|
||||
else:
|
||||
def __init__(self, name: _Identifier, **kwargs: Unpack[_Attributes[int]]) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self, *, name: _Identifier = ..., default_value: expr | None = ..., **kwargs: Unpack[_Attributes[int]]
|
||||
) -> Self: ...
|
||||
|
||||
class TypeAlias(stmt):
|
||||
__match_args__ = ("name", "type_params", "value")
|
||||
name: Name
|
||||
@@ -1231,3 +1664,13 @@ if sys.version_info >= (3, 12):
|
||||
def __init__(
|
||||
self, name: Name, type_params: list[type_param], value: expr, **kwargs: Unpack[_Attributes[int]]
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def __replace__(
|
||||
self,
|
||||
*,
|
||||
name: Name = ...,
|
||||
type_params: list[type_param] = ...,
|
||||
value: expr = ...,
|
||||
**kwargs: Unpack[_Attributes[int]],
|
||||
) -> Self: ...
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import sys
|
||||
from abc import abstractmethod
|
||||
from types import MappingProxyType
|
||||
from typing import ( # noqa: Y022,Y038,Y057
|
||||
from typing import ( # noqa: Y022,Y038
|
||||
AbstractSet as Set,
|
||||
AsyncGenerator as AsyncGenerator,
|
||||
AsyncIterable as AsyncIterable,
|
||||
AsyncIterator as AsyncIterator,
|
||||
Awaitable as Awaitable,
|
||||
ByteString as ByteString,
|
||||
Callable as Callable,
|
||||
Collection as Collection,
|
||||
Container as Container,
|
||||
@@ -59,8 +58,12 @@ __all__ = [
|
||||
"ValuesView",
|
||||
"Sequence",
|
||||
"MutableSequence",
|
||||
"ByteString",
|
||||
]
|
||||
if sys.version_info < (3, 14):
|
||||
from typing import ByteString as ByteString # noqa: Y057
|
||||
|
||||
__all__ += ["ByteString"]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
__all__ += ["Buffer"]
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ class _CDataMeta(type):
|
||||
# By default mypy complains about the following two methods, because strictly speaking cls
|
||||
# might not be a Type[_CT]. However this can never actually happen, because the only class that
|
||||
# uses _CDataMeta as its metaclass is _CData. So it's safe to ignore the errors here.
|
||||
def __mul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc]
|
||||
def __rmul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc]
|
||||
def __mul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
|
||||
def __rmul__(cls: type[_CT], other: int) -> type[Array[_CT]]: ... # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
|
||||
|
||||
class _CData(metaclass=_CDataMeta):
|
||||
_b_base_: int
|
||||
|
||||
@@ -493,7 +493,7 @@ class _CursesWindow:
|
||||
def instr(self, y: int, x: int, n: int = ...) -> bytes: ...
|
||||
def is_linetouched(self, line: int, /) -> bool: ...
|
||||
def is_wintouched(self) -> bool: ...
|
||||
def keypad(self, yes: bool) -> None: ...
|
||||
def keypad(self, yes: bool, /) -> None: ...
|
||||
def leaveok(self, yes: bool) -> None: ...
|
||||
def move(self, new_y: int, new_x: int) -> None: ...
|
||||
def mvderwin(self, y: int, x: int) -> None: ...
|
||||
|
||||
@@ -1,17 +1,38 @@
|
||||
import sys
|
||||
from _typeshed import StrPath
|
||||
from collections.abc import Mapping
|
||||
from typing import Final, Literal, TypedDict, type_check_only
|
||||
|
||||
LC_CTYPE: int
|
||||
LC_COLLATE: int
|
||||
LC_TIME: int
|
||||
LC_MONETARY: int
|
||||
LC_NUMERIC: int
|
||||
LC_ALL: int
|
||||
CHAR_MAX: int
|
||||
@type_check_only
|
||||
class _LocaleConv(TypedDict):
|
||||
decimal_point: str
|
||||
grouping: list[int]
|
||||
thousands_sep: str
|
||||
int_curr_symbol: str
|
||||
currency_symbol: str
|
||||
p_cs_precedes: Literal[0, 1, 127]
|
||||
n_cs_precedes: Literal[0, 1, 127]
|
||||
p_sep_by_space: Literal[0, 1, 127]
|
||||
n_sep_by_space: Literal[0, 1, 127]
|
||||
mon_decimal_point: str
|
||||
frac_digits: int
|
||||
int_frac_digits: int
|
||||
mon_thousands_sep: str
|
||||
mon_grouping: list[int]
|
||||
positive_sign: str
|
||||
negative_sign: str
|
||||
p_sign_posn: Literal[0, 1, 2, 3, 4, 127]
|
||||
n_sign_posn: Literal[0, 1, 2, 3, 4, 127]
|
||||
|
||||
LC_CTYPE: Final[int]
|
||||
LC_COLLATE: Final[int]
|
||||
LC_TIME: Final[int]
|
||||
LC_MONETARY: Final[int]
|
||||
LC_NUMERIC: Final[int]
|
||||
LC_ALL: Final[int]
|
||||
CHAR_MAX: Final = 127
|
||||
|
||||
def setlocale(category: int, locale: str | None = None, /) -> str: ...
|
||||
def localeconv() -> Mapping[str, int | str | list[int]]: ...
|
||||
def localeconv() -> _LocaleConv: ...
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
def getencoding() -> str: ...
|
||||
@@ -25,67 +46,67 @@ def strxfrm(string: str, /) -> str: ...
|
||||
if sys.platform != "win32":
|
||||
LC_MESSAGES: int
|
||||
|
||||
ABDAY_1: int
|
||||
ABDAY_2: int
|
||||
ABDAY_3: int
|
||||
ABDAY_4: int
|
||||
ABDAY_5: int
|
||||
ABDAY_6: int
|
||||
ABDAY_7: int
|
||||
ABDAY_1: Final[int]
|
||||
ABDAY_2: Final[int]
|
||||
ABDAY_3: Final[int]
|
||||
ABDAY_4: Final[int]
|
||||
ABDAY_5: Final[int]
|
||||
ABDAY_6: Final[int]
|
||||
ABDAY_7: Final[int]
|
||||
|
||||
ABMON_1: int
|
||||
ABMON_2: int
|
||||
ABMON_3: int
|
||||
ABMON_4: int
|
||||
ABMON_5: int
|
||||
ABMON_6: int
|
||||
ABMON_7: int
|
||||
ABMON_8: int
|
||||
ABMON_9: int
|
||||
ABMON_10: int
|
||||
ABMON_11: int
|
||||
ABMON_12: int
|
||||
ABMON_1: Final[int]
|
||||
ABMON_2: Final[int]
|
||||
ABMON_3: Final[int]
|
||||
ABMON_4: Final[int]
|
||||
ABMON_5: Final[int]
|
||||
ABMON_6: Final[int]
|
||||
ABMON_7: Final[int]
|
||||
ABMON_8: Final[int]
|
||||
ABMON_9: Final[int]
|
||||
ABMON_10: Final[int]
|
||||
ABMON_11: Final[int]
|
||||
ABMON_12: Final[int]
|
||||
|
||||
DAY_1: int
|
||||
DAY_2: int
|
||||
DAY_3: int
|
||||
DAY_4: int
|
||||
DAY_5: int
|
||||
DAY_6: int
|
||||
DAY_7: int
|
||||
DAY_1: Final[int]
|
||||
DAY_2: Final[int]
|
||||
DAY_3: Final[int]
|
||||
DAY_4: Final[int]
|
||||
DAY_5: Final[int]
|
||||
DAY_6: Final[int]
|
||||
DAY_7: Final[int]
|
||||
|
||||
ERA: int
|
||||
ERA_D_T_FMT: int
|
||||
ERA_D_FMT: int
|
||||
ERA_T_FMT: int
|
||||
ERA: Final[int]
|
||||
ERA_D_T_FMT: Final[int]
|
||||
ERA_D_FMT: Final[int]
|
||||
ERA_T_FMT: Final[int]
|
||||
|
||||
MON_1: int
|
||||
MON_2: int
|
||||
MON_3: int
|
||||
MON_4: int
|
||||
MON_5: int
|
||||
MON_6: int
|
||||
MON_7: int
|
||||
MON_8: int
|
||||
MON_9: int
|
||||
MON_10: int
|
||||
MON_11: int
|
||||
MON_12: int
|
||||
MON_1: Final[int]
|
||||
MON_2: Final[int]
|
||||
MON_3: Final[int]
|
||||
MON_4: Final[int]
|
||||
MON_5: Final[int]
|
||||
MON_6: Final[int]
|
||||
MON_7: Final[int]
|
||||
MON_8: Final[int]
|
||||
MON_9: Final[int]
|
||||
MON_10: Final[int]
|
||||
MON_11: Final[int]
|
||||
MON_12: Final[int]
|
||||
|
||||
CODESET: int
|
||||
D_T_FMT: int
|
||||
D_FMT: int
|
||||
T_FMT: int
|
||||
T_FMT_AMPM: int
|
||||
AM_STR: int
|
||||
PM_STR: int
|
||||
CODESET: Final[int]
|
||||
D_T_FMT: Final[int]
|
||||
D_FMT: Final[int]
|
||||
T_FMT: Final[int]
|
||||
T_FMT_AMPM: Final[int]
|
||||
AM_STR: Final[int]
|
||||
PM_STR: Final[int]
|
||||
|
||||
RADIXCHAR: int
|
||||
THOUSEP: int
|
||||
YESEXPR: int
|
||||
NOEXPR: int
|
||||
CRNCYSTR: int
|
||||
ALT_DIGITS: int
|
||||
RADIXCHAR: Final[int]
|
||||
THOUSEP: Final[int]
|
||||
YESEXPR: Final[int]
|
||||
NOEXPR: Final[int]
|
||||
CRNCYSTR: Final[int]
|
||||
ALT_DIGITS: Final[int]
|
||||
|
||||
def nl_langinfo(key: int, /) -> str: ...
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
from _typeshed import SupportsGetItem
|
||||
from collections.abc import Callable, Container, Iterable, MutableMapping, MutableSequence, Sequence
|
||||
from typing import Any, AnyStr, Generic, Protocol, SupportsAbs, SupportsIndex, TypeVar, final, overload
|
||||
from typing_extensions import ParamSpec, TypeAlias, TypeVarTuple, Unpack
|
||||
from typing_extensions import ParamSpec, TypeAlias, TypeIs, TypeVarTuple, Unpack
|
||||
|
||||
_R = TypeVar("_R")
|
||||
_T = TypeVar("_T")
|
||||
@@ -145,3 +145,7 @@ if sys.version_info >= (3, 11):
|
||||
def call(obj: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs) -> _R: ...
|
||||
|
||||
def _compare_digest(a: AnyStr, b: AnyStr, /) -> bool: ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
def is_none(a: object, /) -> TypeIs[None]: ...
|
||||
def is_not_none(a: _T | None, /) -> TypeIs[_T]: ...
|
||||
|
||||
@@ -28,9 +28,9 @@ S_IFDIR: Final = 0o040000
|
||||
|
||||
# These are 0 on systems that don't support the specific kind of file.
|
||||
# Example: Linux doesn't support door files, so S_IFDOOR is 0 on linux.
|
||||
S_IFDOOR: int
|
||||
S_IFPORT: int
|
||||
S_IFWHT: int
|
||||
S_IFDOOR: Final[int]
|
||||
S_IFPORT: Final[int]
|
||||
S_IFWHT: Final[int]
|
||||
|
||||
S_ISUID: Final = 0o4000
|
||||
S_ISGID: Final = 0o2000
|
||||
@@ -79,9 +79,9 @@ def S_ISWHT(mode: int, /) -> bool: ...
|
||||
def filemode(mode: int, /) -> str: ...
|
||||
|
||||
if sys.platform == "win32":
|
||||
IO_REPARSE_TAG_SYMLINK: int
|
||||
IO_REPARSE_TAG_MOUNT_POINT: int
|
||||
IO_REPARSE_TAG_APPEXECLINK: int
|
||||
IO_REPARSE_TAG_SYMLINK: Final = 0xA000000C
|
||||
IO_REPARSE_TAG_MOUNT_POINT: Final = 0xA0000003
|
||||
IO_REPARSE_TAG_APPEXECLINK: Final = 0x8000001B
|
||||
|
||||
if sys.platform == "win32":
|
||||
FILE_ATTRIBUTE_ARCHIVE: Final = 32
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import signal
|
||||
import sys
|
||||
from _typeshed import structseq
|
||||
from collections.abc import Callable
|
||||
@@ -16,16 +17,39 @@ class LockType:
|
||||
def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: ...
|
||||
def release(self) -> None: ...
|
||||
def locked(self) -> bool: ...
|
||||
def acquire_lock(self, blocking: bool = True, timeout: float = -1) -> bool: ...
|
||||
def release_lock(self) -> None: ...
|
||||
def locked_lock(self) -> bool: ...
|
||||
def __enter__(self) -> bool: ...
|
||||
def __exit__(
|
||||
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
||||
) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
@final
|
||||
class _ThreadHandle:
|
||||
ident: int
|
||||
|
||||
def join(self, timeout: float | None = None, /) -> None: ...
|
||||
def is_done(self) -> bool: ...
|
||||
def _set_done(self) -> None: ...
|
||||
|
||||
def start_joinable_thread(
|
||||
function: Callable[[], object], handle: _ThreadHandle | None = None, daemon: bool = True
|
||||
) -> _ThreadHandle: ...
|
||||
lock = LockType
|
||||
|
||||
@overload
|
||||
def start_new_thread(function: Callable[[Unpack[_Ts]], object], args: tuple[Unpack[_Ts]], /) -> int: ...
|
||||
@overload
|
||||
def start_new_thread(function: Callable[..., object], args: tuple[Any, ...], kwargs: dict[str, Any], /) -> int: ...
|
||||
def interrupt_main() -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
def interrupt_main(signum: signal.Signals = ..., /) -> None: ...
|
||||
|
||||
else:
|
||||
def interrupt_main() -> None: ...
|
||||
|
||||
def exit() -> NoReturn: ...
|
||||
def allocate_lock() -> LockType: ...
|
||||
def get_ident() -> int: ...
|
||||
|
||||
@@ -106,8 +106,8 @@ EXCEPTION: Final = 8
|
||||
READABLE: Final = 2
|
||||
WRITABLE: Final = 4
|
||||
|
||||
TCL_VERSION: str
|
||||
TK_VERSION: str
|
||||
TCL_VERSION: Final[str]
|
||||
TK_VERSION: Final[str]
|
||||
|
||||
@final
|
||||
class TkttType:
|
||||
|
||||
@@ -99,6 +99,20 @@ if sys.platform == "win32":
|
||||
SEC_RESERVE: Final = 0x4000000
|
||||
SEC_WRITECOMBINE: Final = 0x40000000
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
STARTF_FORCEOFFFEEDBACK: Final = 0x80
|
||||
STARTF_FORCEONFEEDBACK: Final = 0x40
|
||||
STARTF_PREVENTPINNING: Final = 0x2000
|
||||
STARTF_RUNFULLSCREEN: Final = 0x20
|
||||
STARTF_TITLEISAPPID: Final = 0x1000
|
||||
STARTF_TITLEISLINKNAME: Final = 0x800
|
||||
STARTF_UNTRUSTEDSOURCE: Final = 0x8000
|
||||
STARTF_USECOUNTCHARS: Final = 0x8
|
||||
STARTF_USEFILLATTRIBUTE: Final = 0x10
|
||||
STARTF_USEHOTKEY: Final = 0x200
|
||||
STARTF_USEPOSITION: Final = 0x4
|
||||
STARTF_USESIZE: Final = 0x2
|
||||
|
||||
STARTF_USESHOWWINDOW: Final = 0x1
|
||||
STARTF_USESTDHANDLES: Final = 0x100
|
||||
|
||||
@@ -250,6 +264,20 @@ if sys.platform == "win32":
|
||||
def cancel(self) -> None: ...
|
||||
def getbuffer(self) -> bytes | None: ...
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
def BatchedWaitForMultipleObjects(
|
||||
handle_seq: Sequence[int], wait_all: bool, milliseconds: int = 0xFFFFFFFF
|
||||
) -> list[int]: ...
|
||||
def CreateEventW(security_attributes: int, manual_reset: bool, initial_state: bool, name: str | None) -> int: ...
|
||||
def CreateMutexW(security_attributes: int, initial_owner: bool, name: str) -> int: ...
|
||||
def GetLongPathName(path: str) -> str: ...
|
||||
def GetShortPathName(path: str) -> str: ...
|
||||
def OpenEventW(desired_access: int, inherit_handle: bool, name: str) -> int: ...
|
||||
def OpenMutexW(desired_access: int, inherit_handle: bool, name: str) -> int: ...
|
||||
def ReleaseMutex(mutex: int) -> None: ...
|
||||
def ResetEvent(event: int) -> None: ...
|
||||
def SetEvent(event: int) -> None: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
def CopyFile2(existing_file_name: str, new_file_name: str, flags: int, progress_routine: int | None = None) -> int: ...
|
||||
def NeedCurrentDirectoryForExePath(exe_name: str, /) -> bool: ...
|
||||
|
||||
@@ -357,7 +357,17 @@ class Action(_AttributeHolder):
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
class BooleanOptionalAction(Action):
|
||||
if sys.version_info >= (3, 13):
|
||||
if sys.version_info >= (3, 14):
|
||||
def __init__(
|
||||
self,
|
||||
option_strings: Sequence[str],
|
||||
dest: str,
|
||||
default: bool | None = None,
|
||||
required: bool = False,
|
||||
help: str | None = None,
|
||||
deprecated: bool = False,
|
||||
) -> None: ...
|
||||
elif sys.version_info >= (3, 13):
|
||||
@overload
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -10,27 +10,28 @@ class _ABC(type):
|
||||
if sys.version_info >= (3, 9):
|
||||
def __init__(cls, *args: Unused) -> None: ...
|
||||
|
||||
@deprecated("Replaced by ast.Constant; removal scheduled for Python 3.14")
|
||||
class Num(Constant, metaclass=_ABC):
|
||||
value: int | float | complex
|
||||
if sys.version_info < (3, 14):
|
||||
@deprecated("Replaced by ast.Constant; removed in Python 3.14")
|
||||
class Num(Constant, metaclass=_ABC):
|
||||
value: int | float | complex
|
||||
|
||||
@deprecated("Replaced by ast.Constant; removal scheduled for Python 3.14")
|
||||
class Str(Constant, metaclass=_ABC):
|
||||
value: str
|
||||
# Aliases for value, for backwards compatibility
|
||||
s: str
|
||||
@deprecated("Replaced by ast.Constant; removed in Python 3.14")
|
||||
class Str(Constant, metaclass=_ABC):
|
||||
value: str
|
||||
# Aliases for value, for backwards compatibility
|
||||
s: str
|
||||
|
||||
@deprecated("Replaced by ast.Constant; removal scheduled for Python 3.14")
|
||||
class Bytes(Constant, metaclass=_ABC):
|
||||
value: bytes
|
||||
# Aliases for value, for backwards compatibility
|
||||
s: bytes
|
||||
@deprecated("Replaced by ast.Constant; removed in Python 3.14")
|
||||
class Bytes(Constant, metaclass=_ABC):
|
||||
value: bytes
|
||||
# Aliases for value, for backwards compatibility
|
||||
s: bytes
|
||||
|
||||
@deprecated("Replaced by ast.Constant; removal scheduled for Python 3.14")
|
||||
class NameConstant(Constant, metaclass=_ABC): ...
|
||||
@deprecated("Replaced by ast.Constant; removed in Python 3.14")
|
||||
class NameConstant(Constant, metaclass=_ABC): ...
|
||||
|
||||
@deprecated("Replaced by ast.Constant; removal scheduled for Python 3.14")
|
||||
class Ellipsis(Constant, metaclass=_ABC): ...
|
||||
@deprecated("Replaced by ast.Constant; removed in Python 3.14")
|
||||
class Ellipsis(Constant, metaclass=_ABC): ...
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
class slice(AST): ...
|
||||
|
||||
@@ -151,13 +151,13 @@ if sys.version_info >= (3, 10):
|
||||
@overload
|
||||
def gather(*coros_or_futures: _FutureLike[_T], return_exceptions: Literal[False] = False) -> Future[list[_T]]: ... # type: ignore[overload-overlap]
|
||||
@overload
|
||||
def gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: bool) -> Future[tuple[_T1 | BaseException]]: ... # type: ignore[overload-overlap]
|
||||
def gather(coro_or_future1: _FutureLike[_T1], /, *, return_exceptions: bool) -> Future[tuple[_T1 | BaseException]]: ...
|
||||
@overload
|
||||
def gather( # type: ignore[overload-overlap]
|
||||
def gather(
|
||||
coro_or_future1: _FutureLike[_T1], coro_or_future2: _FutureLike[_T2], /, *, return_exceptions: bool
|
||||
) -> Future[tuple[_T1 | BaseException, _T2 | BaseException]]: ...
|
||||
@overload
|
||||
def gather( # type: ignore[overload-overlap]
|
||||
def gather(
|
||||
coro_or_future1: _FutureLike[_T1],
|
||||
coro_or_future2: _FutureLike[_T2],
|
||||
coro_or_future3: _FutureLike[_T3],
|
||||
@@ -166,7 +166,7 @@ if sys.version_info >= (3, 10):
|
||||
return_exceptions: bool,
|
||||
) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException]]: ...
|
||||
@overload
|
||||
def gather( # type: ignore[overload-overlap]
|
||||
def gather(
|
||||
coro_or_future1: _FutureLike[_T1],
|
||||
coro_or_future2: _FutureLike[_T2],
|
||||
coro_or_future3: _FutureLike[_T3],
|
||||
@@ -176,7 +176,7 @@ if sys.version_info >= (3, 10):
|
||||
return_exceptions: bool,
|
||||
) -> Future[tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException]]: ...
|
||||
@overload
|
||||
def gather( # type: ignore[overload-overlap]
|
||||
def gather(
|
||||
coro_or_future1: _FutureLike[_T1],
|
||||
coro_or_future2: _FutureLike[_T2],
|
||||
coro_or_future3: _FutureLike[_T3],
|
||||
@@ -189,7 +189,7 @@ if sys.version_info >= (3, 10):
|
||||
tuple[_T1 | BaseException, _T2 | BaseException, _T3 | BaseException, _T4 | BaseException, _T5 | BaseException]
|
||||
]: ...
|
||||
@overload
|
||||
def gather( # type: ignore[overload-overlap]
|
||||
def gather(
|
||||
coro_or_future1: _FutureLike[_T1],
|
||||
coro_or_future2: _FutureLike[_T2],
|
||||
coro_or_future3: _FutureLike[_T3],
|
||||
|
||||
@@ -159,7 +159,7 @@ if sys.platform != "win32":
|
||||
|
||||
class _UnixSelectorEventLoop(BaseSelectorEventLoop):
|
||||
if sys.version_info >= (3, 13):
|
||||
async def create_unix_server( # type: ignore[override]
|
||||
async def create_unix_server(
|
||||
self,
|
||||
protocol_factory: _ProtocolFactory,
|
||||
path: StrPath | None = None,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# ruff: noqa: PYI036 # This is the module declaring BaseException
|
||||
import _ast
|
||||
import _typeshed
|
||||
import sys
|
||||
@@ -33,7 +34,8 @@ from collections.abc import Awaitable, Callable, Iterable, Iterator, MutableSet,
|
||||
from io import BufferedRandom, BufferedReader, BufferedWriter, FileIO, TextIOWrapper
|
||||
from types import CellType, CodeType, TracebackType
|
||||
|
||||
# mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping} are imported from collections.abc in builtins.pyi
|
||||
# mypy crashes if any of {ByteString, Sequence, MutableSequence, Mapping, MutableMapping}
|
||||
# are imported from collections.abc in builtins.pyi
|
||||
from typing import ( # noqa: Y022
|
||||
IO,
|
||||
Any,
|
||||
@@ -967,7 +969,9 @@ class tuple(Sequence[_T_co]):
|
||||
if sys.version_info >= (3, 9):
|
||||
def __class_getitem__(cls, item: Any, /) -> GenericAlias: ...
|
||||
|
||||
# Doesn't exist at runtime, but deleting this breaks mypy. See #2999
|
||||
# Doesn't exist at runtime, but deleting this breaks mypy and pyright. See:
|
||||
# https://github.com/python/typeshed/issues/7580
|
||||
# https://github.com/python/mypy/issues/8240
|
||||
@final
|
||||
@type_check_only
|
||||
class function:
|
||||
@@ -1084,7 +1088,8 @@ class dict(MutableMapping[_KT, _VT]):
|
||||
def keys(self) -> dict_keys[_KT, _VT]: ...
|
||||
def values(self) -> dict_values[_KT, _VT]: ...
|
||||
def items(self) -> dict_items[_KT, _VT]: ...
|
||||
# Signature of `dict.fromkeys` should be kept identical to `fromkeys` methods of `OrderedDict`/`ChainMap`/`UserDict` in `collections`
|
||||
# Signature of `dict.fromkeys` should be kept identical to
|
||||
# `fromkeys` methods of `OrderedDict`/`ChainMap`/`UserDict` in `collections`
|
||||
# TODO: the true signature of `dict.fromkeys` is not expressible in the current type system.
|
||||
# See #3800 & https://github.com/python/typing/issues/548#issuecomment-683336963.
|
||||
@classmethod
|
||||
@@ -1744,7 +1749,7 @@ _SupportsSumNoDefaultT = TypeVar("_SupportsSumNoDefaultT", bound=_SupportsSumWit
|
||||
# without creating many false-positive errors (see #7578).
|
||||
# Instead, we special-case the most common examples of this: bool and literal integers.
|
||||
@overload
|
||||
def sum(iterable: Iterable[bool | _LiteralInteger], /, start: int = 0) -> int: ... # type: ignore[overload-overlap]
|
||||
def sum(iterable: Iterable[bool | _LiteralInteger], /, start: int = 0) -> int: ...
|
||||
@overload
|
||||
def sum(iterable: Iterable[_SupportsSumNoDefaultT], /) -> _SupportsSumNoDefaultT | Literal[0]: ...
|
||||
@overload
|
||||
@@ -1752,9 +1757,8 @@ def sum(iterable: Iterable[_AddableT1], /, start: _AddableT2) -> _AddableT1 | _A
|
||||
|
||||
# The argument to `vars()` has to have a `__dict__` attribute, so the second overload can't be annotated with `object`
|
||||
# (A "SupportsDunderDict" protocol doesn't work)
|
||||
# Use a type: ignore to make complaints about overlapping overloads go away
|
||||
@overload
|
||||
def vars(object: type, /) -> types.MappingProxyType[str, Any]: ... # type: ignore[overload-overlap]
|
||||
def vars(object: type, /) -> types.MappingProxyType[str, Any]: ...
|
||||
@overload
|
||||
def vars(object: Any = ..., /) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from collections.abc import Callable
|
||||
from typing import IO, Any, Final
|
||||
from typing_extensions import LiteralString
|
||||
|
||||
__all__ = ["Cmd"]
|
||||
|
||||
PROMPT: Final = "(Cmd) "
|
||||
IDENTCHARS: str # Too big to be `Literal`
|
||||
IDENTCHARS: Final[LiteralString] # Too big to be `Literal`
|
||||
|
||||
class Cmd:
|
||||
prompt: str
|
||||
|
||||
@@ -80,7 +80,7 @@ class _Encoder(Protocol):
|
||||
def __call__(self, input: str, errors: str = ..., /) -> tuple[bytes, int]: ... # signature of Codec().encode
|
||||
|
||||
class _Decoder(Protocol):
|
||||
def __call__(self, input: bytes, errors: str = ..., /) -> tuple[str, int]: ... # signature of Codec().decode
|
||||
def __call__(self, input: ReadableBuffer, errors: str = ..., /) -> tuple[str, int]: ... # signature of Codec().decode
|
||||
|
||||
class _StreamReader(Protocol):
|
||||
def __call__(self, stream: _ReadableStream, errors: str = ..., /) -> StreamReader: ...
|
||||
|
||||
@@ -345,15 +345,15 @@ class _OrderedDictValuesView(ValuesView[_VT_co], Reversible[_VT_co]):
|
||||
# but they are not exposed anywhere)
|
||||
# pyright doesn't have a specific error code for subclassing error!
|
||||
@final
|
||||
class _odict_keys(dict_keys[_KT_co, _VT_co], Reversible[_KT_co]): # type: ignore[misc] # pyright: ignore
|
||||
class _odict_keys(dict_keys[_KT_co, _VT_co], Reversible[_KT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
|
||||
def __reversed__(self) -> Iterator[_KT_co]: ...
|
||||
|
||||
@final
|
||||
class _odict_items(dict_items[_KT_co, _VT_co], Reversible[tuple[_KT_co, _VT_co]]): # type: ignore[misc] # pyright: ignore
|
||||
class _odict_items(dict_items[_KT_co, _VT_co], Reversible[tuple[_KT_co, _VT_co]]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
|
||||
def __reversed__(self) -> Iterator[tuple[_KT_co, _VT_co]]: ...
|
||||
|
||||
@final
|
||||
class _odict_values(dict_values[_KT_co, _VT_co], Reversible[_VT_co], Generic[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore
|
||||
class _odict_values(dict_values[_KT_co, _VT_co], Reversible[_VT_co], Generic[_KT_co, _VT_co]): # type: ignore[misc] # pyright: ignore[reportGeneralTypeIssues]
|
||||
def __reversed__(self) -> Iterator[_VT_co]: ...
|
||||
|
||||
class OrderedDict(dict[_KT, _VT], Reversible[_KT], Generic[_KT, _VT]):
|
||||
@@ -475,7 +475,8 @@ class ChainMap(MutableMapping[_KT, _VT]):
|
||||
def pop(self, key: _KT, default: _T) -> _VT | _T: ...
|
||||
def copy(self) -> Self: ...
|
||||
__copy__ = copy
|
||||
# All arguments to `fromkeys` are passed to `dict.fromkeys` at runtime, so the signature should be kept in line with `dict.fromkeys`.
|
||||
# All arguments to `fromkeys` are passed to `dict.fromkeys` at runtime,
|
||||
# so the signature should be kept in line with `dict.fromkeys`.
|
||||
@classmethod
|
||||
@overload
|
||||
def fromkeys(cls, iterable: Iterable[_T]) -> ChainMap[_T, Any | None]: ...
|
||||
|
||||
@@ -55,6 +55,7 @@ class AbstractAsyncContextManager(Protocol[_T_co, _ExitT_co]):
|
||||
) -> _ExitT_co: ...
|
||||
|
||||
class ContextDecorator:
|
||||
def _recreate_cm(self) -> Self: ...
|
||||
def __call__(self, func: _F) -> _F: ...
|
||||
|
||||
class _GeneratorContextManager(AbstractContextManager[_T_co, bool | None], ContextDecorator):
|
||||
@@ -80,6 +81,7 @@ if sys.version_info >= (3, 10):
|
||||
_AF = TypeVar("_AF", bound=Callable[..., Awaitable[Any]])
|
||||
|
||||
class AsyncContextDecorator:
|
||||
def _recreate_cm(self) -> Self: ...
|
||||
def __call__(self, func: _AF) -> _AF: ...
|
||||
|
||||
class _AsyncGeneratorContextManager(AbstractAsyncContextManager[_T_co, bool | None], AsyncContextDecorator):
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import sys
|
||||
from typing import Any, Protocol, TypeVar
|
||||
from typing_extensions import ParamSpec, Self
|
||||
from typing_extensions import Self
|
||||
|
||||
__all__ = ["Error", "copy", "deepcopy"]
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_SR = TypeVar("_SR", bound=_SupportsReplace[Any])
|
||||
_P = ParamSpec("_P")
|
||||
_SR = TypeVar("_SR", bound=_SupportsReplace)
|
||||
|
||||
class _SupportsReplace(Protocol[_P]):
|
||||
class _SupportsReplace(Protocol):
|
||||
# In reality doesn't support args, but there's no other great way to express this.
|
||||
def __replace__(self, *args: _P.args, **kwargs: _P.kwargs) -> Self: ...
|
||||
def __replace__(self, *args: Any, **kwargs: Any) -> Self: ...
|
||||
|
||||
# None in CPython but non-None in Jython
|
||||
PyStringMap: Any
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import sys
|
||||
from typing import Final
|
||||
|
||||
if sys.platform != "win32":
|
||||
class _Method: ...
|
||||
METHOD_CRYPT: _Method
|
||||
METHOD_MD5: _Method
|
||||
METHOD_SHA256: _Method
|
||||
METHOD_SHA512: _Method
|
||||
METHOD_BLOWFISH: _Method
|
||||
METHOD_CRYPT: Final[_Method]
|
||||
METHOD_MD5: Final[_Method]
|
||||
METHOD_SHA256: Final[_Method]
|
||||
METHOD_SHA512: Final[_Method]
|
||||
METHOD_BLOWFISH: Final[_Method]
|
||||
methods: list[_Method]
|
||||
def mksalt(method: _Method | None = None, *, rounds: int | None = None) -> str: ...
|
||||
def crypt(word: str, salt: str | _Method | None = None) -> str: ...
|
||||
|
||||
@@ -185,3 +185,8 @@ if sys.version_info >= (3, 12):
|
||||
c_time_t: type[c_int32 | c_int64] # alias for one or the other at runtime
|
||||
|
||||
class py_object(_CanCastTo, _SimpleCData[_T]): ...
|
||||
|
||||
if sys.version_info >= (3, 14):
|
||||
class c_float_complex(_SimpleCData[complex]): ...
|
||||
class c_double_complex(_SimpleCData[complex]): ...
|
||||
class c_longdouble_complex(_SimpleCData[complex]): ...
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import sys
|
||||
from _ctypes import RTLD_GLOBAL as RTLD_GLOBAL, RTLD_LOCAL as RTLD_LOCAL, Structure, Union
|
||||
from ctypes import DEFAULT_MODE as DEFAULT_MODE, cdll as cdll, pydll as pydll, pythonapi as pythonapi
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from _ctypes import SIZEOF_TIME_T as SIZEOF_TIME_T
|
||||
|
||||
if sys.platform == "win32":
|
||||
from ctypes import oledll as oledll, windll as windll
|
||||
from ctypes import Structure, Union
|
||||
|
||||
# At runtime, the native endianness is an alias for Structure,
|
||||
# while the other is a subclass with a metaclass added in.
|
||||
|
||||
@@ -5,7 +5,7 @@ from _typeshed import DataclassInstance
|
||||
from builtins import type as Type # alias to avoid name clashes with fields named "type"
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from typing import Any, Generic, Literal, Protocol, TypeVar, overload
|
||||
from typing_extensions import TypeAlias, TypeIs
|
||||
from typing_extensions import Never, TypeAlias, TypeIs
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
from types import GenericAlias
|
||||
@@ -213,6 +213,10 @@ else:
|
||||
) -> Any: ...
|
||||
|
||||
def fields(class_or_instance: DataclassInstance | type[DataclassInstance]) -> tuple[Field[Any], ...]: ...
|
||||
|
||||
# HACK: `obj: Never` typing matches if object argument is using `Any` type.
|
||||
@overload
|
||||
def is_dataclass(obj: Never) -> TypeIs[DataclassInstance | type[DataclassInstance]]: ... # type: ignore[narrowed-type-not-subtype] # pyright: ignore[reportGeneralTypeIssues]
|
||||
@overload
|
||||
def is_dataclass(obj: type) -> TypeIs[type[DataclassInstance]]: ...
|
||||
@overload
|
||||
@@ -225,18 +229,17 @@ if sys.version_info >= (3, 9):
|
||||
else:
|
||||
class _InitVarMeta(type):
|
||||
# Not used, instead `InitVar.__class_getitem__` is called.
|
||||
# pyright ignore is needed because pyright (not unreasonably) thinks this
|
||||
# is an invalid use of InitVar.
|
||||
def __getitem__(self, params: Any) -> InitVar[Any]: ... # pyright: ignore
|
||||
# pyright (not unreasonably) thinks this is an invalid use of InitVar.
|
||||
def __getitem__(self, params: Any) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
class InitVar(Generic[_T], metaclass=_InitVarMeta):
|
||||
type: Type[_T]
|
||||
def __init__(self, type: Type[_T]) -> None: ...
|
||||
if sys.version_info >= (3, 9):
|
||||
@overload
|
||||
def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ... # pyright: ignore
|
||||
def __class_getitem__(cls, type: Type[_T]) -> InitVar[_T]: ... # pyright: ignore[reportInvalidTypeForm]
|
||||
@overload
|
||||
def __class_getitem__(cls, type: Any) -> InitVar[Any]: ... # pyright: ignore
|
||||
def __class_getitem__(cls, type: Any) -> InitVar[Any]: ... # pyright: ignore[reportInvalidTypeForm]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
def make_dataclass(
|
||||
|
||||
@@ -265,12 +265,12 @@ class datetime(date):
|
||||
def fromtimestamp(cls, timestamp: float, /, tz: _TzInfo | None = ...) -> Self: ...
|
||||
|
||||
@classmethod
|
||||
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .fromtimestamp(datetime.UTC)")
|
||||
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .fromtimestamp(datetime.timezone.utc)")
|
||||
def utcfromtimestamp(cls, t: float, /) -> Self: ...
|
||||
@classmethod
|
||||
def now(cls, tz: _TzInfo | None = None) -> Self: ...
|
||||
@classmethod
|
||||
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.UTC)")
|
||||
@deprecated("Use timezone-aware objects to represent datetimes in UTC; e.g. by calling .now(datetime.timezone.utc)")
|
||||
def utcnow(cls) -> Self: ...
|
||||
@classmethod
|
||||
def combine(cls, date: _Date, time: _Time, tzinfo: _TzInfo | None = ...) -> Self: ...
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
from _typeshed import BytesPath, Incomplete, StrOrBytesPath, StrPath, Unused
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Iterable
|
||||
from distutils.command.bdist import bdist
|
||||
from distutils.command.bdist_dumb import bdist_dumb
|
||||
from distutils.command.bdist_rpm import bdist_rpm
|
||||
from distutils.command.build import build
|
||||
from distutils.command.build_clib import build_clib
|
||||
from distutils.command.build_ext import build_ext
|
||||
from distutils.command.build_py import build_py
|
||||
from distutils.command.build_scripts import build_scripts
|
||||
from distutils.command.check import check
|
||||
from distutils.command.clean import clean
|
||||
from distutils.command.config import config
|
||||
from distutils.command.install import install
|
||||
from distutils.command.install_data import install_data
|
||||
from distutils.command.install_egg_info import install_egg_info
|
||||
from distutils.command.install_headers import install_headers
|
||||
from distutils.command.install_lib import install_lib
|
||||
from distutils.command.install_scripts import install_scripts
|
||||
from distutils.command.register import register
|
||||
from distutils.command.sdist import sdist
|
||||
from distutils.command.upload import upload
|
||||
from distutils.dist import Distribution
|
||||
from distutils.file_util import _BytesPathT, _StrPathT
|
||||
from typing import Any, ClassVar, Literal, TypeVar, overload
|
||||
@@ -10,6 +30,7 @@ _CommandT = TypeVar("_CommandT", bound=Command)
|
||||
_Ts = TypeVarTuple("_Ts")
|
||||
|
||||
class Command:
|
||||
dry_run: Literal[0, 1] # Exposed from __getattr_. Same as Distribution.dry_run
|
||||
distribution: Distribution
|
||||
# Any to work around variance issues
|
||||
sub_commands: ClassVar[list[tuple[str, Callable[[Any], bool] | None]]]
|
||||
@@ -28,8 +49,108 @@ class Command:
|
||||
def ensure_dirname(self, option: str) -> None: ...
|
||||
def get_command_name(self) -> str: ...
|
||||
def set_undefined_options(self, src_cmd: str, *option_pairs: tuple[str, str]) -> None: ...
|
||||
# NOTE: This list comes directly from the distutils/command folder. Minus bdist_msi and bdist_wininst.
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["bdist"], create: bool | Literal[0, 1] = 1) -> bdist: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["bdist_dumb"], create: bool | Literal[0, 1] = 1) -> bdist_dumb: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["bdist_rpm"], create: bool | Literal[0, 1] = 1) -> bdist_rpm: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["build"], create: bool | Literal[0, 1] = 1) -> build: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["build_clib"], create: bool | Literal[0, 1] = 1) -> build_clib: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["build_ext"], create: bool | Literal[0, 1] = 1) -> build_ext: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["build_py"], create: bool | Literal[0, 1] = 1) -> build_py: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["build_scripts"], create: bool | Literal[0, 1] = 1) -> build_scripts: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["check"], create: bool | Literal[0, 1] = 1) -> check: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["clean"], create: bool | Literal[0, 1] = 1) -> clean: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["config"], create: bool | Literal[0, 1] = 1) -> config: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["install"], create: bool | Literal[0, 1] = 1) -> install: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["install_data"], create: bool | Literal[0, 1] = 1) -> install_data: ...
|
||||
@overload
|
||||
def get_finalized_command(
|
||||
self, command: Literal["install_egg_info"], create: bool | Literal[0, 1] = 1
|
||||
) -> install_egg_info: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["install_headers"], create: bool | Literal[0, 1] = 1) -> install_headers: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["install_lib"], create: bool | Literal[0, 1] = 1) -> install_lib: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["install_scripts"], create: bool | Literal[0, 1] = 1) -> install_scripts: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["register"], create: bool | Literal[0, 1] = 1) -> register: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["sdist"], create: bool | Literal[0, 1] = 1) -> sdist: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: Literal["upload"], create: bool | Literal[0, 1] = 1) -> upload: ...
|
||||
@overload
|
||||
def get_finalized_command(self, command: str, create: bool | Literal[0, 1] = 1) -> Command: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["bdist"], reinit_subcommands: bool | Literal[0, 1] = 0) -> bdist: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["bdist_dumb"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> bdist_dumb: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["bdist_rpm"], reinit_subcommands: bool | Literal[0, 1] = 0) -> bdist_rpm: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build"], reinit_subcommands: bool | Literal[0, 1] = 0) -> build: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["build_clib"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> build_clib: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build_ext"], reinit_subcommands: bool | Literal[0, 1] = 0) -> build_ext: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build_py"], reinit_subcommands: bool | Literal[0, 1] = 0) -> build_py: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["build_scripts"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> build_scripts: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["check"], reinit_subcommands: bool | Literal[0, 1] = 0) -> check: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["clean"], reinit_subcommands: bool | Literal[0, 1] = 0) -> clean: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["config"], reinit_subcommands: bool | Literal[0, 1] = 0) -> config: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["install"], reinit_subcommands: bool | Literal[0, 1] = 0) -> install: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["install_data"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> install_data: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["install_egg_info"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> install_egg_info: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["install_headers"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> install_headers: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["install_lib"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> install_lib: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["install_scripts"], reinit_subcommands: bool | Literal[0, 1] = 0
|
||||
) -> install_scripts: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["register"], reinit_subcommands: bool | Literal[0, 1] = 0) -> register: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["sdist"], reinit_subcommands: bool | Literal[0, 1] = 0) -> sdist: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["upload"], reinit_subcommands: bool | Literal[0, 1] = 0) -> upload: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: str, reinit_subcommands: bool | Literal[0, 1] = 0) -> Command: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: _CommandT, reinit_subcommands: bool | Literal[0, 1] = 0) -> _CommandT: ...
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import sys
|
||||
|
||||
from . import (
|
||||
bdist,
|
||||
bdist_dumb,
|
||||
bdist_rpm,
|
||||
build,
|
||||
build_clib,
|
||||
build_ext,
|
||||
build_py,
|
||||
build_scripts,
|
||||
check,
|
||||
clean,
|
||||
install,
|
||||
install_data,
|
||||
install_headers,
|
||||
install_lib,
|
||||
install_scripts,
|
||||
register,
|
||||
sdist,
|
||||
upload,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"build",
|
||||
"build_py",
|
||||
"build_ext",
|
||||
"build_clib",
|
||||
"build_scripts",
|
||||
"clean",
|
||||
"install",
|
||||
"install_lib",
|
||||
"install_headers",
|
||||
"install_scripts",
|
||||
"install_data",
|
||||
"sdist",
|
||||
"register",
|
||||
"bdist",
|
||||
"bdist_dumb",
|
||||
"bdist_rpm",
|
||||
"check",
|
||||
"upload",
|
||||
]
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
from . import bdist_wininst
|
||||
|
||||
__all__ += ["bdist_wininst"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, ClassVar, Literal
|
||||
from typing import Any, ClassVar, Final, Literal
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from ..cmd import Command
|
||||
@@ -22,7 +22,7 @@ class SilentReporter(_Reporter):
|
||||
) -> None: ...
|
||||
def system_message(self, level, message, *children, **kwargs): ...
|
||||
|
||||
HAS_DOCUTILS: bool
|
||||
HAS_DOCUTILS: Final[bool]
|
||||
|
||||
class check(Command):
|
||||
description: str
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from _typeshed import StrOrBytesPath
|
||||
from collections.abc import Sequence
|
||||
from re import Pattern
|
||||
from typing import Any, ClassVar, Literal
|
||||
from typing import Any, ClassVar, Final, Literal
|
||||
|
||||
from ..ccompiler import CCompiler
|
||||
from ..cmd import Command
|
||||
|
||||
LANG_EXT: dict[str, str]
|
||||
LANG_EXT: Final[dict[str, str]]
|
||||
|
||||
class config(Command):
|
||||
description: str
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import Any, ClassVar
|
||||
from typing import Any, ClassVar, Final, Literal
|
||||
|
||||
from ..cmd import Command
|
||||
|
||||
HAS_USER_SITE: bool
|
||||
SCHEME_KEYS: tuple[str, ...]
|
||||
INSTALL_SCHEMES: dict[str, dict[Any, Any]]
|
||||
HAS_USER_SITE: Final[bool]
|
||||
|
||||
SCHEME_KEYS: Final[tuple[Literal["purelib"], Literal["platlib"], Literal["headers"], Literal["scripts"], Literal["data"]]]
|
||||
INSTALL_SCHEMES: Final[dict[str, dict[str, str]]]
|
||||
|
||||
if sys.version_info < (3, 10):
|
||||
WINDOWS_SCHEME: Final[dict[str, str]]
|
||||
|
||||
class install(Command):
|
||||
description: str
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import Any, ClassVar
|
||||
from typing import Any, ClassVar, Final
|
||||
|
||||
from ..cmd import Command
|
||||
|
||||
PYTHON_SOURCE_EXTENSION: str
|
||||
PYTHON_SOURCE_EXTENSION: Final = ".py"
|
||||
|
||||
class install_lib(Command):
|
||||
description: str
|
||||
|
||||
@@ -3,9 +3,9 @@ from collections.abc import Mapping
|
||||
from distutils.cmd import Command as Command
|
||||
from distutils.dist import Distribution as Distribution
|
||||
from distutils.extension import Extension as Extension
|
||||
from typing import Any, Literal
|
||||
from typing import Any, Final, Literal
|
||||
|
||||
USAGE: str
|
||||
USAGE: Final[str]
|
||||
|
||||
def gen_usage(script_name: StrOrBytesPath) -> str: ...
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
from distutils.unixccompiler import UnixCCompiler
|
||||
from distutils.version import LooseVersion
|
||||
from re import Pattern
|
||||
from typing import Literal
|
||||
from typing import Final, Literal
|
||||
|
||||
def get_msvcr() -> list[str] | None: ...
|
||||
|
||||
class CygwinCCompiler(UnixCCompiler): ...
|
||||
class Mingw32CCompiler(CygwinCCompiler): ...
|
||||
|
||||
CONFIG_H_OK: str
|
||||
CONFIG_H_NOTOK: str
|
||||
CONFIG_H_UNCERTAIN: str
|
||||
CONFIG_H_OK: Final = "ok"
|
||||
CONFIG_H_NOTOK: Final = "not ok"
|
||||
CONFIG_H_UNCERTAIN: Final = "uncertain"
|
||||
|
||||
def check_config_h() -> tuple[Literal["ok", "not ok", "uncertain"], str]: ...
|
||||
|
||||
RE_VERSION: Pattern[bytes]
|
||||
RE_VERSION: Final[Pattern[bytes]]
|
||||
|
||||
def get_versions() -> tuple[LooseVersion | None, ...]: ...
|
||||
def is_cygwingcc() -> bool: ...
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
DEBUG: bool | None
|
||||
from typing import Final
|
||||
|
||||
DEBUG: Final[str | None]
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
from _typeshed import Incomplete, StrOrBytesPath, StrPath, SupportsWrite
|
||||
from collections.abc import Iterable, MutableMapping
|
||||
from distutils.cmd import Command
|
||||
from distutils.command.bdist import bdist
|
||||
from distutils.command.bdist_dumb import bdist_dumb
|
||||
from distutils.command.bdist_rpm import bdist_rpm
|
||||
from distutils.command.build import build
|
||||
from distutils.command.build_clib import build_clib
|
||||
from distutils.command.build_ext import build_ext
|
||||
from distutils.command.build_py import build_py
|
||||
from distutils.command.build_scripts import build_scripts
|
||||
from distutils.command.check import check
|
||||
from distutils.command.clean import clean
|
||||
from distutils.command.config import config
|
||||
from distutils.command.install import install
|
||||
from distutils.command.install_data import install_data
|
||||
from distutils.command.install_egg_info import install_egg_info
|
||||
from distutils.command.install_headers import install_headers
|
||||
from distutils.command.install_lib import install_lib
|
||||
from distutils.command.install_scripts import install_scripts
|
||||
from distutils.command.register import register
|
||||
from distutils.command.sdist import sdist
|
||||
from distutils.command.upload import upload
|
||||
from re import Pattern
|
||||
from typing import IO, ClassVar, Literal, TypeVar, overload
|
||||
from typing_extensions import TypeAlias
|
||||
@@ -63,18 +83,14 @@ class Distribution:
|
||||
def __init__(self, attrs: MutableMapping[str, Incomplete] | None = None) -> None: ...
|
||||
def get_option_dict(self, command: str) -> dict[str, tuple[str, str]]: ...
|
||||
def parse_config_files(self, filenames: Iterable[str] | None = None) -> None: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: str, create: Literal[1, True] = 1) -> Command: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: str, create: Literal[0, False]) -> Command | None: ...
|
||||
global_options: ClassVar[_OptionsList]
|
||||
common_usage: ClassVar[str]
|
||||
display_options: ClassVar[_OptionsList]
|
||||
display_option_names: ClassVar[list[str]]
|
||||
negative_opt: ClassVar[dict[str, str]]
|
||||
verbose: int
|
||||
dry_run: int
|
||||
help: int
|
||||
verbose: Literal[0, 1]
|
||||
dry_run: Literal[0, 1]
|
||||
help: Literal[0, 1]
|
||||
command_packages: list[str] | None
|
||||
script_name: str | None
|
||||
script_args: list[str] | None
|
||||
@@ -108,8 +124,137 @@ class Distribution:
|
||||
def print_commands(self) -> None: ...
|
||||
def get_command_list(self): ...
|
||||
def get_command_packages(self): ...
|
||||
# NOTE: This list comes directly from the distutils/command folder. Minus bdist_msi and bdist_wininst.
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["bdist"], create: Literal[1, True] = 1) -> bdist: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["bdist_dumb"], create: Literal[1, True] = 1) -> bdist_dumb: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["bdist_rpm"], create: Literal[1, True] = 1) -> bdist_rpm: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["build"], create: Literal[1, True] = 1) -> build: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["build_clib"], create: Literal[1, True] = 1) -> build_clib: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["build_ext"], create: Literal[1, True] = 1) -> build_ext: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["build_py"], create: Literal[1, True] = 1) -> build_py: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["build_scripts"], create: Literal[1, True] = 1) -> build_scripts: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["check"], create: Literal[1, True] = 1) -> check: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["clean"], create: Literal[1, True] = 1) -> clean: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["config"], create: Literal[1, True] = 1) -> config: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["install"], create: Literal[1, True] = 1) -> install: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["install_data"], create: Literal[1, True] = 1) -> install_data: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["install_egg_info"], create: Literal[1, True] = 1) -> install_egg_info: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["install_headers"], create: Literal[1, True] = 1) -> install_headers: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["install_lib"], create: Literal[1, True] = 1) -> install_lib: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["install_scripts"], create: Literal[1, True] = 1) -> install_scripts: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["register"], create: Literal[1, True] = 1) -> register: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["sdist"], create: Literal[1, True] = 1) -> sdist: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: Literal["upload"], create: Literal[1, True] = 1) -> upload: ...
|
||||
@overload
|
||||
def get_command_obj(self, command: str, create: Literal[1, True] = 1) -> Command: ...
|
||||
# Not replicating the overloads for "Command | None", user may use "isinstance"
|
||||
@overload
|
||||
def get_command_obj(self, command: str, create: Literal[0, False]) -> Command | None: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["bdist"]) -> type[bdist]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["bdist_dumb"]) -> type[bdist_dumb]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["bdist_rpm"]) -> type[bdist_rpm]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["build"]) -> type[build]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["build_clib"]) -> type[build_clib]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["build_ext"]) -> type[build_ext]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["build_py"]) -> type[build_py]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["build_scripts"]) -> type[build_scripts]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["check"]) -> type[check]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["clean"]) -> type[clean]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["config"]) -> type[config]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["install"]) -> type[install]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["install_data"]) -> type[install_data]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["install_egg_info"]) -> type[install_egg_info]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["install_headers"]) -> type[install_headers]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["install_lib"]) -> type[install_lib]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["install_scripts"]) -> type[install_scripts]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["register"]) -> type[register]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["sdist"]) -> type[sdist]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: Literal["upload"]) -> type[upload]: ...
|
||||
@overload
|
||||
def get_command_class(self, command: str) -> type[Command]: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["bdist"], reinit_subcommands: bool = False) -> bdist: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["bdist_dumb"], reinit_subcommands: bool = False) -> bdist_dumb: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["bdist_rpm"], reinit_subcommands: bool = False) -> bdist_rpm: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build"], reinit_subcommands: bool = False) -> build: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build_clib"], reinit_subcommands: bool = False) -> build_clib: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build_ext"], reinit_subcommands: bool = False) -> build_ext: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build_py"], reinit_subcommands: bool = False) -> build_py: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["build_scripts"], reinit_subcommands: bool = False) -> build_scripts: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["check"], reinit_subcommands: bool = False) -> check: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["clean"], reinit_subcommands: bool = False) -> clean: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["config"], reinit_subcommands: bool = False) -> config: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["install"], reinit_subcommands: bool = False) -> install: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["install_data"], reinit_subcommands: bool = False) -> install_data: ...
|
||||
@overload
|
||||
def reinitialize_command(
|
||||
self, command: Literal["install_egg_info"], reinit_subcommands: bool = False
|
||||
) -> install_egg_info: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["install_headers"], reinit_subcommands: bool = False) -> install_headers: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["install_lib"], reinit_subcommands: bool = False) -> install_lib: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["install_scripts"], reinit_subcommands: bool = False) -> install_scripts: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["register"], reinit_subcommands: bool = False) -> register: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["sdist"], reinit_subcommands: bool = False) -> sdist: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: Literal["upload"], reinit_subcommands: bool = False) -> upload: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: str, reinit_subcommands: bool = False) -> Command: ...
|
||||
@overload
|
||||
def reinitialize_command(self, command: _CommandT, reinit_subcommands: bool = False) -> _CommandT: ...
|
||||
@@ -125,7 +270,7 @@ class Distribution:
|
||||
def has_data_files(self) -> bool: ...
|
||||
def is_pure(self) -> bool: ...
|
||||
|
||||
# Getter methods generated in __init__
|
||||
# Default getter methods generated in __init__ from self.metadata._METHOD_BASENAMES
|
||||
def get_name(self) -> str: ...
|
||||
def get_version(self) -> str: ...
|
||||
def get_fullname(self) -> str: ...
|
||||
@@ -147,3 +292,26 @@ class Distribution:
|
||||
def get_requires(self) -> list[str]: ...
|
||||
def get_provides(self) -> list[str]: ...
|
||||
def get_obsoletes(self) -> list[str]: ...
|
||||
|
||||
# Default attributes generated in __init__ from self.display_option_names
|
||||
help_commands: bool | Literal[0]
|
||||
name: str | Literal[0]
|
||||
version: str | Literal[0]
|
||||
fullname: str | Literal[0]
|
||||
author: str | Literal[0]
|
||||
author_email: str | Literal[0]
|
||||
maintainer: str | Literal[0]
|
||||
maintainer_email: str | Literal[0]
|
||||
contact: str | Literal[0]
|
||||
contact_email: str | Literal[0]
|
||||
url: str | Literal[0]
|
||||
license: str | Literal[0]
|
||||
licence: str | Literal[0]
|
||||
description: str | Literal[0]
|
||||
long_description: str | Literal[0]
|
||||
platforms: str | list[str] | Literal[0]
|
||||
classifiers: str | list[str] | Literal[0]
|
||||
keywords: str | list[str] | Literal[0]
|
||||
provides: list[str] | Literal[0]
|
||||
requires: list[str] | Literal[0]
|
||||
obsoletes: list[str] | Literal[0]
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from collections.abc import Iterable, Mapping
|
||||
from re import Pattern
|
||||
from typing import Any, overload
|
||||
from typing import Any, Final, overload
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
_Option: TypeAlias = tuple[str, str | None, str]
|
||||
_GR: TypeAlias = tuple[list[str], OptionDummy]
|
||||
|
||||
longopt_pat: str
|
||||
longopt_re: Pattern[str]
|
||||
neg_alias_re: Pattern[str]
|
||||
longopt_xlate: dict[int, int]
|
||||
longopt_pat: Final = r"[a-zA-Z](?:[a-zA-Z0-9-]*)"
|
||||
longopt_re: Final[Pattern[str]]
|
||||
neg_alias_re: Final[Pattern[str]]
|
||||
longopt_xlate: Final[dict[int, int]]
|
||||
|
||||
class FancyGetopt:
|
||||
def __init__(self, option_table: list[_Option] | None = None) -> None: ...
|
||||
@@ -25,7 +25,7 @@ def fancy_getopt(
|
||||
options: list[_Option], negative_opt: Mapping[_Option, _Option], object: Any, args: list[str] | None
|
||||
) -> list[str] | _GR: ...
|
||||
|
||||
WS_TRANS: dict[int, str]
|
||||
WS_TRANS: Final[dict[int, str]]
|
||||
|
||||
def wrap_text(text: str, width: int) -> list[str]: ...
|
||||
def translate_longopt(opt: str) -> str: ...
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from typing import Any
|
||||
from typing import Any, Final
|
||||
|
||||
DEBUG: int
|
||||
INFO: int
|
||||
WARN: int
|
||||
ERROR: int
|
||||
FATAL: int
|
||||
DEBUG: Final = 1
|
||||
INFO: Final = 2
|
||||
WARN: Final = 3
|
||||
ERROR: Final = 4
|
||||
FATAL: Final = 5
|
||||
|
||||
class Log:
|
||||
def __init__(self, threshold: int = 3) -> None: ...
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import sys
|
||||
from collections.abc import Mapping
|
||||
from distutils.ccompiler import CCompiler
|
||||
from typing import Literal, overload
|
||||
from typing import Final, Literal, overload
|
||||
from typing_extensions import deprecated
|
||||
|
||||
PREFIX: str
|
||||
EXEC_PREFIX: str
|
||||
BASE_PREFIX: str
|
||||
BASE_EXEC_PREFIX: str
|
||||
project_base: str
|
||||
python_build: bool
|
||||
PREFIX: Final[str]
|
||||
EXEC_PREFIX: Final[str]
|
||||
BASE_PREFIX: Final[str]
|
||||
BASE_EXEC_PREFIX: Final[str]
|
||||
project_base: Final[str]
|
||||
python_build: Final[bool]
|
||||
|
||||
def expand_makefile_vars(s: str, vars: Mapping[str, str]) -> str: ...
|
||||
@overload
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from _typeshed import ExcInfo
|
||||
from collections.abc import Callable
|
||||
from typing import Any, NamedTuple
|
||||
from typing_extensions import TypeAlias
|
||||
from typing import Any, ClassVar, NamedTuple
|
||||
from typing_extensions import Self, TypeAlias
|
||||
|
||||
__all__ = [
|
||||
"register_optionflag",
|
||||
@@ -41,9 +42,22 @@ __all__ = [
|
||||
"debug",
|
||||
]
|
||||
|
||||
class TestResults(NamedTuple):
|
||||
failed: int
|
||||
attempted: int
|
||||
# MyPy errors on conditionals within named tuples.
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
class TestResults(NamedTuple):
|
||||
def __new__(cls, failed: int, attempted: int, *, skipped: int = 0) -> Self: ... # type: ignore[misc]
|
||||
skipped: int
|
||||
failed: int
|
||||
attempted: int
|
||||
_fields: ClassVar = ("failed", "attempted") # type: ignore[misc]
|
||||
__match_args__ = ("failed", "attempted") # type: ignore[misc]
|
||||
__doc__: None # type: ignore[misc]
|
||||
|
||||
else:
|
||||
class TestResults(NamedTuple):
|
||||
failed: int
|
||||
attempted: int
|
||||
|
||||
OPTIONFLAGS_BY_NAME: dict[str, int]
|
||||
|
||||
@@ -134,6 +148,8 @@ class DocTestRunner:
|
||||
original_optionflags: int
|
||||
tries: int
|
||||
failures: int
|
||||
if sys.version_info >= (3, 13):
|
||||
skips: int
|
||||
test: DocTest
|
||||
def __init__(self, checker: OutputChecker | None = None, verbose: bool | None = None, optionflags: int = 0) -> None: ...
|
||||
def report_start(self, out: _Out, test: DocTest, example: Example) -> None: ...
|
||||
|
||||
@@ -16,6 +16,10 @@ TOKEN_ENDS: Final[set[str]]
|
||||
ASPECIALS: Final[set[str]]
|
||||
ATTRIBUTE_ENDS: Final[set[str]]
|
||||
EXTENDED_ATTRIBUTE_ENDS: Final[set[str]]
|
||||
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
NLSET: Final[set[str]]
|
||||
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
SPECIALSNL: Final[set[str]]
|
||||
|
||||
def quote_string(value: Any) -> str: ...
|
||||
|
||||
|
||||
@@ -3,12 +3,34 @@ from collections.abc import Callable
|
||||
from email.errors import MessageDefect
|
||||
from email.header import Header
|
||||
from email.message import Message
|
||||
from typing import Any
|
||||
from typing_extensions import Self
|
||||
|
||||
class _PolicyBase:
|
||||
def __add__(self, other: Any) -> Self: ...
|
||||
def clone(self, **kw: Any) -> Self: ...
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
max_line_length: int | None = 78,
|
||||
linesep: str = "\n",
|
||||
cte_type: str = "8bit",
|
||||
raise_on_defect: bool = False,
|
||||
mangle_from_: bool = ..., # default depends on sub-class
|
||||
message_factory: Callable[[Policy], Message] | None = None,
|
||||
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
verify_generated_headers: bool = True,
|
||||
) -> None: ...
|
||||
def clone(
|
||||
self,
|
||||
*,
|
||||
max_line_length: int | None = ...,
|
||||
linesep: str = ...,
|
||||
cte_type: str = ...,
|
||||
raise_on_defect: bool = ...,
|
||||
mangle_from_: bool = ...,
|
||||
message_factory: Callable[[Policy], Message] | None = ...,
|
||||
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
verify_generated_headers: bool = ...,
|
||||
) -> Self: ...
|
||||
def __add__(self, other: Policy) -> Self: ...
|
||||
|
||||
class Policy(_PolicyBase, metaclass=ABCMeta):
|
||||
max_line_length: int | None
|
||||
@@ -17,16 +39,9 @@ class Policy(_PolicyBase, metaclass=ABCMeta):
|
||||
raise_on_defect: bool
|
||||
mangle_from_: bool
|
||||
message_factory: Callable[[Policy], Message] | None
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
max_line_length: int | None = 78,
|
||||
linesep: str = "\n",
|
||||
cte_type: str = "8bit",
|
||||
raise_on_defect: bool = False,
|
||||
mangle_from_: bool = False,
|
||||
message_factory: Callable[[Policy], Message] | None = None,
|
||||
) -> None: ...
|
||||
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
verify_generated_headers: bool
|
||||
|
||||
def handle_defect(self, obj: Message, defect: MessageDefect) -> None: ...
|
||||
def register_defect(self, obj: Message, defect: MessageDefect) -> None: ...
|
||||
def header_max_count(self, name: str) -> int | None: ...
|
||||
|
||||
@@ -7,6 +7,9 @@ class BoundaryError(MessageParseError): ...
|
||||
class MultipartConversionError(MessageError, TypeError): ...
|
||||
class CharsetError(MessageError): ...
|
||||
|
||||
# Added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
class HeaderWriteError(MessageError): ...
|
||||
|
||||
class MessageDefect(ValueError):
|
||||
def __init__(self, line: str | None = None) -> None: ...
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ class Message(Generic[_HeaderT, _HeaderParamT]):
|
||||
def get_payload(self, i: None = None, *, decode: Literal[True]) -> _EncodedPayloadType | Any: ...
|
||||
@overload # not multipart, IDEM but w/o kwarg
|
||||
def get_payload(self, i: None, decode: Literal[True]) -> _EncodedPayloadType | Any: ...
|
||||
# If `charset=None` and payload supports both `encode` AND `decode`, then an invalid payload could be passed, but this is unlikely
|
||||
# If `charset=None` and payload supports both `encode` AND `decode`,
|
||||
# then an invalid payload could be passed, but this is unlikely
|
||||
# Not[_SupportsEncodeToPayload]
|
||||
@overload
|
||||
def set_payload(
|
||||
@@ -146,7 +147,11 @@ class Message(Generic[_HeaderT, _HeaderParamT]):
|
||||
class MIMEPart(Message[_HeaderRegistryT, _HeaderRegistryParamT]):
|
||||
def __init__(self, policy: Policy | None = None) -> None: ...
|
||||
def get_body(self, preferencelist: Sequence[str] = ("related", "html", "plain")) -> MIMEPart[_HeaderRegistryT] | None: ...
|
||||
def iter_attachments(self) -> Iterator[MIMEPart[_HeaderRegistryT]]: ...
|
||||
def attach(self, payload: Self) -> None: ... # type: ignore[override]
|
||||
# The attachments are created via type(self) in the attach method. It's theoretically
|
||||
# possible to sneak other attachment types into a MIMEPart instance, but could cause
|
||||
# cause unforseen consequences.
|
||||
def iter_attachments(self) -> Iterator[Self]: ...
|
||||
def iter_parts(self) -> Iterator[MIMEPart[_HeaderRegistryT]]: ...
|
||||
def get_content(self, *args: Any, content_manager: ContentManager | None = None, **kw: Any) -> Any: ...
|
||||
def set_content(self, *args: Any, content_manager: ContentManager | None = None, **kw: Any) -> None: ...
|
||||
|
||||
@@ -30,20 +30,12 @@ _PDTZ: TypeAlias = tuple[int, int, int, int, int, int, int, int, int, int | None
|
||||
def quote(str: str) -> str: ...
|
||||
def unquote(str: str) -> str: ...
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
def parseaddr(addr: str | list[str], *, strict: bool = True) -> tuple[str, str]: ...
|
||||
|
||||
else:
|
||||
def parseaddr(addr: str) -> tuple[str, str]: ...
|
||||
|
||||
# `strict` parameter added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
def parseaddr(addr: str | list[str], *, strict: bool = True) -> tuple[str, str]: ...
|
||||
def formataddr(pair: tuple[str | None, str], charset: str | Charset = "utf-8") -> str: ...
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
def getaddresses(fieldvalues: Iterable[str], *, strict: bool = True) -> list[tuple[str, str]]: ...
|
||||
|
||||
else:
|
||||
def getaddresses(fieldvalues: Iterable[str]) -> list[tuple[str, str]]: ...
|
||||
|
||||
# `strict` parameter added in Python 3.8.20, 3.9.20, 3.10.15, 3.11.10, 3.12.5
|
||||
def getaddresses(fieldvalues: Iterable[str], *, strict: bool = True) -> list[tuple[str, str]]: ...
|
||||
@overload
|
||||
def parsedate(data: None) -> None: ...
|
||||
@overload
|
||||
@@ -66,7 +58,10 @@ def mktime_tz(data: _PDTZ) -> int: ...
|
||||
def formatdate(timeval: float | None = None, localtime: bool = False, usegmt: bool = False) -> str: ...
|
||||
def format_datetime(dt: datetime.datetime, usegmt: bool = False) -> str: ...
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
if sys.version_info >= (3, 14):
|
||||
def localtime(dt: datetime.datetime | None = None) -> datetime.datetime: ...
|
||||
|
||||
elif sys.version_info >= (3, 12):
|
||||
@overload
|
||||
def localtime(dt: datetime.datetime | None = None) -> datetime.datetime: ...
|
||||
@overload
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user