Compare commits
160 Commits
dcreager/i
...
david/comp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e5358de2b | ||
|
|
1f3ff48b4f | ||
|
|
5e027a43ff | ||
|
|
22728808aa | ||
|
|
a04ddf2a55 | ||
|
|
3a806ecaa1 | ||
|
|
a29009e4ed | ||
|
|
19f3424a1a | ||
|
|
d4a5772d96 | ||
|
|
efa8a3ddcc | ||
|
|
46fe17767d | ||
|
|
1f7a29d347 | ||
|
|
618bfaf884 | ||
|
|
b1c61cb2ee | ||
|
|
97e6fc3793 | ||
|
|
38351e00ee | ||
|
|
26c37b1e0e | ||
|
|
7db5a924af | ||
|
|
349f93389e | ||
|
|
bb979e05ac | ||
|
|
10d3e64ccd | ||
|
|
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
|
||||
|
||||
|
||||
97
CHANGELOG.md
97
CHANGELOG.md
@@ -1,5 +1,102 @@
|
||||
# Changelog
|
||||
|
||||
## 0.9.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- Recognize all symbols named `TYPE_CHECKING` for `in_type_checking_block` ([#15719](https://github.com/astral-sh/ruff/pull/15719))
|
||||
- \[`flake8-comprehensions`\] Handle builtins at top of file correctly for `unnecessary-dict-comprehension-for-iterable` (`C420`) ([#15837](https://github.com/astral-sh/ruff/pull/15837))
|
||||
- \[`flake8-logging`\] `.exception()` and `exc_info=` outside exception handlers (`LOG004`, `LOG014`) ([#15799](https://github.com/astral-sh/ruff/pull/15799))
|
||||
- \[`flake8-pyi`\] Fix incorrect behaviour of `custom-typevar-return-type` preview-mode autofix if `typing` was already imported (`PYI019`) ([#15853](https://github.com/astral-sh/ruff/pull/15853))
|
||||
- \[`flake8-pyi`\] Fix more complex cases (`PYI019`) ([#15821](https://github.com/astral-sh/ruff/pull/15821))
|
||||
- \[`flake8-pyi`\] Make `PYI019` autofixable for `.py` files in preview mode as well as stubs ([#15889](https://github.com/astral-sh/ruff/pull/15889))
|
||||
- \[`flake8-pyi`\] Remove type parameter correctly when it is the last (`PYI019`) ([#15854](https://github.com/astral-sh/ruff/pull/15854))
|
||||
- \[`pylint`\] Fix missing parens in unsafe fix for `unnecessary-dunder-call` (`PLC2801`) ([#15762](https://github.com/astral-sh/ruff/pull/15762))
|
||||
- \[`pyupgrade`\] Better messages and diagnostic range (`UP015`) ([#15872](https://github.com/astral-sh/ruff/pull/15872))
|
||||
- \[`pyupgrade`\] Rename private type parameters in PEP 695 generics (`UP049`) ([#15862](https://github.com/astral-sh/ruff/pull/15862))
|
||||
- \[`refurb`\] Also report non-name expressions (`FURB169`) ([#15905](https://github.com/astral-sh/ruff/pull/15905))
|
||||
- \[`refurb`\] Mark fix as unsafe if there are comments (`FURB171`) ([#15832](https://github.com/astral-sh/ruff/pull/15832))
|
||||
- \[`ruff`\] Classes with mixed type variable style (`RUF053`) ([#15841](https://github.com/astral-sh/ruff/pull/15841))
|
||||
- \[`airflow`\] `BashOperator` has been moved to `airflow.providers.standard.operators.bash.BashOperator` (`AIR302`) ([#15922](https://github.com/astral-sh/ruff/pull/15922))
|
||||
- \[`flake8-pyi`\] Add autofix for unused-private-type-var (`PYI018`) ([#15999](https://github.com/astral-sh/ruff/pull/15999))
|
||||
- \[`flake8-pyi`\] Significantly improve accuracy of `PYI019` if preview mode is enabled ([#15888](https://github.com/astral-sh/ruff/pull/15888))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Preserve triple quotes and prefixes for strings ([#15818](https://github.com/astral-sh/ruff/pull/15818))
|
||||
- \[`flake8-comprehensions`\] Skip when `TypeError` present from too many (kw)args for `C410`,`C411`, and `C418` ([#15838](https://github.com/astral-sh/ruff/pull/15838))
|
||||
- \[`flake8-pyi`\] Rename `PYI019` and improve its diagnostic message ([#15885](https://github.com/astral-sh/ruff/pull/15885))
|
||||
- \[`pep8-naming`\] Ignore `@override` methods (`N803`) ([#15954](https://github.com/astral-sh/ruff/pull/15954))
|
||||
- \[`pyupgrade`\] Reuse replacement logic from `UP046` and `UP047` to preserve more comments (`UP040`) ([#15840](https://github.com/astral-sh/ruff/pull/15840))
|
||||
- \[`ruff`\] Analyze deferred annotations before enforcing `mutable-(data)class-default` and `function-call-in-dataclass-default-argument` (`RUF008`,`RUF009`,`RUF012`) ([#15921](https://github.com/astral-sh/ruff/pull/15921))
|
||||
- \[`pycodestyle`\] Exempt `sys.path += ...` calls (`E402`) ([#15980](https://github.com/astral-sh/ruff/pull/15980))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Config error only when `flake8-import-conventions` alias conflicts with `isort.required-imports` bound name ([#15918](https://github.com/astral-sh/ruff/pull/15918))
|
||||
- Workaround Even Better TOML crash related to `allOf` ([#15992](https://github.com/astral-sh/ruff/pull/15992))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`flake8-comprehensions`\] Unnecessary `list` comprehension (rewrite as a `set` comprehension) (`C403`) - Handle extraneous parentheses around list comprehension ([#15877](https://github.com/astral-sh/ruff/pull/15877))
|
||||
- \[`flake8-comprehensions`\] Handle trailing comma in fixes for `unnecessary-generator-list/set` (`C400`,`C401`) ([#15929](https://github.com/astral-sh/ruff/pull/15929))
|
||||
- \[`flake8-pyi`\] Fix several correctness issues with `custom-type-var-return-type` (`PYI019`) ([#15851](https://github.com/astral-sh/ruff/pull/15851))
|
||||
- \[`pep8-naming`\] Consider any number of leading underscore for `N801` ([#15988](https://github.com/astral-sh/ruff/pull/15988))
|
||||
- \[`pyflakes`\] Visit forward annotations in `TypeAliasType` as types (`F401`) ([#15829](https://github.com/astral-sh/ruff/pull/15829))
|
||||
- \[`pylint`\] Correct min/max auto-fix and suggestion for (`PL1730`) ([#15930](https://github.com/astral-sh/ruff/pull/15930))
|
||||
- \[`refurb`\] Handle unparenthesized tuples correctly (`FURB122`, `FURB142`) ([#15953](https://github.com/astral-sh/ruff/pull/15953))
|
||||
- \[`refurb`\] Avoid `None | None` as well as better detection and fix (`FURB168`) ([#15779](https://github.com/astral-sh/ruff/pull/15779))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add deprecation warning for `ruff-lsp` related settings ([#15850](https://github.com/astral-sh/ruff/pull/15850))
|
||||
- Docs (`linter.md`): clarify that Python files are always searched for in subdirectories ([#15882](https://github.com/astral-sh/ruff/pull/15882))
|
||||
- Fix a typo in `non_pep695_generic_class.rs` ([#15946](https://github.com/astral-sh/ruff/pull/15946))
|
||||
- Improve Docs: Pylint subcategories' codes ([#15909](https://github.com/astral-sh/ruff/pull/15909))
|
||||
- Remove non-existing `lint.extendIgnore` editor setting ([#15844](https://github.com/astral-sh/ruff/pull/15844))
|
||||
- Update black deviations ([#15928](https://github.com/astral-sh/ruff/pull/15928))
|
||||
- Mention `UP049` in `UP046` and `UP047`, add `See also` section to `UP040` ([#15956](https://github.com/astral-sh/ruff/pull/15956))
|
||||
- Add instance variable examples to `RUF012` ([#15982](https://github.com/astral-sh/ruff/pull/15982))
|
||||
- Explain precedence for `ignore` and `select` config ([#15883](https://github.com/astral-sh/ruff/pull/15883))
|
||||
|
||||
## 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
|
||||
|
||||
917
Cargo.lock
generated
917
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
22
Cargo.toml
@@ -74,11 +74,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" }
|
||||
@@ -116,7 +118,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 +136,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 +166,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 +179,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 +315,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.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.5/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.5
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -452,6 +452,7 @@ Ruff is used by a number of major open-source projects and companies, including:
|
||||
- ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus))
|
||||
- [Ibis](https://github.com/ibis-project/ibis)
|
||||
- [ivy](https://github.com/unifyai/ivy)
|
||||
- [JAX](https://github.com/jax-ml/jax)
|
||||
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
|
||||
- [Kraken Tech](https://kraken.tech/)
|
||||
- [LangChain](https://github.com/hwchase17/langchain)
|
||||
|
||||
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)]
|
||||
@@ -63,6 +66,14 @@ pub(crate) struct CheckCommand {
|
||||
#[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,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::io::{self, BufWriter, Write};
|
||||
use std::process::{ExitCode, Termination};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::args::{Args, CheckCommand, Command};
|
||||
@@ -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,9 +53,17 @@ 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());
|
||||
@@ -84,13 +96,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 +131,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 +167,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 +187,7 @@ impl MainLoop {
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
min_error_severity,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
@@ -213,7 +245,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 +261,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(())
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ pep440_rs = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
@@ -40,8 +41,9 @@ insta = { workspace = true, features = ["redactions", "ron"] }
|
||||
|
||||
[features]
|
||||
default = ["zstd"]
|
||||
zstd = ["red_knot_vendored/zstd"]
|
||||
deflate = ["red_knot_vendored/deflate"]
|
||||
schemars = ["dep:schemars", "ruff_db/schemars", "red_knot_python_semantic/schemars"]
|
||||
zstd = ["red_knot_vendored/zstd"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -18,13 +18,16 @@ use thiserror::Error;
|
||||
/// The options for the project.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Options {
|
||||
/// Configures the type checking environment.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub environment: Option<EnvironmentOptions>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub src: Option<SrcOptions>,
|
||||
|
||||
/// Configures the enabled lints and their severity.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub rules: Option<Rules>,
|
||||
}
|
||||
@@ -177,10 +180,22 @@ impl Options {
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct EnvironmentOptions {
|
||||
/// Specifies the version of Python that will be used to execute the source code.
|
||||
/// The version should be specified as a string in the format `M.m` where `M` is the major version
|
||||
/// and `m` is the minor (e.g. "3.0" or "3.6").
|
||||
/// If a version is provided, knot will generate errors if the source code makes use of language features
|
||||
/// that are not supported in that version.
|
||||
/// It will also tailor its use of type stub files, which conditionalizes type definitions based on the version.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub python_version: Option<RangedValue<PythonVersion>>,
|
||||
|
||||
/// Specifies the target platform that will be used to execute the source code.
|
||||
/// If specified, Red Knot will tailor its use of type stub files,
|
||||
/// which conditionalize type definitions based on the platform.
|
||||
///
|
||||
/// If no platform is specified, knot will use `all` or the current platform in the LSP use case.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub python_platform: Option<RangedValue<PythonPlatform>>,
|
||||
|
||||
@@ -204,6 +219,7 @@ pub struct EnvironmentOptions {
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct SrcOptions {
|
||||
/// The root of the project, used for finding first-party modules.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -212,7 +228,9 @@ pub struct SrcOptions {
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", transparent)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct Rules {
|
||||
#[cfg_attr(feature = "schemars", schemars(with = "schema::Rules"))]
|
||||
inner: FxHashMap<RangedValue<String>, RangedValue<Level>>,
|
||||
}
|
||||
|
||||
@@ -226,6 +244,69 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
mod schema {
|
||||
use crate::DEFAULT_LINT_REGISTRY;
|
||||
use red_knot_python_semantic::lint::Level;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{
|
||||
InstanceType, Metadata, ObjectValidation, Schema, SchemaObject, SubschemaValidation,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
|
||||
pub(super) struct Rules;
|
||||
|
||||
impl JsonSchema for Rules {
|
||||
fn schema_name() -> String {
|
||||
"Rules".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
|
||||
let registry = &*DEFAULT_LINT_REGISTRY;
|
||||
|
||||
let level_schema = gen.subschema_for::<Level>();
|
||||
|
||||
let properties: schemars::Map<String, Schema> = registry
|
||||
.lints()
|
||||
.iter()
|
||||
.map(|lint| {
|
||||
(
|
||||
lint.name().to_string(),
|
||||
Schema::Object(SchemaObject {
|
||||
metadata: Some(Box::new(Metadata {
|
||||
title: Some(lint.summary().to_string()),
|
||||
description: Some(lint.documentation()),
|
||||
deprecated: lint.status.is_deprecated(),
|
||||
default: Some(lint.default_level.to_string().into()),
|
||||
..Metadata::default()
|
||||
})),
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
one_of: Some(vec![level_schema.clone()]),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
properties,
|
||||
// Allow unknown rules: Red Knot will warn about them.
|
||||
// It gives a better experience when using an older Red Knot version because
|
||||
// the schema will not deny rules that have been removed in newer versions.
|
||||
additional_properties: Some(Box::new(level_schema)),
|
||||
..ObjectValidation::default()
|
||||
})),
|
||||
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KnotTomlError {
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::combine::Combine;
|
||||
use crate::Db;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_macros::Combine;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
@@ -70,15 +71,19 @@ impl Drop for ValueSourceGuard {
|
||||
///
|
||||
/// This ensures that two resolved configurations are identical even if the position of a value has changed
|
||||
/// or if the values were loaded from different sources.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
#[serde(transparent)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct RangedValue<T> {
|
||||
value: T,
|
||||
#[serde(skip)]
|
||||
source: ValueSource,
|
||||
|
||||
/// The byte range of `value` in `source`.
|
||||
///
|
||||
/// Can be `None` because not all sources support a range.
|
||||
/// For example, arguments provided on the CLI won't have a range attached.
|
||||
#[serde(skip)]
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
@@ -266,18 +271,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Serialize for RangedValue<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.value.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
/// A possibly relative path in a configuration file.
|
||||
///
|
||||
/// Relative paths in configuration files or from CLI options
|
||||
@@ -286,9 +279,19 @@ where
|
||||
/// * CLI: The path is relative to the current working directory
|
||||
/// * Configuration file: The path is relative to the project's root.
|
||||
#[derive(
|
||||
Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash,
|
||||
Debug,
|
||||
Clone,
|
||||
serde::Serialize,
|
||||
serde::Deserialize,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
Combine,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct RelativePathBuf(RangedValue<SystemPathBuf>);
|
||||
|
||||
impl RelativePathBuf {
|
||||
@@ -325,13 +328,3 @@ impl RelativePathBuf {
|
||||
SystemPath::absolute(&self.0, relative_to)
|
||||
}
|
||||
}
|
||||
|
||||
impl Combine for RelativePathBuf {
|
||||
fn combine(self, other: Self) -> Self {
|
||||
Self(self.0.combine(other.0))
|
||||
}
|
||||
|
||||
fn combine_with(&mut self, other: Self) {
|
||||
self.0.combine_with(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -36,6 +36,7 @@ thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { 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,427 @@ 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 multi-target assignments
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
self.a = self.b = 1
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
#### Augmented assignments
|
||||
|
||||
```py
|
||||
class Weird:
|
||||
def __iadd__(self, other: None) -> str:
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
self.w = Weird()
|
||||
self.w += None
|
||||
|
||||
# TODO: Mypy and pyright do not support this, but it would be great if we could
|
||||
# infer `Unknown | str` or at least `Unknown | Weird | str` here.
|
||||
reveal_type(C().w) # revealed: Unknown | Weird
|
||||
```
|
||||
|
||||
#### 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
|
||||
|
||||
reveal_type(c_instance.a2) # revealed: Unknown | Literal[1]
|
||||
|
||||
reveal_type(c_instance.b2) # revealed: Unknown | Literal["a"]
|
||||
|
||||
reveal_type(c_instance.c2) # revealed: Unknown | int
|
||||
reveal_type(c_instance.d2) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
#### Starred assignments
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
self.a, *self.b = (1, 2, 3)
|
||||
|
||||
c_instance = C()
|
||||
reveal_type(c_instance.a) # revealed: Unknown | Literal[1]
|
||||
reveal_type(c_instance.b) # revealed: Unknown | @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
#### 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 NonIterable: ...
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
for self.x in IntIterable():
|
||||
pass
|
||||
|
||||
for _, self.y in TupleIterable():
|
||||
pass
|
||||
|
||||
# TODO: We should emit a diagnostic here
|
||||
for self.z in NonIterable():
|
||||
pass
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | int
|
||||
|
||||
reveal_type(C().y) # revealed: Unknown | str
|
||||
```
|
||||
|
||||
#### Attributes defined in `with` statements
|
||||
|
||||
```py
|
||||
class ContextManager:
|
||||
def __enter__(self) -> int | None: ...
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None: ...
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
with ContextManager() as self.x:
|
||||
pass
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# TODO: Should be `Unknown | int | None`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Attributes defined in comprehensions
|
||||
|
||||
```py
|
||||
class IntIterator:
|
||||
def __next__(self) -> int:
|
||||
return 1
|
||||
|
||||
class IntIterable:
|
||||
def __iter__(self) -> IntIterator:
|
||||
return IntIterator()
|
||||
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
[... for self.a in IntIterable()]
|
||||
|
||||
c_instance = C()
|
||||
|
||||
# TODO: Should be `Unknown | int`
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(c_instance.a) # 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
|
||||
```
|
||||
|
||||
#### Diagnostics are reported for the right-hand side of attribute assignments
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self) -> None:
|
||||
# error: [too-many-positional-arguments]
|
||||
self.x: int = len(1, 2, 3)
|
||||
```
|
||||
|
||||
### Pure class variables (`ClassVar`)
|
||||
@@ -221,13 +597,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 +653,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 +860,9 @@ reveal_type(Foo.__class__) # revealed: Literal[type]
|
||||
|
||||
## Module attributes
|
||||
|
||||
```py path=mod.py
|
||||
`mod.py`:
|
||||
|
||||
```py
|
||||
global_symbol: str = "a"
|
||||
```
|
||||
|
||||
@@ -471,13 +896,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 +931,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 +940,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 +950,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 +967,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 +988,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,41 @@ 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
|
||||
```
|
||||
|
||||
## Relative imports at the top of a search path
|
||||
|
||||
Relative imports at the top of a search path result in a runtime error:
|
||||
`ImportError: attempted relative import with no known parent package`. That's why Red Knot should
|
||||
disallow them.
|
||||
|
||||
`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]))
|
||||
|
||||
@@ -132,6 +132,27 @@ static_assert(not is_disjoint_from(Intersection[X, Z], Y))
|
||||
static_assert(not is_disjoint_from(Intersection[Y, Z], X))
|
||||
```
|
||||
|
||||
## Negation / complement
|
||||
|
||||
The complement of a type `T` is disjoint from `T`. In fact, it is disjoint from every subtype of
|
||||
`T`:
|
||||
|
||||
```py
|
||||
from knot_extensions import Not, Intersection, is_disjoint_from, static_assert
|
||||
|
||||
class T: ...
|
||||
class S(T): ...
|
||||
|
||||
static_assert(is_disjoint_from(Not[T], T))
|
||||
static_assert(is_disjoint_from(Not[T], S))
|
||||
|
||||
static_assert(is_disjoint_from(Intersection[T, Any], Not[T]))
|
||||
static_assert(is_disjoint_from(Not[T], Intersection[T, Any]))
|
||||
|
||||
static_assert(is_disjoint_from(Intersection[S, Any], Not[T]))
|
||||
static_assert(is_disjoint_from(Not[T], Intersection[S, Any]))
|
||||
```
|
||||
|
||||
## Special types
|
||||
|
||||
### `Never`
|
||||
@@ -151,7 +172,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]))
|
||||
@@ -244,7 +265,8 @@ static_assert(not is_disjoint_from(TypeOf[f], object))
|
||||
### `AlwaysTruthy` and `AlwaysFalsy`
|
||||
|
||||
```py
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy, Intersection, Not, is_disjoint_from, static_assert
|
||||
from typing import Literal
|
||||
|
||||
static_assert(is_disjoint_from(None, AlwaysTruthy))
|
||||
static_assert(not is_disjoint_from(None, AlwaysFalsy))
|
||||
@@ -255,6 +277,14 @@ static_assert(not is_disjoint_from(str, AlwaysTruthy))
|
||||
|
||||
static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy))
|
||||
static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy))
|
||||
|
||||
type Truthy = Not[AlwaysFalsy]
|
||||
type Falsy = Not[AlwaysTruthy]
|
||||
|
||||
type AmbiguousTruthiness = Intersection[Truthy, Falsy]
|
||||
|
||||
static_assert(is_disjoint_from(AlwaysTruthy, AmbiguousTruthiness))
|
||||
static_assert(is_disjoint_from(AlwaysFalsy, AmbiguousTruthiness))
|
||||
```
|
||||
|
||||
### Instance types versus `type[T]` types
|
||||
|
||||
@@ -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,6 +1,8 @@
|
||||
use core::fmt;
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::fmt::Formatter;
|
||||
use std::hash::Hasher;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -36,13 +38,20 @@ pub struct LintMetadata {
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(rename_all = "kebab-case")
|
||||
)]
|
||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub enum Level {
|
||||
/// # Ignore
|
||||
///
|
||||
/// The lint is disabled and should not run.
|
||||
Ignore,
|
||||
|
||||
/// # Warn
|
||||
///
|
||||
/// The lint is enabled and diagnostic should have a warning severity.
|
||||
Warn,
|
||||
|
||||
/// # Error
|
||||
///
|
||||
/// The lint is enabled and diagnostics have an error severity.
|
||||
Error,
|
||||
}
|
||||
@@ -61,6 +70,16 @@ impl Level {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Level {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Level::Ignore => f.write_str("ignore"),
|
||||
Level::Warn => f.write_str("warn"),
|
||||
Level::Error => f.write_str("error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Level> for Severity {
|
||||
type Error = ();
|
||||
|
||||
@@ -84,9 +103,11 @@ impl LintMetadata {
|
||||
|
||||
/// Returns the documentation line by line with one leading space and all trailing whitespace removed.
|
||||
pub fn documentation_lines(&self) -> impl Iterator<Item = &str> {
|
||||
self.raw_documentation
|
||||
.lines()
|
||||
.map(|line| line.strip_prefix(' ').unwrap_or(line).trim_end())
|
||||
self.raw_documentation.lines().map(|line| {
|
||||
line.strip_prefix(char::is_whitespace)
|
||||
.unwrap_or(line)
|
||||
.trim_end()
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the documentation as a single string.
|
||||
@@ -180,6 +201,10 @@ impl LintStatus {
|
||||
pub const fn is_removed(&self) -> bool {
|
||||
matches!(self, LintStatus::Removed { .. })
|
||||
}
|
||||
|
||||
pub const fn is_deprecated(&self) -> bool {
|
||||
matches!(self, LintStatus::Deprecated { .. })
|
||||
}
|
||||
}
|
||||
|
||||
/// Declares a lint rule with the given metadata.
|
||||
@@ -223,7 +248,7 @@ macro_rules! declare_lint {
|
||||
$vis static $name: $crate::lint::LintMetadata = $crate::lint::LintMetadata {
|
||||
name: ruff_db::diagnostic::LintName::of(ruff_macros::kebab_case!($name)),
|
||||
summary: $summary,
|
||||
raw_documentation: concat!($($doc,)+ "\n"),
|
||||
raw_documentation: concat!($($doc, '\n',)+),
|
||||
status: $status,
|
||||
file: file!(),
|
||||
line: line!(),
|
||||
|
||||
@@ -11,6 +11,7 @@ pub enum PythonPlatform {
|
||||
/// Do not make any assumptions about the target platform.
|
||||
#[default]
|
||||
All,
|
||||
|
||||
/// Assume a specific target platform like `linux`, `darwin` or `win32`.
|
||||
///
|
||||
/// We use a string (instead of individual enum variants), as the set of possible platforms
|
||||
@@ -28,3 +29,77 @@ impl Display for PythonPlatform {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "schemars")]
|
||||
mod schema {
|
||||
use crate::PythonPlatform;
|
||||
use schemars::_serde_json::Value;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation};
|
||||
use schemars::JsonSchema;
|
||||
|
||||
impl JsonSchema for PythonPlatform {
|
||||
fn schema_name() -> String {
|
||||
"PythonPlatform".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
// Hard code some well known values, but allow any other string as well.
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
any_of: Some(vec.
|
||||
Schema::Object(SchemaObject {
|
||||
const_value: Some(Value::String("all".to_string())),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(
|
||||
"Do not make any assumptions about the target platform."
|
||||
.to_string(),
|
||||
),
|
||||
..Metadata::default()
|
||||
})),
|
||||
|
||||
..SchemaObject::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
const_value: Some(Value::String("darwin".to_string())),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some("Darwin".to_string()),
|
||||
..Metadata::default()
|
||||
})),
|
||||
|
||||
..SchemaObject::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
const_value: Some(Value::String("linux".to_string())),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some("Linux".to_string()),
|
||||
..Metadata::default()
|
||||
})),
|
||||
|
||||
..SchemaObject::default()
|
||||
}),
|
||||
Schema::Object(SchemaObject {
|
||||
const_value: Some(Value::String("win32".to_string())),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some("Windows".to_string()),
|
||||
..Metadata::default()
|
||||
})),
|
||||
|
||||
..SchemaObject::default()
|
||||
}),
|
||||
]),
|
||||
|
||||
..SubschemaValidation::default()
|
||||
})),
|
||||
|
||||
..SchemaObject::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,20 @@ impl PythonVersion {
|
||||
minor: 13,
|
||||
};
|
||||
|
||||
pub fn iter() -> impl Iterator<Item = PythonVersion> {
|
||||
[
|
||||
PythonVersion::PY37,
|
||||
PythonVersion::PY38,
|
||||
PythonVersion::PY39,
|
||||
PythonVersion::PY310,
|
||||
PythonVersion::PY311,
|
||||
PythonVersion::PY312,
|
||||
PythonVersion::PY313,
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn free_threaded_build_available(self) -> bool {
|
||||
self >= PythonVersion::PY313
|
||||
}
|
||||
@@ -69,40 +83,86 @@ impl fmt::Display for PythonVersion {
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::Deserialize<'de> for PythonVersion {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let as_str = String::deserialize(deserializer)?;
|
||||
mod serde {
|
||||
use crate::PythonVersion;
|
||||
|
||||
if let Some((major, minor)) = as_str.split_once('.') {
|
||||
let major = major
|
||||
.parse()
|
||||
.map_err(|err| serde::de::Error::custom(format!("invalid major version: {err}")))?;
|
||||
let minor = minor
|
||||
.parse()
|
||||
.map_err(|err| serde::de::Error::custom(format!("invalid minor version: {err}")))?;
|
||||
impl<'de> serde::Deserialize<'de> for PythonVersion {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let as_str = String::deserialize(deserializer)?;
|
||||
|
||||
Ok((major, minor).into())
|
||||
} else {
|
||||
let major = as_str.parse().map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"invalid python-version: {err}, expected: `major.minor`"
|
||||
))
|
||||
})?;
|
||||
if let Some((major, minor)) = as_str.split_once('.') {
|
||||
let major = major.parse().map_err(|err| {
|
||||
serde::de::Error::custom(format!("invalid major version: {err}"))
|
||||
})?;
|
||||
let minor = minor.parse().map_err(|err| {
|
||||
serde::de::Error::custom(format!("invalid minor version: {err}"))
|
||||
})?;
|
||||
|
||||
Ok((major, 0).into())
|
||||
Ok((major, minor).into())
|
||||
} else {
|
||||
let major = as_str.parse().map_err(|err| {
|
||||
serde::de::Error::custom(format!(
|
||||
"invalid python-version: {err}, expected: `major.minor`"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok((major, 0).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for PythonVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for PythonVersion {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
#[cfg(feature = "schemars")]
|
||||
mod schemars {
|
||||
use super::PythonVersion;
|
||||
use schemars::schema::{Metadata, Schema, SchemaObject, SubschemaValidation};
|
||||
use schemars::JsonSchema;
|
||||
use schemars::_serde_json::Value;
|
||||
|
||||
impl JsonSchema for PythonVersion {
|
||||
fn schema_name() -> String {
|
||||
"PythonVersion".to_string()
|
||||
}
|
||||
|
||||
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> Schema {
|
||||
let sub_schemas = std::iter::once(Schema::Object(SchemaObject {
|
||||
instance_type: Some(schemars::schema::InstanceType::String.into()),
|
||||
string: Some(Box::new(schemars::schema::StringValidation {
|
||||
pattern: Some(r"^\d+\.\d+$".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
}))
|
||||
.chain(Self::iter().map(|v| {
|
||||
Schema::Object(SchemaObject {
|
||||
const_value: Some(Value::String(v.to_string())),
|
||||
metadata: Some(Box::new(Metadata {
|
||||
description: Some(format!("Python {v}")),
|
||||
..Metadata::default()
|
||||
})),
|
||||
..SchemaObject::default()
|
||||
})
|
||||
}));
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
subschemas: Some(Box::new(SubschemaValidation {
|
||||
any_of: Some(sub_schemas.collect()),
|
||||
..Default::default()
|
||||
})),
|
||||
..SchemaObject::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -21,6 +22,7 @@ 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;
|
||||
@@ -30,7 +32,7 @@ mod use_def;
|
||||
|
||||
pub(crate) use self::use_def::{
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
|
||||
DeclarationsIterator, ScopedVisibilityConstraintId,
|
||||
DeclarationsIterator,
|
||||
};
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
|
||||
@@ -93,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<'_> {
|
||||
@@ -139,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> {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use crate::{
|
||||
semantic_index::{ast_ids::ScopedExpressionId, expression::Expression},
|
||||
unpack::Unpack,
|
||||
};
|
||||
|
||||
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> },
|
||||
|
||||
/// An attribute assignment where the right-hand side is an iterable, for example
|
||||
/// `for self.x in <iterable>`.
|
||||
Iterable { iterable: Expression<'db> },
|
||||
|
||||
/// An attribute assignment where the left-hand side is an unpacking expression,
|
||||
/// e.g. `self.x, self.y = <value>`.
|
||||
Unpack {
|
||||
attribute_expression_id: ScopedExpressionId,
|
||||
unpack: Unpack<'db>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) type AttributeAssignments<'db> = FxHashMap<Name, Vec<AttributeAssignment<'db>>>;
|
||||
@@ -6,30 +6,29 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_index::IndexVec;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
|
||||
use ruff_python_ast::{self as ast, ExprContext};
|
||||
|
||||
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,24 @@ impl LoopState {
|
||||
}
|
||||
}
|
||||
|
||||
struct ScopeInfo {
|
||||
file_scope_id: FileScopeId,
|
||||
loop_state: LoopState,
|
||||
}
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db> {
|
||||
// Builder state
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
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,6 +90,7 @@ 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> {
|
||||
@@ -95,6 +102,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
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 +120,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 +133,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 +141,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) {
|
||||
@@ -171,16 +199,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 +230,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 +370,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 +382,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 +394,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 +437,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 +516,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 +538,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), expression_node)
|
||||
},
|
||||
expression_kind,
|
||||
countme::Count::default(),
|
||||
);
|
||||
self.expressions_by_node
|
||||
@@ -599,7 +679,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 +742,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 +785,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 +950,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 +981,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 +993,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 +1108,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 +1128,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 +1189,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 {
|
||||
@@ -1066,6 +1231,20 @@ where
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
..
|
||||
}) => {
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Iterable {
|
||||
iterable: iter_expr,
|
||||
},
|
||||
);
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -1270,6 +1449,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);
|
||||
}
|
||||
@@ -1279,7 +1473,7 @@ where
|
||||
fn visit_expr(&mut self, expr: &'ast ast::Expr) {
|
||||
self.scopes_by_expression
|
||||
.insert(expr.into(), self.current_scope());
|
||||
self.current_ast_ids().record_expression(expr);
|
||||
let expression_id = self.current_ast_ids().record_expression(expr);
|
||||
|
||||
match expr {
|
||||
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
|
||||
@@ -1508,7 +1702,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();
|
||||
|
||||
@@ -1537,6 +1732,35 @@ where
|
||||
|
||||
self.simplify_visibility_constraints(pre_op);
|
||||
}
|
||||
ast::Expr::Attribute(ast::ExprAttribute {
|
||||
value: object,
|
||||
attr,
|
||||
ctx: ExprContext::Store,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(
|
||||
CurrentAssignment::Assign {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
}
|
||||
| CurrentAssignment::For {
|
||||
unpack: Some(unpack),
|
||||
..
|
||||
},
|
||||
) = self.current_assignment()
|
||||
{
|
||||
self.register_attribute_assignment(
|
||||
object,
|
||||
attr,
|
||||
AttributeAssignment::Unpack {
|
||||
attribute_expression_id: expression_id,
|
||||
unpack,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
_ => {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -443,7 +445,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,16 +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;
|
||||
use self::symbol_state::{
|
||||
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
|
||||
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
|
||||
};
|
||||
pub(crate) use self::symbol_state::{ScopedConstraintId, ScopedVisibilityConstraintId};
|
||||
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;
|
||||
|
||||
@@ -285,7 +287,7 @@ pub(crate) struct UseDefMap<'db> {
|
||||
/// Array of [`Constraint`] in this scope.
|
||||
all_constraints: AllConstraints<'db>,
|
||||
|
||||
/// Array of [`VisibilityConstraint`]s in this scope.
|
||||
/// Array of visibility constraints in this scope.
|
||||
visibility_constraints: VisibilityConstraints<'db>,
|
||||
|
||||
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
|
||||
@@ -486,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
|
||||
@@ -510,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(),
|
||||
@@ -520,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
|
||||
@@ -534,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 {
|
||||
@@ -553,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:
|
||||
@@ -603,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,
|
||||
@@ -639,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) {
|
||||
@@ -684,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.
|
||||
@@ -717,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,
|
||||
|
||||
@@ -49,7 +49,8 @@ use ruff_index::newtype_index;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::semantic_index::use_def::bitset::{BitSet, BitSetIterator};
|
||||
use crate::semantic_index::use_def::VisibilityConstraints;
|
||||
use crate::semantic_index::use_def::VisibilityConstraintsBuilder;
|
||||
use crate::visibility_constraints::ScopedVisibilityConstraintId;
|
||||
|
||||
/// A newtype-index for a definition in a particular scope.
|
||||
#[newtype_index]
|
||||
@@ -99,18 +100,6 @@ type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
|
||||
/// Iterate over all constraints for a single binding.
|
||||
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
const INLINE_VISIBILITY_CONSTRAINTS: usize = 4;
|
||||
type InlineVisibilityConstraintsArray =
|
||||
[ScopedVisibilityConstraintId; INLINE_VISIBILITY_CONSTRAINTS];
|
||||
@@ -164,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 {
|
||||
@@ -180,7 +169,7 @@ impl SymbolDeclarations {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -248,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());
|
||||
@@ -256,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.
|
||||
@@ -270,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 {
|
||||
@@ -287,7 +279,7 @@ impl SymbolBindings {
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -360,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.
|
||||
@@ -373,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
|
||||
@@ -401,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
|
||||
@@ -568,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<>"]);
|
||||
}
|
||||
@@ -576,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>"]);
|
||||
@@ -584,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);
|
||||
@@ -601,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);
|
||||
@@ -614,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);
|
||||
@@ -655,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));
|
||||
|
||||
@@ -669,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));
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user