Compare commits
157 Commits
dcreager/i
...
dcreager/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70a5373791 | ||
|
|
8e880c8f61 | ||
|
|
5e34d79cc1 | ||
|
|
b0beb93517 | ||
|
|
391917bc87 | ||
|
|
f587a89a3b | ||
|
|
5a7650d5ee | ||
|
|
ff5e65f6f5 | ||
|
|
88ef456757 | ||
|
|
1e1470073c | ||
|
|
ca237345d9 | ||
|
|
5ec0cb32f8 | ||
|
|
e73374c146 | ||
|
|
382349f85a | ||
|
|
80efb01e5d | ||
|
|
c8b2cc4e00 | ||
|
|
25a84c7b18 | ||
|
|
1c71f9b8c4 | ||
|
|
84ceddcbd9 | ||
|
|
ba2f0e998d | ||
|
|
18b497a913 | ||
|
|
7cac0da44d | ||
|
|
b66cc94f9b | ||
|
|
e345307260 | ||
|
|
5588c75d65 | ||
|
|
9d2105b863 | ||
|
|
8fcac0ff36 | ||
|
|
81059d05fc | ||
|
|
24bab7e82e | ||
|
|
d0555f7b5c | ||
|
|
0906554357 | ||
|
|
d296f602e7 | ||
|
|
d47088c8f8 | ||
|
|
1f0ad675d3 | ||
|
|
a84b27e679 | ||
|
|
8d4679b3ae | ||
|
|
b40a7cce15 | ||
|
|
54b3849dfb | ||
|
|
ffd94e9ace | ||
|
|
c816542704 | ||
|
|
3f958a9d4c | ||
|
|
2ebb5e8d4b | ||
|
|
c69b19fe1d | ||
|
|
076d35fb93 | ||
|
|
16f2a93fca | ||
|
|
eb08345fd5 | ||
|
|
7ca778f492 | ||
|
|
827a076a2f | ||
|
|
4855e0b288 | ||
|
|
44ddd98d7e | ||
|
|
82cb8675dd | ||
|
|
5852217198 | ||
|
|
700e969c56 | ||
|
|
4c15d7a559 | ||
|
|
e15419396c | ||
|
|
444b055cec | ||
|
|
6bb32355ef | ||
|
|
cb71393332 | ||
|
|
64e64d2681 | ||
|
|
9d83e76a3b | ||
|
|
5bf0e2e95e | ||
|
|
24c1cf71cb | ||
|
|
f23802e219 | ||
|
|
ff87ea8d42 | ||
|
|
cc60701b59 | ||
|
|
b5e5271adf | ||
|
|
9a33924a65 | ||
|
|
15dd3b5ebd | ||
|
|
b848afeae8 | ||
|
|
de4d9979eb | ||
|
|
ba02294af3 | ||
|
|
11cfe2ea8a | ||
|
|
0529ad67d7 | ||
|
|
102c2eec12 | ||
|
|
dc5e922221 | ||
|
|
62075afe4f | ||
|
|
dfe1b849d0 | ||
|
|
9c64d65552 | ||
|
|
83243de93d | ||
|
|
638186afbd | ||
|
|
d082c1b202 | ||
|
|
30d5e9a2af | ||
|
|
a613345274 | ||
|
|
c81f6c0bd2 | ||
|
|
ba534d1931 | ||
|
|
7e1db01041 | ||
|
|
6331dd6272 | ||
|
|
4fe78db16c | ||
|
|
f5f74c95c5 | ||
|
|
464a893f5d | ||
|
|
a53626a8b2 | ||
|
|
b08ce5fb18 | ||
|
|
418aa35041 | ||
|
|
813a76e9e2 | ||
|
|
3c09100484 | ||
|
|
770b7f3439 | ||
|
|
d9a1034db0 | ||
|
|
bcdb3f9840 | ||
|
|
942d7f395a | ||
|
|
b58f2c399e | ||
|
|
fab86de3ef | ||
|
|
c5c0b724fb | ||
|
|
0d191a13c1 | ||
|
|
b2cb757fa8 | ||
|
|
ce769f6ae2 | ||
|
|
44ac17b3ba | ||
|
|
f1418be81c | ||
|
|
59be5f5278 | ||
|
|
4df0796d61 | ||
|
|
172f62d8f4 | ||
|
|
071862af5a | ||
|
|
fe516e24f5 | ||
|
|
4f2aea8d50 | ||
|
|
5c77898693 | ||
|
|
854ab03078 | ||
|
|
b0b8b06241 | ||
|
|
451f251a31 | ||
|
|
13cf3e65f1 | ||
|
|
56f956a238 | ||
|
|
7a10a40b0d | ||
|
|
3125332ec1 | ||
|
|
15d886a502 | ||
|
|
e1c9d10863 | ||
|
|
23c98849fc | ||
|
|
d151ca85d3 | ||
|
|
6c1e19592e | ||
|
|
0f1035b930 | ||
|
|
2c3d889dbb | ||
|
|
4bec8ba731 | ||
|
|
6090408f65 | ||
|
|
72a4d343ff | ||
|
|
786099a872 | ||
|
|
ca53eefa6f | ||
|
|
98d20a8219 | ||
|
|
9c938442e5 | ||
|
|
9bf138c45a | ||
|
|
e994970538 | ||
|
|
c161e4fb12 | ||
|
|
646f1942aa | ||
|
|
0a2139f496 | ||
|
|
2ef94e5f3e | ||
|
|
3a08570a68 | ||
|
|
2da8c3776b | ||
|
|
fac0360310 | ||
|
|
0ff71bc3f3 | ||
|
|
43fbbdc71b | ||
|
|
a8fb6f0f87 | ||
|
|
23baf3a2c8 | ||
|
|
d0709093fe | ||
|
|
101a6ba805 | ||
|
|
5bb87f8eb6 | ||
|
|
37925ac442 | ||
|
|
cb3361e682 | ||
|
|
c824140fa8 | ||
|
|
f85ea1bf46 | ||
|
|
a77a32b7d4 | ||
|
|
d8c2d20325 |
@@ -8,3 +8,7 @@ benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --"
|
||||
# See: https://github.com/astral-sh/ruff/issues/11503
|
||||
[target.'cfg(all(target_env="msvc", target_os = "windows"))']
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
|
||||
[target.'wasm32-unknown-unknown']
|
||||
# See https://docs.rs/getrandom/latest/getrandom/#webassembly-support
|
||||
rustflags = ["--cfg", 'getrandom_backend="wasm_js"']
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -280,7 +280,7 @@ jobs:
|
||||
|
||||
cargo-build-msrv:
|
||||
name: "cargo build (msrv)"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-latest-8
|
||||
needs: determine_changes
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
@@ -430,7 +430,7 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
name: Download baseline Ruff binary
|
||||
with:
|
||||
name: ruff
|
||||
|
||||
4
.github/workflows/pr-comment.yaml
vendored
4
.github/workflows/pr-comment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
name: Download pull request number
|
||||
with:
|
||||
name: pr-number
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
echo "pr-number=$(<pr-number)" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- uses: dawidd6/action-download-artifact@v7
|
||||
- uses: dawidd6/action-download-artifact@v8
|
||||
name: "Download ecosystem results"
|
||||
id: download-ecosystem-result
|
||||
if: steps.pr-number.outputs.pr-number
|
||||
|
||||
@@ -5,6 +5,7 @@ exclude: |
|
||||
.github/workflows/release.yml|
|
||||
crates/red_knot_vendored/vendor/.*|
|
||||
crates/red_knot_project/resources/.*|
|
||||
crates/ruff_benchmark/resources/.*|
|
||||
crates/ruff_linter/resources/.*|
|
||||
crates/ruff_linter/src/rules/.*/snapshots/.*|
|
||||
crates/ruff_notebook/resources/.*|
|
||||
@@ -23,7 +24,7 @@ repos:
|
||||
- id: validate-pyproject
|
||||
|
||||
- repo: https://github.com/executablebooks/mdformat
|
||||
rev: 0.7.21
|
||||
rev: 0.7.22
|
||||
hooks:
|
||||
- id: mdformat
|
||||
additional_dependencies:
|
||||
@@ -36,7 +37,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
||||
rev: v0.43.0
|
||||
rev: v0.44.0
|
||||
hooks:
|
||||
- id: markdownlint-fix
|
||||
exclude: |
|
||||
@@ -56,10 +57,10 @@ repos:
|
||||
.*?invalid(_.+)*_syntax\.md
|
||||
)$
|
||||
additional_dependencies:
|
||||
- black==24.10.0
|
||||
- black==25.1.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.29.4
|
||||
rev: v1.29.5
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -73,7 +74,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.9.2
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -91,12 +92,12 @@ repos:
|
||||
# zizmor detects security vulnerabilities in GitHub Actions workflows.
|
||||
# Additional configuration for the tool is found in `.github/zizmor.yml`
|
||||
- repo: https://github.com/woodruffw/zizmor-pre-commit
|
||||
rev: v1.2.2
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
- repo: https://github.com/python-jsonschema/check-jsonschema
|
||||
rev: 0.31.0
|
||||
rev: 0.31.1
|
||||
hooks:
|
||||
- id: check-github-workflows
|
||||
|
||||
|
||||
38
CHANGELOG.md
38
CHANGELOG.md
@@ -1,5 +1,43 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`airflow`\] Extend airflow context parameter check for `BaseOperator.execute` (`AIR302`) ([#15713](https://github.com/astral-sh/ruff/pull/15713))
|
||||
- \[`airflow`\] Update `AIR302` to check for deprecated context keys ([#15144](https://github.com/astral-sh/ruff/pull/15144))
|
||||
- \[`flake8-bandit`\] Permit suspicious imports within stub files (`S4`) ([#15822](https://github.com/astral-sh/ruff/pull/15822))
|
||||
- \[`pylint`\] Do not trigger `PLR6201` on empty collections ([#15732](https://github.com/astral-sh/ruff/pull/15732))
|
||||
- \[`refurb`\] Do not emit diagnostic when loop variables are used outside loop body (`FURB122`) ([#15757](https://github.com/astral-sh/ruff/pull/15757))
|
||||
- \[`ruff`\] Add support for more `re` patterns (`RUF055`) ([#15764](https://github.com/astral-sh/ruff/pull/15764))
|
||||
- \[`ruff`\] Check for shadowed `map` before suggesting fix (`RUF058`) ([#15790](https://github.com/astral-sh/ruff/pull/15790))
|
||||
- \[`ruff`\] Do not emit diagnostic when all arguments to `zip()` are variadic (`RUF058`) ([#15744](https://github.com/astral-sh/ruff/pull/15744))
|
||||
- \[`ruff`\] Parenthesize fix when argument spans multiple lines for `unnecessary-round` (`RUF057`) ([#15703](https://github.com/astral-sh/ruff/pull/15703))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Preserve quote style in generated code ([#15726](https://github.com/astral-sh/ruff/pull/15726), [#15778](https://github.com/astral-sh/ruff/pull/15778), [#15794](https://github.com/astral-sh/ruff/pull/15794))
|
||||
- \[`flake8-bugbear`\] Exempt `NewType` calls where the original type is immutable (`B008`) ([#15765](https://github.com/astral-sh/ruff/pull/15765))
|
||||
- \[`pylint`\] Honor banned top-level imports by `TID253` in `PLC0415`. ([#15628](https://github.com/astral-sh/ruff/pull/15628))
|
||||
- \[`pyupgrade`\] Ignore `is_typeddict` and `TypedDict` for `deprecated-import` (`UP035`) ([#15800](https://github.com/astral-sh/ruff/pull/15800))
|
||||
|
||||
### CLI
|
||||
|
||||
- Fix formatter warning message for `flake8-quotes` option ([#15788](https://github.com/astral-sh/ruff/pull/15788))
|
||||
- Implement tab autocomplete for `ruff config` ([#15603](https://github.com/astral-sh/ruff/pull/15603))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-comprehensions`\] Do not emit `unnecessary-map` diagnostic when lambda has different arity (`C417`) ([#15802](https://github.com/astral-sh/ruff/pull/15802))
|
||||
- \[`flake8-comprehensions`\] Parenthesize `sorted` when needed for `unnecessary-call-around-sorted` (`C413`) ([#15825](https://github.com/astral-sh/ruff/pull/15825))
|
||||
- \[`pyupgrade`\] Handle end-of-line comments for `quoted-annotation` (`UP037`) ([#15824](https://github.com/astral-sh/ruff/pull/15824))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add missing config docstrings ([#15803](https://github.com/astral-sh/ruff/pull/15803))
|
||||
- Add references to `trio.run_process` and `anyio.run_process` ([#15761](https://github.com/astral-sh/ruff/pull/15761))
|
||||
- Use `uv init --lib` in tutorial ([#15718](https://github.com/astral-sh/ruff/pull/15718))
|
||||
|
||||
## 0.9.3
|
||||
|
||||
### Preview features
|
||||
|
||||
1028
Cargo.lock
generated
1028
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
25
Cargo.toml
@@ -22,6 +22,7 @@ ruff_graph = { path = "crates/ruff_graph" }
|
||||
ruff_index = { path = "crates/ruff_index" }
|
||||
ruff_linter = { path = "crates/ruff_linter" }
|
||||
ruff_macros = { path = "crates/ruff_macros" }
|
||||
ruff_metrics = { path = "crates/ruff_metrics" }
|
||||
ruff_notebook = { path = "crates/ruff_notebook" }
|
||||
ruff_python_ast = { path = "crates/ruff_python_ast" }
|
||||
ruff_python_codegen = { path = "crates/ruff_python_codegen" }
|
||||
@@ -74,11 +75,13 @@ env_logger = { version = "0.11.0" }
|
||||
etcetera = { version = "0.8.0" }
|
||||
fern = { version = "0.7.0" }
|
||||
filetime = { version = "0.2.23" }
|
||||
getrandom = { version = "0.3.1" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
globwalk = { version = "0.9.1" }
|
||||
hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
"raw-entry",
|
||||
"equivalent",
|
||||
"inline-more",
|
||||
] }
|
||||
ignore = { version = "0.4.22" }
|
||||
@@ -103,6 +106,8 @@ lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f
|
||||
] }
|
||||
matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
metrics = { version = "0.24.1" }
|
||||
metrics-util = { version = "0.19.0" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "8.0.0" }
|
||||
@@ -116,7 +121,7 @@ proc-macro2 = { version = "1.0.79" }
|
||||
pyproject-toml = { version = "0.13.4" }
|
||||
quick-junit = { version = "0.5.0" }
|
||||
quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
rand = { version = "0.9.0" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
@@ -134,7 +139,12 @@ serde_with = { version = "3.6.0", default-features = false, features = [
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
smallvec = { version = "1.13.2" }
|
||||
snapbox = { version = "0.6.0", features = ["diff", "term-svg", "cmd", "examples"] }
|
||||
snapbox = { version = "0.6.0", features = [
|
||||
"diff",
|
||||
"term-svg",
|
||||
"cmd",
|
||||
"examples",
|
||||
] }
|
||||
static_assertions = "1.1.0"
|
||||
strum = { version = "0.26.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.26.0" }
|
||||
@@ -159,7 +169,6 @@ unicode-ident = { version = "1.0.12" }
|
||||
unicode-width = { version = "0.2.0" }
|
||||
unicode_names2 = { version = "1.2.2" }
|
||||
unicode-normalization = { version = "0.1.23" }
|
||||
ureq = { version = "2.9.6" }
|
||||
url = { version = "2.5.0" }
|
||||
uuid = { version = "1.6.1", features = [
|
||||
"v4",
|
||||
@@ -173,6 +182,10 @@ wasm-bindgen-test = { version = "0.3.42" }
|
||||
wild = { version = "2" }
|
||||
zip = { version = "0.6.6", default-features = false }
|
||||
|
||||
[workspace.metadata.cargo-shear]
|
||||
ignored = ["getrandom"]
|
||||
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "warn"
|
||||
unreachable_pub = "warn"
|
||||
@@ -305,7 +318,11 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
|
||||
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
|
||||
|
||||
@@ -149,8 +149,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.9.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.3/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.9.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.4/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -183,7 +183,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.9.3
|
||||
rev: v0.9.4
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -16,6 +16,7 @@ red_knot_python_semantic = { workspace = true }
|
||||
red_knot_project = { workspace = true, features = ["zstd"] }
|
||||
red_knot_server = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os", "cache"] }
|
||||
ruff_metrics = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
@@ -24,6 +25,7 @@ colored = { workspace = true }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
crossbeam = { workspace = true }
|
||||
ctrlc = { version = "3.4.4" }
|
||||
metrics = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true, features = ["release_max_level_debug"] }
|
||||
|
||||
104
crates/red_knot/build.rs
Normal file
104
crates/red_knot/build.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
// The workspace root directory is not available without walking up the tree
|
||||
// https://github.com/rust-lang/cargo/issues/3946
|
||||
let workspace_root = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
|
||||
.join("..")
|
||||
.join("..");
|
||||
|
||||
commit_info(&workspace_root);
|
||||
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
let target = std::env::var("TARGET").unwrap();
|
||||
println!("cargo::rustc-env=RUST_HOST_TARGET={target}");
|
||||
}
|
||||
|
||||
fn commit_info(workspace_root: &Path) {
|
||||
// If not in a git repository, do not attempt to retrieve commit information
|
||||
let git_dir = workspace_root.join(".git");
|
||||
if !git_dir.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(git_head_path) = git_head(&git_dir) {
|
||||
println!("cargo:rerun-if-changed={}", git_head_path.display());
|
||||
|
||||
let git_head_contents = fs::read_to_string(git_head_path);
|
||||
if let Ok(git_head_contents) = git_head_contents {
|
||||
// The contents are either a commit or a reference in the following formats
|
||||
// - "<commit>" when the head is detached
|
||||
// - "ref <ref>" when working on a branch
|
||||
// If a commit, checking if the HEAD file has changed is sufficient
|
||||
// If a ref, we need to add the head file for that ref to rebuild on commit
|
||||
let mut git_ref_parts = git_head_contents.split_whitespace();
|
||||
git_ref_parts.next();
|
||||
if let Some(git_ref) = git_ref_parts.next() {
|
||||
let git_ref_path = git_dir.join(git_ref);
|
||||
println!("cargo:rerun-if-changed={}", git_ref_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let output = match Command::new("git")
|
||||
.arg("log")
|
||||
.arg("-1")
|
||||
.arg("--date=short")
|
||||
.arg("--abbrev=9")
|
||||
.arg("--format=%H %h %cd %(describe)")
|
||||
.output()
|
||||
{
|
||||
Ok(output) if output.status.success() => output,
|
||||
_ => return,
|
||||
};
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
let mut parts = stdout.split_whitespace();
|
||||
let mut next = || parts.next().unwrap();
|
||||
let _commit_hash = next();
|
||||
println!("cargo::rustc-env=RED_KNOT_COMMIT_SHORT_HASH={}", next());
|
||||
println!("cargo::rustc-env=RED_KNOT_COMMIT_DATE={}", next());
|
||||
|
||||
// Describe can fail for some commits
|
||||
// https://git-scm.com/docs/pretty-formats#Documentation/pretty-formats.txt-emdescribeoptionsem
|
||||
if let Some(describe) = parts.next() {
|
||||
let mut describe_parts = describe.split('-');
|
||||
let _last_tag = describe_parts.next().unwrap();
|
||||
|
||||
// If this is the tagged commit, this component will be missing
|
||||
println!(
|
||||
"cargo::rustc-env=RED_KNOT_LAST_TAG_DISTANCE={}",
|
||||
describe_parts.next().unwrap_or("0")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn git_head(git_dir: &Path) -> Option<PathBuf> {
|
||||
// The typical case is a standard git repository.
|
||||
let git_head_path = git_dir.join("HEAD");
|
||||
if git_head_path.exists() {
|
||||
return Some(git_head_path);
|
||||
}
|
||||
if !git_dir.is_file() {
|
||||
return None;
|
||||
}
|
||||
// If `.git/HEAD` doesn't exist and `.git` is actually a file,
|
||||
// then let's try to attempt to read it as a worktree. If it's
|
||||
// a worktree, then its contents will look like this, e.g.:
|
||||
//
|
||||
// gitdir: /home/andrew/astral/uv/main/.git/worktrees/pr2
|
||||
//
|
||||
// And the HEAD file we want to watch will be at:
|
||||
//
|
||||
// /home/andrew/astral/uv/main/.git/worktrees/pr2/HEAD
|
||||
let contents = fs::read_to_string(git_dir).ok()?;
|
||||
let (label, worktree_path) = contents.split_once(':')?;
|
||||
if label != "gitdir" {
|
||||
return None;
|
||||
}
|
||||
let worktree_path = worktree_path.trim();
|
||||
Some(PathBuf::from(worktree_path))
|
||||
}
|
||||
@@ -25,6 +25,9 @@ pub(crate) enum Command {
|
||||
|
||||
/// Start the language server
|
||||
Server,
|
||||
|
||||
/// Display Red Knot's version
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -60,9 +63,24 @@ pub(crate) struct CheckCommand {
|
||||
#[clap(flatten)]
|
||||
pub(crate) verbosity: Verbosity,
|
||||
|
||||
/// Whether to output metrics about type-checking performance. If you provide a path, metrics
|
||||
/// will be written to that file. If you provide this option but don't provide a path, metrics
|
||||
/// will be written to a file called `metrics.json` in the current directory. We will _append_
|
||||
/// metrics to the file if it already exists.
|
||||
#[arg(long, value_name = "PATH", default_missing_value="metrics.json", num_args=0..=1)]
|
||||
pub(crate) metrics: Option<SystemPathBuf>,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub(crate) rules: RulesArg,
|
||||
|
||||
/// Use exit code 1 if there are any warning-level diagnostics.
|
||||
#[arg(long, conflicts_with = "exit_zero")]
|
||||
pub(crate) error_on_warning: bool,
|
||||
|
||||
/// Always use exit code 0, even when there are error-level diagnostics.
|
||||
#[arg(long)]
|
||||
pub(crate) exit_zero: bool,
|
||||
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[arg(long, short = 'W')]
|
||||
pub(crate) watch: bool,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
use anyhow::Context;
|
||||
use colored::Colorize;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_metrics::JsonRecorder;
|
||||
use std::fmt;
|
||||
use std::fs::File;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::BufWriter;
|
||||
use tracing::{Event, Subscriber};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
@@ -252,3 +254,18 @@ where
|
||||
writeln!(writer)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn setup_metrics(dest: Option<&SystemPathBuf>) {
|
||||
// If --metrics is not provided at all, don't collect any metrics.
|
||||
let Some(dest) = dest else {
|
||||
return;
|
||||
};
|
||||
|
||||
let dest = OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(dest.as_std_path())
|
||||
.expect("cannot open metrics file");
|
||||
let recorder = JsonRecorder::new(dest);
|
||||
metrics::set_global_recorder(recorder).expect("metrics recorder already registered");
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::io::{self, BufWriter, Write};
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::args::{Args, CheckCommand, Command};
|
||||
use crate::logging::setup_tracing;
|
||||
use crate::logging::{setup_metrics, setup_tracing};
|
||||
use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
@@ -12,7 +15,7 @@ use red_knot_project::watch;
|
||||
use red_knot_project::watch::ProjectWatcher;
|
||||
use red_knot_project::{ProjectDatabase, ProjectMetadata};
|
||||
use red_knot_server::run_server;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
@@ -20,6 +23,7 @@ mod args;
|
||||
mod logging;
|
||||
mod python_version;
|
||||
mod verbosity;
|
||||
mod version;
|
||||
|
||||
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
|
||||
pub fn main() -> ExitStatus {
|
||||
@@ -49,13 +53,22 @@ fn run() -> anyhow::Result<ExitStatus> {
|
||||
match args.command {
|
||||
Command::Server => run_server().map(|()| ExitStatus::Success),
|
||||
Command::Check(check_args) => run_check(check_args),
|
||||
Command::Version => version().map(|()| ExitStatus::Success),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn version() -> Result<()> {
|
||||
let mut stdout = BufWriter::new(io::stdout().lock());
|
||||
let version_info = crate::version::version();
|
||||
writeln!(stdout, "red knot {}", &version_info)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
let verbosity = args.verbosity.level();
|
||||
countme::enable(verbosity.is_trace());
|
||||
let _guard = setup_tracing(verbosity)?;
|
||||
setup_metrics(args.metrics.as_ref());
|
||||
|
||||
// The base path to which all CLI arguments are relative to.
|
||||
let cli_base_path = {
|
||||
@@ -84,13 +97,20 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
let min_error_severity = if args.error_on_warning {
|
||||
Severity::Warning
|
||||
} else {
|
||||
Severity::Error
|
||||
};
|
||||
|
||||
let cli_options = args.into_options();
|
||||
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
|
||||
workspace_metadata.apply_cli_options(cli_options.clone());
|
||||
|
||||
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
@@ -112,7 +132,11 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
|
||||
std::mem::forget(db);
|
||||
|
||||
Ok(exit_status)
|
||||
if exit_zero {
|
||||
Ok(ExitStatus::Success)
|
||||
} else {
|
||||
Ok(exit_status)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
@@ -144,10 +168,18 @@ struct MainLoop {
|
||||
watcher: Option<ProjectWatcher>,
|
||||
|
||||
cli_options: Options,
|
||||
|
||||
/// The minimum severity to consider an error when deciding the exit status.
|
||||
///
|
||||
/// TODO(micha): Get from the terminal settings.
|
||||
min_error_severity: Severity,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
fn new(
|
||||
cli_options: Options,
|
||||
min_error_severity: Severity,
|
||||
) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
@@ -156,6 +188,7 @@ impl MainLoop {
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
min_error_severity,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
@@ -213,7 +246,10 @@ impl MainLoop {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
let has_diagnostics = !result.is_empty();
|
||||
let failed = result
|
||||
.iter()
|
||||
.any(|diagnostic| diagnostic.severity() >= self.min_error_severity);
|
||||
|
||||
if check_revision == revision {
|
||||
#[allow(clippy::print_stdout)]
|
||||
for diagnostic in result {
|
||||
@@ -226,7 +262,7 @@ impl MainLoop {
|
||||
}
|
||||
|
||||
if self.watcher.is_none() {
|
||||
return if has_diagnostics {
|
||||
return if failed {
|
||||
ExitStatus::Failure
|
||||
} else {
|
||||
ExitStatus::Success
|
||||
|
||||
105
crates/red_knot/src/version.rs
Normal file
105
crates/red_knot/src/version.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Code for representing Red Knot's release version number.
|
||||
use std::fmt;
|
||||
|
||||
/// Information about the git repository where Red Knot was built from.
|
||||
pub(crate) struct CommitInfo {
|
||||
short_commit_hash: String,
|
||||
commit_date: String,
|
||||
commits_since_last_tag: u32,
|
||||
}
|
||||
|
||||
/// Red Knot's version.
|
||||
pub(crate) struct VersionInfo {
|
||||
/// Red Knot's version, such as "0.5.1"
|
||||
version: String,
|
||||
/// Information about the git commit we may have been built from.
|
||||
///
|
||||
/// `None` if not built from a git repo or if retrieval failed.
|
||||
commit_info: Option<CommitInfo>,
|
||||
}
|
||||
|
||||
impl fmt::Display for VersionInfo {
|
||||
/// Formatted version information: `<version>[+<commits>] (<commit> <date>)`
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.version)?;
|
||||
|
||||
if let Some(ref ci) = self.commit_info {
|
||||
if ci.commits_since_last_tag > 0 {
|
||||
write!(f, "+{}", ci.commits_since_last_tag)?;
|
||||
}
|
||||
write!(f, " ({} {})", ci.short_commit_hash, ci.commit_date)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns information about Red Knot's version.
|
||||
pub(crate) fn version() -> VersionInfo {
|
||||
// Environment variables are only read at compile-time
|
||||
macro_rules! option_env_str {
|
||||
($name:expr) => {
|
||||
option_env!($name).map(|s| s.to_string())
|
||||
};
|
||||
}
|
||||
|
||||
// This version is pulled from Cargo.toml and set by Cargo
|
||||
let version = option_env_str!("CARGO_PKG_VERSION").unwrap();
|
||||
|
||||
// Commit info is pulled from git and set by `build.rs`
|
||||
let commit_info =
|
||||
option_env_str!("RED_KNOT_COMMIT_SHORT_HASH").map(|short_commit_hash| CommitInfo {
|
||||
short_commit_hash,
|
||||
commit_date: option_env_str!("RED_KNOT_COMMIT_DATE").unwrap(),
|
||||
commits_since_last_tag: option_env_str!("RED_KNOT_LAST_TAG_DISTANCE")
|
||||
.as_deref()
|
||||
.map_or(0, |value| value.parse::<u32>().unwrap_or(0)),
|
||||
});
|
||||
|
||||
VersionInfo {
|
||||
version,
|
||||
commit_info,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use super::{CommitInfo, VersionInfo};
|
||||
|
||||
#[test]
|
||||
fn version_formatting() {
|
||||
let version = VersionInfo {
|
||||
version: "0.0.0".to_string(),
|
||||
commit_info: None,
|
||||
};
|
||||
assert_snapshot!(version, @"0.0.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_formatting_with_commit_info() {
|
||||
let version = VersionInfo {
|
||||
version: "0.0.0".to_string(),
|
||||
commit_info: Some(CommitInfo {
|
||||
short_commit_hash: "53b0f5d92".to_string(),
|
||||
commit_date: "2023-10-19".to_string(),
|
||||
commits_since_last_tag: 0,
|
||||
}),
|
||||
};
|
||||
assert_snapshot!(version, @"0.0.0 (53b0f5d92 2023-10-19)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_formatting_with_commits_since_last_tag() {
|
||||
let version = VersionInfo {
|
||||
version: "0.0.0".to_string(),
|
||||
commit_info: Some(CommitInfo {
|
||||
short_commit_hash: "53b0f5d92".to_string(),
|
||||
commit_date: "2023-10-19".to_string(),
|
||||
commits_since_last_tag: 24,
|
||||
}),
|
||||
};
|
||||
assert_snapshot!(version, @"0.0.0+24 (53b0f5d92 2023-10-19)");
|
||||
}
|
||||
}
|
||||
@@ -28,14 +28,21 @@ fn config_override() -> anyhow::Result<()> {
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-attribute] <temp_dir>/test.py:5:7 Type `<module 'sys'>` has no attribute `last_exc`
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-attribute
|
||||
--> <temp_dir>/test.py:5:7
|
||||
|
|
||||
4 | # Access `sys.last_exc` that was only added in Python 3.12
|
||||
5 | print(sys.last_exc)
|
||||
| ^^^^^^^^^^^^ Type `<module 'sys'>` has no attribute `last_exc`
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
|
||||
success: true
|
||||
@@ -91,14 +98,22 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
])?;
|
||||
|
||||
// Make sure that the CLI fails when the `libs` directory is not in the search path.
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-import] <temp_dir>/child/test.py:2:1 Cannot resolve import `utils`
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import
|
||||
--> <temp_dir>/child/test.py:2:6
|
||||
|
|
||||
2 | from utils import add
|
||||
| ^^^^^ Cannot resolve import `utils`
|
||||
3 |
|
||||
4 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
"#);
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
success: true
|
||||
@@ -180,15 +195,31 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:division-by-zero
|
||||
--> <temp_dir>/test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
warning: lint:possibly-unresolved-reference
|
||||
--> <temp_dir>/test.py:7:7
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
case.write_file(
|
||||
"pyproject.toml",
|
||||
@@ -199,14 +230,22 @@ fn configuration_rule_severity() -> anyhow::Result<()> {
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: lint:division-by-zero
|
||||
--> <temp_dir>/test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -230,16 +269,42 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:unresolved-import] <temp_dir>/test.py:2:8 Cannot resolve import `does_not_exit`
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:4:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:9:7 Name `x` used when possibly not defined
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import
|
||||
--> <temp_dir>/test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
| ^^^^^^^^^^^^^ Cannot resolve import `does_not_exit`
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
error: lint:division-by-zero
|
||||
--> <temp_dir>/test.py:4:5
|
||||
|
|
||||
2 | import does_not_exit
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
5 |
|
||||
6 | for a in range(0, y):
|
||||
|
|
||||
|
||||
warning: lint:possibly-unresolved-reference
|
||||
--> <temp_dir>/test.py:9:7
|
||||
|
|
||||
7 | x = a
|
||||
8 |
|
||||
9 | print(x) # possibly-unresolved-reference
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case
|
||||
@@ -250,15 +315,33 @@ fn cli_rule_severity() -> anyhow::Result<()> {
|
||||
.arg("division-by-zero")
|
||||
.arg("--warn")
|
||||
.arg("unresolved-import"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
@r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning[lint:unresolved-import] <temp_dir>/test.py:2:8 Cannot resolve import `does_not_exit`
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:4:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning: lint:unresolved-import
|
||||
--> <temp_dir>/test.py:2:8
|
||||
|
|
||||
2 | import does_not_exit
|
||||
| ------------- Cannot resolve import `does_not_exit`
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
|
|
||||
|
||||
warning: lint:division-by-zero
|
||||
--> <temp_dir>/test.py:4:5
|
||||
|
|
||||
2 | import does_not_exit
|
||||
3 |
|
||||
4 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
5 |
|
||||
6 | for a in range(0, y):
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -282,15 +365,31 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
|
||||
// Assert that there's a possibly unresolved reference diagnostic
|
||||
// and that division-by-zero has a severity of error by default.
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
warning[lint:possibly-unresolved-reference] <temp_dir>/test.py:7:7 Name `x` used when possibly not defined
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:division-by-zero
|
||||
--> <temp_dir>/test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ^^^^^ Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
warning: lint:possibly-unresolved-reference
|
||||
--> <temp_dir>/test.py:7:7
|
||||
|
|
||||
5 | x = a
|
||||
6 |
|
||||
7 | print(x) # possibly-unresolved-reference
|
||||
| - Name `x` used when possibly not defined
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
assert_cmd_snapshot!(
|
||||
case
|
||||
@@ -302,14 +401,22 @@ fn cli_rule_severity_precedence() -> anyhow::Result<()> {
|
||||
// Override the error severity with warning
|
||||
.arg("--ignore")
|
||||
.arg("possibly-unresolved-reference"),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[lint:division-by-zero] <temp_dir>/test.py:2:5 Cannot divide object of type `Literal[4]` by zero
|
||||
@r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: lint:division-by-zero
|
||||
--> <temp_dir>/test.py:2:5
|
||||
|
|
||||
2 | y = 4 / 0
|
||||
| ----- Cannot divide object of type `Literal[4]` by zero
|
||||
3 |
|
||||
4 | for a in range(0, y):
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
"
|
||||
|
||||
----- stderr -----
|
||||
"###
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -329,14 +436,21 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
("test.py", "print(10)"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unknown-rule] <temp_dir>/pyproject.toml:3:1 Unknown lint rule `division-by-zer`
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: unknown-rule
|
||||
--> <temp_dir>/pyproject.toml:3:1
|
||||
|
|
||||
2 | [tool.knot.rules]
|
||||
3 | division-by-zer = "warn" # incorrect rule name
|
||||
| --------------- Unknown lint rule `division-by-zer`
|
||||
|
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -346,14 +460,228 @@ fn configuration_unknown_rules() -> anyhow::Result<()> {
|
||||
fn cli_unknown_rules() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", "print(10)")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning[unknown-rule] Unknown lint rule `division-by-zer`
|
||||
assert_cmd_snapshot!(case.command().arg("--ignore").arg("division-by-zer"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: unknown-rule: Unknown lint rule `division-by-zer`
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_code_only_warnings() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference
|
||||
--> <temp_dir>/test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_code_only_info() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
reveal_type(1)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type
|
||||
--> <temp_dir>/test.py:3:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| -------------- info: Revealed type is `Literal[1]`
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_code_only_info_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
from typing_extensions import reveal_type
|
||||
reveal_type(1)
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
info: revealed-type
|
||||
--> <temp_dir>/test.py:3:1
|
||||
|
|
||||
2 | from typing_extensions import reveal_type
|
||||
3 | reveal_type(1)
|
||||
| -------------- info: Revealed type is `Literal[1]`
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file("test.py", r"print(x) # [unresolved-reference]")?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference
|
||||
--> <temp_dir>/test.py:1:7
|
||||
|
|
||||
1 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference
|
||||
--> <temp_dir>/test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
|
||||
error: lint:non-subscriptable
|
||||
--> <temp_dir>/test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_code_both_warnings_and_errors_and_error_on_warning_is_true() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r###"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"###,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--error-on-warning"), @r###"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference
|
||||
--> <temp_dir>/test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
|
||||
error: lint:non-subscriptable
|
||||
--> <temp_dir>/test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
|
||||
let case = TestCase::with_file(
|
||||
"test.py",
|
||||
r#"
|
||||
print(x) # [unresolved-reference]
|
||||
print(4[1]) # [non-subscriptable]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--exit-zero"), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
warning: lint:unresolved-reference
|
||||
--> <temp_dir>/test.py:2:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
| - Name `x` used when not defined
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
|
|
||||
|
||||
error: lint:non-subscriptable
|
||||
--> <temp_dir>/test.py:3:7
|
||||
|
|
||||
2 | print(x) # [unresolved-reference]
|
||||
3 | print(4[1]) # [non-subscriptable]
|
||||
| ^ Cannot subscript object of type `Literal[4]` with no `__getitem__` method
|
||||
|
|
||||
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -270,6 +270,8 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
|
||||
/// Whether or not the .py/.pyi version of this file is expected to fail
|
||||
#[rustfmt::skip]
|
||||
const KNOWN_FAILURES: &[(&str, bool, bool)] = &[
|
||||
// related to circular references in nested functions
|
||||
("crates/ruff_linter/resources/test/fixtures/flake8_return/RET503.py", false, true),
|
||||
// related to circular references in class definitions
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true),
|
||||
|
||||
@@ -30,6 +30,7 @@ countme = { workspace = true }
|
||||
drop_bomb = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
metrics = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Deferred annotations in stubs always resolve
|
||||
|
||||
```pyi path=mod.pyi
|
||||
`mod.pyi`:
|
||||
|
||||
```pyi
|
||||
def get_foo() -> Foo: ...
|
||||
class Foo: ...
|
||||
```
|
||||
|
||||
@@ -106,7 +106,7 @@ def union_example(
|
||||
Literal["B"],
|
||||
Literal[True],
|
||||
None,
|
||||
]
|
||||
],
|
||||
):
|
||||
reveal_type(x) # revealed: Unknown | Literal[-1, "A", b"A", b"\x00", b"\x07", 0, 1, "B", "foo", "bar", True] | None
|
||||
```
|
||||
@@ -116,7 +116,9 @@ def union_example(
|
||||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
Literal.
|
||||
|
||||
```pyi path=other.pyi
|
||||
`other.pyi`:
|
||||
|
||||
```pyi
|
||||
from typing import _SpecialForm
|
||||
|
||||
Literal: _SpecialForm
|
||||
|
||||
@@ -25,7 +25,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]` is not
|
||||
|
||||
## Tuple annotations are understood
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing_extensions import Unpack
|
||||
|
||||
a: tuple[()] = ()
|
||||
@@ -40,7 +42,9 @@ i: tuple[str | int, str | int] = (42, 42)
|
||||
j: tuple[str | int] = (42,)
|
||||
```
|
||||
|
||||
```py path=script.py
|
||||
`script.py`:
|
||||
|
||||
```py
|
||||
from module import a, b, c, d, e, f, g, h, i, j
|
||||
|
||||
reveal_type(a) # revealed: tuple[()]
|
||||
@@ -114,7 +118,7 @@ reveal_type(x) # revealed: Foo
|
||||
|
||||
## Annotations in stub files are deferred
|
||||
|
||||
```pyi path=main.pyi
|
||||
```pyi
|
||||
x: Foo
|
||||
|
||||
class Foo: ...
|
||||
@@ -125,7 +129,7 @@ reveal_type(x) # revealed: Foo
|
||||
|
||||
## Annotated assignments in stub files are inferred correctly
|
||||
|
||||
```pyi path=main.pyi
|
||||
```pyi
|
||||
x: int = 1
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
@@ -13,123 +13,90 @@ accessed on the class itself.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, value2: int, flag: bool = False) -> None:
|
||||
# bound but not declared
|
||||
self.pure_instance_variable1 = "value set in __init__"
|
||||
|
||||
# bound but not declared - with type inferred from parameter
|
||||
self.pure_instance_variable2 = value2
|
||||
|
||||
# declared but not bound
|
||||
self.pure_instance_variable3: bytes
|
||||
|
||||
# declared and bound
|
||||
self.pure_instance_variable4: bool = True
|
||||
|
||||
# possibly undeclared/unbound
|
||||
def __init__(self, param: int | None, flag: bool = False) -> None:
|
||||
value = 1 if flag else "a"
|
||||
self.inferred_from_value = value
|
||||
self.inferred_from_other_attribute = self.inferred_from_value
|
||||
self.inferred_from_param = param
|
||||
self.declared_only: bytes
|
||||
self.declared_and_bound: bool = True
|
||||
if flag:
|
||||
self.pure_instance_variable5: str = "possibly set in __init__"
|
||||
self.possibly_undeclared_unbound: str = "possibly set in __init__"
|
||||
|
||||
c_instance = C(1)
|
||||
|
||||
# TODO: should be `Literal["value set in __init__"]`, or `Unknown | Literal[…]` to allow
|
||||
# assignments to this unannotated attribute from other scopes.
|
||||
reveal_type(c_instance.pure_instance_variable1) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
|
||||
|
||||
# TODO: should be `int`
|
||||
reveal_type(c_instance.pure_instance_variable2) # revealed: @Todo(implicit instance attribute)
|
||||
# TODO: Same here. This should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
|
||||
# TODO: should be `bytes`
|
||||
reveal_type(c_instance.pure_instance_variable3) # revealed: @Todo(implicit instance attribute)
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||
|
||||
# TODO: should be `bool`
|
||||
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||
|
||||
# TODO: should be `str`
|
||||
# We probably don't want to emit a diagnostic for this being possibly undeclared/unbound.
|
||||
# mypy and pyright do not show an error here.
|
||||
reveal_type(c_instance.pure_instance_variable5) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.possibly_undeclared_unbound) # revealed: str
|
||||
|
||||
# TODO: If we choose to infer a precise `Literal[…]` type for the instance attribute (see
|
||||
# above), this should be an error: incompatible types in assignment. If we choose to infer
|
||||
# a gradual `Unknown | Literal[…]` type, this assignment is fine.
|
||||
c_instance.pure_instance_variable1 = "value set on instance"
|
||||
# This assignment is fine, as we infer `Unknown | Literal[1, "a"]` for `inferred_from_value`.
|
||||
c_instance.inferred_from_value = "value set on instance"
|
||||
|
||||
# This assignment is also fine:
|
||||
c_instance.inferred_from_param = None
|
||||
|
||||
# TODO: this should be an error (incompatible types in assignment)
|
||||
c_instance.pure_instance_variable2 = "incompatible"
|
||||
c_instance.inferred_from_param = "incompatible"
|
||||
|
||||
# TODO: we already show an error here but the message might be improved?
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
# error: [unresolved-attribute] "Type `Literal[C]` has no attribute `pure_instance_variable1`"
|
||||
reveal_type(C.pure_instance_variable1) # revealed: Unknown
|
||||
# error: [unresolved-attribute] "Type `Literal[C]` has no attribute `inferred_from_value`"
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error (pure instance variables cannot be accessed on the class)
|
||||
# mypy shows no error here, but pyright raises "reportAttributeAccessIssue"
|
||||
C.pure_instance_variable1 = "overwritten on class"
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
|
||||
c_instance.pure_instance_variable4 = False
|
||||
# This assignment is fine:
|
||||
c_instance.declared_and_bound = False
|
||||
|
||||
# TODO: After this assignment to the attribute within this scope, we may eventually want to narrow
|
||||
# the `bool` type (see above) for this instance variable to `Literal[False]` here. This is unsound
|
||||
# in general (we don't know what else happened to `c_instance` between the assignment and the use
|
||||
# here), but mypy and pyright support this. In conclusion, this could be `bool` but should probably
|
||||
# be `Literal[False]`.
|
||||
reveal_type(c_instance.pure_instance_variable4) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||
```
|
||||
|
||||
#### Variable declared in class body and declared/bound in `__init__`
|
||||
#### Variable declared in class body and possibly bound in `__init__`
|
||||
|
||||
The same rule applies even if the variable is *declared* (not bound!) in the class body: it is still
|
||||
a pure instance variable.
|
||||
|
||||
```py
|
||||
class C:
|
||||
pure_instance_variable: str
|
||||
declared_and_bound: str | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.pure_instance_variable = "value set in __init__"
|
||||
self.declared_and_bound = "value set in __init__"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: str
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: str | None
|
||||
|
||||
# TODO: we currently plan to emit a diagnostic here. Note that both mypy
|
||||
# and pyright show no error in this case! So we may reconsider this in
|
||||
# the future, if it turns out to produce too many false positives.
|
||||
reveal_type(C.pure_instance_variable) # revealed: str
|
||||
reveal_type(C.declared_and_bound) # revealed: str | None
|
||||
|
||||
# TODO: same as above. We plan to emit a diagnostic here, even if both mypy
|
||||
# and pyright allow this.
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
C.declared_and_bound = "overwritten on class"
|
||||
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `pure_instance_variable` of type `str`"
|
||||
c_instance.pure_instance_variable = 1
|
||||
```
|
||||
|
||||
#### Variable only defined in unrelated method
|
||||
|
||||
We also recognize pure instance variables if they are defined in a method that is not `__init__`.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def set_instance_variable(self) -> None:
|
||||
self.pure_instance_variable = "value set in method"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# Not that we would use this in static analysis, but for a more realistic example, let's actually
|
||||
# call the method, so that the attribute is bound if this example is actually run.
|
||||
c_instance.set_instance_variable()
|
||||
|
||||
# TODO: should be `Literal["value set in method"]` or `Unknown | Literal[…]` (see above).
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: @Todo(implicit instance attribute)
|
||||
|
||||
# TODO: We already show an error here, but the message might be improved?
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_instance_variable) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to attribute `declared_and_bound` of type `str | None`"
|
||||
c_instance.declared_and_bound = 1
|
||||
```
|
||||
|
||||
#### Variable declared in class body and not bound anywhere
|
||||
@@ -139,18 +106,341 @@ instance variable and allow access to it via instances.
|
||||
|
||||
```py
|
||||
class C:
|
||||
pure_instance_variable: str
|
||||
only_declared: str
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.pure_instance_variable) # revealed: str
|
||||
reveal_type(c_instance.only_declared) # revealed: str
|
||||
|
||||
# TODO: mypy and pyright do not show an error here, but we plan to emit a diagnostic.
|
||||
# The type could be changed to 'Unknown' if we decide to emit an error?
|
||||
reveal_type(C.pure_instance_variable) # revealed: str
|
||||
reveal_type(C.only_declared) # revealed: str
|
||||
|
||||
# TODO: mypy and pyright do not show an error here, but we plan to emit one.
|
||||
C.pure_instance_variable = "overwritten on class"
|
||||
C.only_declared = "overwritten on class"
|
||||
```
|
||||
|
||||
#### Mixed declarations/bindings in class body and `__init__`
|
||||
|
||||
```py
|
||||
class C:
|
||||
only_declared_in_body: str | None
|
||||
declared_in_body_and_init: str | None
|
||||
|
||||
declared_in_body_defined_in_init: str | None
|
||||
|
||||
bound_in_body_declared_in_init = "a"
|
||||
|
||||
bound_in_body_and_init = None
|
||||
|
||||
def __init__(self, flag) -> None:
|
||||
self.only_declared_in_init: str | None
|
||||
self.declared_in_body_and_init: str | None = None
|
||||
|
||||
self.declared_in_body_defined_in_init = "a"
|
||||
|
||||
self.bound_in_body_declared_in_init: str | None
|
||||
|
||||
if flag:
|
||||
self.bound_in_body_and_init = "a"
|
||||
|
||||
c_instance = C(True)
|
||||
|
||||
reveal_type(c_instance.only_declared_in_body) # revealed: str | None
|
||||
reveal_type(c_instance.only_declared_in_init) # revealed: str | None
|
||||
reveal_type(c_instance.declared_in_body_and_init) # revealed: str | None
|
||||
|
||||
reveal_type(c_instance.declared_in_body_defined_in_init) # revealed: str | None
|
||||
|
||||
reveal_type(c_instance.bound_in_body_declared_in_init) # revealed: str | None
|
||||
|
||||
reveal_type(c_instance.bound_in_body_and_init) # revealed: Unknown | None | Literal["a"]
|
||||
```
|
||||
|
||||
#### Variable defined in non-`__init__` method
|
||||
|
||||
We also recognize pure instance variables if they are defined in a method that is not `__init__`.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, param: int | None, flag: bool = False) -> None:
|
||||
self.initialize(param, flag)
|
||||
|
||||
def initialize(self, param: int | None, flag: bool) -> None:
|
||||
value = 1 if flag else "a"
|
||||
self.inferred_from_value = value
|
||||
self.inferred_from_other_attribute = self.inferred_from_value
|
||||
self.inferred_from_param = param
|
||||
self.declared_only: bytes
|
||||
self.declared_and_bound: bool = True
|
||||
|
||||
c_instance = C(1)
|
||||
|
||||
reveal_type(c_instance.inferred_from_value) # revealed: Unknown | Literal[1, "a"]
|
||||
|
||||
# TODO: Should be `Unknown | Literal[1, "a"]`
|
||||
reveal_type(c_instance.inferred_from_other_attribute) # revealed: Unknown
|
||||
|
||||
# TODO: Should be `int | None`
|
||||
reveal_type(c_instance.inferred_from_param) # revealed: Unknown | int | None
|
||||
|
||||
reveal_type(c_instance.declared_only) # revealed: bytes
|
||||
|
||||
reveal_type(c_instance.declared_and_bound) # revealed: bool
|
||||
|
||||
# TODO: We already show an error here, but the message might be improved?
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.inferred_from_value) # revealed: Unknown
|
||||
|
||||
# TODO: this should be an error
|
||||
C.inferred_from_value = "overwritten on class"
|
||||
```
|
||||
|
||||
#### Variable defined in multiple methods
|
||||
|
||||
If we see multiple un-annotated assignments to a single attribute (`self.x` below), we build the
|
||||
union of all inferred types (and `Unknown`). If we see multiple conflicting declarations of the same
|
||||
attribute, that should be an error.
|
||||
|
||||
```py
|
||||
def get_int() -> int:
|
||||
return 0
|
||||
|
||||
def get_str() -> str:
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
z: int
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.x = get_int()
|
||||
self.y: int = 1
|
||||
|
||||
def other_method(self):
|
||||
self.x = get_str()
|
||||
|
||||
# TODO: this redeclaration should be an error
|
||||
self.y: str = "a"
|
||||
|
||||
# TODO: this redeclaration should be an error
|
||||
self.z: str = "a"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.x) # revealed: Unknown | int | str
|
||||
reveal_type(c_instance.y) # revealed: int
|
||||
reveal_type(c_instance.z) # revealed: int
|
||||
```
|
||||
|
||||
#### Attributes defined in tuple unpackings
|
||||
|
||||
```py
|
||||
def returns_tuple() -> tuple[int, str]:
|
||||
return (1, "a")
|
||||
|
||||
class C:
|
||||
a1, b1 = (1, "a")
|
||||
c1, d1 = returns_tuple()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.a2, self.b2 = (1, "a")
|
||||
self.c2, self.d2 = returns_tuple()
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a1) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b1) # revealed: Unknown | Literal["a"]
|
||||
reveal_type(c_instance.c1) # revealed: Unknown | int
|
||||
reveal_type(c_instance.d1) # revealed: Unknown | str
|
||||
|
||||
# TODO: This should be supported (no error; type should be: `Unknown | Literal[1]`)
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a2) # revealed: Unknown
|
||||
|
||||
# TODO: This should be supported (no error; type should be: `Unknown | Literal["a"]`)
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.b2) # revealed: Unknown
|
||||
|
||||
# TODO: Similar for these two (should be `Unknown | int` and `Unknown | str`, respectively)
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.c2) # revealed: Unknown
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.d2) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in for-loop (unpacking)
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 1
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
class TupleIterator:
|
||||
def __next__(self) -> tuple[int, str]:
|
||||
return (1, "a")
|
||||
|
||||
class TupleIterable:
|
||||
def __iter__(self) -> TupleIterator:
|
||||
return TupleIterator()
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
for self.x in IntIterable():
|
||||
pass
|
||||
|
||||
for _, self.y in TupleIterable():
|
||||
pass
|
||||
|
||||
# TODO: Pyright fully supports these, mypy detects the presence of the attributes,
|
||||
# but infers type `Any` for both of them. We should infer `int` and `str` here:
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().y) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Conditionally declared / bound attributes
|
||||
|
||||
We currently do not raise a diagnostic or change behavior if an attribute is only conditionally
|
||||
defined. This is consistent with what mypy and pyright do.
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class C:
|
||||
def f(self) -> None:
|
||||
if flag():
|
||||
self.a1: str | None = "a"
|
||||
self.b1 = 1
|
||||
if flag():
|
||||
def f(self) -> None:
|
||||
self.a2: str | None = "a"
|
||||
self.b2 = 1
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a1) # revealed: str | None
|
||||
reveal_type(c_instance.a2) # revealed: str | None
|
||||
reveal_type(c_instance.b1) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b2) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
#### Methods that does not use `self` as a first parameter
|
||||
|
||||
```py
|
||||
class C:
|
||||
# This might trigger a stylistic lint like `invalid-first-argument-name-for-method`, but
|
||||
# it should be supported in general:
|
||||
def __init__(this) -> None:
|
||||
this.declared_and_bound: str | None = "a"
|
||||
|
||||
reveal_type(C().declared_and_bound) # revealed: str | None
|
||||
```
|
||||
|
||||
#### Aliased `self` parameter
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
this = self
|
||||
this.declared_and_bound: str | None = "a"
|
||||
|
||||
# This would ideally be `str | None`, but mypy/pyright don't support this either,
|
||||
# so `Unknown` + a diagnostic is also fine.
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().declared_and_bound) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Static methods do not influence implicitly defined attributes
|
||||
|
||||
```py
|
||||
class Other:
|
||||
x: int
|
||||
|
||||
class C:
|
||||
@staticmethod
|
||||
def f(other: Other) -> None:
|
||||
other.x = 1
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
|
||||
# This also works if `staticmethod` is aliased:
|
||||
|
||||
my_staticmethod = staticmethod
|
||||
|
||||
class D:
|
||||
@my_staticmethod
|
||||
def f(other: Other) -> None:
|
||||
other.x = 1
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(D.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(D().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
If `staticmethod` is something else, that should not influence the behavior:
|
||||
|
||||
```py
|
||||
def staticmethod(f):
|
||||
return f
|
||||
|
||||
class C:
|
||||
@staticmethod
|
||||
def f(self) -> None:
|
||||
self.x = 1
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
And if `staticmethod` is fully qualified, that should also be recognized:
|
||||
|
||||
```py
|
||||
import builtins
|
||||
|
||||
class Other:
|
||||
x: int
|
||||
|
||||
class C:
|
||||
@builtins.staticmethod
|
||||
def f(other: Other) -> None:
|
||||
other.x = 1
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
#### Attributes defined in statically-known-to-be-false branches
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
# We use a "significantly complex" condition here (instead of just `False`)
|
||||
# for a proper comparison with mypy and pyright, which distinguish between
|
||||
# conditions that can be resolved from a simple pattern matching and those
|
||||
# that need proper type inference.
|
||||
if (2 + 3) < 4:
|
||||
self.x: str = "a"
|
||||
|
||||
# TODO: Ideally, this would result in a `unresolved-attribute` error. But mypy and pyright
|
||||
# do not support this either (for conditions that can only be resolved to `False` in type
|
||||
# inference), so it does not seem to be particularly important.
|
||||
reveal_type(C().x) # revealed: str
|
||||
```
|
||||
|
||||
### Pure class variables (`ClassVar`)
|
||||
@@ -221,13 +511,13 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
C.pure_class_variable = "overwritten on class"
|
||||
|
||||
# TODO: should be `Literal["overwritten on class"]`
|
||||
# TODO: should be `Unknown | Literal["value set in class method"]` or
|
||||
# Literal["overwritten on class"]`, once/if we support local narrowing.
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
c_instance = C()
|
||||
# TODO: should be `Literal["overwritten on class"]`
|
||||
reveal_type(c_instance.pure_class_variable) # revealed: @Todo(implicit instance attribute)
|
||||
reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
|
||||
|
||||
# TODO: should raise an error.
|
||||
c_instance.pure_class_variable = "value set on instance"
|
||||
@@ -277,6 +567,53 @@ reveal_type(C.variable_with_class_default1) # revealed: str
|
||||
reveal_type(c_instance.variable_with_class_default1) # revealed: str
|
||||
```
|
||||
|
||||
### Inheritance of class/instance attributes
|
||||
|
||||
#### Instance variable defined in a base class
|
||||
|
||||
```py
|
||||
class Base:
|
||||
declared_in_body: int | None = 1
|
||||
|
||||
base_class_attribute_1: str | None
|
||||
base_class_attribute_2: str | None
|
||||
base_class_attribute_3: str | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.defined_in_init: str | None = "value in base"
|
||||
|
||||
class Intermediate(Base):
|
||||
# Re-declaring base class attributes with the *same *type is fine:
|
||||
base_class_attribute_1: str | None = None
|
||||
|
||||
# Re-declaring them with a *narrower type* is unsound, because modifications
|
||||
# through a `Base` reference could violate that constraint.
|
||||
#
|
||||
# Mypy does not report an error here, but pyright does: "… overrides symbol
|
||||
# of same name in class "Base". Variable is mutable so its type is invariant"
|
||||
#
|
||||
# We should introduce a diagnostic for this. Whether or not that should be
|
||||
# enabled by default can still be discussed.
|
||||
#
|
||||
# TODO: This should be an error
|
||||
base_class_attribute_2: str
|
||||
|
||||
# Re-declaring attributes with a *wider type* directly violates LSP.
|
||||
#
|
||||
# In this case, both mypy and pyright report an error.
|
||||
#
|
||||
# TODO: This should be an error
|
||||
base_class_attribute_3: str | int | None
|
||||
|
||||
class Derived(Intermediate): ...
|
||||
|
||||
reveal_type(Derived.declared_in_body) # revealed: int | None
|
||||
|
||||
reveal_type(Derived().declared_in_body) # revealed: int | None
|
||||
|
||||
reveal_type(Derived().defined_in_init) # revealed: str | None
|
||||
```
|
||||
|
||||
## Union of attributes
|
||||
|
||||
```py
|
||||
@@ -437,7 +774,9 @@ reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
|
||||
## Module attributes
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
global_symbol: str = "a"
|
||||
```
|
||||
|
||||
@@ -471,13 +810,19 @@ for mod.global_symbol in IntIterable():
|
||||
|
||||
## Nested attributes
|
||||
|
||||
```py path=outer/__init__.py
|
||||
`outer/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=outer/nested/__init__.py
|
||||
`outer/nested/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=outer/nested/inner.py
|
||||
`outer/nested/inner.py`:
|
||||
|
||||
```py
|
||||
class Outer:
|
||||
class Nested:
|
||||
class Inner:
|
||||
@@ -500,7 +845,7 @@ outer.nested.inner.Outer.Nested.Inner.attr = "a"
|
||||
Most attribute accesses on function-literal types are delegated to `types.FunctionType`, since all
|
||||
functions are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
reveal_type(f.__defaults__) # revealed: @Todo(full tuple[...] support) | None
|
||||
@@ -509,9 +854,7 @@ reveal_type(f.__kwdefaults__) # revealed: @Todo(generics) | None
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
def f(): ...
|
||||
|
||||
```py
|
||||
reveal_type(f.__get__) # revealed: @Todo(`__get__` method on functions)
|
||||
reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
```
|
||||
@@ -521,14 +864,14 @@ reveal_type(f.__call__) # revealed: @Todo(`__call__` method on functions)
|
||||
Most attribute accesses on int-literal types are delegated to `builtins.int`, since all literal
|
||||
integers are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
```py
|
||||
reveal_type((2).bit_length) # revealed: @Todo(bound method)
|
||||
reveal_type((2).denominator) # revealed: @Todo(@property)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
```py
|
||||
reveal_type((2).numerator) # revealed: Literal[2]
|
||||
reveal_type((2).real) # revealed: Literal[2]
|
||||
```
|
||||
@@ -538,14 +881,14 @@ reveal_type((2).real) # revealed: Literal[2]
|
||||
Most attribute accesses on bool-literal types are delegated to `builtins.bool`, since all literal
|
||||
bols are instances of that class:
|
||||
|
||||
```py path=a.py
|
||||
```py
|
||||
reveal_type(True.__and__) # revealed: @Todo(bound method)
|
||||
reveal_type(False.__or__) # revealed: @Todo(bound method)
|
||||
```
|
||||
|
||||
Some attributes are special-cased, however:
|
||||
|
||||
```py path=b.py
|
||||
```py
|
||||
reveal_type(True.numerator) # revealed: Literal[1]
|
||||
reveal_type(False.real) # revealed: Literal[0]
|
||||
```
|
||||
@@ -559,6 +902,90 @@ reveal_type(b"foo".join) # revealed: @Todo(bound method)
|
||||
reveal_type(b"foo".endswith) # revealed: @Todo(bound method)
|
||||
```
|
||||
|
||||
## Instance attribute edge cases
|
||||
|
||||
### Assignment to attribute that does not correspond to the instance
|
||||
|
||||
```py
|
||||
class Other:
|
||||
x: int = 1
|
||||
|
||||
class C:
|
||||
def __init__(self, other: Other) -> None:
|
||||
other.x = 1
|
||||
|
||||
def f(c: C):
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
### Nested classes
|
||||
|
||||
```py
|
||||
class Outer:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
|
||||
class Middle:
|
||||
# has no 'x' attribute
|
||||
|
||||
class Inner:
|
||||
def __init__(self):
|
||||
self.x: str = "a"
|
||||
|
||||
reveal_type(Outer().x) # revealed: int
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
Outer.Middle().x
|
||||
|
||||
reveal_type(Outer.Middle.Inner().x) # revealed: str
|
||||
```
|
||||
|
||||
### Shadowing of `self`
|
||||
|
||||
```py
|
||||
class Other:
|
||||
x: int = 1
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
# Redeclaration of self. `self` does not refer to the instance anymore.
|
||||
self: Other = Other()
|
||||
self.x: int = 1
|
||||
|
||||
# TODO: this should be an error
|
||||
C().x
|
||||
```
|
||||
|
||||
### Assignment to `self` after nested function
|
||||
|
||||
```py
|
||||
class Other:
|
||||
x: str = "a"
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
def nested_function(self: Other):
|
||||
self.x = "b"
|
||||
self.x: int = 1
|
||||
|
||||
reveal_type(C().x) # revealed: int
|
||||
```
|
||||
|
||||
### Assignment to `self` from nested function
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
def set_attribute(value: str):
|
||||
self.x: str = value
|
||||
set_attribute("a")
|
||||
|
||||
# TODO: ideally, this would be `str`. Mypy supports this, pyright does not.
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C().x) # revealed: Unknown
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
Some of the tests in the *Class and instance variables* section draw inspiration from
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
## Class instances
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
@@ -136,6 +138,8 @@ reveal_type(No() // Yes()) # revealed: Unknown
|
||||
## Subclass reflections override superclass dunders
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
@@ -294,6 +298,8 @@ itself. (For these operators to work on the class itself, they would have to be
|
||||
class's type, i.e. `type`.)
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
@@ -312,6 +318,8 @@ reveal_type(No + No) # revealed: Unknown
|
||||
## Subclass
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Yes:
|
||||
def __add__(self, other) -> Literal["+"]:
|
||||
return "+"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Boundness and declaredness: public uses
|
||||
|
||||
This document demonstrates how type-inference and diagnostics works for *public* uses of a symbol,
|
||||
This document demonstrates how type-inference and diagnostics work for *public* uses of a symbol,
|
||||
that is, a use of a symbol from another scope. If a symbol has a declared type in its local scope
|
||||
(e.g. `int`), we use that as the symbol's "public type" (the type of the symbol from the perspective
|
||||
of other scopes) even if there is a more precise local inferred type for the symbol (`Literal[1]`).
|
||||
@@ -34,20 +34,28 @@ In particular, we should raise errors in the "possibly-undeclared-and-unbound" a
|
||||
### Declared and bound
|
||||
|
||||
If a symbol has a declared type (`int`), we use that even if there is a more precise inferred type
|
||||
(`Literal[1]`), or a conflicting inferred type (`Literal[2]`):
|
||||
(`Literal[1]`), or a conflicting inferred type (`str` vs. `Literal[2]` below):
|
||||
|
||||
```py path=mod.py
|
||||
x: int = 1
|
||||
`mod.py`:
|
||||
|
||||
# error: [invalid-assignment]
|
||||
y: str = 2
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def any() -> Any: ...
|
||||
|
||||
a: int = 1
|
||||
b: str = 2 # error: [invalid-assignment]
|
||||
c: Any = 3
|
||||
d: int = any()
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y
|
||||
from mod import a, b, c, d
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: Any
|
||||
reveal_type(d) # revealed: int
|
||||
```
|
||||
|
||||
### Declared and possibly unbound
|
||||
@@ -55,22 +63,33 @@ reveal_type(y) # revealed: str
|
||||
If a symbol is declared and *possibly* unbound, we trust that other module and use the declared type
|
||||
without raising an error.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def any() -> Any: ...
|
||||
def flag() -> bool: ...
|
||||
|
||||
x: int
|
||||
y: str
|
||||
a: int
|
||||
b: str
|
||||
c: Any
|
||||
d: int
|
||||
|
||||
if flag:
|
||||
x = 1
|
||||
# error: [invalid-assignment]
|
||||
y = 2
|
||||
a = 1
|
||||
b = 2 # error: [invalid-assignment]
|
||||
c = 3
|
||||
d = any()
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y
|
||||
from mod import a, b, c, d
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: str
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: str
|
||||
reveal_type(c) # revealed: Any
|
||||
reveal_type(d) # revealed: int
|
||||
```
|
||||
|
||||
### Declared and unbound
|
||||
@@ -78,14 +97,20 @@ reveal_type(y) # revealed: str
|
||||
Similarly, if a symbol is declared but unbound, we do not raise an error. We trust that this symbol
|
||||
is available somehow and simply use the declared type.
|
||||
|
||||
```py path=mod.py
|
||||
x: int
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
a: int
|
||||
b: Any
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: Any
|
||||
```
|
||||
|
||||
## Possibly undeclared
|
||||
@@ -95,61 +120,70 @@ reveal_type(x) # revealed: int
|
||||
If a symbol is possibly undeclared but definitely bound, we use the union of the declared and
|
||||
inferred types:
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def any() -> Any: ...
|
||||
def flag() -> bool: ...
|
||||
|
||||
x = 1
|
||||
y = 2
|
||||
z = 3
|
||||
a = 1
|
||||
b = 2
|
||||
c = 3
|
||||
d = any()
|
||||
if flag():
|
||||
x: int
|
||||
y: Any
|
||||
# error: [invalid-declaration]
|
||||
z: str
|
||||
a: int
|
||||
b: Any
|
||||
c: str # error: [invalid-declaration]
|
||||
d: int
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x, y, z
|
||||
from mod import a, b, c, d
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(y) # revealed: Literal[2] | Any
|
||||
reveal_type(z) # revealed: Literal[3] | Unknown
|
||||
reveal_type(a) # revealed: int
|
||||
reveal_type(b) # revealed: Literal[2] | Any
|
||||
reveal_type(c) # revealed: Literal[3] | Unknown
|
||||
reveal_type(d) # revealed: Any | int
|
||||
|
||||
# External modifications of `x` that violate the declared type are not allowed:
|
||||
# External modifications of `a` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
x = None
|
||||
a = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and possibly unbound
|
||||
|
||||
If a symbol is possibly undeclared and possibly unbound, we also use the union of the declared and
|
||||
inferred types. This case is interesting because the "possibly declared" definition might not be the
|
||||
same as the "possibly bound" definition (symbol `y`). Note that we raise a `possibly-unbound-import`
|
||||
error for both `x` and `y`:
|
||||
same as the "possibly bound" definition (symbol `b`). Note that we raise a `possibly-unbound-import`
|
||||
error for both `a` and `b`:
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
```py path=mod.py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
x: Any = 1
|
||||
y = 2
|
||||
a: Any = 1
|
||||
b = 2
|
||||
else:
|
||||
y: str
|
||||
b: str
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [possibly-unbound-import]
|
||||
# error: [possibly-unbound-import]
|
||||
from mod import x, y
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(x) # revealed: Literal[1] | Any
|
||||
reveal_type(y) # revealed: Literal[2] | str
|
||||
reveal_type(a) # revealed: Literal[1] | Any
|
||||
reveal_type(b) # revealed: Literal[2] | str
|
||||
|
||||
# External modifications of `y` that violate the declared type are not allowed:
|
||||
# External modifications of `b` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
y = None
|
||||
b = None
|
||||
```
|
||||
|
||||
### Possibly undeclared and unbound
|
||||
@@ -157,40 +191,53 @@ y = None
|
||||
If a symbol is possibly undeclared and definitely unbound, we currently do not raise an error. This
|
||||
seems inconsistent when compared to the case just above.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
x: int
|
||||
a: int
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this should raise an error. Once we fix this, update the section description and the table
|
||||
# on top of this document.
|
||||
from mod import x
|
||||
from mod import a
|
||||
|
||||
reveal_type(x) # revealed: int
|
||||
reveal_type(a) # revealed: int
|
||||
|
||||
# External modifications to `x` that violate the declared type are not allowed:
|
||||
# External modifications to `a` that violate the declared type are not allowed:
|
||||
# error: [invalid-assignment]
|
||||
x = None
|
||||
a = None
|
||||
```
|
||||
|
||||
## Undeclared
|
||||
|
||||
### Undeclared but bound
|
||||
|
||||
```py path=mod.py
|
||||
x = 1
|
||||
If a symbol is *undeclared*, we use the union of `Unknown` with the inferred type. Note that we
|
||||
treat this case differently from the case where a symbol is implicitly declared with `Unknown`,
|
||||
possibly due to the usage of an unknown name in the annotation:
|
||||
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
# Undeclared:
|
||||
a = 1
|
||||
|
||||
# Implicitly declared with `Unknown`, due to the usage of an unknown name in the annotation:
|
||||
b: SomeUnknownName = 1 # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py
|
||||
from mod import x
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
# All external modifications of `x` are allowed:
|
||||
x = None
|
||||
# All external modifications of `a` are allowed:
|
||||
a = None
|
||||
```
|
||||
|
||||
### Undeclared and possibly unbound
|
||||
@@ -198,39 +245,45 @@ x = None
|
||||
If a symbol is undeclared and *possibly* unbound, we currently do not raise an error. This seems
|
||||
inconsistent when compared to the "possibly-undeclared-and-possibly-unbound" case.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag:
|
||||
x = 1
|
||||
a = 1
|
||||
b: SomeUnknownName = 1 # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py
|
||||
# TODO: this should raise an error. Once we fix this, update the section description and the table
|
||||
# on top of this document.
|
||||
from mod import x
|
||||
from mod import a, b
|
||||
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
reveal_type(a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
# All external modifications of `x` are allowed:
|
||||
x = None
|
||||
# All external modifications of `a` are allowed:
|
||||
a = None
|
||||
```
|
||||
|
||||
### Undeclared and unbound
|
||||
|
||||
If a symbol is undeclared *and* unbound, we infer `Unknown` and raise an error.
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
if False:
|
||||
x: int = 1
|
||||
a: int = 1
|
||||
```
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
from mod import x
|
||||
from mod import a
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(a) # revealed: Unknown
|
||||
|
||||
# Modifications allowed in this case:
|
||||
x = None
|
||||
a = None
|
||||
```
|
||||
|
||||
@@ -6,6 +6,8 @@ If we have an intersection type `A & B` and we get a definitive true/false answe
|
||||
types, we can infer that the result for the intersection type is also true/false:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Base: ...
|
||||
|
||||
class Child1(Base):
|
||||
|
||||
@@ -33,7 +33,7 @@ reveal_type(a >= b) # revealed: Literal[False]
|
||||
|
||||
Even when tuples have different lengths, comparisons should be handled appropriately.
|
||||
|
||||
```py path=different_length.py
|
||||
```py
|
||||
a = (1, 2, 3)
|
||||
b = (1, 2, 3, 4)
|
||||
|
||||
@@ -102,7 +102,7 @@ reveal_type(a >= b) # revealed: bool
|
||||
However, if the lexicographic comparison completes without reaching a point where str and int are
|
||||
compared, Python will still produce a result based on the prior elements.
|
||||
|
||||
```py path=short_circuit.py
|
||||
```py
|
||||
a = (1, 2)
|
||||
b = (999999, "hello")
|
||||
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# Descriptor protocol
|
||||
|
||||
[Descriptors] let objects customize attribute lookup, storage, and deletion.
|
||||
|
||||
A descriptor is an attribute value that has one of the methods in the descriptor protocol. Those
|
||||
methods are `__get__()`, `__set__()`, and `__delete__()`. If any of those methods are defined for an
|
||||
attribute, it is said to be a descriptor.
|
||||
|
||||
## Basic example
|
||||
|
||||
An introductory example, modeled after a [simple example] in the primer on descriptors, involving a
|
||||
descriptor that returns a constant value:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Ten:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
|
||||
return 10
|
||||
|
||||
def __set__(self, instance: object, value: Literal[10]) -> None:
|
||||
pass
|
||||
|
||||
class C:
|
||||
ten = Ten()
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: this should be `Literal[10]`
|
||||
reveal_type(c.ten) # revealed: Unknown | Ten
|
||||
|
||||
# TODO: This should `Literal[10]`
|
||||
reveal_type(C.ten) # revealed: Unknown | Ten
|
||||
|
||||
# These are fine:
|
||||
c.ten = 10
|
||||
C.ten = 10
|
||||
|
||||
# TODO: Both of these should be errors
|
||||
c.ten = 11
|
||||
C.ten = 11
|
||||
```
|
||||
|
||||
## Different types for `__get__` and `__set__`
|
||||
|
||||
The return type of `__get__` and the value type of `__set__` can be different:
|
||||
|
||||
```py
|
||||
class FlexibleInt:
|
||||
def __init__(self):
|
||||
self._value: int | None = None
|
||||
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int | None:
|
||||
return self._value
|
||||
|
||||
def __set__(self, instance: object, value: int | str) -> None:
|
||||
self._value = int(value)
|
||||
|
||||
class C:
|
||||
flexible_int = FlexibleInt()
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
|
||||
c.flexible_int = 42 # okay
|
||||
c.flexible_int = "42" # also okay!
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
|
||||
# TODO: should be an error
|
||||
c.flexible_int = None # not okay
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
```
|
||||
|
||||
## Built-in `property` descriptor
|
||||
|
||||
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
|
||||
determined by the return type of the `name` method and the parameter type of the setter,
|
||||
respectively.
|
||||
|
||||
```py
|
||||
class C:
|
||||
_name: str | None = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name or "Unset"
|
||||
# TODO: No diagnostic should be emitted here
|
||||
# error: [unresolved-attribute] "Type `Literal[name]` has no attribute `setter`"
|
||||
@name.setter
|
||||
def name(self, value: str | None) -> None:
|
||||
self._value = value
|
||||
|
||||
c = C()
|
||||
|
||||
reveal_type(c._name) # revealed: str | None
|
||||
|
||||
# Should be `str`
|
||||
reveal_type(c.name) # revealed: @Todo(bound method)
|
||||
|
||||
# Should be `builtins.property`
|
||||
reveal_type(C.name) # revealed: Literal[name]
|
||||
|
||||
# This is fine:
|
||||
c.name = "new"
|
||||
|
||||
c.name = None
|
||||
|
||||
# TODO: this should be an error
|
||||
c.name = 42
|
||||
```
|
||||
|
||||
## Built-in `classmethod` descriptor
|
||||
|
||||
Similarly to `property`, `classmethod` decorator creates an implicit descriptor that binds the first
|
||||
argument to the class instead of the instance.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, value: str) -> None:
|
||||
self._name: str = value
|
||||
|
||||
@classmethod
|
||||
def factory(cls, value: str) -> "C":
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls) -> str:
|
||||
return cls.__name__
|
||||
|
||||
c1 = C.factory("test") # okay
|
||||
|
||||
# TODO: should be `C`
|
||||
reveal_type(c1) # revealed: @Todo(return type)
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(C.get_name()) # revealed: @Todo(return type)
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
|
||||
```
|
||||
|
||||
## Descriptors only work when used as class variables
|
||||
|
||||
From the descriptor guide:
|
||||
|
||||
> Descriptors only work when used as class variables. When put in instances, they have no effect.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Ten:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
|
||||
return 10
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.ten = Ten()
|
||||
|
||||
reveal_type(C().ten) # revealed: Unknown | Ten
|
||||
```
|
||||
|
||||
## Descriptors distinguishing between class and instance access
|
||||
|
||||
Overloads can be used to distinguish between when a descriptor is accessed on a class object and
|
||||
when it is accessed on an instance. A real-world example of this is the `__get__` method on
|
||||
`types.FunctionType`.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString, overload
|
||||
|
||||
class Descriptor:
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None = None, /) -> Literal["called on instance"]: ...
|
||||
def __get__(self, instance, owner=None, /) -> LiteralString:
|
||||
if instance:
|
||||
return "called on instance"
|
||||
else:
|
||||
return "called on class object"
|
||||
|
||||
class C:
|
||||
d = Descriptor()
|
||||
|
||||
# TODO: should be `Literal["called on class object"]
|
||||
reveal_type(C.d) # revealed: Unknown | Descriptor
|
||||
|
||||
# TODO: should be `Literal["called on instance"]
|
||||
reveal_type(C().d) # revealed: Unknown | Descriptor
|
||||
```
|
||||
|
||||
[descriptors]: https://docs.python.org/3/howto/descriptor.html
|
||||
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant
|
||||
@@ -0,0 +1,21 @@
|
||||
# Unpacking
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Right hand side not iterable
|
||||
|
||||
```py
|
||||
a, b = 1 # error: [not-iterable]
|
||||
```
|
||||
|
||||
## Too many values to unpack
|
||||
|
||||
```py
|
||||
a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
## Too few values to unpack
|
||||
|
||||
```py
|
||||
a, b = (1,) # error: [invalid-assignment]
|
||||
```
|
||||
@@ -0,0 +1,87 @@
|
||||
# Unresolved import diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Using `from` with an unresolvable module
|
||||
|
||||
This example demonstrates the diagnostic when a `from` style import is used with a module that could
|
||||
not be found:
|
||||
|
||||
```py
|
||||
from does_not_exist import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with too many leading dots
|
||||
|
||||
This example demonstrates the diagnostic when a `from` style import is used with a presumptively
|
||||
valid path, but where there are too many leading dots.
|
||||
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
def add(x, y):
|
||||
return x + y
|
||||
```
|
||||
|
||||
`package/subpackage/subsubpackage/__init__.py`:
|
||||
|
||||
```py
|
||||
from ....foo import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with an unknown current module
|
||||
|
||||
This is another case handled separately in Red Knot, where a `.` provokes relative module name
|
||||
resolution, but where the module name is not resolvable.
|
||||
|
||||
```py
|
||||
from .does_not_exist import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with an unknown nested module
|
||||
|
||||
Like the previous test, but with sub-modules to ensure the span is correct.
|
||||
|
||||
```py
|
||||
from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with a resolvable module but unresolvable item
|
||||
|
||||
This ensures that diagnostics for an unresolvable item inside a resolvable import highlight the item
|
||||
and not the entire `from ... import ...` statement.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
does_exist1 = 1
|
||||
does_exist2 = 2
|
||||
```
|
||||
|
||||
```py
|
||||
from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
```
|
||||
|
||||
## An unresolvable import that does not use `from`
|
||||
|
||||
This ensures that an unresolvable `import ...` statement highlights just the module name and not the
|
||||
entire statement.
|
||||
|
||||
```py
|
||||
import does_not_exist # error: [unresolved-import]
|
||||
|
||||
x = does_not_exist.foo
|
||||
```
|
||||
@@ -78,7 +78,7 @@ def _(a: type[Unknown], b: type[Any]):
|
||||
Tuple types with the same elements are the same.
|
||||
|
||||
```py
|
||||
from typing_extensions import assert_type
|
||||
from typing_extensions import Any, assert_type
|
||||
|
||||
from knot_extensions import Unknown
|
||||
|
||||
|
||||
@@ -124,42 +124,49 @@ def _(e: Exception | type[Exception] | None):
|
||||
## Exception cause is not an exception
|
||||
|
||||
```py
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
def _():
|
||||
try:
|
||||
raise EOFError() from GeneratorExit # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
def _():
|
||||
try:
|
||||
raise StopIteration from MemoryError() # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
def _():
|
||||
try:
|
||||
raise BufferError() from None # fine
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
def _():
|
||||
try:
|
||||
raise ZeroDivisionError from False # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
def _():
|
||||
try:
|
||||
raise SystemExit from bool() # error: [invalid-raise]
|
||||
except:
|
||||
...
|
||||
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except KeyboardInterrupt as e: # fine
|
||||
reveal_type(e) # revealed: KeyboardInterrupt
|
||||
raise LookupError from e # fine
|
||||
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
def _():
|
||||
try:
|
||||
raise
|
||||
except int as e: # error: [invalid-exception-caught]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
raise KeyError from e
|
||||
|
||||
def _(e: Exception | type[Exception]):
|
||||
raise ModuleNotFoundError from e # fine
|
||||
|
||||
@@ -29,7 +29,7 @@ completing. The type of `x` at the beginning of the `except` suite in this examp
|
||||
`x = could_raise_returns_str()` redefinition, but we *also* could have jumped to the `except` suite
|
||||
*after* that redefinition.
|
||||
|
||||
```py path=union_type_inferred.py
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
@@ -50,10 +50,7 @@ reveal_type(x) # revealed: str | Literal[2]
|
||||
If `x` has the same type at the end of both branches, however, the branches unify and `x` is not
|
||||
inferred as having a union type following the `try`/`except` block:
|
||||
|
||||
```py path=branches_unify_to_non_union_type.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -133,7 +130,7 @@ the `except` suite:
|
||||
- At the end of `else`, `x == 3`
|
||||
- At the end of `except`, `x == 2`
|
||||
|
||||
```py path=single_except.py
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
@@ -161,9 +158,6 @@ been executed in its entirety, or the `try` suite and the `else` suite must both
|
||||
in their entireties:
|
||||
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -192,7 +186,7 @@ A `finally` suite is *always* executed. As such, if we reach the `reveal_type` c
|
||||
this example, we know that `x` *must* have been reassigned to `2` during the `finally` suite. The
|
||||
type of `x` at the end of the example is therefore `Literal[2]`:
|
||||
|
||||
```py path=redef_in_finally.py
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
@@ -217,10 +211,7 @@ at this point than there were when we were inside the `finally` block.
|
||||
(Our current model does *not* correctly infer the types *inside* `finally` suites, however; this is
|
||||
still a TODO item for us.)
|
||||
|
||||
```py path=no_redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -249,7 +240,7 @@ suites:
|
||||
exception raised in the `except` suite to cause us to jump to the `finally` suite before the
|
||||
`except` suite ran to completion
|
||||
|
||||
```py path=redef_in_finally.py
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
@@ -286,16 +277,7 @@ itself. (In some control-flow possibilities, some exceptions were merely *suspen
|
||||
`finally` suite; these lead to the scope's termination following the conclusion of the `finally`
|
||||
suite.)
|
||||
|
||||
```py path=no_redef_in_finally.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
try:
|
||||
@@ -317,16 +299,7 @@ reveal_type(x) # revealed: str | bool
|
||||
|
||||
An example with multiple `except` branches and a `finally` branch:
|
||||
|
||||
```py path=multiple_except_branches.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
```py
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
@@ -364,7 +337,7 @@ If the exception handler has an `else` branch, we must also take into account th
|
||||
control flow could have jumped to the `finally` suite from partway through the `else` suite due to
|
||||
an exception raised *there*.
|
||||
|
||||
```py path=single_except_branch.py
|
||||
```py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
@@ -407,22 +380,7 @@ reveal_type(x) # revealed: bool | float
|
||||
|
||||
The same again, this time with multiple `except` branches:
|
||||
|
||||
```py path=multiple_except_branches.py
|
||||
def could_raise_returns_str() -> str:
|
||||
return "foo"
|
||||
|
||||
def could_raise_returns_bytes() -> bytes:
|
||||
return b"foo"
|
||||
|
||||
def could_raise_returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
def could_raise_returns_memoryview() -> memoryview:
|
||||
return memoryview(b"")
|
||||
|
||||
def could_raise_returns_float() -> float:
|
||||
return 3.14
|
||||
|
||||
```py
|
||||
def could_raise_returns_range() -> range:
|
||||
return range(42)
|
||||
|
||||
|
||||
@@ -54,7 +54,9 @@ reveal_type("x" or "y" and "") # revealed: Literal["x"]
|
||||
|
||||
## Evaluates to builtin
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
redefined_builtin_bool: type[bool] = bool
|
||||
|
||||
def my_bool(x) -> bool:
|
||||
|
||||
@@ -28,6 +28,8 @@ reveal_type(1 if 0 else 2) # revealed: Literal[2]
|
||||
The test inside an if expression should not affect code outside of the expression.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(flag: bool):
|
||||
x: Literal[42, "hello"] = 42 if flag else "hello"
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ In type stubs, classes can reference themselves in their base class definitions.
|
||||
|
||||
This should hold true even with generics at play.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Seq[T]: ...
|
||||
|
||||
# TODO not error on the subscripting
|
||||
|
||||
@@ -9,7 +9,9 @@ E = D
|
||||
reveal_type(E) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
@@ -22,7 +24,9 @@ D = b.C
|
||||
reveal_type(D) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
@@ -34,10 +38,14 @@ import a.b
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
@@ -49,13 +57,19 @@ import a.b.c
|
||||
reveal_type(a.b.c.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b/__init__.py
|
||||
`a/b/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b/c.py
|
||||
`a/b/c.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
@@ -67,10 +81,14 @@ import a.b as b
|
||||
reveal_type(b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
@@ -82,18 +100,34 @@ import a.b.c as c
|
||||
reveal_type(c.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b/__init__.py
|
||||
`a/b/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b/c.py
|
||||
`a/b/c.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Unresolvable module import
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
```
|
||||
|
||||
## Unresolvable submodule imports
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
# Topmost component resolvable, submodule not resolvable:
|
||||
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
@@ -102,5 +136,7 @@ import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
@@ -29,13 +29,17 @@ builtins from the "actual" vendored typeshed:
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class Custom: ...
|
||||
|
||||
custom_builtin: Custom
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
@@ -56,12 +60,16 @@ that point:
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
foo = bar
|
||||
bar = 1
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
## Maybe unbound
|
||||
|
||||
```py path=maybe_unbound.py
|
||||
`maybe_unbound.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
@@ -29,7 +31,9 @@ reveal_type(y) # revealed: Unknown | Literal[3]
|
||||
|
||||
## Maybe unbound annotated
|
||||
|
||||
```py path=maybe_unbound_annotated.py
|
||||
`maybe_unbound_annotated.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
@@ -60,7 +64,9 @@ reveal_type(y) # revealed: int
|
||||
|
||||
Importing a possibly undeclared name still gives us its declared type:
|
||||
|
||||
```py path=maybe_undeclared.py
|
||||
`maybe_undeclared.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
@@ -76,11 +82,15 @@ reveal_type(x) # revealed: int
|
||||
|
||||
## Reimport
|
||||
|
||||
```py path=c.py
|
||||
`c.py`:
|
||||
|
||||
```py
|
||||
def f(): ...
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
@@ -102,11 +112,15 @@ reveal_type(f) # revealed: Literal[f, f]
|
||||
When we have a declared type in one path and only an inferred-from-definition type in the other, we
|
||||
should still be able to unify those:
|
||||
|
||||
```py path=c.pyi
|
||||
`c.pyi`:
|
||||
|
||||
```pyi
|
||||
x: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
@@ -8,11 +8,15 @@ import a.b
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Via from/import
|
||||
@@ -23,11 +27,15 @@ from a import b
|
||||
reveal_type(b) # revealed: int
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Via both
|
||||
@@ -40,11 +48,15 @@ reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
## Via both (backwards)
|
||||
@@ -65,11 +77,15 @@ reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
b: int = 42
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
[from-import]: https://docs.python.org/3/reference/simple_stmts.html#the-import-statement
|
||||
|
||||
@@ -18,7 +18,9 @@ reveal_type(baz) # revealed: Unknown
|
||||
|
||||
## Unresolved import from resolved module
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py
|
||||
@@ -29,7 +31,9 @@ reveal_type(thing) # revealed: Unknown
|
||||
|
||||
## Resolved import of symbol from unresolved import
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
import foo as foo # error: "Cannot resolve import `foo`"
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
@@ -46,7 +50,9 @@ reveal_type(foo) # revealed: Unknown
|
||||
|
||||
## No implicit shadowing
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
x: int
|
||||
```
|
||||
|
||||
@@ -58,7 +64,9 @@ x = "foo" # error: [invalid-assignment] "Object of type `Literal["foo"]"
|
||||
|
||||
## Import cycle
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[object]]
|
||||
@@ -69,7 +77,9 @@ class C(b.B): ...
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
from a import A
|
||||
|
||||
class B(A): ...
|
||||
|
||||
@@ -23,9 +23,13 @@ reveal_type(b) # revealed: <module 'a.b'>
|
||||
reveal_type(b.c) # revealed: int
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
c: int = 1
|
||||
```
|
||||
|
||||
@@ -2,10 +2,14 @@
|
||||
|
||||
## Non-existent
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
@@ -13,14 +17,20 @@ reveal_type(X) # revealed: Unknown
|
||||
|
||||
## Simple
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
@@ -28,14 +38,20 @@ reveal_type(X) # revealed: int
|
||||
|
||||
## Dotted
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo/bar/baz.py
|
||||
`package/foo/bar/baz.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo.bar.baz import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
@@ -43,11 +59,15 @@ reveal_type(X) # revealed: int
|
||||
|
||||
## Bare to package
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
@@ -55,7 +75,9 @@ reveal_type(X) # revealed: int
|
||||
|
||||
## Non-existent + bare to package
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
@@ -63,19 +85,25 @@ reveal_type(X) # revealed: Unknown
|
||||
|
||||
## Dunder init
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
from .foo import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
## Non-existent + dunder init
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
from .foo import X # error: [unresolved-import]
|
||||
|
||||
reveal_type(X) # revealed: Unknown
|
||||
@@ -83,14 +111,20 @@ reveal_type(X) # revealed: Unknown
|
||||
|
||||
## Long relative import
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/subpackage/subsubpackage/bar.py
|
||||
`package/subpackage/subsubpackage/bar.py`:
|
||||
|
||||
```py
|
||||
from ...foo import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
@@ -98,14 +132,20 @@ reveal_type(X) # revealed: int
|
||||
|
||||
## Unbound symbol
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
x # error: [unresolved-reference]
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from .foo import x # error: [unresolved-import]
|
||||
|
||||
reveal_type(x) # revealed: Unknown
|
||||
@@ -113,14 +153,20 @@ reveal_type(x) # revealed: Unknown
|
||||
|
||||
## Bare to module
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import foo
|
||||
|
||||
reveal_type(foo.X) # revealed: int
|
||||
@@ -131,10 +177,14 @@ reveal_type(foo.X) # revealed: int
|
||||
This test verifies that we emit an error when we try to import a symbol that is neither a submodule
|
||||
nor an attribute of `package`.
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import foo # error: [unresolved-import]
|
||||
|
||||
reveal_type(foo) # revealed: Unknown
|
||||
@@ -148,17 +198,53 @@ submodule when that submodule name appears in the `imported_modules` set. That m
|
||||
that are imported via `from...import` are not visible to our type inference if you also access that
|
||||
submodule via the attribute on its parent package.
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/foo.py
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
```py path=package/bar.py
|
||||
`package/bar.py`:
|
||||
|
||||
```py
|
||||
from . import foo
|
||||
import package
|
||||
|
||||
# error: [unresolved-attribute] "Type `<module 'package'>` has no attribute `foo`"
|
||||
reveal_type(package.foo.X) # revealed: Unknown
|
||||
```
|
||||
|
||||
## In the src-root
|
||||
|
||||
`parser.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
`__main__.py`:
|
||||
|
||||
```py
|
||||
from .parser import X
|
||||
|
||||
reveal_type(X) # revealed: int
|
||||
```
|
||||
|
||||
## Beyond the src-root
|
||||
|
||||
`parser.py`:
|
||||
|
||||
```py
|
||||
X: int = 42
|
||||
```
|
||||
|
||||
`__main__.py`:
|
||||
|
||||
```py
|
||||
from ..parser import X # error: [unresolved-import]
|
||||
```
|
||||
|
||||
@@ -9,7 +9,9 @@ y = x
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
```py path=b.pyi
|
||||
`b.pyi`:
|
||||
|
||||
```pyi
|
||||
x: int
|
||||
```
|
||||
|
||||
@@ -22,6 +24,8 @@ y = x
|
||||
reveal_type(y) # revealed: int
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
x: int = 1
|
||||
```
|
||||
|
||||
@@ -32,10 +32,14 @@ reveal_type(a.b.C) # revealed: Literal[C]
|
||||
import a.b
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
@@ -55,14 +59,20 @@ reveal_type(a.b) # revealed: <module 'a.b'>
|
||||
reveal_type(a.b.C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
```py path=q.py
|
||||
`q.py`:
|
||||
|
||||
```py
|
||||
import a as a
|
||||
import a.b as b
|
||||
```
|
||||
@@ -83,18 +93,26 @@ reveal_type(sub.b) # revealed: <module 'sub.b'>
|
||||
reveal_type(attr.b) # revealed: <module 'attr.b'>
|
||||
```
|
||||
|
||||
```py path=sub/__init__.py
|
||||
`sub/__init__.py`:
|
||||
|
||||
```py
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=sub/b.py
|
||||
`sub/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=attr/__init__.py
|
||||
`attr/__init__.py`:
|
||||
|
||||
```py
|
||||
from . import b as _
|
||||
|
||||
b = 1
|
||||
```
|
||||
|
||||
```py path=attr/b.py
|
||||
`attr/b.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
@@ -808,6 +808,7 @@ Dynamic types do not cancel each other out. Intersecting an unknown set of value
|
||||
of another unknown set of values is not necessarily empty, so we keep the positive contribution:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
|
||||
def any(
|
||||
@@ -830,6 +831,7 @@ def unknown(
|
||||
We currently do not simplify mixed dynamic types, but might consider doing so in the future:
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from knot_extensions import Intersection, Not, Unknown
|
||||
|
||||
def mixed(
|
||||
|
||||
@@ -31,7 +31,9 @@ reveal_type(TC) # revealed: Literal[True]
|
||||
Make sure we only use our special handling for `typing.TYPE_CHECKING` and not for other constants
|
||||
with the same name:
|
||||
|
||||
```py path=constants.py
|
||||
`constants.py`:
|
||||
|
||||
```py
|
||||
TYPE_CHECKING: bool = False
|
||||
```
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ python-version = "3.10"
|
||||
Here, we simply make sure that we pick up the global configuration from the root section:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -25,6 +27,8 @@ reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
The same should work for arbitrarily nested sections:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -38,6 +42,8 @@ python-version = "3.11"
|
||||
```
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info[:2] == (3, 11)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -46,6 +52,8 @@ reveal_type(sys.version_info[:2] == (3, 11)) # revealed: Literal[True]
|
||||
There is no global state. This section should again use the root configuration:
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info[:2] == (3, 10)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -63,5 +71,7 @@ python-version = "3.12"
|
||||
### Grandchild
|
||||
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info[:2] == (3, 12)) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
@@ -19,13 +19,17 @@ typeshed = "/typeshed"
|
||||
|
||||
We can then place custom stub files in `/typeshed/stdlib`, for example:
|
||||
|
||||
```pyi path=/typeshed/stdlib/builtins.pyi
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class BuiltinClass: ...
|
||||
|
||||
builtin_symbol: BuiltinClass
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/sys/__init__.pyi
|
||||
`/typeshed/stdlib/sys/__init__.pyi`:
|
||||
|
||||
```pyi
|
||||
version = "my custom Python"
|
||||
```
|
||||
|
||||
@@ -54,15 +58,21 @@ python-version = "3.10"
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/old_module.pyi
|
||||
`/typeshed/stdlib/old_module.pyi`:
|
||||
|
||||
```pyi
|
||||
class OldClass: ...
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/new_module.pyi
|
||||
`/typeshed/stdlib/new_module.pyi`:
|
||||
|
||||
```pyi
|
||||
class NewClass: ...
|
||||
```
|
||||
|
||||
```text path=/typeshed/stdlib/VERSIONS
|
||||
`/typeshed/stdlib/VERSIONS`:
|
||||
|
||||
```text
|
||||
old_module: 3.0-
|
||||
new_module: 3.11-
|
||||
```
|
||||
@@ -86,7 +96,9 @@ simple untyped definition is enough to make `reveal_type` work in tests:
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
```pyi path=/typeshed/stdlib/typing_extensions.pyi
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ reveal_type(D.__class__) # revealed: Literal[SignatureMismatch]
|
||||
|
||||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class A(B): ... # error: [cyclic-class-definition]
|
||||
class B(C): ... # error: [cyclic-class-definition]
|
||||
class C(A): ... # error: [cyclic-class-definition]
|
||||
|
||||
@@ -347,7 +347,7 @@ reveal_type(unknown_object.__mro__) # revealed: Unknown
|
||||
|
||||
These are invalid, but we need to be able to handle them gracefully without panicking.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-definition]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
@@ -365,7 +365,7 @@ reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[objec
|
||||
|
||||
These are similarly unlikely, but we still shouldn't crash:
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
class Baz(Foo): ... # error: [cyclic-class-definition]
|
||||
@@ -377,7 +377,7 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
||||
|
||||
## Classes with cycles in their MROs, and multiple inheritance
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-definition]
|
||||
class Bar(Baz): ... # error: [cyclic-class-definition]
|
||||
@@ -390,7 +390,7 @@ reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[objec
|
||||
|
||||
## Classes with cycles in their MRO, and a sub-graph
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-definition]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-definition]
|
||||
|
||||
@@ -57,6 +57,8 @@ def _(flag1: bool, flag2: bool, flag3: bool, flag4: bool):
|
||||
## Multiple predicates
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
|
||||
@@ -67,6 +69,8 @@ def _(flag1: bool, flag2: bool):
|
||||
## Mix of `and` and `or`
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(flag1: bool, flag2: bool):
|
||||
class A: ...
|
||||
x: A | None | Literal[1] = A() if flag1 else None if flag2 else 1
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
## Value Literals
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def foo() -> Literal[0, -1, True, False, "", "foo", b"", b"bar", None] | tuple[()]:
|
||||
return 0
|
||||
|
||||
@@ -123,6 +125,8 @@ always returns a fixed value.
|
||||
These types can always be fully narrowed in boolean contexts, as shown below:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class T:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
@@ -149,6 +153,8 @@ else:
|
||||
## Narrowing Complex Intersection and Union
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
@@ -181,6 +187,8 @@ if isinstance(x, str) and not isinstance(x, B):
|
||||
## Narrowing Multiple Variables
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def f(x: Literal[0, 1], y: Literal["", "hello"]):
|
||||
if x and y and not x and not y:
|
||||
reveal_type(x) # revealed: Never
|
||||
@@ -222,6 +230,8 @@ reveal_type(y) # revealed: A
|
||||
## Truthiness of classes
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class MetaAmbiguous(type):
|
||||
def __bool__(self) -> bool: ...
|
||||
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
|
||||
Regression test for [this issue](https://github.com/astral-sh/ruff/issues/14334).
|
||||
|
||||
```py path=base.py
|
||||
`base.py`:
|
||||
|
||||
```py
|
||||
# error: [invalid-base]
|
||||
class Base(2): ...
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
# No error here
|
||||
from base import Base
|
||||
```
|
||||
|
||||
@@ -13,7 +13,7 @@ if returns_bool():
|
||||
chr: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
reveal_type(chr) # revealed: int | Literal[chr]
|
||||
```
|
||||
|
||||
## Conditionally global or builtin, with annotation
|
||||
@@ -28,5 +28,5 @@ if returns_bool():
|
||||
chr: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
reveal_type(chr) # revealed: int | Literal[chr]
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ def foo():
|
||||
However, three attributes on `types.ModuleType` are not present as implicit module globals; these
|
||||
are excluded:
|
||||
|
||||
```py path=unbound_dunders.py
|
||||
```py
|
||||
# error: [unresolved-reference]
|
||||
# revealed: Unknown
|
||||
reveal_type(__getattr__)
|
||||
@@ -54,10 +54,10 @@ inside the module:
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
reveal_type(typing.__init__) # revealed: @Todo(bound method)
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
reveal_type(typing.__eq__) # revealed: @Todo(bound method)
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
@@ -70,9 +70,7 @@ Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType`
|
||||
dynamic imports; but we ignore that for module-literal types where we know exactly which module
|
||||
we're dealing with:
|
||||
|
||||
```py path=__getattr__.py
|
||||
import typing
|
||||
|
||||
```py
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(typing.__getattr__) # revealed: Unknown
|
||||
```
|
||||
@@ -83,13 +81,17 @@ It's impossible to override the `__dict__` attribute of `types.ModuleType` insta
|
||||
module; we should prioritise the attribute in the `types.ModuleType` stub over a variable named
|
||||
`__dict__` in the module's global namespace:
|
||||
|
||||
```py path=foo.py
|
||||
`foo.py`:
|
||||
|
||||
```py
|
||||
__dict__ = "foo"
|
||||
|
||||
reveal_type(__dict__) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
```py path=bar.py
|
||||
`bar.py`:
|
||||
|
||||
```py
|
||||
import foo
|
||||
from foo import __dict__ as foo_dict
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
Parameter `x` of type `str` is shadowed and reassigned with a new `int` value inside the function.
|
||||
No diagnostics should be generated.
|
||||
|
||||
```py path=a.py
|
||||
```py
|
||||
def f(x: str):
|
||||
x: int = int(x)
|
||||
```
|
||||
|
||||
## Implicit error
|
||||
|
||||
```py path=a.py
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explicit if this is intentional"
|
||||
@@ -20,7 +20,7 @@ f = 1 # error: "Implicit shadowing of function `f`; annotate to make it explici
|
||||
|
||||
## Explicit shadowing
|
||||
|
||||
```py path=a.py
|
||||
```py
|
||||
def f(): ...
|
||||
|
||||
f: int = 1
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: basic.md - Structures - Unresolvable module import
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:8
|
||||
|
|
||||
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: basic.md - Structures - Unresolvable submodule imports
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | # Topmost component resolvable, submodule not resolvable:
|
||||
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
3 |
|
||||
4 | # Topmost component unresolvable:
|
||||
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
```
|
||||
|
||||
## a/__init__.py
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:2:8
|
||||
|
|
||||
1 | # Topmost component resolvable, submodule not resolvable:
|
||||
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
| ^^^^^ Cannot resolve import `a.foo`
|
||||
3 |
|
||||
4 | # Topmost component unresolvable:
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:5:8
|
||||
|
|
||||
4 | # Topmost component unresolvable:
|
||||
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
| ^^^^^ Cannot resolve import `b.foo`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Right hand side not iterable
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = 1 # error: [not-iterable]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:not-iterable
|
||||
--> /src/mdtest_snippet.py:1:8
|
||||
|
|
||||
1 | a, b = 1 # error: [not-iterable]
|
||||
| ^ Object of type `Literal[1]` is not iterable
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Too few values to unpack
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = (1,) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-assignment
|
||||
--> /src/mdtest_snippet.py:1:1
|
||||
|
|
||||
1 | a, b = (1,) # error: [invalid-assignment]
|
||||
| ^^^^ Not enough values to unpack (expected 2, got 1)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unpacking.md - Unpacking - Too many values to unpack
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unpacking.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:invalid-assignment
|
||||
--> /src/mdtest_snippet.py:1:1
|
||||
|
|
||||
1 | a, b = (1, 2, 3) # error: [invalid-assignment]
|
||||
| ^^^^ Too many values to unpack (expected 2, got 3)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - An unresolvable import that does not use `from`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | import does_not_exist # error: [unresolved-import]
|
||||
2 |
|
||||
3 | x = does_not_exist.foo
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:8
|
||||
|
|
||||
1 | import does_not_exist # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
2 |
|
||||
3 | x = does_not_exist.foo
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with a resolvable module but unresolvable item
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## a.py
|
||||
|
||||
```
|
||||
1 | does_exist1 = 1
|
||||
2 | does_exist2 = 2
|
||||
```
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:28
|
||||
|
|
||||
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown current module
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from .does_not_exist import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:7
|
||||
|
|
||||
1 | from .does_not_exist import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown nested module
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:7
|
||||
|
|
||||
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unresolvable module
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet.py
|
||||
|
||||
```
|
||||
1 | from does_not_exist import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet.py:1:6
|
||||
|
|
||||
1 | from does_not_exist import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with too many leading dots
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## package/__init__.py
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## package/foo.py
|
||||
|
||||
```
|
||||
1 | def add(x, y):
|
||||
2 | return x + y
|
||||
```
|
||||
|
||||
## package/subpackage/subsubpackage/__init__.py
|
||||
|
||||
```
|
||||
1 | from ....foo import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/package/subpackage/subsubpackage/__init__.py:1:10
|
||||
|
|
||||
1 | from ....foo import add # error: [unresolved-import]
|
||||
| ^^^ Cannot resolve import `....foo`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -7,35 +7,36 @@ branches whose conditions we can statically determine to be always true or alway
|
||||
useful for `sys.version_info` branches, which can make new features available based on the Python
|
||||
version:
|
||||
|
||||
```py path=module1.py
|
||||
import sys
|
||||
|
||||
if sys.version_info >= (3, 9):
|
||||
SomeFeature: str = "available"
|
||||
```
|
||||
|
||||
If we can statically determine that the condition is always true, then we can also understand that
|
||||
`SomeFeature` is always bound, without raising any errors:
|
||||
|
||||
```py path=test1.py
|
||||
from module1 import SomeFeature
|
||||
```py
|
||||
import sys
|
||||
|
||||
# SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
|
||||
reveal_type(SomeFeature) # revealed: str
|
||||
class C:
|
||||
if sys.version_info >= (3, 9):
|
||||
SomeFeature: str = "available"
|
||||
|
||||
# C.SomeFeature is unconditionally available here, because we are on Python 3.9 or newer:
|
||||
reveal_type(C.SomeFeature) # revealed: str
|
||||
```
|
||||
|
||||
Another scenario where this is useful is for `typing.TYPE_CHECKING` branches, which are often used
|
||||
for conditional imports:
|
||||
|
||||
```py path=module2.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
class SomeType: ...
|
||||
```
|
||||
|
||||
```py path=test2.py
|
||||
`main.py`:
|
||||
|
||||
```py
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from module2 import SomeType
|
||||
from module import SomeType
|
||||
|
||||
# `SomeType` is unconditionally available here for type checkers:
|
||||
def f(s: SomeType) -> None: ...
|
||||
@@ -167,7 +168,11 @@ statically known conditions, but here, we show that the results are truly based
|
||||
not some special handling of specific conditions in semantic index building. We use two modules to
|
||||
demonstrate this, since semantic index building is inherently single-module:
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class AlwaysTrue:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
@@ -1424,7 +1429,9 @@ def f():
|
||||
|
||||
#### Always false, unbound
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if False:
|
||||
symbol = 1
|
||||
```
|
||||
@@ -1436,7 +1443,9 @@ from module import symbol
|
||||
|
||||
#### Always true, bound
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if True:
|
||||
symbol = 1
|
||||
```
|
||||
@@ -1448,7 +1457,9 @@ from module import symbol
|
||||
|
||||
#### Ambiguous, possibly unbound
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
@@ -1463,7 +1474,9 @@ from module import symbol
|
||||
|
||||
#### Always false, undeclared
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if False:
|
||||
symbol: int
|
||||
```
|
||||
@@ -1477,7 +1490,9 @@ reveal_type(symbol) # revealed: Unknown
|
||||
|
||||
#### Always true, declared
|
||||
|
||||
```py path=module.py
|
||||
`module.py`:
|
||||
|
||||
```py
|
||||
if True:
|
||||
symbol: int
|
||||
```
|
||||
@@ -1487,37 +1502,6 @@ if True:
|
||||
from module import symbol
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
We currently have a limitation in the complexity (depth) of the visibility constraints that are
|
||||
supported. This is to avoid pathological cases that would require us to recurse deeply.
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or (x := 2) # fmt: skip
|
||||
|
||||
# This still works fine:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
y = 1
|
||||
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or (y := 2) # fmt: skip
|
||||
|
||||
# TODO: This should ideally be `Literal[2]` as well:
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Unsupported features
|
||||
|
||||
We do not support full unreachable code analysis yet. We also raise diagnostics from
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
In type stubs, classes can reference themselves in their base class definitions. For example, in
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
```py path=a.pyi
|
||||
```pyi
|
||||
class Foo[T]: ...
|
||||
|
||||
# TODO: actually is subscriptable
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
The ellipsis literal `...` can be used as a placeholder default value for a function parameter, in a
|
||||
stub file only, regardless of the type of the parameter.
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
def f(x: int = ...) -> None:
|
||||
reveal_type(x) # revealed: int
|
||||
|
||||
@@ -18,7 +18,7 @@ def f2(x: str = ...) -> None:
|
||||
The ellipsis literal can be assigned to a class or module symbol, regardless of its declared type,
|
||||
in a stub file only.
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
y: bytes = ...
|
||||
reveal_type(y) # revealed: bytes
|
||||
x = ...
|
||||
@@ -35,7 +35,7 @@ reveal_type(Foo.y) # revealed: int
|
||||
No diagnostic is emitted if an ellipsis literal is "unpacked" in a stub file as part of an
|
||||
assignment statement:
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
x, y = ...
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: Unknown
|
||||
@@ -46,7 +46,7 @@ reveal_type(y) # revealed: Unknown
|
||||
Iterating over an ellipsis literal as part of a `for` loop in a stub is invalid, however, and
|
||||
results in a diagnostic:
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
# error: [not-iterable] "Object of type `ellipsis` is not iterable"
|
||||
for a, b in ...:
|
||||
reveal_type(a) # revealed: Unknown
|
||||
@@ -72,7 +72,7 @@ reveal_type(b) # revealed: ellipsis
|
||||
|
||||
There is no special treatment of the builtin name `Ellipsis` in stubs, only of `...` literals.
|
||||
|
||||
```py path=test.pyi
|
||||
```pyi
|
||||
# error: 7 [invalid-parameter-default] "Default value of type `ellipsis` is not assignable to annotated parameter type `int`"
|
||||
def f(x: int = Ellipsis) -> None: ...
|
||||
```
|
||||
|
||||
@@ -97,7 +97,7 @@ reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
|
||||
`typing.Tuple` can be used interchangeably with `tuple`:
|
||||
|
||||
```py
|
||||
from typing import Tuple
|
||||
from typing import Any, Tuple
|
||||
|
||||
class A: ...
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ...
|
||||
```py
|
||||
# error: [invalid-syntax]
|
||||
# error: [unused-ignore-comment]
|
||||
def test( # knot: ignore
|
||||
def test($): # knot: ignore
|
||||
pass
|
||||
```
|
||||
|
||||
<!-- blacken-docs:on -->
|
||||
|
||||
@@ -37,7 +37,7 @@ child expression now suppresses errors in the outer expression.
|
||||
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
|
||||
`"test"` and adding `"other"` to the result of the cast.
|
||||
|
||||
```py path=nested.py
|
||||
```py
|
||||
# fmt: off
|
||||
from typing import cast
|
||||
|
||||
@@ -151,7 +151,7 @@ b = a / 0
|
||||
```py
|
||||
"""
|
||||
File level suppressions must come before any non-trivia token,
|
||||
including module docstrings.
|
||||
including module docstrings.
|
||||
"""
|
||||
|
||||
# error: [unused-ignore-comment] "Unused blanket `type: ignore` directive"
|
||||
|
||||
@@ -86,14 +86,20 @@ reveal_type(bar >= (3, 9)) # revealed: Literal[True]
|
||||
|
||||
Only comparisons with the symbol `version_info` from the `sys` module produce literal types:
|
||||
|
||||
```py path=package/__init__.py
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=package/sys.py
|
||||
`package/sys.py`:
|
||||
|
||||
```py
|
||||
version_info: tuple[int, int] = (4, 2)
|
||||
```
|
||||
|
||||
```py path=package/script.py
|
||||
`package/script.py`:
|
||||
|
||||
```py
|
||||
from .sys import version_info
|
||||
|
||||
reveal_type(version_info >= (3, 9)) # revealed: bool
|
||||
@@ -103,7 +109,7 @@ reveal_type(version_info >= (3, 9)) # revealed: bool
|
||||
|
||||
The fields of `sys.version_info` can be accessed by name:
|
||||
|
||||
```py path=a.py
|
||||
```py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.major >= 3) # revealed: Literal[True]
|
||||
@@ -114,9 +120,7 @@ reveal_type(sys.version_info.minor >= 10) # revealed: Literal[False]
|
||||
But the `micro`, `releaselevel` and `serial` fields are inferred as `@Todo` until we support
|
||||
properties on instance types:
|
||||
|
||||
```py path=b.py
|
||||
import sys
|
||||
|
||||
```py
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(@property)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(@property)
|
||||
|
||||
@@ -0,0 +1,663 @@
|
||||
# Terminal statements
|
||||
|
||||
## Introduction
|
||||
|
||||
Terminal statements complicate a naive control-flow analysis.
|
||||
|
||||
As a simple example:
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> str:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
raise ValueError
|
||||
return x
|
||||
|
||||
def g(cond: bool):
|
||||
if cond:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
else:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
```
|
||||
|
||||
In `f`, we should be able to determine that the `else` branch ends in a terminal statement, and that
|
||||
the `return` statement can only be executed when the condition is true. We should therefore consider
|
||||
the reference always bound, even though `x` is only bound in the true branch.
|
||||
|
||||
Similarly, in `g`, we should see that the assignment of the value `"terminal"` can never be seen by
|
||||
the final `reveal_type`.
|
||||
|
||||
## `return`
|
||||
|
||||
A `return` statement is terminal; bindings that occur before it are not visible after it.
|
||||
|
||||
```py
|
||||
def resolved_reference(cond: bool) -> str:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
return "early"
|
||||
return x # no possibly-unresolved-reference diagnostic!
|
||||
|
||||
def return_in_then_branch(cond: bool):
|
||||
if cond:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
else:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
|
||||
def return_in_else_branch(cond: bool):
|
||||
if cond:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
else:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
|
||||
def return_in_both_branches(cond: bool):
|
||||
if cond:
|
||||
x = "terminal1"
|
||||
reveal_type(x) # revealed: Literal["terminal1"]
|
||||
return
|
||||
else:
|
||||
x = "terminal2"
|
||||
reveal_type(x) # revealed: Literal["terminal2"]
|
||||
return
|
||||
|
||||
def return_in_try(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "test"
|
||||
return
|
||||
except:
|
||||
# TODO: Literal["before"]
|
||||
reveal_type(x) # revealed: Literal["before", "test"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
finally:
|
||||
reveal_type(x) # revealed: Literal["before", "test"]
|
||||
reveal_type(x) # revealed: Literal["before", "test"]
|
||||
|
||||
def return_in_nested_then_branch(cond1: bool, cond2: bool):
|
||||
if cond1:
|
||||
x = "test1"
|
||||
reveal_type(x) # revealed: Literal["test1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
else:
|
||||
x = "test2"
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
reveal_type(x) # revealed: Literal["test1", "test2"]
|
||||
|
||||
def return_in_nested_else_branch(cond1: bool, cond2: bool):
|
||||
if cond1:
|
||||
x = "test1"
|
||||
reveal_type(x) # revealed: Literal["test1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "test2"
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
else:
|
||||
x = "terminal"
|
||||
reveal_type(x) # revealed: Literal["terminal"]
|
||||
return
|
||||
reveal_type(x) # revealed: Literal["test2"]
|
||||
reveal_type(x) # revealed: Literal["test1", "test2"]
|
||||
|
||||
def return_in_both_nested_branches(cond1: bool, cond2: bool):
|
||||
if cond1:
|
||||
x = "test"
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
else:
|
||||
x = "terminal0"
|
||||
if cond2:
|
||||
x = "terminal1"
|
||||
reveal_type(x) # revealed: Literal["terminal1"]
|
||||
return
|
||||
else:
|
||||
x = "terminal2"
|
||||
reveal_type(x) # revealed: Literal["terminal2"]
|
||||
return
|
||||
reveal_type(x) # revealed: Literal["test"]
|
||||
```
|
||||
|
||||
## `continue`
|
||||
|
||||
A `continue` statement jumps back to the top of the innermost loop. This makes it terminal within
|
||||
the loop body: definitions before it are not visible after it within the rest of the loop body. They
|
||||
are likely visible after the loop body, since loops do not introduce new scopes. (Statically known
|
||||
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
|
||||
loop are not visible outside of it.)
|
||||
|
||||
TODO: We are not currently modeling the cyclic control flow for loops, pending fixpoint support in
|
||||
Salsa. The false positives in this section are because of that, and not our terminal statement
|
||||
support. See [ruff#14160](https://github.com/astral-sh/ruff/issues/14160) for more details.
|
||||
|
||||
```py
|
||||
def resolved_reference(cond: bool) -> str:
|
||||
while True:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
continue
|
||||
return x
|
||||
|
||||
def continue_in_then_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
else:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
# TODO: Should be Literal["before", "loop", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||
|
||||
def continue_in_else_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
# TODO: Should be Literal["before", "loop", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||
|
||||
def continue_in_both_branches(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "continue1"
|
||||
reveal_type(x) # revealed: Literal["continue1"]
|
||||
continue
|
||||
else:
|
||||
x = "continue2"
|
||||
reveal_type(x) # revealed: Literal["continue2"]
|
||||
continue
|
||||
# TODO: Should be Literal["before", "continue1", "continue2"]
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
|
||||
def continue_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
else:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
|
||||
|
||||
def continue_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
else:
|
||||
x = "continue"
|
||||
reveal_type(x) # revealed: Literal["continue"]
|
||||
continue
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
|
||||
|
||||
def continue_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "continue1"
|
||||
reveal_type(x) # revealed: Literal["continue1"]
|
||||
continue
|
||||
else:
|
||||
x = "continue2"
|
||||
reveal_type(x) # revealed: Literal["continue2"]
|
||||
continue
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
# TODO: Should be Literal["before", "loop", "continue1", "continue2"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop"]
|
||||
```
|
||||
|
||||
## `break`
|
||||
|
||||
A `break` statement jumps to the end of the innermost loop. This makes it terminal within the loop
|
||||
body: definitions before it are not visible after it within the rest of the loop body. They are
|
||||
likely visible after the loop body, since loops do not introduce new scopes. (Statically known
|
||||
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
|
||||
loop are not visible outside of it.)
|
||||
|
||||
```py
|
||||
def resolved_reference(cond: bool) -> str:
|
||||
while True:
|
||||
if cond:
|
||||
x = "test"
|
||||
else:
|
||||
break
|
||||
return x
|
||||
return x # error: [unresolved-reference]
|
||||
|
||||
def break_in_then_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
else:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["before", "break", "loop"]
|
||||
|
||||
def break_in_else_branch(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop", "break"]
|
||||
|
||||
def break_in_both_branches(cond: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond:
|
||||
x = "break1"
|
||||
reveal_type(x) # revealed: Literal["break1"]
|
||||
break
|
||||
else:
|
||||
x = "break2"
|
||||
reveal_type(x) # revealed: Literal["break2"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["before", "break1", "break2"]
|
||||
|
||||
def break_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
else:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "break", "loop2"]
|
||||
|
||||
def break_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop1"
|
||||
reveal_type(x) # revealed: Literal["loop1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "loop2"
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
else:
|
||||
x = "break"
|
||||
reveal_type(x) # revealed: Literal["break"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["loop2"]
|
||||
reveal_type(x) # revealed: Literal["loop1", "loop2"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop1", "loop2", "break"]
|
||||
|
||||
def break_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
|
||||
x = "before"
|
||||
for _ in range(i):
|
||||
if cond1:
|
||||
x = "loop"
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "break1"
|
||||
reveal_type(x) # revealed: Literal["break1"]
|
||||
break
|
||||
else:
|
||||
x = "break2"
|
||||
reveal_type(x) # revealed: Literal["break2"]
|
||||
break
|
||||
reveal_type(x) # revealed: Literal["loop"]
|
||||
reveal_type(x) # revealed: Literal["before", "loop", "break1", "break2"]
|
||||
```
|
||||
|
||||
## `raise`
|
||||
|
||||
A `raise` statement is terminal. If it occurs in a lexically containing `try` statement, it will
|
||||
jump to one of the `except` clauses (if it matches the value being raised), or to the `else` clause
|
||||
(if none match). Currently, we assume definitions from before the `raise` are visible in all
|
||||
`except` and `else` clauses. (In the future, we might analyze the `except` clauses to see which ones
|
||||
match the value being raised, and limit visibility to those clauses.) Definitions from before the
|
||||
`raise` are not visible in any `else` clause, but are visible in `except` clauses or after the
|
||||
containing `try` statement (since control flow may have passed through an `except`).
|
||||
|
||||
Currently we assume that an exception could be raised anywhere within a `try` block. We may want to
|
||||
implement a more precise understanding of where exceptions (barring `KeyboardInterrupt` and
|
||||
`MemoryError`) can and cannot actually be raised.
|
||||
|
||||
```py
|
||||
def raise_in_then_branch(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise", "else"]
|
||||
|
||||
def raise_in_else_branch(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
else:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise"]
|
||||
|
||||
def raise_in_both_branches(cond: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond:
|
||||
x = "raise1"
|
||||
reveal_type(x) # revealed: Literal["raise1"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "raise2"
|
||||
reveal_type(x) # revealed: Literal["raise2"]
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
else:
|
||||
# This branch is unreachable, since all control flows in the `try` clause raise exceptions.
|
||||
# As a result, this binding should never be reachable, since new bindings are visible only
|
||||
# when they are reachable.
|
||||
x = "unreachable"
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
|
||||
|
||||
def raise_in_nested_then_branch(cond1: bool, cond2: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond1:
|
||||
x = "else1"
|
||||
reveal_type(x) # revealed: Literal["else1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "else2"
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
|
||||
|
||||
def raise_in_nested_else_branch(cond1: bool, cond2: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond1:
|
||||
x = "else1"
|
||||
reveal_type(x) # revealed: Literal["else1"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "else2"
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
else:
|
||||
x = "raise"
|
||||
reveal_type(x) # revealed: Literal["raise"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["else2"]
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else1", "else2"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
|
||||
|
||||
def raise_in_both_nested_branches(cond1: bool, cond2: bool):
|
||||
x = "before"
|
||||
try:
|
||||
if cond1:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
else:
|
||||
if cond2:
|
||||
x = "raise1"
|
||||
reveal_type(x) # revealed: Literal["raise1"]
|
||||
raise ValueError
|
||||
else:
|
||||
x = "raise2"
|
||||
reveal_type(x) # revealed: Literal["raise2"]
|
||||
raise ValueError
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
except ValueError:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
except:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
else:
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
finally:
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
|
||||
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
|
||||
```
|
||||
|
||||
## Terminal in `try` with `finally` clause
|
||||
|
||||
TODO: we don't yet model that a `break` or `continue` in a `try` block will jump to a `finally`
|
||||
clause before it jumps to end/start of the loop.
|
||||
|
||||
```py
|
||||
def f():
|
||||
x = 1
|
||||
while True:
|
||||
try:
|
||||
break
|
||||
finally:
|
||||
x = 2
|
||||
# TODO: should be Literal[2]
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
## Nested functions
|
||||
|
||||
Free references inside of a function body refer to variables defined in the containing scope.
|
||||
Function bodies are _lazy scopes_: at runtime, these references are not resolved immediately at the
|
||||
point of the function definition. Instead, they are resolved _at the time of the call_, which means
|
||||
that their values (and types) can be different for different invocations. For simplicity, we instead
|
||||
resolve free references _at the end of the containing scope_. That means that in the examples below,
|
||||
all of the `x` bindings should be visible to the `reveal_type`, regardless of where we place the
|
||||
`return` statements.
|
||||
|
||||
TODO: These currently produce the wrong results, but not because of our terminal statement support.
|
||||
See [ruff#15777](https://github.com/astral-sh/ruff/issues/15777) for more details.
|
||||
|
||||
```py
|
||||
def top_level_return(cond1: bool, cond2: bool):
|
||||
x = 1
|
||||
|
||||
def g():
|
||||
# TODO eliminate Unknown
|
||||
reveal_type(x) # revealed: Unknown | Literal[1, 2, 3]
|
||||
if cond1:
|
||||
if cond2:
|
||||
x = 2
|
||||
else:
|
||||
x = 3
|
||||
return
|
||||
|
||||
def return_from_if(cond1: bool, cond2: bool):
|
||||
x = 1
|
||||
|
||||
def g():
|
||||
# TODO: Literal[1, 2, 3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1]
|
||||
if cond1:
|
||||
if cond2:
|
||||
x = 2
|
||||
else:
|
||||
x = 3
|
||||
return
|
||||
|
||||
def return_from_nested_if(cond1: bool, cond2: bool):
|
||||
x = 1
|
||||
|
||||
def g():
|
||||
# TODO: Literal[1, 2, 3]
|
||||
reveal_type(x) # revealed: Unknown | Literal[1, 3]
|
||||
if cond1:
|
||||
if cond2:
|
||||
x = 2
|
||||
return
|
||||
else:
|
||||
x = 3
|
||||
```
|
||||
|
||||
## Statically known terminal statements
|
||||
|
||||
We model reachability using the same visibility constraints that we use to model statically known
|
||||
bounds. In this example, we see that the `return` statement is always executed, and therefore that
|
||||
the `"b"` assignment is not visible to the `reveal_type`.
|
||||
|
||||
```py
|
||||
def _(cond: bool):
|
||||
x = "a"
|
||||
if cond:
|
||||
x = "b"
|
||||
if True:
|
||||
return
|
||||
|
||||
reveal_type(x) # revealed: Literal["a"]
|
||||
```
|
||||
|
||||
## Bindings after a terminal statement are unreachable
|
||||
|
||||
Any bindings introduced after a terminal statement are unreachable, and are currently considered not
|
||||
visible. We [anticipate](https://github.com/astral-sh/ruff/issues/15797) that we want to provide a
|
||||
more useful analysis for code after terminal statements.
|
||||
|
||||
```py
|
||||
def f(cond: bool) -> str:
|
||||
x = "before"
|
||||
if cond:
|
||||
reveal_type(x) # revealed: Literal["before"]
|
||||
return
|
||||
x = "after-return"
|
||||
# TODO: no unresolved-reference error
|
||||
# error: [unresolved-reference]
|
||||
reveal_type(x) # revealed: Unknown
|
||||
else:
|
||||
x = "else"
|
||||
reveal_type(x) # revealed: Literal["else"]
|
||||
```
|
||||
@@ -15,6 +15,7 @@ directly.
|
||||
### Negation
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from knot_extensions import Not, static_assert
|
||||
|
||||
def negate(n1: Not[int], n2: Not[Not[int]], n3: Not[Not[Not[int]]]) -> None:
|
||||
@@ -34,7 +35,7 @@ n: Not[int, str]
|
||||
|
||||
```py
|
||||
from knot_extensions import Intersection, Not, is_subtype_of, static_assert
|
||||
from typing_extensions import Never
|
||||
from typing_extensions import Literal, Never
|
||||
|
||||
class S: ...
|
||||
class T: ...
|
||||
@@ -83,8 +84,11 @@ def explicit_unknown(x: Unknown, y: tuple[str, Unknown], z: Unknown = 1) -> None
|
||||
reveal_type(x) # revealed: Unknown
|
||||
reveal_type(y) # revealed: tuple[str, Unknown]
|
||||
reveal_type(z) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
# Unknown can be subclassed, just like Any
|
||||
`Unknown` can be subclassed, just like `Any`:
|
||||
|
||||
```py
|
||||
class C(Unknown): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Unknown, Literal[object]]
|
||||
@@ -237,9 +241,12 @@ error_message = "A custom message "
|
||||
error_message += "constructed from multiple string literals"
|
||||
# error: "Static assertion error: A custom message constructed from multiple string literals"
|
||||
static_assert(False, error_message)
|
||||
```
|
||||
|
||||
# There are limitations to what we can still infer as a string literal. In those cases,
|
||||
# we simply fall back to the default message.
|
||||
There are limitations to what we can still infer as a string literal. In those cases, we simply fall
|
||||
back to the default message:
|
||||
|
||||
```py
|
||||
shouted_message = "A custom message".upper()
|
||||
# error: "Static assertion error: argument evaluates to `False`"
|
||||
static_assert(False, shouted_message)
|
||||
@@ -304,6 +311,7 @@ static_assert(not is_assignable_to(int, str))
|
||||
|
||||
```py
|
||||
from knot_extensions import is_disjoint_from, static_assert
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_disjoint_from(None, int))
|
||||
static_assert(not is_disjoint_from(Literal[2] | str, int))
|
||||
@@ -326,6 +334,7 @@ static_assert(not is_fully_static(type[Any]))
|
||||
|
||||
```py
|
||||
from knot_extensions import is_singleton, static_assert
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_singleton(None))
|
||||
static_assert(is_singleton(Literal[True]))
|
||||
@@ -338,6 +347,7 @@ static_assert(not is_singleton(Literal["a"]))
|
||||
|
||||
```py
|
||||
from knot_extensions import is_single_valued, static_assert
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_single_valued(None))
|
||||
static_assert(is_single_valued(Literal[True]))
|
||||
@@ -367,8 +377,11 @@ static_assert(is_subtype_of(TypeOf[str], type[str]))
|
||||
|
||||
class Base: ...
|
||||
class Derived(Base): ...
|
||||
```
|
||||
|
||||
# `TypeOf` can be used in annotations:
|
||||
`TypeOf` can also be used in annotations:
|
||||
|
||||
```py
|
||||
def type_of_annotation() -> None:
|
||||
t1: TypeOf[Base] = Base
|
||||
t2: TypeOf[Base] = Derived # error: [invalid-assignment]
|
||||
|
||||
@@ -39,7 +39,9 @@ def f(c: type[A]):
|
||||
reveal_type(c) # revealed: type[A]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
```
|
||||
|
||||
@@ -52,23 +54,31 @@ def f(c: type[a.B]):
|
||||
reveal_type(c) # revealed: type[B]
|
||||
```
|
||||
|
||||
```py path=a.py
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
class B: ...
|
||||
```
|
||||
|
||||
## Deeply qualified class literal from another module
|
||||
|
||||
```py path=a/test.py
|
||||
`a/test.py`:
|
||||
|
||||
```py
|
||||
import a.b
|
||||
|
||||
def f(c: type[a.b.C]):
|
||||
reveal_type(c) # revealed: type[C]
|
||||
```
|
||||
|
||||
```py path=a/__init__.py
|
||||
`a/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
```py path=a/b.py
|
||||
`a/b.py`:
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
```
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ This file contains tests for non-fully-static `type[]` types, such as `type[Any]
|
||||
## Simple
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: type[Any], y: type[str]):
|
||||
reveal_type(x) # revealed: type[Any]
|
||||
# TODO: could be `<object.__repr__ type> & Any`
|
||||
|
||||
@@ -41,7 +41,7 @@ static types can be assignable to gradual types):
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
static_assert(is_assignable_to(Unknown, Literal[1]))
|
||||
static_assert(is_assignable_to(Any, Literal[1]))
|
||||
@@ -333,7 +333,7 @@ assignable to any arbitrary type.
|
||||
|
||||
```py
|
||||
from knot_extensions import static_assert, is_assignable_to, Unknown
|
||||
from typing_extensions import Never, Any
|
||||
from typing_extensions import Never, Any, Literal
|
||||
|
||||
static_assert(is_assignable_to(Never, str))
|
||||
static_assert(is_assignable_to(Never, Literal[1]))
|
||||
|
||||
@@ -151,7 +151,7 @@ static_assert(is_disjoint_from(Never, object))
|
||||
### `None`
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import is_disjoint_from, static_assert
|
||||
|
||||
static_assert(is_disjoint_from(None, Literal[True]))
|
||||
@@ -245,6 +245,7 @@ static_assert(not is_disjoint_from(TypeOf[f], object))
|
||||
|
||||
```py
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_disjoint_from(None, AlwaysTruthy))
|
||||
static_assert(not is_disjoint_from(None, AlwaysFalsy))
|
||||
|
||||
@@ -84,4 +84,38 @@ static_assert(
|
||||
)
|
||||
```
|
||||
|
||||
## Unions containing tuples containing tuples containing unions (etc.)
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, static_assert, Intersection
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
|
||||
static_assert(
|
||||
is_equivalent_to(
|
||||
tuple[tuple[tuple[P | Q]]] | P,
|
||||
tuple[tuple[tuple[Q | P]]] | P,
|
||||
)
|
||||
)
|
||||
static_assert(
|
||||
is_equivalent_to(
|
||||
tuple[tuple[tuple[tuple[tuple[Intersection[P, Q]]]]]],
|
||||
tuple[tuple[tuple[tuple[tuple[Intersection[Q, P]]]]]],
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
## Intersections containing tuples containing unions
|
||||
|
||||
```py
|
||||
from knot_extensions import is_equivalent_to, static_assert, Intersection
|
||||
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
static_assert(is_equivalent_to(Intersection[tuple[P | Q], R], Intersection[tuple[Q | P], R]))
|
||||
```
|
||||
|
||||
[the equivalence relation]: https://typing.readthedocs.io/en/latest/spec/glossary.html#term-equivalent
|
||||
|
||||
@@ -54,6 +54,7 @@ static_assert(not is_gradual_equivalent_to(str | int | bytes, int | str | dict))
|
||||
|
||||
```py
|
||||
from knot_extensions import Unknown, is_gradual_equivalent_to, static_assert
|
||||
from typing import Any
|
||||
|
||||
static_assert(is_gradual_equivalent_to(tuple[str, Any], tuple[str, Unknown]))
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ static_assert(is_subtype_of(tuple[int], tuple))
|
||||
|
||||
```py
|
||||
from knot_extensions import is_subtype_of, static_assert
|
||||
from typing import Literal
|
||||
|
||||
class A: ...
|
||||
class B1(A): ...
|
||||
@@ -271,6 +272,7 @@ static_assert(is_subtype_of(Never, AlwaysFalsy))
|
||||
|
||||
```py
|
||||
from knot_extensions import AlwaysTruthy, AlwaysFalsy, is_subtype_of, static_assert
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_subtype_of(Literal[1], AlwaysTruthy))
|
||||
static_assert(is_subtype_of(Literal[0], AlwaysFalsy))
|
||||
@@ -309,7 +311,7 @@ static_assert(is_subtype_of(TypeOf[1:2:3], slice))
|
||||
### Special forms
|
||||
|
||||
```py
|
||||
from typing import _SpecialForm
|
||||
from typing import _SpecialForm, Literal
|
||||
from knot_extensions import TypeOf, is_subtype_of, static_assert
|
||||
|
||||
static_assert(is_subtype_of(TypeOf[Literal], _SpecialForm))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Truthiness
|
||||
|
||||
## Literals
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy
|
||||
@@ -45,3 +47,31 @@ def _(
|
||||
reveal_type(bool(c)) # revealed: bool
|
||||
reveal_type(bool(d)) # revealed: bool
|
||||
```
|
||||
|
||||
## Instances
|
||||
|
||||
Checks that we don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
|
||||
### __bool__ is bool
|
||||
|
||||
```py
|
||||
class BoolIsBool:
|
||||
__bool__ = bool
|
||||
|
||||
reveal_type(bool(BoolIsBool())) # revealed: bool
|
||||
```
|
||||
|
||||
### Conditional __bool__ method
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class Boom:
|
||||
if flag():
|
||||
__bool__ = bool
|
||||
else:
|
||||
__bool__ = int
|
||||
|
||||
reveal_type(bool(Boom())) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -19,11 +19,17 @@ static_assert(is_equivalent_to(Never, tuple[int, Never]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, Never, str]))
|
||||
static_assert(is_equivalent_to(Never, tuple[int, tuple[str, Never]]))
|
||||
static_assert(is_equivalent_to(Never, tuple[tuple[str, Never], int]))
|
||||
```
|
||||
|
||||
# The empty tuple is *not* equivalent to Never!
|
||||
The empty `tuple` is *not* equivalent to `Never`!
|
||||
|
||||
```py
|
||||
static_assert(not is_equivalent_to(Never, tuple[()]))
|
||||
```
|
||||
|
||||
# NoReturn is just a different spelling of Never, so the same is true for NoReturn
|
||||
`NoReturn` is just a different spelling of `Never`, so the same is true for `NoReturn`:
|
||||
|
||||
```py
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[NoReturn, int]))
|
||||
static_assert(is_equivalent_to(NoReturn, tuple[int, NoReturn]))
|
||||
|
||||
@@ -67,6 +67,8 @@ c.a = 2
|
||||
## Too many arguments
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
class C:
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` expects exactly one type parameter"
|
||||
x: ClassVar[int, str] = 1
|
||||
@@ -75,6 +77,8 @@ class C:
|
||||
## Illegal `ClassVar` in type expression
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
class C:
|
||||
# error: [invalid-type-form] "Type qualifier `typing.ClassVar` is not allowed in type expressions (only in annotation expressions)"
|
||||
x: ClassVar | int
|
||||
@@ -86,6 +90,8 @@ class C:
|
||||
## Used outside of a class
|
||||
|
||||
```py
|
||||
from typing import ClassVar
|
||||
|
||||
# TODO: this should be an error
|
||||
x: ClassVar[int] = 1
|
||||
```
|
||||
|
||||
@@ -28,7 +28,9 @@ reveal_type(not b) # revealed: Literal[False]
|
||||
reveal_type(not warnings) # revealed: Literal[False]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
`b.py`:
|
||||
|
||||
```py
|
||||
y = 1
|
||||
```
|
||||
|
||||
@@ -123,6 +125,8 @@ classes without a `__bool__` method, with or without `__len__`, must be inferred
|
||||
truthiness.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class AlwaysTrue:
|
||||
def __bool__(self) -> Literal[True]:
|
||||
return True
|
||||
@@ -137,15 +141,6 @@ class AlwaysFalse:
|
||||
# revealed: Literal[True]
|
||||
reveal_type(not AlwaysFalse())
|
||||
|
||||
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
class BoolIsBool:
|
||||
# TODO: The `type[bool]` declaration here is a workaround to avoid running into
|
||||
# https://github.com/astral-sh/ruff/issues/15672
|
||||
__bool__: type[bool] = bool
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not BoolIsBool())
|
||||
|
||||
# At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because
|
||||
# a subclass could add a `__bool__` method.
|
||||
class NoBoolMethod: ...
|
||||
|
||||
@@ -282,7 +282,7 @@ reveal_type(b) # revealed: Unknown
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\u9E6C"
|
||||
(a, b) = "\u9e6c"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -292,7 +292,7 @@ reveal_type(b) # revealed: Unknown
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
|
||||
(a, b) = "\U0010FFFF"
|
||||
(a, b) = "\U0010ffff"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: Unknown
|
||||
@@ -301,7 +301,7 @@ reveal_type(b) # revealed: Unknown
|
||||
### Surrogates
|
||||
|
||||
```py
|
||||
(a, b) = "\uD800\uDFFF"
|
||||
(a, b) = "\ud800\udfff"
|
||||
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
@@ -361,6 +361,8 @@ def _(arg: tuple[int, int, int] | tuple[int, str, bytes] | tuple[int, int, str])
|
||||
### Nested
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
def _(arg: tuple[int, tuple[str, bytes]] | tuple[tuple[int, bytes], Literal["ab"]]):
|
||||
a, (b, c) = arg
|
||||
reveal_type(a) # revealed: int | tuple[int, bytes]
|
||||
|
||||
@@ -88,6 +88,8 @@ with Manager():
|
||||
## Context manager with non-callable `__exit__` attribute
|
||||
|
||||
```py
|
||||
from typing_extensions import Self
|
||||
|
||||
class Manager:
|
||||
def __enter__(self) -> Self: ...
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
|
||||
@@ -5,20 +6,23 @@ use salsa::plumbing::AsId;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_index::IndexVec;
|
||||
use ruff_index::{IndexSlice, IndexVec};
|
||||
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIds;
|
||||
use crate::semantic_index::attribute_assignment::AttributeAssignments;
|
||||
use crate::semantic_index::builder::SemanticIndexBuilder;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionNodeKey};
|
||||
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 mod ast_ids;
|
||||
pub mod attribute_assignment;
|
||||
mod builder;
|
||||
pub(crate) mod constraint;
|
||||
pub mod definition;
|
||||
@@ -27,7 +31,8 @@ pub mod symbol;
|
||||
mod use_def;
|
||||
|
||||
pub(crate) use self::use_def::{
|
||||
BindingWithConstraints, DeclarationWithConstraint, ScopedVisibilityConstraintId, UseDefMap,
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
|
||||
DeclarationsIterator,
|
||||
};
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
|
||||
@@ -90,6 +95,25 @@ pub(crate) fn use_def_map<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<UseD
|
||||
index.use_def_map(scope.file_scope_id(db))
|
||||
}
|
||||
|
||||
/// Returns all attribute assignments for a specific class body scope.
|
||||
///
|
||||
/// Using [`attribute_assignments`] over [`semantic_index`] has the advantage that
|
||||
/// Salsa can avoid invalidating dependent queries if this scope's instance attributes
|
||||
/// are unchanged.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn attribute_assignments<'db>(
|
||||
db: &'db dyn Db,
|
||||
class_body_scope: ScopeId<'db>,
|
||||
) -> Option<Arc<AttributeAssignments<'db>>> {
|
||||
let file = class_body_scope.file(db);
|
||||
let index = semantic_index(db, file);
|
||||
|
||||
index
|
||||
.attribute_assignments
|
||||
.get(&class_body_scope.file_scope_id(db))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Returns the module global scope of `file`.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
@@ -136,6 +160,10 @@ pub(crate) struct SemanticIndex<'db> {
|
||||
|
||||
/// Flags about the global scope (code usage impacting inference)
|
||||
has_future_annotations: bool,
|
||||
|
||||
/// Maps from class body scopes to attribute assignments that were found
|
||||
/// in methods of that class.
|
||||
attribute_assignments: FxHashMap<FileScopeId, Arc<AttributeAssignments<'db>>>,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndex<'db> {
|
||||
@@ -203,43 +231,20 @@ impl<'db> SemanticIndex<'db> {
|
||||
|
||||
/// Returns an iterator over the descendent scopes of `scope`.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn descendent_scopes(
|
||||
&self,
|
||||
scope_id: FileScopeId,
|
||||
) -> impl Iterator<Item = (FileScopeId, &Scope)> + '_ {
|
||||
let scope = &self.scopes[scope_id];
|
||||
let scopes = &self.scopes[scope.descendents.clone()];
|
||||
let mut next_id = scope_id + 1;
|
||||
scopes.iter().map(move |descendent| {
|
||||
let result = (next_id, descendent);
|
||||
next_id = next_id + 1;
|
||||
result
|
||||
})
|
||||
pub(crate) fn descendent_scopes(&self, scope: FileScopeId) -> DescendentsIter {
|
||||
DescendentsIter::new(self, scope)
|
||||
}
|
||||
|
||||
/// Returns an iterator over the direct child scopes of `scope`.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn child_scopes(
|
||||
&self,
|
||||
scope_id: FileScopeId,
|
||||
) -> impl Iterator<Item = (FileScopeId, &Scope)> + '_ {
|
||||
self.descendent_scopes(scope_id)
|
||||
.filter(move |(_, scope)| scope.parent == Some(scope_id))
|
||||
pub(crate) fn child_scopes(&self, scope: FileScopeId) -> ChildrenIter {
|
||||
ChildrenIter::new(self, scope)
|
||||
}
|
||||
|
||||
/// Returns an iterator over all ancestors of `scope`, starting with `scope` itself.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn ancestor_scopes(
|
||||
&self,
|
||||
scope_id: FileScopeId,
|
||||
) -> impl Iterator<Item = (FileScopeId, &Scope)> + '_ {
|
||||
let mut next_id = Some(scope_id);
|
||||
std::iter::from_fn(move || {
|
||||
let current_id = next_id?;
|
||||
let current = &self.scopes[current_id];
|
||||
next_id = current.parent;
|
||||
Some((current_id, current))
|
||||
})
|
||||
pub(crate) fn ancestor_scopes(&self, scope: FileScopeId) -> AncestorsIter {
|
||||
AncestorsIter::new(self, scope)
|
||||
}
|
||||
|
||||
/// Returns the [`Definition`] salsa ingredient for `definition_key`.
|
||||
@@ -286,6 +291,98 @@ impl<'db> SemanticIndex<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AncestorsIter<'a> {
|
||||
scopes: &'a IndexSlice<FileScopeId, Scope>,
|
||||
next_id: Option<FileScopeId>,
|
||||
}
|
||||
|
||||
impl<'a> AncestorsIter<'a> {
|
||||
fn new(module_symbol_table: &'a SemanticIndex, start: FileScopeId) -> Self {
|
||||
Self {
|
||||
scopes: &module_symbol_table.scopes,
|
||||
next_id: Some(start),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AncestorsIter<'a> {
|
||||
type Item = (FileScopeId, &'a Scope);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current_id = self.next_id?;
|
||||
let current = &self.scopes[current_id];
|
||||
self.next_id = current.parent;
|
||||
|
||||
Some((current_id, current))
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for AncestorsIter<'_> {}
|
||||
|
||||
pub struct DescendentsIter<'a> {
|
||||
next_id: FileScopeId,
|
||||
descendents: std::slice::Iter<'a, Scope>,
|
||||
}
|
||||
|
||||
impl<'a> DescendentsIter<'a> {
|
||||
fn new(symbol_table: &'a SemanticIndex, scope_id: FileScopeId) -> Self {
|
||||
let scope = &symbol_table.scopes[scope_id];
|
||||
let scopes = &symbol_table.scopes[scope.descendents.clone()];
|
||||
|
||||
Self {
|
||||
next_id: scope_id + 1,
|
||||
descendents: scopes.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for DescendentsIter<'a> {
|
||||
type Item = (FileScopeId, &'a Scope);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let descendent = self.descendents.next()?;
|
||||
let id = self.next_id;
|
||||
self.next_id = self.next_id + 1;
|
||||
|
||||
Some((id, descendent))
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.descendents.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for DescendentsIter<'_> {}
|
||||
|
||||
impl ExactSizeIterator for DescendentsIter<'_> {}
|
||||
|
||||
pub struct ChildrenIter<'a> {
|
||||
parent: FileScopeId,
|
||||
descendents: DescendentsIter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> ChildrenIter<'a> {
|
||||
fn new(module_symbol_table: &'a SemanticIndex, parent: FileScopeId) -> Self {
|
||||
let descendents = DescendentsIter::new(module_symbol_table, parent);
|
||||
|
||||
Self {
|
||||
parent,
|
||||
descendents,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for ChildrenIter<'a> {
|
||||
type Item = (FileScopeId, &'a Scope);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.descendents
|
||||
.find(|(_, scope)| scope.parent == Some(self.parent))
|
||||
}
|
||||
}
|
||||
|
||||
impl FusedIterator for ChildrenIter<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::semantic_index::expression::Expression;
|
||||
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
/// Describes an (annotated) attribute assignment that we discovered in a method
|
||||
/// body, typically of the form `self.x: int`, `self.x: int = …` or `self.x = …`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum AttributeAssignment<'db> {
|
||||
/// An attribute assignment with an explicit type annotation, either
|
||||
/// `self.x: <annotation>` or `self.x: <annotation> = …`.
|
||||
Annotated { annotation: Expression<'db> },
|
||||
|
||||
/// An attribute assignment without a type annotation, e.g. `self.x = <value>`.
|
||||
Unannotated { value: Expression<'db> },
|
||||
}
|
||||
|
||||
pub(crate) type AttributeAssignments<'db> = FxHashMap<Name, Vec<AttributeAssignment<'db>>>;
|
||||
@@ -14,22 +14,21 @@ use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
use crate::semantic_index::ast_ids::AstIdsBuilder;
|
||||
use crate::semantic_index::attribute_assignment::{AttributeAssignment, AttributeAssignments};
|
||||
use crate::semantic_index::constraint::PatternConstraintKind;
|
||||
use crate::semantic_index::definition::{
|
||||
AssignmentDefinitionNodeRef, ComprehensionDefinitionNodeRef, Definition, DefinitionNodeKey,
|
||||
DefinitionNodeRef, ForStmtDefinitionNodeRef, ImportFromDefinitionNodeRef,
|
||||
};
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::expression::{Expression, ExpressionKind};
|
||||
use crate::semantic_index::symbol::{
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
|
||||
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopeKind, ScopedSymbolId,
|
||||
SymbolTableBuilder,
|
||||
};
|
||||
use crate::semantic_index::use_def::{
|
||||
FlowSnapshot, ScopedConstraintId, ScopedVisibilityConstraintId, UseDefMapBuilder,
|
||||
};
|
||||
use crate::semantic_index::use_def::{FlowSnapshot, ScopedConstraintId, UseDefMapBuilder};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::unpack::{Unpack, UnpackValue};
|
||||
use crate::visibility_constraints::VisibilityConstraint;
|
||||
use crate::visibility_constraints::{ScopedVisibilityConstraintId, VisibilityConstraintsBuilder};
|
||||
use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
|
||||
@@ -53,17 +52,27 @@ impl LoopState {
|
||||
}
|
||||
}
|
||||
|
||||
struct ScopeInfo {
|
||||
file_scope_id: FileScopeId,
|
||||
loop_state: LoopState,
|
||||
}
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db> {
|
||||
// Builder state
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
// A shared clone of the path of the file being analyzed. We use this as a label for all of the
|
||||
// metrics that we export, and this avoids cloning the path into a new string each time.
|
||||
file_path: Arc<str>,
|
||||
module: &'db ParsedModule,
|
||||
scope_stack: Vec<(FileScopeId, LoopState)>,
|
||||
scope_stack: Vec<ScopeInfo>,
|
||||
/// The assignments we're currently visiting, with
|
||||
/// the most recent visit at the end of the Vec
|
||||
current_assignments: Vec<CurrentAssignment<'db>>,
|
||||
/// The match case we're currently visiting.
|
||||
current_match_case: Option<CurrentMatchCase<'db>>,
|
||||
/// The name of the first function parameter of the innermost function that we're currently visiting.
|
||||
current_first_parameter_name: Option<&'db str>,
|
||||
|
||||
/// Flow states at each `break` in the current loop.
|
||||
loop_break_states: Vec<FlowSnapshot>,
|
||||
@@ -84,17 +93,21 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
definitions_by_node: FxHashMap<DefinitionNodeKey, Definition<'db>>,
|
||||
expressions_by_node: FxHashMap<ExpressionNodeKey, Expression<'db>>,
|
||||
imported_modules: FxHashSet<ModuleName>,
|
||||
attribute_assignments: FxHashMap<FileScopeId, AttributeAssignments<'db>>,
|
||||
}
|
||||
|
||||
impl<'db> SemanticIndexBuilder<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, file: File, parsed: &'db ParsedModule) -> Self {
|
||||
let file_path = Arc::from(file.path(db).as_str());
|
||||
let mut builder = Self {
|
||||
db,
|
||||
file,
|
||||
file_path,
|
||||
module: parsed,
|
||||
scope_stack: Vec::new(),
|
||||
current_assignments: vec![],
|
||||
current_match_case: None,
|
||||
current_first_parameter_name: None,
|
||||
loop_break_states: vec![],
|
||||
try_node_context_stack_manager: TryNodeContextStackManager::default(),
|
||||
|
||||
@@ -112,6 +125,8 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
expressions_by_node: FxHashMap::default(),
|
||||
|
||||
imported_modules: FxHashSet::default(),
|
||||
|
||||
attribute_assignments: FxHashMap::default(),
|
||||
};
|
||||
|
||||
builder.push_scope_with_parent(NodeWithScopeRef::Module, None);
|
||||
@@ -123,7 +138,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
*self
|
||||
.scope_stack
|
||||
.last()
|
||||
.map(|(scope, _)| scope)
|
||||
.map(|ScopeInfo { file_scope_id, .. }| file_scope_id)
|
||||
.expect("Always to have a root scope")
|
||||
}
|
||||
|
||||
@@ -131,14 +146,32 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.scope_stack
|
||||
.last()
|
||||
.expect("Always to have a root scope")
|
||||
.1
|
||||
.loop_state
|
||||
}
|
||||
|
||||
/// Returns the scope ID of the surrounding class body scope if the current scope
|
||||
/// is a method inside a class body. Returns `None` otherwise, e.g. if the current
|
||||
/// scope is a function body outside of a class, or if the current scope is not a
|
||||
/// function body.
|
||||
fn is_method_of_class(&self) -> Option<FileScopeId> {
|
||||
let mut scopes_rev = self.scope_stack.iter().rev();
|
||||
let current = scopes_rev.next()?;
|
||||
let parent = scopes_rev.next()?;
|
||||
|
||||
match (
|
||||
self.scopes[current.file_scope_id].kind(),
|
||||
self.scopes[parent.file_scope_id].kind(),
|
||||
) {
|
||||
(ScopeKind::Function, ScopeKind::Class) => Some(parent.file_scope_id),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_inside_loop(&mut self, state: LoopState) {
|
||||
self.scope_stack
|
||||
.last_mut()
|
||||
.expect("Always to have a root scope")
|
||||
.1 = state;
|
||||
.loop_state = state;
|
||||
}
|
||||
|
||||
fn push_scope(&mut self, node: NodeWithScopeRef) {
|
||||
@@ -158,6 +191,13 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
};
|
||||
self.try_node_context_stack_manager.enter_nested_scope();
|
||||
|
||||
metrics::counter!(
|
||||
"semantic_index.scope_count",
|
||||
"file" => self.file_path.clone(),
|
||||
"kind" => scope.kind().as_str(),
|
||||
)
|
||||
.increment(1);
|
||||
|
||||
let file_scope_id = self.scopes.push(scope);
|
||||
self.symbol_tables.push(SymbolTableBuilder::default());
|
||||
self.use_def_maps.push(UseDefMapBuilder::default());
|
||||
@@ -171,16 +211,20 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
|
||||
debug_assert_eq!(ast_id_scope, file_scope_id);
|
||||
|
||||
self.scope_stack.push((file_scope_id, LoopState::NotInLoop));
|
||||
self.scope_stack.push(ScopeInfo {
|
||||
file_scope_id,
|
||||
loop_state: LoopState::NotInLoop,
|
||||
});
|
||||
}
|
||||
|
||||
fn pop_scope(&mut self) -> FileScopeId {
|
||||
let (id, _) = self.scope_stack.pop().expect("Root scope to be present");
|
||||
let ScopeInfo { file_scope_id, .. } =
|
||||
self.scope_stack.pop().expect("Root scope to be present");
|
||||
let children_end = self.scopes.next_index();
|
||||
let scope = &mut self.scopes[id];
|
||||
let scope = &mut self.scopes[file_scope_id];
|
||||
scope.descendents = scope.descendents.start..children_end;
|
||||
self.try_node_context_stack_manager.exit_scope();
|
||||
id
|
||||
file_scope_id
|
||||
}
|
||||
|
||||
fn current_symbol_table(&mut self) -> &mut SymbolTableBuilder {
|
||||
@@ -198,6 +242,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
&self.use_def_maps[scope_id]
|
||||
}
|
||||
|
||||
fn current_visibility_constraints_mut(&mut self) -> &mut VisibilityConstraintsBuilder<'db> {
|
||||
let scope_id = self.current_scope();
|
||||
&mut self.use_def_maps[scope_id].visibility_constraints
|
||||
}
|
||||
|
||||
fn current_ast_ids(&mut self) -> &mut AstIdsBuilder {
|
||||
let scope_id = self.current_scope();
|
||||
&mut self.ast_ids[scope_id]
|
||||
@@ -333,21 +382,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
id
|
||||
}
|
||||
|
||||
/// Adds a new visibility constraint, but does not record it. Returns the constraint ID
|
||||
/// for later recording using [`SemanticIndexBuilder::record_visibility_constraint_id`].
|
||||
fn add_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: VisibilityConstraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
.add_visibility_constraint(constraint)
|
||||
}
|
||||
|
||||
/// Records a previously added visibility constraint by applying it to all live bindings
|
||||
/// and declarations.
|
||||
fn record_visibility_constraint_id(&mut self, constraint: ScopedVisibilityConstraintId) {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint_id(constraint);
|
||||
.record_visibility_constraint(constraint);
|
||||
}
|
||||
|
||||
/// Negates the given visibility constraint and then adds it to all live bindings and declarations.
|
||||
@@ -355,8 +394,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
&mut self,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint(VisibilityConstraint::VisibleIfNot(constraint))
|
||||
let id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_not_constraint(constraint);
|
||||
self.record_visibility_constraint_id(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Records a visibility constraint by applying it to all live bindings and declarations.
|
||||
@@ -364,14 +406,23 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
&mut self,
|
||||
constraint: Constraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
|
||||
let id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 0);
|
||||
self.record_visibility_constraint_id(id);
|
||||
id
|
||||
}
|
||||
|
||||
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
|
||||
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
|
||||
/// Records that all remaining statements in the current block are unreachable, and therefore
|
||||
/// not visible.
|
||||
fn mark_unreachable(&mut self) {
|
||||
self.current_use_def_map_mut().mark_unreachable();
|
||||
}
|
||||
|
||||
/// Records a visibility constraint that always evaluates to "ambiguous".
|
||||
fn record_ambiguous_visibility(&mut self) {
|
||||
self.current_use_def_map_mut()
|
||||
.record_visibility_constraint(VisibilityConstraint::Ambiguous)
|
||||
.record_visibility_constraint(ScopedVisibilityConstraintId::AMBIGUOUS);
|
||||
}
|
||||
|
||||
/// Simplifies (resets) visibility constraints on all live bindings and declarations that did
|
||||
@@ -398,6 +449,32 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.current_assignments.last_mut()
|
||||
}
|
||||
|
||||
/// Records the fact that we saw an attribute assignment of the form
|
||||
/// `object.attr: <annotation>( = …)` or `object.attr = <value>`.
|
||||
fn register_attribute_assignment(
|
||||
&mut self,
|
||||
object: &ast::Expr,
|
||||
attr: &'db ast::Identifier,
|
||||
attribute_assignment: AttributeAssignment<'db>,
|
||||
) {
|
||||
if let Some(class_body_scope) = self.is_method_of_class() {
|
||||
// We only care about attribute assignments to the first parameter of a method,
|
||||
// i.e. typically `self` or `cls`.
|
||||
let accessed_object_refers_to_first_parameter =
|
||||
object.as_name_expr().map(|name| name.id.as_str())
|
||||
== self.current_first_parameter_name;
|
||||
|
||||
if accessed_object_refers_to_first_parameter {
|
||||
self.attribute_assignments
|
||||
.entry(class_body_scope)
|
||||
.or_default()
|
||||
.entry(attr.id().clone())
|
||||
.or_default()
|
||||
.push(attribute_assignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_pattern_constraint(
|
||||
&mut self,
|
||||
subject: Expression<'db>,
|
||||
@@ -451,6 +528,20 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
/// 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) -> Expression<'db> {
|
||||
self.add_standalone_expression_impl(expression_node, ExpressionKind::Normal)
|
||||
}
|
||||
|
||||
/// Same as [`SemanticIndexBuilder::add_standalone_expression`], but marks the expression as a
|
||||
/// *type* expression, which makes sure that it will later be inferred as such.
|
||||
fn add_standalone_type_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
|
||||
self.add_standalone_expression_impl(expression_node, ExpressionKind::TypeExpression)
|
||||
}
|
||||
|
||||
fn add_standalone_expression_impl(
|
||||
&mut self,
|
||||
expression_node: &ast::Expr,
|
||||
expression_kind: ExpressionKind,
|
||||
) -> Expression<'db> {
|
||||
let expression = Expression::new(
|
||||
self.db,
|
||||
self.file,
|
||||
@@ -459,6 +550,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), expression_node)
|
||||
},
|
||||
expression_kind,
|
||||
countme::Count::default(),
|
||||
);
|
||||
self.expressions_by_node
|
||||
@@ -599,7 +691,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
}
|
||||
|
||||
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
|
||||
let symbol = self.add_symbol(parameter.parameter.name.id().clone());
|
||||
let symbol = self.add_symbol(parameter.name().id().clone());
|
||||
|
||||
let definition = self.add_definition(symbol, parameter);
|
||||
|
||||
@@ -662,6 +754,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
use_def_maps,
|
||||
imported_modules: Arc::new(self.imported_modules),
|
||||
has_future_annotations: self.has_future_annotations,
|
||||
attribute_assignments: self
|
||||
.attribute_assignments
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, Arc::new(v)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -700,7 +797,38 @@ where
|
||||
|
||||
builder.declare_parameters(parameters);
|
||||
|
||||
builder.visit_body(body);
|
||||
let mut first_parameter_name = parameters
|
||||
.iter_non_variadic_params()
|
||||
.next()
|
||||
.map(|first_param| first_param.parameter.name.id().as_str());
|
||||
std::mem::swap(
|
||||
&mut builder.current_first_parameter_name,
|
||||
&mut first_parameter_name,
|
||||
);
|
||||
|
||||
// TODO: Fix how we determine the public types of symbols in a
|
||||
// function-like scope: https://github.com/astral-sh/ruff/issues/15777
|
||||
//
|
||||
// In the meantime, visit the function body, but treat the last statement
|
||||
// specially if it is a return. If it is, this would cause all definitions
|
||||
// in the function to be marked as non-visible with our current treatment
|
||||
// of terminal statements. Since we currently model the externally visible
|
||||
// definitions in a function scope as the set of bindings that are visible
|
||||
// at the end of the body, we then consider this function to have no
|
||||
// externally visible definitions. To get around this, we take a flow
|
||||
// snapshot just before processing the return statement, and use _that_ as
|
||||
// the "end-of-body" state that we resolve external references against.
|
||||
if let Some((last_stmt, first_stmts)) = body.split_last() {
|
||||
builder.visit_body(first_stmts);
|
||||
let pre_return_state = matches!(last_stmt, ast::Stmt::Return(_))
|
||||
.then(|| builder.flow_snapshot());
|
||||
builder.visit_stmt(last_stmt);
|
||||
if let Some(pre_return_state) = pre_return_state {
|
||||
builder.flow_restore(pre_return_state);
|
||||
}
|
||||
}
|
||||
|
||||
builder.current_first_parameter_name = first_parameter_name;
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
@@ -834,6 +962,19 @@ where
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Unannotated { value },
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -852,6 +993,7 @@ where
|
||||
ast::Stmt::AnnAssign(node) => {
|
||||
debug_assert_eq!(&self.current_assignments, &[]);
|
||||
self.visit_expr(&node.annotation);
|
||||
let annotation = self.add_standalone_type_expression(&node.annotation);
|
||||
if let Some(value) = &node.value {
|
||||
self.visit_expr(value);
|
||||
}
|
||||
@@ -863,6 +1005,20 @@ where
|
||||
) {
|
||||
self.push_assignment(node.into());
|
||||
self.visit_expr(&node.target);
|
||||
|
||||
if let ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) = &*node.target
|
||||
{
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Annotated { annotation },
|
||||
);
|
||||
}
|
||||
|
||||
self.pop_assignment();
|
||||
} else {
|
||||
self.visit_expr(&node.target);
|
||||
@@ -964,6 +1120,16 @@ where
|
||||
let pre_loop = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(test);
|
||||
|
||||
// We need multiple copies of the visibility constraint for the while condition,
|
||||
// since we need to model situations where the first evaluation of the condition
|
||||
// returns True, but a later evaluation returns False.
|
||||
let first_vis_constraint_id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 0);
|
||||
let later_vis_constraint_id = self
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 1);
|
||||
|
||||
// Save aside any break states from an outer loop
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
|
||||
@@ -974,26 +1140,42 @@ where
|
||||
self.visit_body(body);
|
||||
self.set_inside_loop(outer_loop_state);
|
||||
|
||||
let vis_constraint_id = self.record_visibility_constraint(constraint);
|
||||
// If the body is executed, we know that we've evaluated the condition at least
|
||||
// once, and that the first evaluation was True. We might not have evaluated the
|
||||
// condition more than once, so we can't assume that later evaluations were True.
|
||||
// So the body's full visibility constraint is `first`.
|
||||
let body_vis_constraint_id = first_vis_constraint_id;
|
||||
self.record_visibility_constraint_id(body_vis_constraint_id);
|
||||
|
||||
// Get the break states from the body of this loop, and restore the saved outer
|
||||
// ones.
|
||||
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.clone());
|
||||
// We execute the `else` once the condition evaluates to false. This could happen
|
||||
// without ever executing the body, if the condition is false the first time it's
|
||||
// tested. So the starting flow state of the `else` clause is the union of:
|
||||
// - the pre-loop state with a visibility constraint that the first evaluation of
|
||||
// the while condition was false,
|
||||
// - the post-body state (which already has a visibility constraint that the
|
||||
// first evaluation was true) with a visibility constraint that a _later_
|
||||
// evaluation of the while condition was false.
|
||||
// To model this correctly, we need two copies of the while condition constraint,
|
||||
// since the first and later evaluations might produce different results.
|
||||
let post_body = self.flow_snapshot();
|
||||
self.flow_restore(pre_loop.clone());
|
||||
self.record_negated_visibility_constraint(first_vis_constraint_id);
|
||||
self.flow_merge(post_body);
|
||||
self.record_negated_constraint(constraint);
|
||||
self.visit_body(orelse);
|
||||
self.record_negated_visibility_constraint(vis_constraint_id);
|
||||
self.record_negated_visibility_constraint(later_vis_constraint_id);
|
||||
|
||||
// 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 {
|
||||
let snapshot = self.flow_snapshot();
|
||||
self.flow_restore(break_state);
|
||||
self.record_visibility_constraint(constraint);
|
||||
self.record_visibility_constraint_id(body_vis_constraint_id);
|
||||
self.flow_merge(snapshot);
|
||||
}
|
||||
|
||||
@@ -1019,11 +1201,6 @@ where
|
||||
}
|
||||
self.visit_body(body);
|
||||
}
|
||||
ast::Stmt::Break(_) => {
|
||||
if self.loop_state().is_inside() {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::For(
|
||||
for_stmt @ ast::StmtFor {
|
||||
@@ -1270,6 +1447,21 @@ where
|
||||
// - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702
|
||||
self.visit_body(finalbody);
|
||||
}
|
||||
|
||||
ast::Stmt::Raise(_) | ast::Stmt::Return(_) | ast::Stmt::Continue(_) => {
|
||||
walk_stmt(self, stmt);
|
||||
// Everything in the current block after a terminal statement is unreachable.
|
||||
self.mark_unreachable();
|
||||
}
|
||||
|
||||
ast::Stmt::Break(_) => {
|
||||
if self.loop_state().is_inside() {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
// Everything in the current block after a terminal statement is unreachable.
|
||||
self.mark_unreachable();
|
||||
}
|
||||
|
||||
_ => {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
@@ -1508,7 +1700,8 @@ where
|
||||
ast::BoolOp::Or => self.add_negated_constraint(constraint),
|
||||
};
|
||||
let visibility_constraint = self
|
||||
.add_visibility_constraint(VisibilityConstraint::VisibleIf(constraint));
|
||||
.current_visibility_constraints_mut()
|
||||
.add_atom(constraint, 0);
|
||||
|
||||
let after_expr = self.flow_snapshot();
|
||||
|
||||
|
||||
@@ -5,20 +5,20 @@ use crate::db::Db;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) struct Constraint<'db> {
|
||||
pub(crate) node: ConstraintNode<'db>,
|
||||
pub(crate) is_positive: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum ConstraintNode<'db> {
|
||||
Expression(Expression<'db>),
|
||||
Pattern(PatternConstraint<'db>),
|
||||
}
|
||||
|
||||
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, Hash, PartialEq)]
|
||||
pub(crate) enum PatternConstraintKind<'db> {
|
||||
Singleton(Singleton, Option<Expression<'db>>),
|
||||
Value(Expression<'db>, Option<Expression<'db>>),
|
||||
|
||||
@@ -5,6 +5,16 @@ use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use salsa;
|
||||
|
||||
/// Whether or not this expression should be inferred as a normal expression or
|
||||
/// a type expression. For example, in `self.x: <annotation> = <value>`, the
|
||||
/// `<annotation>` is inferred as a type expression, while `<value>` is inferred
|
||||
/// as a normal expression.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub(crate) enum ExpressionKind {
|
||||
Normal,
|
||||
TypeExpression,
|
||||
}
|
||||
|
||||
/// An independently type-inferable expression.
|
||||
///
|
||||
/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment.
|
||||
@@ -35,6 +45,10 @@ pub(crate) struct Expression<'db> {
|
||||
#[return_ref]
|
||||
pub(crate) node_ref: AstNodeRef<ast::Expr>,
|
||||
|
||||
/// Should this expression be inferred as a normal expression or a type expression?
|
||||
#[id]
|
||||
pub(crate) kind: ExpressionKind,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Expression<'static>>,
|
||||
}
|
||||
|
||||
@@ -119,6 +119,7 @@ impl<'db> ScopeId<'db> {
|
||||
self.node(db).scope_kind(),
|
||||
ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::Lambda
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Comprehension
|
||||
)
|
||||
@@ -203,6 +204,7 @@ pub enum ScopeKind {
|
||||
Annotation,
|
||||
Class,
|
||||
Function,
|
||||
Lambda,
|
||||
Comprehension,
|
||||
TypeAlias,
|
||||
}
|
||||
@@ -211,6 +213,18 @@ impl ScopeKind {
|
||||
pub const fn is_comprehension(self) -> bool {
|
||||
matches!(self, ScopeKind::Comprehension)
|
||||
}
|
||||
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Module => "Module",
|
||||
Self::Annotation => "Annotation",
|
||||
Self::Class => "Class",
|
||||
Self::Function => "Function",
|
||||
Self::Lambda => "Lambda",
|
||||
Self::Comprehension => "Comprehension",
|
||||
Self::TypeAlias => "TypeAlias",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Symbol table for a specific [`Scope`].
|
||||
@@ -443,7 +457,8 @@ impl NodeWithScopeKind {
|
||||
match self {
|
||||
Self::Module => ScopeKind::Module,
|
||||
Self::Class(_) => ScopeKind::Class,
|
||||
Self::Function(_) | Self::Lambda(_) => ScopeKind::Function,
|
||||
Self::Function(_) => ScopeKind::Function,
|
||||
Self::Lambda(_) => ScopeKind::Lambda,
|
||||
Self::FunctionTypeParameters(_)
|
||||
| Self::ClassTypeParameters(_)
|
||||
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
|
||||
|
||||
@@ -255,13 +255,18 @@
|
||||
//! 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.
|
||||
pub(crate) use self::symbol_state::{ScopedConstraintId, ScopedVisibilityConstraintId};
|
||||
use self::symbol_state::{ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState};
|
||||
pub(crate) use self::symbol_state::ScopedConstraintId;
|
||||
use self::symbol_state::{
|
||||
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
|
||||
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
|
||||
};
|
||||
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use crate::semantic_index::use_def::symbol_state::DeclarationIdWithConstraint;
|
||||
use crate::visibility_constraints::{VisibilityConstraint, VisibilityConstraints};
|
||||
use crate::visibility_constraints::{
|
||||
ScopedVisibilityConstraintId, VisibilityConstraints, VisibilityConstraintsBuilder,
|
||||
};
|
||||
use ruff_index::IndexVec;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -282,8 +287,8 @@ pub(crate) struct UseDefMap<'db> {
|
||||
/// Array of [`Constraint`] in this scope.
|
||||
all_constraints: AllConstraints<'db>,
|
||||
|
||||
/// Array of [`VisibilityConstraint`]s in this scope.
|
||||
pub(crate) visibility_constraints: VisibilityConstraints<'db>,
|
||||
/// Array of visibility constraints in this scope.
|
||||
visibility_constraints: VisibilityConstraints<'db>,
|
||||
|
||||
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
|
||||
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
|
||||
@@ -310,24 +315,21 @@ impl<'db> UseDefMap<'db> {
|
||||
pub(crate) fn bindings_at_use(
|
||||
&self,
|
||||
use_id: ScopedUseId,
|
||||
) -> impl Iterator<Item = BindingWithConstraints<'db, impl Iterator<Item = Constraint<'db>> + '_>> + '_
|
||||
{
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
self.bindings_iterator(&self.bindings_by_use[use_id])
|
||||
}
|
||||
|
||||
pub(crate) fn public_bindings(
|
||||
&self,
|
||||
symbol: ScopedSymbolId,
|
||||
) -> impl Iterator<Item = BindingWithConstraints<'db, impl Iterator<Item = Constraint<'db>> + '_>> + '_
|
||||
{
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
self.bindings_iterator(self.public_symbols[symbol].bindings())
|
||||
}
|
||||
|
||||
pub(crate) fn bindings_at_declaration(
|
||||
&self,
|
||||
declaration: Definition<'db>,
|
||||
) -> impl Iterator<Item = BindingWithConstraints<'db, impl Iterator<Item = Constraint<'db>> + '_>> + '_
|
||||
{
|
||||
) -> BindingWithConstraintsIterator<'_, 'db> {
|
||||
if let SymbolDefinitions::Bindings(bindings) = &self.definitions_by_definition[&declaration]
|
||||
{
|
||||
self.bindings_iterator(bindings)
|
||||
@@ -336,10 +338,10 @@ impl<'db> UseDefMap<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn declarations_at_binding(
|
||||
&self,
|
||||
pub(crate) fn declarations_at_binding<'map>(
|
||||
&'map self,
|
||||
binding: Definition<'db>,
|
||||
) -> impl Iterator<Item = DeclarationWithConstraint<'db>> + '_ {
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
if let SymbolDefinitions::Declarations(declarations) =
|
||||
&self.definitions_by_definition[&binding]
|
||||
{
|
||||
@@ -349,10 +351,10 @@ impl<'db> UseDefMap<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn public_declarations(
|
||||
&self,
|
||||
pub(crate) fn public_declarations<'map>(
|
||||
&'map self,
|
||||
symbol: ScopedSymbolId,
|
||||
) -> impl Iterator<Item = DeclarationWithConstraint<'db>> + '_ {
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
let declarations = self.public_symbols[symbol].declarations();
|
||||
self.declarations_iterator(declarations)
|
||||
}
|
||||
@@ -360,35 +362,24 @@ impl<'db> UseDefMap<'db> {
|
||||
fn bindings_iterator<'map>(
|
||||
&'map self,
|
||||
bindings: &'map SymbolBindings,
|
||||
) -> impl Iterator<
|
||||
Item = BindingWithConstraints<'db, impl Iterator<Item = Constraint<'db>> + 'map>,
|
||||
> + 'map {
|
||||
bindings
|
||||
.iter()
|
||||
.map(|binding_id_with_constraints| BindingWithConstraints {
|
||||
binding: self.all_definitions[binding_id_with_constraints.definition],
|
||||
constraints: binding_id_with_constraints
|
||||
.constraint_ids
|
||||
.map(|constraint_id| self.all_constraints[constraint_id]),
|
||||
visibility_constraint: binding_id_with_constraints.visibility_constraint,
|
||||
})
|
||||
) -> BindingWithConstraintsIterator<'map, 'db> {
|
||||
BindingWithConstraintsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
all_constraints: &self.all_constraints,
|
||||
visibility_constraints: &self.visibility_constraints,
|
||||
inner: bindings.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn declarations_iterator<'map>(
|
||||
&'map self,
|
||||
declarations: &'map SymbolDeclarations,
|
||||
) -> impl Iterator<Item = DeclarationWithConstraint<'db>> + 'map {
|
||||
declarations.iter().map(
|
||||
move |DeclarationIdWithConstraint {
|
||||
definition,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
DeclarationWithConstraint {
|
||||
declaration: self.all_definitions[definition],
|
||||
visibility_constraint,
|
||||
}
|
||||
},
|
||||
)
|
||||
) -> DeclarationsIterator<'map, 'db> {
|
||||
DeclarationsIterator {
|
||||
all_definitions: &self.all_definitions,
|
||||
visibility_constraints: &self.visibility_constraints,
|
||||
inner: declarations.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,17 +390,89 @@ enum SymbolDefinitions {
|
||||
Declarations(SymbolDeclarations),
|
||||
}
|
||||
|
||||
pub(crate) struct BindingWithConstraints<'db, I> {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: BindingIdWithConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
|
||||
type Item = BindingWithConstraints<'map, 'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let all_constraints = self.all_constraints;
|
||||
|
||||
self.inner
|
||||
.next()
|
||||
.map(|binding_id_with_constraints| BindingWithConstraints {
|
||||
binding: self.all_definitions[binding_id_with_constraints.definition],
|
||||
constraints: ConstraintsIterator {
|
||||
all_constraints,
|
||||
constraint_ids: binding_id_with_constraints.constraint_ids,
|
||||
},
|
||||
visibility_constraint: binding_id_with_constraints.visibility_constraint,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
|
||||
|
||||
pub(crate) struct BindingWithConstraints<'map, 'db> {
|
||||
pub(crate) binding: Option<Definition<'db>>,
|
||||
pub(crate) constraints: I,
|
||||
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
|
||||
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(crate) struct ConstraintsIterator<'map, 'db> {
|
||||
all_constraints: &'map AllConstraints<'db>,
|
||||
constraint_ids: ConstraintIdIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'db> Iterator for ConstraintsIterator<'_, '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, Option<Definition<'db>>>,
|
||||
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
|
||||
inner: DeclarationIdIterator<'map>,
|
||||
}
|
||||
|
||||
pub(crate) struct DeclarationWithConstraint<'db> {
|
||||
pub(crate) declaration: Option<Definition<'db>>,
|
||||
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
|
||||
type Item = DeclarationWithConstraint<'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(
|
||||
|DeclarationIdWithConstraint {
|
||||
definition,
|
||||
visibility_constraint,
|
||||
}| {
|
||||
DeclarationWithConstraint {
|
||||
declaration: self.all_definitions[definition],
|
||||
visibility_constraint,
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -425,8 +488,8 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Append-only array of [`Constraint`].
|
||||
all_constraints: AllConstraints<'db>,
|
||||
|
||||
/// Append-only array of [`VisibilityConstraint`].
|
||||
visibility_constraints: VisibilityConstraints<'db>,
|
||||
/// Builder of visibility constraints.
|
||||
pub(super) visibility_constraints: VisibilityConstraintsBuilder<'db>,
|
||||
|
||||
/// A constraint which describes the visibility of the unbound/undeclared state, i.e.
|
||||
/// whether or not the start of the scope is visible. This is important for cases like
|
||||
@@ -449,7 +512,7 @@ impl Default for UseDefMapBuilder<'_> {
|
||||
Self {
|
||||
all_definitions: IndexVec::from_iter([None]),
|
||||
all_constraints: IndexVec::new(),
|
||||
visibility_constraints: VisibilityConstraints::default(),
|
||||
visibility_constraints: VisibilityConstraintsBuilder::default(),
|
||||
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
bindings_by_use: IndexVec::new(),
|
||||
definitions_by_definition: FxHashMap::default(),
|
||||
@@ -459,6 +522,10 @@ impl Default for UseDefMapBuilder<'_> {
|
||||
}
|
||||
|
||||
impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn mark_unreachable(&mut self) {
|
||||
self.record_visibility_constraint(ScopedVisibilityConstraintId::ALWAYS_FALSE);
|
||||
}
|
||||
|
||||
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
|
||||
let new_symbol = self
|
||||
.symbol_states
|
||||
@@ -473,7 +540,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
binding,
|
||||
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
|
||||
);
|
||||
symbol_state.record_binding(def_id);
|
||||
symbol_state.record_binding(def_id, self.scope_start_visibility);
|
||||
}
|
||||
|
||||
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
|
||||
@@ -492,35 +559,18 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
new_constraint_id
|
||||
}
|
||||
|
||||
pub(super) fn add_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: VisibilityConstraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.visibility_constraints.add(constraint)
|
||||
}
|
||||
|
||||
pub(super) fn record_visibility_constraint_id(
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for state in &mut self.symbol_states {
|
||||
state.record_visibility_constraint(&mut self.visibility_constraints, constraint);
|
||||
}
|
||||
|
||||
self.scope_start_visibility = self
|
||||
.visibility_constraints
|
||||
.add_and_constraint(self.scope_start_visibility, constraint);
|
||||
}
|
||||
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
constraint: VisibilityConstraint<'db>,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
let new_constraint_id = self.add_visibility_constraint(constraint);
|
||||
self.record_visibility_constraint_id(new_constraint_id);
|
||||
new_constraint_id
|
||||
}
|
||||
|
||||
/// This method resets the visibility constraints for all symbols to a previous state
|
||||
/// *if* there have been no new declarations or bindings since then. Consider the
|
||||
/// following example:
|
||||
@@ -542,7 +592,11 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
|
||||
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
|
||||
|
||||
self.scope_start_visibility = snapshot.scope_start_visibility;
|
||||
// If there are any control flow paths that have become unreachable between `snapshot` and
|
||||
// now, then it's not valid to simplify any visibility constraints to `snapshot`.
|
||||
if self.scope_start_visibility != snapshot.scope_start_visibility {
|
||||
return;
|
||||
}
|
||||
|
||||
// Note that this loop terminates when we reach a symbol not present in the snapshot.
|
||||
// This means we keep visibility constraints for all new symbols, which is intended,
|
||||
@@ -578,7 +632,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
let def_id = self.all_definitions.push(Some(definition));
|
||||
let symbol_state = &mut self.symbol_states[symbol];
|
||||
symbol_state.record_declaration(def_id);
|
||||
symbol_state.record_binding(def_id);
|
||||
symbol_state.record_binding(def_id, self.scope_start_visibility);
|
||||
}
|
||||
|
||||
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
|
||||
@@ -623,6 +677,21 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
/// 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) {
|
||||
// As an optimization, if we know statically that either of the snapshots is always
|
||||
// unreachable, we can leave it out of the merged result entirely. Note that we cannot
|
||||
// perform any type inference at this point, so this is largely limited to unreachability
|
||||
// via terminal statements. If a flow's reachability depends on an expression in the code,
|
||||
// we will include the flow in the merged result; the visibility constraints of its
|
||||
// bindings will include this reachability condition, so that later during type inference,
|
||||
// we can determine whether any particular binding is non-visible due to unreachability.
|
||||
if snapshot.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
return;
|
||||
}
|
||||
if self.scope_start_visibility == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
self.restore(snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -656,7 +725,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
UseDefMap {
|
||||
all_definitions: self.all_definitions,
|
||||
all_constraints: self.all_constraints,
|
||||
visibility_constraints: self.visibility_constraints,
|
||||
visibility_constraints: self.visibility_constraints.build(),
|
||||
bindings_by_use: self.bindings_by_use,
|
||||
public_symbols: self.symbol_states,
|
||||
definitions_by_definition: self.definitions_by_definition,
|
||||
|
||||
@@ -48,8 +48,9 @@ use itertools::{EitherOrBoth, Itertools};
|
||||
use ruff_index::newtype_index;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::semantic_index::use_def::bitset::BitSet;
|
||||
use crate::semantic_index::use_def::VisibilityConstraints;
|
||||
use crate::semantic_index::use_def::bitset::{BitSet, BitSetIterator};
|
||||
use crate::semantic_index::use_def::VisibilityConstraintsBuilder;
|
||||
use crate::visibility_constraints::ScopedVisibilityConstraintId;
|
||||
|
||||
/// A newtype-index for a definition in a particular scope.
|
||||
#[newtype_index]
|
||||
@@ -73,12 +74,14 @@ 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;
|
||||
@@ -94,17 +97,8 @@ type InlineConstraintArray = [Constraints; INLINE_BINDINGS_PER_SYMBOL];
|
||||
/// One [`BitSet`] of applicable [`ScopedConstraintId`]s per live binding.
|
||||
type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
|
||||
|
||||
/// A newtype-index for a visibility constraint in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(crate) struct ScopedVisibilityConstraintId;
|
||||
|
||||
impl ScopedVisibilityConstraintId {
|
||||
/// A special ID that is used for an "always true" / "always visible" constraint.
|
||||
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
|
||||
/// present at index 0.
|
||||
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
|
||||
ScopedVisibilityConstraintId::from_u32(0);
|
||||
}
|
||||
/// Iterate over all constraints for a single binding.
|
||||
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
|
||||
|
||||
const INLINE_VISIBILITY_CONSTRAINTS: usize = 4;
|
||||
type InlineVisibilityConstraintsArray =
|
||||
@@ -116,6 +110,9 @@ type VisibilityConstraintPerDeclaration = SmallVec<InlineVisibilityConstraintsAr
|
||||
/// One [`ScopedVisibilityConstraintId`] per live binding.
|
||||
type VisibilityConstraintPerBinding = SmallVec<InlineVisibilityConstraintsArray>;
|
||||
|
||||
/// Iterator over the visibility constraints for all live bindings/declarations.
|
||||
type VisibilityConstraintsIterator<'a> = std::slice::Iter<'a, ScopedVisibilityConstraintId>;
|
||||
|
||||
/// Live declarations for a single symbol at some point in control flow, with their
|
||||
/// corresponding visibility constraints.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
@@ -156,7 +153,7 @@ impl SymbolDeclarations {
|
||||
/// Add given visibility constraint to all live declarations.
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for existing in &mut self.visibility_constraints {
|
||||
@@ -165,18 +162,14 @@ impl SymbolDeclarations {
|
||||
}
|
||||
|
||||
/// Return an iterator over live declarations for this symbol.
|
||||
pub(super) fn iter(&self) -> impl Iterator<Item = DeclarationIdWithConstraint> + '_ {
|
||||
(self.live_declarations.iter())
|
||||
.zip(self.visibility_constraints.iter())
|
||||
.map(
|
||||
|(declaration, &visibility_constraint)| DeclarationIdWithConstraint {
|
||||
definition: ScopedDefinitionId::from_u32(declaration),
|
||||
visibility_constraint,
|
||||
},
|
||||
)
|
||||
pub(super) fn iter(&self) -> DeclarationIdIterator {
|
||||
DeclarationIdIterator {
|
||||
declarations: self.live_declarations.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraints) {
|
||||
fn merge(&mut self, b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
|
||||
let a = std::mem::take(self);
|
||||
self.live_declarations = a.live_declarations.clone();
|
||||
self.live_declarations.union(&b.live_declarations);
|
||||
@@ -244,7 +237,11 @@ impl SymbolBindings {
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding_id: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
// The new binding replaces all previous live bindings in this path, and has no
|
||||
// constraints.
|
||||
self.live_bindings = Bindings::with(binding_id.into());
|
||||
@@ -252,8 +249,7 @@ impl SymbolBindings {
|
||||
self.constraints.push(Constraints::default());
|
||||
|
||||
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
|
||||
self.visibility_constraints
|
||||
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
self.visibility_constraints.push(visibility_constraint);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
@@ -266,7 +262,7 @@ impl SymbolBindings {
|
||||
/// Add given visibility constraint to all live bindings.
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
for existing in &mut self.visibility_constraints {
|
||||
@@ -275,23 +271,15 @@ impl SymbolBindings {
|
||||
}
|
||||
|
||||
/// Iterate over currently live bindings for this symbol
|
||||
pub(super) fn iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = BindingIdWithConstraints<impl Iterator<Item = ScopedConstraintId> + '_>> + '_
|
||||
{
|
||||
let i = (self.live_bindings.iter())
|
||||
.zip(self.constraints.iter())
|
||||
.zip(self.visibility_constraints.iter());
|
||||
i.map(
|
||||
|((def, constraints), visibility_constraint_id)| BindingIdWithConstraints {
|
||||
definition: ScopedDefinitionId::from_u32(def),
|
||||
constraint_ids: constraints.iter().map(ScopedConstraintId::from_u32),
|
||||
visibility_constraint: *visibility_constraint_id,
|
||||
},
|
||||
)
|
||||
pub(super) fn iter(&self) -> BindingIdWithConstraintsIterator {
|
||||
BindingIdWithConstraintsIterator {
|
||||
definitions: self.live_bindings.iter(),
|
||||
constraints: self.constraints.iter(),
|
||||
visibility_constraints: self.visibility_constraints.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
fn merge(&mut self, mut b: Self, visibility_constraints: &mut VisibilityConstraints) {
|
||||
fn merge(&mut self, mut b: Self, visibility_constraints: &mut VisibilityConstraintsBuilder) {
|
||||
let mut a = std::mem::take(self);
|
||||
self.live_bindings = a.live_bindings.clone();
|
||||
self.live_bindings.union(&b.live_bindings);
|
||||
@@ -364,9 +352,14 @@ impl SymbolState {
|
||||
}
|
||||
|
||||
/// Record a newly-encountered binding for this symbol.
|
||||
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
|
||||
pub(super) fn record_binding(
|
||||
&mut self,
|
||||
binding_id: ScopedDefinitionId,
|
||||
visibility_constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
|
||||
self.bindings.record_binding(binding_id);
|
||||
self.bindings
|
||||
.record_binding(binding_id, visibility_constraint);
|
||||
}
|
||||
|
||||
/// Add given constraint to all live bindings.
|
||||
@@ -377,7 +370,7 @@ impl SymbolState {
|
||||
/// Add given visibility constraint to all live bindings.
|
||||
pub(super) fn record_visibility_constraint(
|
||||
&mut self,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
constraint: ScopedVisibilityConstraintId,
|
||||
) {
|
||||
self.bindings
|
||||
@@ -405,7 +398,7 @@ impl SymbolState {
|
||||
pub(super) fn merge(
|
||||
&mut self,
|
||||
b: SymbolState,
|
||||
visibility_constraints: &mut VisibilityConstraints,
|
||||
visibility_constraints: &mut VisibilityConstraintsBuilder,
|
||||
) {
|
||||
self.bindings.merge(b.bindings, visibility_constraints);
|
||||
self.declarations
|
||||
@@ -425,12 +418,61 @@ impl SymbolState {
|
||||
/// narrowing constraints ([`ScopedConstraintId`]) and a corresponding visibility
|
||||
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraints<I> {
|
||||
pub(super) struct BindingIdWithConstraints<'map> {
|
||||
pub(super) definition: ScopedDefinitionId,
|
||||
pub(super) constraint_ids: I,
|
||||
pub(super) constraint_ids: ConstraintIdIterator<'map>,
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct BindingIdWithConstraintsIterator<'map> {
|
||||
definitions: BindingsIterator<'map>,
|
||||
constraints: ConstraintsIterator<'map>,
|
||||
visibility_constraints: VisibilityConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map> Iterator for BindingIdWithConstraintsIterator<'map> {
|
||||
type Item = BindingIdWithConstraints<'map>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (
|
||||
self.definitions.next(),
|
||||
self.constraints.next(),
|
||||
self.visibility_constraints.next(),
|
||||
) {
|
||||
(None, None, None) => None,
|
||||
(Some(def), Some(constraints), Some(visibility_constraint_id)) => {
|
||||
Some(BindingIdWithConstraints {
|
||||
definition: ScopedDefinitionId::from_u32(def),
|
||||
constraint_ids: ConstraintIdIterator {
|
||||
wrapped: constraints.iter(),
|
||||
},
|
||||
visibility_constraint: *visibility_constraint_id,
|
||||
})
|
||||
}
|
||||
// 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<'_> {}
|
||||
|
||||
/// A single declaration (as [`ScopedDefinitionId`]) with a corresponding visibility
|
||||
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
|
||||
#[derive(Debug)]
|
||||
@@ -439,6 +481,31 @@ pub(super) struct DeclarationIdWithConstraint {
|
||||
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
pub(super) struct DeclarationIdIterator<'map> {
|
||||
pub(crate) declarations: DeclarationsIterator<'map>,
|
||||
pub(crate) visibility_constraints: VisibilityConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
impl Iterator for DeclarationIdIterator<'_> {
|
||||
type Item = DeclarationIdWithConstraint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.declarations.next(), self.visibility_constraints.next()) {
|
||||
(None, None) => None,
|
||||
(Some(declaration), Some(&visibility_constraint)) => {
|
||||
Some(DeclarationIdWithConstraint {
|
||||
definition: ScopedDefinitionId::from_u32(declaration),
|
||||
visibility_constraint,
|
||||
})
|
||||
}
|
||||
// SAFETY: see above.
|
||||
_ => unreachable!("declarations and visibility_constraints length mismatch"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -498,7 +565,10 @@ mod tests {
|
||||
#[test]
|
||||
fn with() {
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
|
||||
assert_bindings(&sym, &["1<>"]);
|
||||
}
|
||||
@@ -506,7 +576,10 @@ mod tests {
|
||||
#[test]
|
||||
fn record_constraint() {
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
assert_bindings(&sym, &["1<0>"]);
|
||||
@@ -514,15 +587,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn merge() {
|
||||
let mut visibility_constraints = VisibilityConstraints::default();
|
||||
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
|
||||
|
||||
// merging the same definition with the same constraint keeps the constraint
|
||||
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1a.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
|
||||
sym1b.record_binding(
|
||||
ScopedDefinitionId::from_u32(1),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
|
||||
|
||||
sym1a.merge(sym1b, &mut visibility_constraints);
|
||||
@@ -531,11 +610,17 @@ mod tests {
|
||||
|
||||
// merging the same definition with differing constraints drops all constraints
|
||||
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym2a.record_binding(
|
||||
ScopedDefinitionId::from_u32(2),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
|
||||
|
||||
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym1b.record_binding(ScopedDefinitionId::from_u32(2));
|
||||
sym1b.record_binding(
|
||||
ScopedDefinitionId::from_u32(2),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
|
||||
|
||||
sym2a.merge(sym1b, &mut visibility_constraints);
|
||||
@@ -544,7 +629,10 @@ mod tests {
|
||||
|
||||
// merging a constrained definition with unbound keeps both
|
||||
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym3a.record_binding(ScopedDefinitionId::from_u32(3));
|
||||
sym3a.record_binding(
|
||||
ScopedDefinitionId::from_u32(3),
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE,
|
||||
);
|
||||
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
|
||||
|
||||
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
@@ -585,7 +673,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn record_declaration_merge() {
|
||||
let mut visibility_constraints = VisibilityConstraints::default();
|
||||
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
@@ -599,7 +687,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn record_declaration_merge_partial_undeclared() {
|
||||
let mut visibility_constraints = VisibilityConstraints::default();
|
||||
let mut visibility_constraints = VisibilityConstraintsBuilder::default();
|
||||
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
|
||||
sym.record_declaration(ScopedDefinitionId::from_u32(1));
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ fn check_unused_suppressions(context: &mut CheckSuppressionsContext) {
|
||||
);
|
||||
|
||||
// Collect all suppressions that are unused after type-checking.
|
||||
for suppression in all.iter() {
|
||||
for suppression in all {
|
||||
if context.diagnostics.is_used(suppression.id()) {
|
||||
continue;
|
||||
}
|
||||
@@ -401,11 +401,23 @@ impl Suppressions {
|
||||
})
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = &Suppression> + '_ {
|
||||
fn iter(&self) -> SuppressionsIter {
|
||||
self.file.iter().chain(&self.line)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type SuppressionsIter<'a> =
|
||||
std::iter::Chain<std::slice::Iter<'a, Suppression>, std::slice::Iter<'a, Suppression>>;
|
||||
|
||||
impl<'a> IntoIterator for &'a Suppressions {
|
||||
type Item = &'a Suppression;
|
||||
type IntoIter = SuppressionsIter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// A `type: ignore` or `knot: ignore` suppression.
|
||||
///
|
||||
/// Suppression comments that suppress multiple codes
|
||||
|
||||
@@ -9,15 +9,6 @@ pub(crate) enum Boundness {
|
||||
PossiblyUnbound,
|
||||
}
|
||||
|
||||
impl Boundness {
|
||||
pub(crate) fn or(self, other: Boundness) -> Boundness {
|
||||
match (self, other) {
|
||||
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
|
||||
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a symbol lookup, which can either be a (possibly unbound) type
|
||||
/// or a completely unbound symbol.
|
||||
///
|
||||
@@ -46,13 +37,6 @@ impl<'db> Symbol<'db> {
|
||||
matches!(self, Symbol::Unbound)
|
||||
}
|
||||
|
||||
pub(crate) fn possibly_unbound(&self) -> bool {
|
||||
match self {
|
||||
Symbol::Type(_, Boundness::PossiblyUnbound) | Symbol::Unbound => true,
|
||||
Symbol::Type(_, Boundness::Bound) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the type of the symbol, ignoring possible unboundness.
|
||||
///
|
||||
/// If the symbol is *definitely* unbound, this function will return `None`. Otherwise,
|
||||
@@ -71,18 +55,32 @@ impl<'db> Symbol<'db> {
|
||||
.expect("Expected a (possibly unbound) type, not an unbound symbol")
|
||||
}
|
||||
|
||||
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
|
||||
///
|
||||
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
|
||||
/// 2. Else, evaluate `fallback_fn()`:
|
||||
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
|
||||
/// b. Else, if `fallback` is definitely unbound, return `self`.
|
||||
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
|
||||
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
|
||||
#[must_use]
|
||||
pub(crate) fn or_fall_back_to(self, db: &'db dyn Db, fallback: &Symbol<'db>) -> Symbol<'db> {
|
||||
match fallback {
|
||||
Symbol::Type(fallback_ty, fallback_boundness) => match self {
|
||||
Symbol::Type(_, Boundness::Bound) => self,
|
||||
Symbol::Type(ty, boundness @ Boundness::PossiblyUnbound) => Symbol::Type(
|
||||
UnionType::from_elements(db, [*fallback_ty, ty]),
|
||||
fallback_boundness.or(boundness),
|
||||
pub(crate) fn or_fall_back_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
fallback_fn: impl FnOnce() -> Self,
|
||||
) -> Self {
|
||||
match self {
|
||||
Symbol::Type(_, Boundness::Bound) => self,
|
||||
Symbol::Unbound => fallback_fn(),
|
||||
Symbol::Type(self_ty, Boundness::PossiblyUnbound) => match fallback_fn() {
|
||||
Symbol::Unbound => self,
|
||||
Symbol::Type(fallback_ty, fallback_boundness) => Symbol::Type(
|
||||
UnionType::from_elements(db, [self_ty, fallback_ty]),
|
||||
fallback_boundness,
|
||||
),
|
||||
Symbol::Unbound => fallback.clone(),
|
||||
},
|
||||
Symbol::Unbound => self,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,44 +108,44 @@ mod tests {
|
||||
|
||||
// Start from an unbound symbol
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Unbound),
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Unbound
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, PossiblyUnbound)),
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, Bound)),
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, Bound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
|
||||
// Start from a possibly unbound symbol
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Unbound),
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
.or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), PossiblyUnbound)
|
||||
.or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), Bound)
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound)
|
||||
);
|
||||
|
||||
// Start from a definitely bound symbol
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Unbound),
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,14 @@ pub use self::subclass_of::SubclassOfType;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{file_to_module, resolve_module, KnownModule};
|
||||
use crate::semantic_index::ast_ids::HasScopedExpressionId;
|
||||
use crate::semantic_index::constraint::Constraint;
|
||||
use crate::semantic_index::attribute_assignment::AttributeAssignment;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{self as symbol, ScopeId, ScopedSymbolId};
|
||||
use crate::semantic_index::{
|
||||
global_scope, imported_modules, semantic_index, symbol_table, use_def_map,
|
||||
BindingWithConstraints, DeclarationWithConstraint, UseDefMap,
|
||||
attribute_assignments, global_scope, imported_modules, semantic_index, symbol_table,
|
||||
use_def_map, BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
|
||||
DeclarationsIterator,
|
||||
};
|
||||
use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol};
|
||||
use crate::suppression::check_suppressions;
|
||||
@@ -53,6 +55,7 @@ mod mro;
|
||||
mod narrow;
|
||||
mod signatures;
|
||||
mod slots;
|
||||
mod statistics;
|
||||
mod string_annotation;
|
||||
mod subclass_of;
|
||||
mod type_ordering;
|
||||
@@ -118,7 +121,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
// on inference from bindings.
|
||||
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
let declared = symbol_from_declarations(db, use_def.as_ref(), declarations);
|
||||
let declared = symbol_from_declarations(db, declarations);
|
||||
let is_final = declared.as_ref().is_ok_and(SymbolAndQualifiers::is_final);
|
||||
let declared = declared.map(|SymbolAndQualifiers(symbol, _)| symbol);
|
||||
|
||||
@@ -128,7 +131,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
// Symbol is possibly declared
|
||||
Ok(Symbol::Type(declared_ty, Boundness::PossiblyUnbound)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, use_def.as_ref(), bindings);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
match inferred {
|
||||
// Symbol is possibly undeclared and definitely unbound
|
||||
@@ -148,7 +151,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
// Symbol is undeclared, return the union of `Unknown` with the inferred type
|
||||
Ok(Symbol::Unbound) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, use_def.as_ref(), bindings);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, is_dunder_slots || is_final)
|
||||
}
|
||||
@@ -253,26 +256,19 @@ fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::N
|
||||
|
||||
/// Looks up a module-global symbol by name in a file.
|
||||
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
let explicit_symbol = symbol(db, global_scope(db, file), name);
|
||||
|
||||
if !explicit_symbol.possibly_unbound() {
|
||||
return explicit_symbol;
|
||||
}
|
||||
|
||||
// Not defined explicitly in the global scope?
|
||||
// All modules are instances of `types.ModuleType`;
|
||||
// look it up there (with a few very special exceptions)
|
||||
if module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
// TODO: this should use `.to_instance(db)`. but we don't understand attribute access
|
||||
// on instance types yet.
|
||||
let module_type_member = KnownClass::ModuleType.to_class_literal(db).member(db, name);
|
||||
return explicit_symbol.or_fall_back_to(db, &module_type_member);
|
||||
}
|
||||
|
||||
explicit_symbol
|
||||
symbol(db, global_scope(db, file), name).or_fall_back_to(db, || {
|
||||
if module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Infer the type of a binding.
|
||||
@@ -321,14 +317,11 @@ fn definition_expression_type<'db>(
|
||||
/// together with boundness information in a [`Symbol`].
|
||||
///
|
||||
/// The type will be a union if there are multiple bindings with different types.
|
||||
fn symbol_from_bindings<'map, 'db: 'map>(
|
||||
fn symbol_from_bindings<'db>(
|
||||
db: &'db dyn Db,
|
||||
use_def: &UseDefMap<'map>,
|
||||
bindings_with_constraints: impl Iterator<
|
||||
Item = BindingWithConstraints<'db, impl Iterator<Item = Constraint<'db>>>,
|
||||
>,
|
||||
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
|
||||
) -> Symbol<'db> {
|
||||
let visibility_constraints = &use_def.visibility_constraints;
|
||||
let visibility_constraints = bindings_with_constraints.visibility_constraints;
|
||||
let mut bindings_with_constraints = bindings_with_constraints.peekable();
|
||||
|
||||
let unbound_visibility = if let Some(BindingWithConstraints {
|
||||
@@ -436,7 +429,7 @@ impl<'db> From<Type<'db>> for SymbolAndQualifiers<'db> {
|
||||
type SymbolFromDeclarationsResult<'db> =
|
||||
Result<SymbolAndQualifiers<'db>, (TypeAndQualifiers<'db>, Box<[Type<'db>]>)>;
|
||||
|
||||
/// Build a declared type from an iterator of [`DeclarationWithConstraint`]s.
|
||||
/// Build a declared type from a [`DeclarationsIterator`].
|
||||
///
|
||||
/// If there is only one declaration, or all declarations declare the same type, returns
|
||||
/// `Ok(..)`. If there are conflicting declarations, returns an `Err(..)` variant with
|
||||
@@ -446,10 +439,9 @@ type SymbolFromDeclarationsResult<'db> =
|
||||
/// [`TypeQualifiers`] that have been specified on the declaration(s).
|
||||
fn symbol_from_declarations<'db>(
|
||||
db: &'db dyn Db,
|
||||
use_def: &UseDefMap,
|
||||
declarations: impl Iterator<Item = DeclarationWithConstraint<'db>>,
|
||||
declarations: DeclarationsIterator<'_, 'db>,
|
||||
) -> SymbolFromDeclarationsResult<'db> {
|
||||
let visibility_constraints = &use_def.visibility_constraints;
|
||||
let visibility_constraints = declarations.visibility_constraints;
|
||||
let mut declarations = declarations.peekable();
|
||||
|
||||
let undeclared_visibility = if let Some(DeclarationWithConstraint {
|
||||
@@ -671,6 +663,10 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::ClassLiteral(..))
|
||||
}
|
||||
|
||||
pub const fn is_instance(&self) -> bool {
|
||||
matches!(self, Type::Instance(..))
|
||||
}
|
||||
|
||||
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module) -> Self {
|
||||
Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule))
|
||||
}
|
||||
@@ -815,6 +811,35 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a normalized version of `self` in which all unions and intersections are sorted
|
||||
/// according to a canonical order, no matter how "deeply" a union/intersection may be nested.
|
||||
#[must_use]
|
||||
pub fn with_sorted_unions(self, db: &'db dyn Db) -> Self {
|
||||
match self {
|
||||
Type::Union(union) => Type::Union(union.to_sorted_union(db)),
|
||||
Type::Intersection(intersection) => {
|
||||
Type::Intersection(intersection.to_sorted_intersection(db))
|
||||
}
|
||||
Type::Tuple(tuple) => Type::Tuple(tuple.with_sorted_unions(db)),
|
||||
Type::LiteralString
|
||||
| Type::Instance(_)
|
||||
| Type::AlwaysFalsy
|
||||
| Type::AlwaysTruthy
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::Dynamic(_)
|
||||
| Type::Never
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::ClassLiteral(_)
|
||||
| Type::KnownInstance(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::SubclassOf(_) => self,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is a [subtype of] type `target`.
|
||||
///
|
||||
/// This method returns `false` if either `self` or `other` is not fully static.
|
||||
@@ -1158,7 +1183,7 @@ impl<'db> Type<'db> {
|
||||
left.is_equivalent_to(db, right)
|
||||
}
|
||||
(Type::Tuple(left), Type::Tuple(right)) => left.is_equivalent_to(db, right),
|
||||
_ => self.is_fully_static(db) && other.is_fully_static(db) && self == other,
|
||||
_ => self == other && self.is_fully_static(db) && other.is_fully_static(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1815,19 +1840,8 @@ impl<'db> Type<'db> {
|
||||
return Truthiness::Ambiguous;
|
||||
};
|
||||
|
||||
// Check if the class has `__bool__ = bool` and avoid infinite recursion, since
|
||||
// `Type::call` on `bool` will call `Type::bool` on the argument.
|
||||
if bool_method
|
||||
.into_class_literal()
|
||||
.is_some_and(|ClassLiteralType { class }| {
|
||||
class.is_known(db, KnownClass::Bool)
|
||||
})
|
||||
{
|
||||
return Truthiness::Ambiguous;
|
||||
}
|
||||
|
||||
if let Some(Type::BooleanLiteral(bool_val)) = bool_method
|
||||
.call(db, &CallArguments::positional([*instance_ty]))
|
||||
.call_bound(db, instance_ty, &CallArguments::positional([]))
|
||||
.return_type(db)
|
||||
{
|
||||
bool_val.into()
|
||||
@@ -2120,6 +2134,52 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an class/instance attribute of this type
|
||||
/// using descriptor protocol.
|
||||
///
|
||||
/// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`.
|
||||
///
|
||||
/// TODO: handle `super()` objects properly
|
||||
#[must_use]
|
||||
fn call_bound(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
receiver_ty: &Type<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> CallOutcome<'db> {
|
||||
debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal());
|
||||
|
||||
match self {
|
||||
Type::FunctionLiteral(..) => {
|
||||
// Functions are always descriptors, so this would effectively call
|
||||
// the function with the instance as the first argument
|
||||
self.call(db, &arguments.with_self(*receiver_ty))
|
||||
}
|
||||
|
||||
Type::Instance(_) | Type::ClassLiteral(_) => {
|
||||
// TODO descriptor protocol. For now, assume non-descriptor and call without `self` argument.
|
||||
self.call(db, arguments)
|
||||
}
|
||||
|
||||
Type::Union(union) => CallOutcome::union(
|
||||
self,
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|elem| elem.call_bound(db, receiver_ty, arguments)),
|
||||
),
|
||||
|
||||
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
|
||||
todo_type!("Type::Intersection.call_bound()"),
|
||||
)),
|
||||
|
||||
// Cases that duplicate, and thus must be kept in sync with, `Type::call()`
|
||||
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
|
||||
|
||||
_ => CallOutcome::not_callable(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a dunder method on the meta type of `self` and call it.
|
||||
fn call_dunder(
|
||||
self,
|
||||
@@ -3729,10 +3789,8 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
let global_lookup = symbol(db, global_scope(db, self.module(db).file()), name);
|
||||
|
||||
// If it's unbound, check if it's present as an instance on `types.ModuleType`
|
||||
// or `builtins.object`.
|
||||
// If it's not found in the global scope, check if it's present as an instance
|
||||
// on `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
// We do a more limited version of this in `global_symbol_ty`,
|
||||
// but there are two crucial differences here:
|
||||
@@ -3746,14 +3804,13 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType`
|
||||
// to help out with dynamic imports; we shouldn't use it for `ModuleLiteral` types
|
||||
// where we know exactly which module we're dealing with.
|
||||
if name != "__getattr__" && global_lookup.possibly_unbound() {
|
||||
// TODO: this should use `.to_instance()`, but we don't understand instance attribute yet
|
||||
let module_type_instance_member =
|
||||
KnownClass::ModuleType.to_class_literal(db).member(db, name);
|
||||
global_lookup.or_fall_back_to(db, &module_type_instance_member)
|
||||
} else {
|
||||
global_lookup
|
||||
}
|
||||
symbol(db, global_scope(db, self.module(db).file()), name).or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4108,9 +4165,76 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: The symbol is not present in any class body, but it could be implicitly
|
||||
// defined in `__init__` or other methods anywhere in the MRO.
|
||||
todo_type!("implicit instance attribute").into()
|
||||
SymbolAndQualifiers(Symbol::Unbound, TypeQualifiers::empty())
|
||||
}
|
||||
|
||||
/// Tries to find declarations/bindings of an instance attribute named `name` that are only
|
||||
/// "implicitly" defined in a method of the class that corresponds to `class_body_scope`.
|
||||
fn implicit_instance_attribute(
|
||||
db: &'db dyn Db,
|
||||
class_body_scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
inferred_type_from_class_body: Option<Type<'db>>,
|
||||
) -> Symbol<'db> {
|
||||
// We use a separate salsa query here to prevent unrelated changes in the AST of an external
|
||||
// file from triggering re-evaluations of downstream queries.
|
||||
// See the `dependency_implicit_instance_attribute` test for more information.
|
||||
#[salsa::tracked]
|
||||
fn infer_expression_type<'db>(db: &'db dyn Db, expression: Expression<'db>) -> Type<'db> {
|
||||
let inference = infer_expression_types(db, expression);
|
||||
let expr_scope = expression.scope(db);
|
||||
inference.expression_type(expression.node_ref(db).scoped_expression_id(db, expr_scope))
|
||||
}
|
||||
|
||||
// If we do not see any declarations of an attribute, neither in the class body nor in
|
||||
// any method, we build a union of `Unknown` with the inferred types of all bindings of
|
||||
// that attribute. We include `Unknown` in that union to account for the fact that the
|
||||
// attribute might be externally modified.
|
||||
let mut union_of_inferred_types = UnionBuilder::new(db).add(Type::unknown());
|
||||
|
||||
if let Some(ty) = inferred_type_from_class_body {
|
||||
union_of_inferred_types = union_of_inferred_types.add(ty);
|
||||
}
|
||||
|
||||
let attribute_assignments = attribute_assignments(db, class_body_scope);
|
||||
|
||||
let Some(attribute_assignments) = attribute_assignments
|
||||
.as_deref()
|
||||
.and_then(|assignments| assignments.get(name))
|
||||
else {
|
||||
if inferred_type_from_class_body.is_some() {
|
||||
return union_of_inferred_types.build().into();
|
||||
}
|
||||
return Symbol::Unbound;
|
||||
};
|
||||
|
||||
for attribute_assignment in attribute_assignments {
|
||||
match attribute_assignment {
|
||||
AttributeAssignment::Annotated { annotation } => {
|
||||
// We found an annotated assignment of one of the following forms (using 'self' in these
|
||||
// examples, but we support arbitrary names for the first parameters of methods):
|
||||
//
|
||||
// self.name: <annotation>
|
||||
// self.name: <annotation> = …
|
||||
|
||||
let annotation_ty = infer_expression_type(db, *annotation);
|
||||
|
||||
// TODO: check if there are conflicting declarations
|
||||
return annotation_ty.into();
|
||||
}
|
||||
AttributeAssignment::Unannotated { value } => {
|
||||
// We found an un-annotated attribute assignment of the form:
|
||||
//
|
||||
// self.name = <value>
|
||||
|
||||
let inferred_ty = infer_expression_type(db, *value);
|
||||
|
||||
union_of_inferred_types = union_of_inferred_types.add(inferred_ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
union_of_inferred_types.build().into()
|
||||
}
|
||||
|
||||
/// A helper function for `instance_member` that looks up the `name` attribute only on
|
||||
@@ -4130,8 +4254,10 @@ impl<'db> Class<'db> {
|
||||
|
||||
let declarations = use_def.public_declarations(symbol_id);
|
||||
|
||||
match symbol_from_declarations(db, use_def.as_ref(), declarations) {
|
||||
match symbol_from_declarations(db, declarations) {
|
||||
Ok(SymbolAndQualifiers(Symbol::Type(declared_ty, _), qualifiers)) => {
|
||||
// The attribute is declared in the class body.
|
||||
|
||||
if let Some(function) = declared_ty.into_function_literal() {
|
||||
// TODO: Eventually, we are going to process all decorators correctly. This is
|
||||
// just a temporary heuristic to provide a broad categorization into properties
|
||||
@@ -4145,22 +4271,26 @@ impl<'db> Class<'db> {
|
||||
SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::Bound), qualifiers)
|
||||
}
|
||||
}
|
||||
Ok(symbol @ SymbolAndQualifiers(Symbol::Unbound, qualifiers)) => {
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, use_def.as_ref(), bindings);
|
||||
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
|
||||
// The attribute is not *declared* in the class body. It could still be declared
|
||||
// in a method, and it could also be *bound* in the class body (and/or in a method).
|
||||
|
||||
SymbolAndQualifiers(
|
||||
widen_type_for_undeclared_public_symbol(db, inferred, symbol.is_final()),
|
||||
qualifiers,
|
||||
)
|
||||
let bindings = use_def.public_bindings(symbol_id);
|
||||
let inferred = symbol_from_bindings(db, bindings);
|
||||
let inferred_ty = inferred.ignore_possibly_unbound();
|
||||
|
||||
Self::implicit_instance_attribute(db, body_scope, name, inferred_ty).into()
|
||||
}
|
||||
Err((declared_ty, _conflicting_declarations)) => {
|
||||
// Ignore conflicting declarations
|
||||
// There are conflicting declarations for this attribute in the class body.
|
||||
SymbolAndQualifiers(declared_ty.inner_type().into(), declared_ty.qualifiers())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Symbol::Unbound.into()
|
||||
// This attribute is neither declared nor bound in the class body.
|
||||
// It could still be implicitly defined in a method.
|
||||
|
||||
Self::implicit_instance_attribute(db, body_scope, name, None).into()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4356,12 +4486,11 @@ impl<'db> UnionType<'db> {
|
||||
/// Create a new union type with the elements sorted according to a canonical ordering.
|
||||
#[must_use]
|
||||
pub fn to_sorted_union(self, db: &'db dyn Db) -> Self {
|
||||
let mut new_elements = self.elements(db).to_vec();
|
||||
for element in &mut new_elements {
|
||||
if let Type::Intersection(intersection) = element {
|
||||
intersection.sort(db);
|
||||
}
|
||||
}
|
||||
let mut new_elements: Vec<Type<'db>> = self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|element| element.with_sorted_unions(db))
|
||||
.collect();
|
||||
new_elements.sort_unstable_by(union_elements_ordering);
|
||||
UnionType::new(db, new_elements.into_boxed_slice())
|
||||
}
|
||||
@@ -4457,19 +4586,24 @@ impl<'db> IntersectionType<'db> {
|
||||
/// according to a canonical ordering.
|
||||
#[must_use]
|
||||
pub fn to_sorted_intersection(self, db: &'db dyn Db) -> Self {
|
||||
let mut positive = self.positive(db).clone();
|
||||
positive.sort_unstable_by(union_elements_ordering);
|
||||
fn normalized_set<'db>(
|
||||
db: &'db dyn Db,
|
||||
elements: &FxOrderSet<Type<'db>>,
|
||||
) -> FxOrderSet<Type<'db>> {
|
||||
let mut elements: FxOrderSet<Type<'db>> = elements
|
||||
.iter()
|
||||
.map(|ty| ty.with_sorted_unions(db))
|
||||
.collect();
|
||||
|
||||
let mut negative = self.negative(db).clone();
|
||||
negative.sort_unstable_by(union_elements_ordering);
|
||||
elements.sort_unstable_by(union_elements_ordering);
|
||||
elements
|
||||
}
|
||||
|
||||
IntersectionType::new(db, positive, negative)
|
||||
}
|
||||
|
||||
/// Perform an in-place sort of this [`IntersectionType`] instance
|
||||
/// according to a canonical ordering.
|
||||
fn sort(&mut self, db: &'db dyn Db) {
|
||||
*self = self.to_sorted_intersection(db);
|
||||
IntersectionType::new(
|
||||
db,
|
||||
normalized_set(db, self.positive(db)),
|
||||
normalized_set(db, self.negative(db)),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_fully_static(self, db: &'db dyn Db) -> bool {
|
||||
@@ -4487,21 +4621,33 @@ impl<'db> IntersectionType<'db> {
|
||||
}
|
||||
|
||||
let self_positive = self.positive(db);
|
||||
|
||||
if !all_fully_static(db, self_positive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let self_negative = self.negative(db);
|
||||
if !all_fully_static(db, self_negative) {
|
||||
let other_positive = other.positive(db);
|
||||
|
||||
if self_positive.len() != other_positive.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_positive = other.positive(db);
|
||||
if !all_fully_static(db, other_positive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let self_negative = self.negative(db);
|
||||
|
||||
if !all_fully_static(db, self_negative) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let other_negative = other.negative(db);
|
||||
|
||||
if self_negative.len() != other_negative.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !all_fully_static(db, other_negative) {
|
||||
return false;
|
||||
}
|
||||
@@ -4510,7 +4656,13 @@ impl<'db> IntersectionType<'db> {
|
||||
return true;
|
||||
}
|
||||
|
||||
self_positive.set_eq(other_positive) && self_negative.set_eq(other_negative)
|
||||
let sorted_self = self.to_sorted_intersection(db);
|
||||
|
||||
if sorted_self == other {
|
||||
return true;
|
||||
}
|
||||
|
||||
sorted_self == other.to_sorted_intersection(db)
|
||||
}
|
||||
|
||||
/// Return `true` if `self` has exactly the same set of possible static materializations as `other`
|
||||
@@ -4612,6 +4764,18 @@ impl<'db> TupleType<'db> {
|
||||
Type::Tuple(Self::new(db, elements.into_boxed_slice()))
|
||||
}
|
||||
|
||||
/// Return a normalized version of `self` in which all unions and intersections are sorted
|
||||
/// according to a canonical order, no matter how "deeply" a union/intersection may be nested.
|
||||
#[must_use]
|
||||
pub fn with_sorted_unions(self, db: &'db dyn Db) -> Self {
|
||||
let elements: Box<[Type<'db>]> = self
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|ty| ty.with_sorted_unions(db))
|
||||
.collect();
|
||||
TupleType::new(db, elements)
|
||||
}
|
||||
|
||||
pub fn is_equivalent_to(self, db: &'db dyn Db, other: Self) -> bool {
|
||||
let self_elements = self.elements(db);
|
||||
let other_elements = other.elements(db);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
|
||||
use ruff_db::display::FormatterJoinExtension;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_ast::str::{Quote, TripleQuotes};
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
@@ -98,7 +98,7 @@ impl Display for DisplayRepresentation<'_> {
|
||||
let escape =
|
||||
AsciiEscape::with_preferred_quote(bytes.value(self.db).as_ref(), Quote::Double);
|
||||
|
||||
escape.bytes_repr().write(f)
|
||||
escape.bytes_repr(TripleQuotes::No).write(f)
|
||||
}
|
||||
Type::SliceLiteral(slice) => {
|
||||
f.write_str("slice[")?;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user