Compare commits
60 Commits
micha/unio
...
david/self
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36ba9707ca | ||
|
|
4aba390838 | ||
|
|
82c01aa662 | ||
|
|
9f446faa6c | ||
|
|
b94d6cf567 | ||
|
|
cd0c97211c | ||
|
|
0e71c9e3bb | ||
|
|
24c90d6953 | ||
|
|
fbff4dec3a | ||
|
|
f3dac27e9a | ||
|
|
e4cefd9bf9 | ||
|
|
9e4ee98109 | ||
|
|
557d583e32 | ||
|
|
f98eebdbab | ||
|
|
c606bf014e | ||
|
|
e8fce20736 | ||
|
|
5a30ec0df6 | ||
|
|
fab1b0d546 | ||
|
|
66abef433b | ||
|
|
fa22bd604a | ||
|
|
0c9165fc3a | ||
|
|
9f6147490b | ||
|
|
b7571c3e24 | ||
|
|
d178d115f3 | ||
|
|
6501782678 | ||
|
|
bca4341dcc | ||
|
|
31ede11774 | ||
|
|
ba9f881687 | ||
|
|
4357a0a3c2 | ||
|
|
c18afa93b3 | ||
|
|
8f04202ee4 | ||
|
|
efe54081d6 | ||
|
|
ac23c99744 | ||
|
|
e5c7d87461 | ||
|
|
de62e39eba | ||
|
|
d285717da8 | ||
|
|
545e9deba3 | ||
|
|
e3d792605f | ||
|
|
1f303a5eb6 | ||
|
|
07d13c6b4a | ||
|
|
e1838aac29 | ||
|
|
4ba847f250 | ||
|
|
13e9fc9362 | ||
|
|
3fda2d17c7 | ||
|
|
931fa06d85 | ||
|
|
e53ac7985d | ||
|
|
e25e7044ba | ||
|
|
b80de52592 | ||
|
|
2917534279 | ||
|
|
f6b2cd5588 | ||
|
|
302fe76c2b | ||
|
|
a90e404c3f | ||
|
|
8358ad8d25 | ||
|
|
2b8b1ef178 | ||
|
|
2efa3fbb62 | ||
|
|
b9da4305e6 | ||
|
|
87043a2415 | ||
|
|
f684b6fff4 | ||
|
|
47f39ed1a0 | ||
|
|
aecdb8c144 |
30
.github/workflows/ci.yaml
vendored
30
.github/workflows/ci.yaml
vendored
@@ -157,6 +157,33 @@ jobs:
|
||||
name: ruff
|
||||
path: target/debug/ruff
|
||||
|
||||
cargo-test-linux-release:
|
||||
name: "cargo test (linux, release)"
|
||||
runs-on: depot-ubuntu-22.04-16
|
||||
needs: determine_changes
|
||||
if: ${{ needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: "Install mold"
|
||||
uses: rui314/setup-mold@v1
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-nextest
|
||||
- name: "Install cargo insta"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-insta
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Run tests"
|
||||
shell: bash
|
||||
env:
|
||||
NEXTEST_PROFILE: "ci"
|
||||
run: cargo insta test --release --all-features --unreferenced reject --test-runner nextest
|
||||
|
||||
cargo-test-windows:
|
||||
name: "cargo test (windows)"
|
||||
runs-on: windows-latest-xlarge
|
||||
@@ -212,7 +239,6 @@ jobs:
|
||||
cargo-build-release:
|
||||
name: "cargo build (release)"
|
||||
runs-on: macos-latest
|
||||
needs: determine_changes
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
@@ -309,7 +335,7 @@ jobs:
|
||||
# Make executable, since artifact download doesn't preserve this
|
||||
chmod +x ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
|
||||
python scripts/fuzz-parser/fuzz.py 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
python scripts/fuzz-parser/fuzz.py --bin ruff 0-500 --test-executable ${{ steps.download-cached-binary.outputs.download-path }}/ruff
|
||||
|
||||
scripts:
|
||||
name: "test scripts"
|
||||
|
||||
2
.github/workflows/daily_fuzz.yaml
vendored
2
.github/workflows/daily_fuzz.yaml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
# but this is outweighed by the fact that a release build takes *much* longer to compile in CI
|
||||
run: cargo build --locked
|
||||
- name: Fuzz
|
||||
run: python scripts/fuzz-parser/fuzz.py $(shuf -i 0-9999999999999999999 -n 1000) --test-executable target/debug/ruff
|
||||
run: python scripts/fuzz-parser/fuzz.py --bin ruff $(shuf -i 0-9999999999999999999 -n 1000) --test-executable target/debug/ruff
|
||||
|
||||
create-issue-on-failure:
|
||||
name: Create an issue if the daily fuzz surfaced any bugs
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
# Breaking Changes
|
||||
|
||||
## 0.8.0
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
|
||||
|
||||
- **Changed location of `pydoclint` diagnostics**
|
||||
|
||||
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
|
||||
|
||||
If you've opted into these preview rules but have them suppressed using
|
||||
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
|
||||
some places, this change may mean that you need to move the `noqa` suppression
|
||||
comments. Most users should be unaffected by this change.
|
||||
|
||||
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
|
||||
|
||||
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
|
||||
|
||||
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
|
||||
|
||||
- **Changes to the line width calculation**
|
||||
|
||||
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
|
||||
|
||||
## 0.7.0
|
||||
|
||||
- The pytest rules `PT001` and `PT023` now default to omitting the decorator parentheses when there are no arguments
|
||||
|
||||
101
CHANGELOG.md
101
CHANGELOG.md
@@ -1,5 +1,104 @@
|
||||
# Changelog
|
||||
|
||||
## 0.8.0
|
||||
|
||||
Check out the [blog post](https://astral.sh/blog/ruff-v0.8.0) for a migration guide and overview of the changes!
|
||||
|
||||
### Breaking changes
|
||||
|
||||
See also, the "Remapped rules" section which may result in disabled rules.
|
||||
|
||||
- **Default to Python 3.9**
|
||||
|
||||
Ruff now defaults to Python 3.9 instead of 3.8 if no explicit Python version is configured using [`ruff.target-version`](https://docs.astral.sh/ruff/settings/#target-version) or [`project.requires-python`](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#python-requires) ([#13896](https://github.com/astral-sh/ruff/pull/13896))
|
||||
|
||||
- **Changed location of `pydoclint` diagnostics**
|
||||
|
||||
[`pydoclint`](https://docs.astral.sh/ruff/rules/#pydoclint-doc) diagnostics now point to the first-line of the problematic docstring. Previously, this was not the case.
|
||||
|
||||
If you've opted into these preview rules but have them suppressed using
|
||||
[`noqa`](https://docs.astral.sh/ruff/linter/#error-suppression) comments in
|
||||
some places, this change may mean that you need to move the `noqa` suppression
|
||||
comments. Most users should be unaffected by this change.
|
||||
|
||||
- **Use XDG (i.e. `~/.local/bin`) instead of the Cargo home directory in the standalone installer**
|
||||
|
||||
Previously, Ruff's installer used `$CARGO_HOME` or `~/.cargo/bin` for its target install directory. Now, Ruff will be installed into `$XDG_BIN_HOME`, `$XDG_DATA_HOME/../bin`, or `~/.local/bin` (in that order).
|
||||
|
||||
This change is only relevant to users of the standalone Ruff installer (using the shell or PowerShell script). If you installed Ruff using uv or pip, you should be unaffected.
|
||||
|
||||
- **Changes to the line width calculation**
|
||||
|
||||
Ruff now uses a new version of the [unicode-width](https://github.com/unicode-rs/unicode-width) Rust crate to calculate the line width. In very rare cases, this may lead to lines containing Unicode characters being reformatted, or being considered too long when they were not before ([`E501`](https://docs.astral.sh/ruff/rules/line-too-long/)).
|
||||
|
||||
### Removed Rules
|
||||
|
||||
The following deprecated rules have been removed:
|
||||
|
||||
- [`missing-type-self`](https://docs.astral.sh/ruff/rules/missing-type-self/) (`ANN101`)
|
||||
- [`missing-type-cls`](https://docs.astral.sh/ruff/rules/missing-type-cls/) (`ANN102`)
|
||||
- [`syntax-error`](https://docs.astral.sh/ruff/rules/syntax-error/) (`E999`)
|
||||
- [`pytest-missing-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-missing-fixture-name-underscore/) (`PT004`)
|
||||
- [`pytest-incorrect-fixture-name-underscore`](https://docs.astral.sh/ruff/rules/pytest-incorrect-fixture-name-underscore/) (`PT005`)
|
||||
- [`unpacked-list-comprehension`](https://docs.astral.sh/ruff/rules/unpacked-list-comprehension/) (`UP027`)
|
||||
|
||||
### Remapped rules
|
||||
|
||||
The following rules have been remapped to new rule codes:
|
||||
|
||||
- [`flake8-type-checking`](https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc): `TCH` to `TC`
|
||||
|
||||
### Stabilization
|
||||
|
||||
The following rules have been stabilized and are no longer in preview:
|
||||
|
||||
- [`builtin-import-shadowing`](https://docs.astral.sh/ruff/rules/builtin-import-shadowing/) (`A004`)
|
||||
- [`mutable-contextvar-default`](https://docs.astral.sh/ruff/rules/mutable-contextvar-default/) (`B039`)
|
||||
- [`fast-api-redundant-response-model`](https://docs.astral.sh/ruff/rules/fast-api-redundant-response-model/) (`FAST001`)
|
||||
- [`fast-api-non-annotated-dependency`](https://docs.astral.sh/ruff/rules/fast-api-non-annotated-dependency/) (`FAST002`)
|
||||
- [`dict-index-missing-items`](https://docs.astral.sh/ruff/rules/dict-index-missing-items/) (`PLC0206`)
|
||||
- [`pep484-style-positional-only-parameter`](https://docs.astral.sh/ruff/rules/pep484-style-positional-only-parameter/) (`PYI063`)
|
||||
- [`redundant-final-literal`](https://docs.astral.sh/ruff/rules/redundant-final-literal/) (`PYI064`)
|
||||
- [`bad-version-info-order`](https://docs.astral.sh/ruff/rules/bad-version-info-order/) (`PYI066`)
|
||||
- [`parenthesize-chained-operators`](https://docs.astral.sh/ruff/rules/parenthesize-chained-operators/) (`RUF021`)
|
||||
- [`unsorted-dunder-all`](https://docs.astral.sh/ruff/rules/unsorted-dunder-all/) (`RUF022`)
|
||||
- [`unsorted-dunder-slots`](https://docs.astral.sh/ruff/rules/unsorted-dunder-slots/) (`RUF023`)
|
||||
- [`assert-with-print-message`](https://docs.astral.sh/ruff/rules/assert-with-print-message/) (`RUF030`)
|
||||
- [`unnecessary-default-type-args`](https://docs.astral.sh/ruff/rules/unnecessary-default-type-args/) (`UP043`)
|
||||
|
||||
The following behaviors have been stabilized:
|
||||
|
||||
- [`ambiguous-variable-name`](https://docs.astral.sh/ruff/rules/ambiguous-variable-name/) (`E741`): Violations in stub files are now ignored. Stub authors typically don't control variable names.
|
||||
- [`printf-string-formatting`](https://docs.astral.sh/ruff/rules/printf-string-formatting/) (`UP031`): Report all `printf`-like usages even if no autofix is available
|
||||
|
||||
The following fixes have been stabilized:
|
||||
|
||||
- [`zip-instead-of-pairwise`](https://docs.astral.sh/ruff/rules/zip-instead-of-pairwise/) (`RUF007`)
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-datetimez`\] Exempt `min.time()` and `max.time()` (`DTZ901`) ([#14394](https://github.com/astral-sh/ruff/pull/14394))
|
||||
- \[`flake8-pie`\] Mark fix as unsafe if the following statement is a string literal (`PIE790`) ([#14393](https://github.com/astral-sh/ruff/pull/14393))
|
||||
- \[`flake8-pyi`\] New rule `redundant-none-literal` (`PYI061`) ([#14316](https://github.com/astral-sh/ruff/pull/14316))
|
||||
- \[`flake8-pyi`\] Add autofix for `redundant-numeric-union` (`PYI041`) ([#14273](https://github.com/astral-sh/ruff/pull/14273))
|
||||
- \[`ruff`\] New rule `map-int-version-parsing` (`RUF048`) ([#14373](https://github.com/astral-sh/ruff/pull/14373))
|
||||
- \[`ruff`\] New rule `redundant-bool-literal` (`RUF038`) ([#14319](https://github.com/astral-sh/ruff/pull/14319))
|
||||
- \[`ruff`\] New rule `unraw-re-pattern` (`RUF039`) ([#14446](https://github.com/astral-sh/ruff/pull/14446))
|
||||
- \[`pycodestyle`\] Exempt `pytest.importorskip()` calls (`E402`) ([#14474](https://github.com/astral-sh/ruff/pull/14474))
|
||||
- \[`pylint`\] Autofix suggests using sets when possible (`PLR1714`) ([#14372](https://github.com/astral-sh/ruff/pull/14372))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- [`invalid-pyproject-toml`](https://docs.astral.sh/ruff/rules/invalid-pyproject-toml/) (`RUF200`): Updated to reflect the provisionally accepted [PEP 639](https://peps.python.org/pep-0639/).
|
||||
- \[`flake8-pyi`\] Avoid panic in unfixable case (`PYI041`) ([#14402](https://github.com/astral-sh/ruff/pull/14402))
|
||||
- \[`flake8-type-checking`\] Correctly handle quotes in subscript expression when generating an autofix ([#14371](https://github.com/astral-sh/ruff/pull/14371))
|
||||
- \[`pylint`\] Suggest correct autofix for `__contains__` (`PLC2801`) ([#14424](https://github.com/astral-sh/ruff/pull/14424))
|
||||
|
||||
### Configuration
|
||||
|
||||
- Ruff now emits a warning instead of an error when a configuration [`ignore`](https://docs.astral.sh/ruff/settings/#lint_ignore)s a rule that has been removed ([#14435](https://github.com/astral-sh/ruff/pull/14435))
|
||||
- Ruff now validates that `lint.flake8-import-conventions.aliases` only uses valid module names and aliases ([#14477](https://github.com/astral-sh/ruff/pull/14477))
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### Preview features
|
||||
@@ -978,7 +1077,7 @@ The following deprecated CLI commands have been removed:
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-bugbear`\] Implement `return-in-generator` (`B901`) ([#11644](https://github.com/astral-sh/ruff/pull/11644))
|
||||
- \[`flake8-pyi`\] Implement `PYI063` ([#11699](https://github.com/astral-sh/ruff/pull/11699))
|
||||
- \[`flake8-pyi`\] Implement `pep484-style-positional-only-parameter` (`PYI063`) ([#11699](https://github.com/astral-sh/ruff/pull/11699))
|
||||
- \[`pygrep_hooks`\] Check blanket ignores via file-level pragmas (`PGH004`) ([#11540](https://github.com/astral-sh/ruff/pull/11540))
|
||||
|
||||
### Rule changes
|
||||
|
||||
130
Cargo.lock
generated
130
Cargo.lock
generated
@@ -413,7 +413,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -693,7 +693,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -704,7 +704,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -758,23 +758,23 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dir-test"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c44bdf9319ad5223afb7eb15a7110452b0adf0373ea6756561b2c708eef0dd1"
|
||||
checksum = "b12781621d53fd9087021f5a338df5c57c04f84a6231c1f4726f45e2e333470b"
|
||||
dependencies = [
|
||||
"dir-test-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dir-test-macros"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "644f96047137dfaa7a09e34d4623f9e52a1926ecc25ba32ad2ba3fc422536b25"
|
||||
checksum = "1340852f50b2285d01a7f598cc5d08b572669c3e09e614925175cc3c26787b91"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -826,7 +826,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1068,9 +1068,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.1"
|
||||
version = "0.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
|
||||
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
@@ -1246,7 +1246,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1319,7 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.15.1",
|
||||
"hashbrown 0.15.2",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1420,7 +1420,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1526,9 +1526,9 @@ checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1586dd7a857d8a61a577afde1a24cc9573ff549eff092d5ce968b1ec93cc61b6"
|
||||
checksum = "fa3e60579a8cba3d86aa4a5f7fc98973cc0fd2ac270bf02f85a9bef09700b075"
|
||||
dependencies = [
|
||||
"chic",
|
||||
"libcst_derive",
|
||||
@@ -1546,7 +1546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1710,9 +1710,9 @@ checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c"
|
||||
|
||||
[[package]]
|
||||
name = "newtype-uuid"
|
||||
version = "1.1.0"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3526cb7c660872e401beaf3297f95f548ce3b4b4bdd8121b7c0713771d7c4a6e"
|
||||
checksum = "4c8781e2ef64806278a55ad223f0bc875772fd40e1fe6e73e8adbf027817229d"
|
||||
dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
@@ -2012,7 +2012,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2127,9 +2127,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.89"
|
||||
version = "1.0.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
|
||||
checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2150,24 +2150,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-junit"
|
||||
version = "0.5.0"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62ffd2f9a162cfae131bed6d9d1ed60adced33be340a94f96952897d7cb0c240"
|
||||
checksum = "3ed1a693391a16317257103ad06a88c6529ac640846021da7c435a06fffdacd7"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"indexmap",
|
||||
"newtype-uuid",
|
||||
"quick-xml",
|
||||
"strip-ansi-escapes",
|
||||
"thiserror 1.0.67",
|
||||
"thiserror 2.0.3",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.36.1"
|
||||
version = "0.37.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc"
|
||||
checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
@@ -2266,7 +2266,7 @@ dependencies = [
|
||||
"compact_str",
|
||||
"countme",
|
||||
"dir-test",
|
||||
"hashbrown 0.15.1",
|
||||
"hashbrown 0.15.2",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
@@ -2489,7 +2489,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.7.4"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2708,7 +2708,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.7.4"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2775,7 +2775,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3023,7 +3023,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.7.4"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3072,6 +3072,7 @@ dependencies = [
|
||||
"ruff_python_ast",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_semantic",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_source_file",
|
||||
"rustc-hash 2.0.0",
|
||||
"schemars",
|
||||
@@ -3194,7 +3195,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -3228,7 +3229,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3277,7 +3278,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3288,7 +3289,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3311,7 +3312,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3352,7 +3353,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3460,7 +3461,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3471,20 +3472,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
version = "2.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3499,7 +3489,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3562,7 +3552,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3573,7 +3563,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
@@ -3603,7 +3593,7 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3614,7 +3604,7 @@ checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3736,7 +3726,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3875,9 +3865,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
|
||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
@@ -3952,9 +3942,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.3"
|
||||
version = "2.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d157f1b96d14500ffdc1f10ba712e780825526c03d9a49b4d0324b0d9113ada"
|
||||
checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
@@ -4006,7 +3996,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4101,7 +4091,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -4135,7 +4125,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -4169,7 +4159,7 @@ checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4472,7 +4462,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4493,7 +4483,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4513,7 +4503,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -4542,7 +4532,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.87",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -65,7 +65,7 @@ compact_str = "0.8.0"
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam = { version = "0.8.4" }
|
||||
dashmap = { version = "6.0.1" }
|
||||
dir-test = { version = "0.3.0" }
|
||||
dir-test = { version = "0.4.0" }
|
||||
dunce = { version = "1.0.5" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
|
||||
@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.7.4/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.4/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.8.0/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.8.0/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.7.4
|
||||
rev: v0.8.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -4,7 +4,6 @@ use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
|
||||
use red_knot_python_semantic::{resolve_module, ModuleName, Program, PythonVersion, SitePackages};
|
||||
use red_knot_workspace::db::{Db, RootDatabase};
|
||||
use red_knot_workspace::watch;
|
||||
@@ -14,7 +13,7 @@ use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::files::{system_path_to_file, File, FileError};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::testing::setup_logging;
|
||||
use ruff_db::testing::{setup_logging, setup_logging_with_filter};
|
||||
use ruff_db::Upcast;
|
||||
|
||||
struct TestCase {
|
||||
@@ -47,6 +46,8 @@ impl TestCase {
|
||||
}
|
||||
|
||||
fn try_stop_watch(&mut self, timeout: Duration) -> Option<Vec<watch::ChangeEvent>> {
|
||||
tracing::debug!("Try stopping watch with timeout {:?}", timeout);
|
||||
|
||||
let watcher = self
|
||||
.watcher
|
||||
.take()
|
||||
@@ -56,8 +57,11 @@ impl TestCase {
|
||||
.changes_receiver
|
||||
.recv_timeout(timeout)
|
||||
.unwrap_or_default();
|
||||
|
||||
watcher.flush();
|
||||
tracing::debug!("Flushed file watcher");
|
||||
watcher.stop();
|
||||
tracing::debug!("Stopping file watcher");
|
||||
|
||||
for event in &self.changes_receiver {
|
||||
all_events.extend(event);
|
||||
@@ -600,6 +604,8 @@ fn directory_moved_to_trash() -> anyhow::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn directory_renamed() -> anyhow::Result<()> {
|
||||
let _tracing = setup_logging_with_filter("file_watching=TRACE,red_knot=TRACE");
|
||||
|
||||
let mut case = setup([
|
||||
("bar.py", "import sub.a"),
|
||||
("sub/__init__.py", ""),
|
||||
@@ -640,6 +646,10 @@ fn directory_renamed() -> anyhow::Result<()> {
|
||||
|
||||
let changes = case.stop_watch();
|
||||
|
||||
for event in &changes {
|
||||
tracing::debug!("Event: {:?}", event);
|
||||
}
|
||||
|
||||
case.apply_changes(changes);
|
||||
|
||||
// `import sub.a` should no longer resolve
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# NoReturn & Never
|
||||
|
||||
`NoReturn` is used to annotate the return type for functions that never return. `Never` is the
|
||||
bottom type, representing the empty set of Python objects. These two annotations can be used
|
||||
interchangeably.
|
||||
|
||||
## Function Return Type Annotation
|
||||
|
||||
```py
|
||||
from typing import NoReturn
|
||||
|
||||
def stop() -> NoReturn:
|
||||
raise RuntimeError("no way")
|
||||
|
||||
# revealed: Never
|
||||
reveal_type(stop())
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import NoReturn, Never, Any
|
||||
|
||||
# error: [invalid-type-parameter] "Type `typing.Never` expected no type parameter"
|
||||
x: Never[int]
|
||||
a1: NoReturn
|
||||
# TODO: Test `Never` is only available in python >= 3.11
|
||||
a2: Never
|
||||
b1: Any
|
||||
b2: int
|
||||
|
||||
def f():
|
||||
# revealed: Never
|
||||
reveal_type(a1)
|
||||
# revealed: Never
|
||||
reveal_type(a2)
|
||||
|
||||
# Never is assignable to all types.
|
||||
v1: int = a1
|
||||
v2: str = a1
|
||||
# Other types are not assignable to Never except for Never (and Any).
|
||||
v3: Never = b1
|
||||
v4: Never = a2
|
||||
v5: Any = b2
|
||||
# error: [invalid-assignment] "Object of type `Literal[1]` is not assignable to `Never`"
|
||||
v6: Never = 1
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import NoReturn, Never
|
||||
|
||||
x: NoReturn
|
||||
y: Never
|
||||
|
||||
def f():
|
||||
# revealed: Never
|
||||
reveal_type(x)
|
||||
# revealed: Never
|
||||
reveal_type(y)
|
||||
```
|
||||
@@ -9,10 +9,10 @@ Ts = TypeVarTuple("Ts")
|
||||
|
||||
def append_int(*args: *Ts) -> tuple[*Ts, int]:
|
||||
# TODO: should show some representation of the variadic generic type
|
||||
reveal_type(args) # revealed: @Todo
|
||||
reveal_type(args) # revealed: @Todo(function parameter type)
|
||||
|
||||
return (*args, 1)
|
||||
|
||||
# TODO should be tuple[Literal[True], Literal["a"], int]
|
||||
reveal_type(append_int(True, "a")) # revealed: @Todo
|
||||
reveal_type(append_int(True, "a")) # revealed: @Todo(full tuple[...] support)
|
||||
```
|
||||
|
||||
@@ -125,7 +125,6 @@ def f7() -> "\x69nt":
|
||||
def f8() -> """int""":
|
||||
return 1
|
||||
|
||||
# error: [annotation-byte-string] "Type expressions cannot use bytes literal"
|
||||
def f9() -> "b'int'":
|
||||
return 1
|
||||
|
||||
@@ -189,3 +188,31 @@ reveal_type(d) # revealed: Foo
|
||||
## Parameter
|
||||
|
||||
TODO: Add tests once parameter inference is supported
|
||||
|
||||
## Invalid expressions
|
||||
|
||||
The expressions in these string annotations aren't valid expressions in this context but we
|
||||
shouldn't panic.
|
||||
|
||||
```py
|
||||
a: "1 or 2"
|
||||
b: "(x := 1)"
|
||||
c: "1 + 2"
|
||||
d: "lambda x: x"
|
||||
e: "x if True else y"
|
||||
f: "{'a': 1, 'b': 2}"
|
||||
g: "{1, 2}"
|
||||
h: "[i for i in range(5)]"
|
||||
i: "{i for i in range(5)}"
|
||||
j: "{i: i for i in range(5)}"
|
||||
k: "(i for i in range(5))"
|
||||
l: "await 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
m: "yield 1"
|
||||
# error: [forward-annotation-syntax-error]
|
||||
n: "yield from 1"
|
||||
o: "1 < 2"
|
||||
p: "call()"
|
||||
r: "[1, 2]"
|
||||
s: "(1, 2)"
|
||||
```
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Union
|
||||
|
||||
## Annotation
|
||||
|
||||
`typing.Union` can be used to construct union types same as `|` operator.
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a1: Union[int, bool]
|
||||
a2: Union[int, Union[float, str]]
|
||||
a3: Union[int, None]
|
||||
a4: Union[Union[float, str]]
|
||||
a5: Union[int]
|
||||
a6: Union[()]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
|
||||
# revealed: int
|
||||
reveal_type(a1)
|
||||
# revealed: int | float | str
|
||||
reveal_type(a2)
|
||||
# revealed: int | None
|
||||
reveal_type(a3)
|
||||
# revealed: float | str
|
||||
reveal_type(a4)
|
||||
# revealed: int
|
||||
reveal_type(a5)
|
||||
# revealed: Never
|
||||
reveal_type(a6)
|
||||
```
|
||||
|
||||
## Assignment
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
a: Union[int, str]
|
||||
a = 1
|
||||
a = ""
|
||||
a1: Union[int, bool]
|
||||
a1 = 1
|
||||
a1 = True
|
||||
# error: [invalid-assignment] "Object of type `Literal[b""]` is not assignable to `int | str`"
|
||||
a = b""
|
||||
```
|
||||
|
||||
## Typing Extensions
|
||||
|
||||
```py
|
||||
from typing_extensions import Union
|
||||
|
||||
a: Union[int, str]
|
||||
|
||||
def f():
|
||||
# revealed: int | str
|
||||
reveal_type(a)
|
||||
```
|
||||
@@ -51,12 +51,12 @@ reveal_type(c) # revealed: tuple[str, int]
|
||||
reveal_type(d) # revealed: tuple[tuple[str, str], tuple[int, int]]
|
||||
|
||||
# TODO: homogenous tuples, PEP-646 tuples
|
||||
reveal_type(e) # revealed: @Todo
|
||||
reveal_type(f) # revealed: @Todo
|
||||
reveal_type(g) # revealed: @Todo
|
||||
reveal_type(e) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(f) # revealed: @Todo(full tuple[...] support)
|
||||
reveal_type(g) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
# TODO: support more kinds of type expressions in annotations
|
||||
reveal_type(h) # revealed: @Todo
|
||||
reveal_type(h) # revealed: @Todo(full tuple[...] support)
|
||||
|
||||
reveal_type(i) # revealed: tuple[str | int, str | int]
|
||||
reveal_type(j) # revealed: tuple[str | int]
|
||||
|
||||
@@ -317,7 +317,7 @@ reveal_type(1 + A()) # revealed: int
|
||||
reveal_type(A() + "foo") # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type("foo" + A()) # revealed: @Todo
|
||||
reveal_type("foo" + A()) # revealed: @Todo(return type)
|
||||
|
||||
reveal_type(A() + b"foo") # revealed: A
|
||||
# TODO should be `A` since `bytes.__add__` doesn't support `A` instances
|
||||
@@ -325,7 +325,7 @@ reveal_type(b"foo" + A()) # revealed: bytes
|
||||
|
||||
reveal_type(A() + ()) # revealed: A
|
||||
# TODO this should be `A`, since `tuple.__add__` doesn't support `A` instances
|
||||
reveal_type(() + A()) # revealed: @Todo
|
||||
reveal_type(() + A()) # revealed: @Todo(return type)
|
||||
|
||||
literal_string_instance = "foo" * 1_000_000_000
|
||||
# the test is not testing what it's meant to be testing if this isn't a `LiteralString`:
|
||||
@@ -334,7 +334,7 @@ reveal_type(literal_string_instance) # revealed: LiteralString
|
||||
reveal_type(A() + literal_string_instance) # revealed: A
|
||||
# TODO should be `A` since `str.__add__` doesn't support `A` instances
|
||||
# TODO overloads
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo
|
||||
reveal_type(literal_string_instance + A()) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Operations involving instances of classes inheriting from `Any`
|
||||
|
||||
@@ -16,7 +16,7 @@ async def get_int_async() -> int:
|
||||
return 42
|
||||
|
||||
# TODO: we don't yet support `types.CoroutineType`, should be generic `Coroutine[Any, Any, int]`
|
||||
reveal_type(get_int_async()) # revealed: @Todo
|
||||
reveal_type(get_int_async()) # revealed: @Todo(generic types.CoroutineType)
|
||||
```
|
||||
|
||||
## Generic
|
||||
@@ -36,6 +36,8 @@ from typing import Callable
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# TODO: This should be resolved once we understand `Callable` annotations
|
||||
# error: [annotation-with-invalid-expression]
|
||||
def decorator(func) -> Callable[[], int]:
|
||||
return foo
|
||||
|
||||
@@ -44,7 +46,7 @@ def bar() -> str:
|
||||
return "bar"
|
||||
|
||||
# TODO: should reveal `int`, as the decorator replaces `bar` with `foo`
|
||||
reveal_type(bar()) # revealed: @Todo
|
||||
reveal_type(bar()) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Invalid callable
|
||||
|
||||
@@ -50,11 +50,11 @@ def foo(
|
||||
help()
|
||||
except x as e:
|
||||
# TODO: should be `AttributeError`
|
||||
reveal_type(e) # revealed: @Todo
|
||||
reveal_type(e) # revealed: @Todo(exception type)
|
||||
except y as f:
|
||||
# TODO: should be `OSError | RuntimeError`
|
||||
reveal_type(f) # revealed: @Todo
|
||||
reveal_type(f) # revealed: @Todo(exception type)
|
||||
except z as g:
|
||||
# TODO: should be `BaseException`
|
||||
reveal_type(g) # revealed: @Todo
|
||||
reveal_type(g) # revealed: @Todo(exception type)
|
||||
```
|
||||
|
||||
@@ -22,3 +22,22 @@ reveal_type(1 if None else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if "" else 2) # revealed: Literal[2]
|
||||
reveal_type(1 if 0 else 2) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
## Leaked Narrowing Constraint
|
||||
|
||||
(issue #14588)
|
||||
|
||||
The test inside an if expression should not affect code outside of the expression.
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x: Literal[42, "hello"] = 42 if bool_instance() else "hello"
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
|
||||
_ = ... if isinstance(x, str) else ...
|
||||
|
||||
reveal_type(x) # revealed: Literal[42] | Literal["hello"]
|
||||
```
|
||||
|
||||
@@ -18,7 +18,7 @@ box: MyBox[int] = MyBox(5)
|
||||
wrong_innards: MyBox[int] = MyBox("five")
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(box.data) # revealed: @Todo
|
||||
reveal_type(box.data) # revealed: @Todo(instance attributes)
|
||||
|
||||
reveal_type(MyBox.box_model_number) # revealed: Literal[695]
|
||||
```
|
||||
@@ -39,7 +39,7 @@ class MySecureBox[T](MyBox[T]): ...
|
||||
secure_box: MySecureBox[int] = MySecureBox(5)
|
||||
reveal_type(secure_box) # revealed: MySecureBox
|
||||
# TODO reveal int
|
||||
reveal_type(secure_box.data) # revealed: @Todo
|
||||
reveal_type(secure_box.data) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Cyclical class definition
|
||||
@@ -60,52 +60,20 @@ reveal_type(S) # revealed: Literal[S]
|
||||
|
||||
## Type params
|
||||
|
||||
A PEP695 type variable defines a value of type `typing.TypeVar` with attributes `__name__`,
|
||||
`__bounds__`, `__constraints__`, and `__default__` (the latter three all lazily evaluated):
|
||||
A PEP695 type variable defines a value of type `typing.TypeVar`.
|
||||
|
||||
```py
|
||||
def f[T, U: A, V: (A, B), W = A, X: A = A1]():
|
||||
def f[T]():
|
||||
reveal_type(T) # revealed: T
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
reveal_type(T.__bound__) # revealed: None
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
reveal_type(T.__default__) # revealed: NoDefault
|
||||
|
||||
reveal_type(U) # revealed: U
|
||||
reveal_type(U.__name__) # revealed: Literal["U"]
|
||||
reveal_type(U.__bound__) # revealed: type[A]
|
||||
reveal_type(U.__constraints__) # revealed: tuple[()]
|
||||
reveal_type(U.__default__) # revealed: NoDefault
|
||||
|
||||
reveal_type(V) # revealed: V
|
||||
reveal_type(V.__name__) # revealed: Literal["V"]
|
||||
reveal_type(V.__bound__) # revealed: None
|
||||
reveal_type(V.__constraints__) # revealed: tuple[type[A], type[B]]
|
||||
reveal_type(V.__default__) # revealed: NoDefault
|
||||
|
||||
reveal_type(W) # revealed: W
|
||||
reveal_type(W.__name__) # revealed: Literal["W"]
|
||||
reveal_type(W.__bound__) # revealed: None
|
||||
reveal_type(W.__constraints__) # revealed: tuple[()]
|
||||
reveal_type(W.__default__) # revealed: type[A]
|
||||
|
||||
reveal_type(X) # revealed: X
|
||||
reveal_type(X.__name__) # revealed: Literal["X"]
|
||||
reveal_type(X.__bound__) # revealed: type[A]
|
||||
reveal_type(X.__constraints__) # revealed: tuple[()]
|
||||
reveal_type(X.__default__) # revealed: type[A1]
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class A1(A): ...
|
||||
```
|
||||
|
||||
## Minimum two constraints
|
||||
|
||||
A typevar with less than two constraints emits a diagnostic and is treated as unconstrained:
|
||||
A typevar with less than two constraints emits a diagnostic:
|
||||
|
||||
```py
|
||||
# error: [invalid-typevar-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
pass
|
||||
```
|
||||
|
||||
@@ -75,10 +75,11 @@ Literal: _SpecialForm
|
||||
```py
|
||||
from other import Literal
|
||||
|
||||
# error: [annotation-with-invalid-expression]
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: @Todo
|
||||
reveal_type(a1) # revealed: @Todo(generics)
|
||||
```
|
||||
|
||||
## Detecting typing_extensions.Literal
|
||||
|
||||
@@ -18,7 +18,7 @@ async def foo():
|
||||
pass
|
||||
|
||||
# TODO: should reveal `Unknown` because `__aiter__` is not defined
|
||||
# revealed: @Todo
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(x)
|
||||
```
|
||||
@@ -40,6 +40,6 @@ async def foo():
|
||||
pass
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
# revealed: @Todo
|
||||
# revealed: @Todo(async iterables/iterators)
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
@@ -52,3 +52,29 @@ else:
|
||||
reveal_type(x) # revealed: Literal[2, 3]
|
||||
reveal_type(y) # revealed: Literal[1, 2, 4]
|
||||
```
|
||||
|
||||
## Nested while loops
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
x = 1
|
||||
|
||||
while flag():
|
||||
x = 2
|
||||
|
||||
while flag():
|
||||
x = 3
|
||||
if flag():
|
||||
break
|
||||
else:
|
||||
x = 4
|
||||
|
||||
if flag():
|
||||
break
|
||||
else:
|
||||
x = 5
|
||||
|
||||
reveal_type(x) # revealed: Literal[3, 4, 5]
|
||||
```
|
||||
|
||||
@@ -171,7 +171,7 @@ def f(*args, **kwargs) -> int: ...
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO should be `type[int]`
|
||||
reveal_type(A.__class__) # revealed: @Todo
|
||||
reveal_type(A.__class__) # revealed: @Todo(metaclass not a class)
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
@@ -17,8 +17,7 @@ reveal_type(__doc__) # revealed: str | None
|
||||
# (needs support for `*` imports)
|
||||
reveal_type(__spec__) # revealed: Unknown | None
|
||||
|
||||
# TODO: generics
|
||||
reveal_type(__path__) # revealed: @Todo
|
||||
reveal_type(__path__) # revealed: @Todo(generics)
|
||||
|
||||
class X:
|
||||
reveal_type(__name__) # revealed: str
|
||||
@@ -64,7 +63,7 @@ reveal_type(typing.__class__) # revealed: Literal[type]
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
reveal_type(typing.__dict__) # revealed: @Todo
|
||||
reveal_type(typing.__dict__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
Typeshed includes a fake `__getattr__` method in the stub for `types.ModuleType` to help out with
|
||||
@@ -96,8 +95,8 @@ from foo import __dict__ as foo_dict
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties, and generics;
|
||||
# should be `dict[str, Any]` for both of these:
|
||||
reveal_type(foo.__dict__) # revealed: @Todo
|
||||
reveal_type(foo_dict) # revealed: @Todo
|
||||
reveal_type(foo.__dict__) # revealed: @Todo(instance attributes)
|
||||
reveal_type(foo_dict) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Conditionally global or `ModuleType` attribute
|
||||
|
||||
@@ -27,7 +27,7 @@ def int_instance() -> int:
|
||||
|
||||
a = b"abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(a) # revealed: @Todo
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -47,11 +47,11 @@ def int_instance() -> int: ...
|
||||
|
||||
byte_slice1 = b[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice1) # revealed: @Todo
|
||||
reveal_type(byte_slice1) # revealed: @Todo(return type)
|
||||
|
||||
def bytes_instance() -> bytes: ...
|
||||
|
||||
byte_slice2 = bytes_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `bytes`
|
||||
reveal_type(byte_slice2) # revealed: @Todo
|
||||
reveal_type(byte_slice2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
@@ -12,13 +12,13 @@ x = [1, 2, 3]
|
||||
reveal_type(x) # revealed: list
|
||||
|
||||
# TODO reveal int
|
||||
reveal_type(x[0]) # revealed: @Todo
|
||||
reveal_type(x[0]) # revealed: @Todo(return type)
|
||||
|
||||
# TODO reveal list
|
||||
reveal_type(x[0:1]) # revealed: @Todo
|
||||
reveal_type(x[0:1]) # revealed: @Todo(return type)
|
||||
|
||||
# TODO error
|
||||
reveal_type(x["a"]) # revealed: @Todo
|
||||
reveal_type(x["a"]) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Assignments within list assignment
|
||||
|
||||
@@ -23,7 +23,7 @@ def int_instance() -> int: ...
|
||||
|
||||
a = "abcde"[int_instance()]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(a) # revealed: @Todo
|
||||
reveal_type(a) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Slices
|
||||
@@ -78,13 +78,13 @@ def int_instance() -> int: ...
|
||||
|
||||
substring1 = s[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `LiteralString`
|
||||
reveal_type(substring1) # revealed: @Todo
|
||||
reveal_type(substring1) # revealed: @Todo(return type)
|
||||
|
||||
def str_instance() -> str: ...
|
||||
|
||||
substring2 = str_instance()[0:5]
|
||||
# TODO: Support overloads... Should be `str`
|
||||
reveal_type(substring2) # revealed: @Todo
|
||||
reveal_type(substring2) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
## Unsupported slice types
|
||||
|
||||
@@ -71,5 +71,5 @@ def int_instance() -> int: ...
|
||||
|
||||
tuple_slice = t[int_instance() : int_instance()]
|
||||
# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]`
|
||||
reveal_type(tuple_slice) # revealed: @Todo
|
||||
reveal_type(tuple_slice) # revealed: @Todo(return type)
|
||||
```
|
||||
|
||||
@@ -112,9 +112,9 @@ properties on instance types:
|
||||
```py path=b.py
|
||||
import sys
|
||||
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo
|
||||
reveal_type(sys.version_info.micro) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.releaselevel) # revealed: @Todo(instance attributes)
|
||||
reveal_type(sys.version_info.serial) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Accessing fields by index/slice
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# Type aliases
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
reveal_type(IntOrStr) # revealed: typing.TypeAliasType
|
||||
reveal_type(IntOrStr.__name__) # revealed: Literal["IntOrStr"]
|
||||
|
||||
x: IntOrStr = 1
|
||||
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
def f() -> None:
|
||||
reveal_type(x) # revealed: int | str
|
||||
```
|
||||
|
||||
## `__value__` attribute
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
|
||||
# TODO: This should either fall back to the specified type from typeshed,
|
||||
# which is `Any`, or be the actual type of the runtime value expression
|
||||
# `int | str`, i.e. `types.UnionType`.
|
||||
reveal_type(IntOrStr.__value__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
|
||||
## Invalid assignment
|
||||
|
||||
```py
|
||||
type OptionalInt = int | None
|
||||
|
||||
# error: [invalid-assignment]
|
||||
x: OptionalInt = "1"
|
||||
```
|
||||
|
||||
## Type aliases in type aliases
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
type IntOrStrOrBytes = IntOrStr | bytes
|
||||
|
||||
x: IntOrStrOrBytes = 1
|
||||
|
||||
def f() -> None:
|
||||
reveal_type(x) # revealed: int | str | bytes
|
||||
```
|
||||
|
||||
## Aliased type aliases
|
||||
|
||||
```py
|
||||
type IntOrStr = int | str
|
||||
MyIntOrStr = IntOrStr
|
||||
|
||||
x: MyIntOrStr = 1
|
||||
|
||||
# error: [invalid-assignment]
|
||||
y: MyIntOrStr = None
|
||||
```
|
||||
|
||||
## Generic type aliases
|
||||
|
||||
```py
|
||||
type ListOrSet[T] = list[T] | set[T]
|
||||
|
||||
# TODO: Should be `tuple[typing.TypeVar | typing.ParamSpec | typing.TypeVarTuple, ...]`,
|
||||
# as specified in the `typeshed` stubs.
|
||||
reveal_type(ListOrSet.__type_params__) # revealed: @Todo(instance attributes)
|
||||
```
|
||||
@@ -84,7 +84,7 @@ reveal_type(b) # revealed: Literal[2]
|
||||
[a, *b, c, d] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
@@ -95,7 +95,7 @@ reveal_type(d) # revealed: Unknown
|
||||
[a, *b, c] = (1, 2)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[2]
|
||||
```
|
||||
|
||||
@@ -105,7 +105,7 @@ reveal_type(c) # revealed: Literal[2]
|
||||
[a, *b, c] = (1, 2, 3)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[3]
|
||||
```
|
||||
|
||||
@@ -115,7 +115,7 @@ reveal_type(c) # revealed: Literal[3]
|
||||
[a, *b, c, d] = (1, 2, 3, 4, 5, 6)
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: Literal[5]
|
||||
reveal_type(d) # revealed: Literal[6]
|
||||
```
|
||||
@@ -127,7 +127,7 @@ reveal_type(d) # revealed: Literal[6]
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Literal[2]
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
### Starred expression (6)
|
||||
@@ -138,7 +138,7 @@ reveal_type(c) # revealed: @Todo
|
||||
reveal_type(a) # revealed: Literal[1]
|
||||
reveal_type(b) # revealed: Unknown
|
||||
reveal_type(c) # revealed: Unknown
|
||||
reveal_type(d) # revealed: @Todo
|
||||
reveal_type(d) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(e) # revealed: Unknown
|
||||
reveal_type(f) # revealed: Unknown
|
||||
```
|
||||
@@ -222,7 +222,7 @@ reveal_type(b) # revealed: LiteralString
|
||||
(a, *b, c, d) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: Unknown
|
||||
```
|
||||
@@ -233,7 +233,7 @@ reveal_type(d) # revealed: Unknown
|
||||
(a, *b, c) = "ab"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[Any] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
@@ -243,7 +243,7 @@ reveal_type(c) # revealed: LiteralString
|
||||
(a, *b, c) = "abc"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
```
|
||||
|
||||
@@ -253,7 +253,7 @@ reveal_type(c) # revealed: LiteralString
|
||||
(a, *b, c, d) = "abcdef"
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
|
||||
reveal_type(b) # revealed: @Todo
|
||||
reveal_type(b) # revealed: @Todo(starred unpacking)
|
||||
reveal_type(c) # revealed: LiteralString
|
||||
reveal_type(d) # revealed: LiteralString
|
||||
```
|
||||
@@ -265,5 +265,5 @@ reveal_type(d) # revealed: LiteralString
|
||||
reveal_type(a) # revealed: LiteralString
|
||||
reveal_type(b) # revealed: LiteralString
|
||||
# TODO: Should be list[int] once support for assigning to starred expression is added
|
||||
reveal_type(c) # revealed: @Todo
|
||||
reveal_type(c) # revealed: @Todo(starred unpacking)
|
||||
```
|
||||
|
||||
@@ -17,5 +17,5 @@ class Manager:
|
||||
|
||||
async def test():
|
||||
async with Manager() as f:
|
||||
reveal_type(f) # revealed: @Todo
|
||||
reveal_type(f) # revealed: @Todo(async with statement)
|
||||
```
|
||||
|
||||
@@ -36,12 +36,25 @@ use super::definition::{
|
||||
|
||||
mod except_handlers;
|
||||
|
||||
/// Are we in a state where a `break` statement is allowed?
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum LoopState {
|
||||
InLoop,
|
||||
NotInLoop,
|
||||
}
|
||||
|
||||
impl LoopState {
|
||||
fn is_inside(self) -> bool {
|
||||
matches!(self, LoopState::InLoop)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db> {
|
||||
// Builder state
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
module: &'db ParsedModule,
|
||||
scope_stack: Vec<FileScopeId>,
|
||||
scope_stack: Vec<(FileScopeId, LoopState)>,
|
||||
/// The assignments we're currently visiting, with
|
||||
/// the most recent visit at the end of the Vec
|
||||
current_assignments: Vec<CurrentAssignment<'db>>,
|
||||
@@ -103,9 +116,24 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
*self
|
||||
.scope_stack
|
||||
.last()
|
||||
.map(|(scope, _)| scope)
|
||||
.expect("Always to have a root scope")
|
||||
}
|
||||
|
||||
fn loop_state(&self) -> LoopState {
|
||||
self.scope_stack
|
||||
.last()
|
||||
.expect("Always to have a root scope")
|
||||
.1
|
||||
}
|
||||
|
||||
fn set_inside_loop(&mut self, state: LoopState) {
|
||||
self.scope_stack
|
||||
.last_mut()
|
||||
.expect("Always to have a root scope")
|
||||
.1 = state;
|
||||
}
|
||||
|
||||
fn push_scope(&mut self, node: NodeWithScopeRef) {
|
||||
let parent = self.current_scope();
|
||||
self.push_scope_with_parent(node, Some(parent));
|
||||
@@ -131,15 +159,16 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default());
|
||||
|
||||
self.scope_ids_by_scope.push(scope_id);
|
||||
self.scopes_by_node.insert(node.node_key(), file_scope_id);
|
||||
let previous = self.scopes_by_node.insert(node.node_key(), file_scope_id);
|
||||
debug_assert_eq!(previous, None);
|
||||
|
||||
debug_assert_eq!(ast_id_scope, file_scope_id);
|
||||
|
||||
self.scope_stack.push(file_scope_id);
|
||||
self.scope_stack.push((file_scope_id, LoopState::NotInLoop));
|
||||
}
|
||||
|
||||
fn pop_scope(&mut self) -> FileScopeId {
|
||||
let id = self.scope_stack.pop().expect("Root scope to be present");
|
||||
let (id, _) = self.scope_stack.pop().expect("Root scope to be present");
|
||||
let children_end = self.scopes.next_index();
|
||||
let scope = &mut self.scopes[id];
|
||||
scope.descendents = scope.descendents.start..children_end;
|
||||
@@ -589,6 +618,27 @@ where
|
||||
},
|
||||
);
|
||||
}
|
||||
ast::Stmt::TypeAlias(type_alias) => {
|
||||
let symbol = self.add_symbol(
|
||||
type_alias
|
||||
.name
|
||||
.as_name_expr()
|
||||
.map(|name| name.id.clone())
|
||||
.unwrap_or("<unknown>".into()),
|
||||
);
|
||||
self.add_definition(symbol, type_alias);
|
||||
self.visit_expr(&type_alias.name);
|
||||
|
||||
self.with_type_params(
|
||||
NodeWithScopeRef::TypeAliasTypeParameters(type_alias),
|
||||
type_alias.type_params.as_ref(),
|
||||
|builder| {
|
||||
builder.push_scope(NodeWithScopeRef::TypeAlias(type_alias));
|
||||
builder.visit_expr(&type_alias.value);
|
||||
builder.pop_scope()
|
||||
},
|
||||
);
|
||||
}
|
||||
ast::Stmt::Import(node) => {
|
||||
for alias in &node.names {
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
@@ -763,7 +813,10 @@ where
|
||||
|
||||
// TODO: definitions created inside the body should be fully visible
|
||||
// to other statements/expressions inside the body --Alex/Carl
|
||||
let outer_loop_state = self.loop_state();
|
||||
self.set_inside_loop(LoopState::InLoop);
|
||||
self.visit_body(body);
|
||||
self.set_inside_loop(outer_loop_state);
|
||||
|
||||
// Get the break states from the body of this loop, and restore the saved outer
|
||||
// ones.
|
||||
@@ -802,7 +855,9 @@ where
|
||||
self.visit_body(body);
|
||||
}
|
||||
ast::Stmt::Break(_) => {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
if self.loop_state().is_inside() {
|
||||
self.loop_break_states.push(self.flow_snapshot());
|
||||
}
|
||||
}
|
||||
|
||||
ast::Stmt::For(
|
||||
@@ -829,7 +884,10 @@ where
|
||||
// TODO: Definitions created by loop variables
|
||||
// (and definitions created inside the body)
|
||||
// are fully visible to other statements/expressions inside the body --Alex/Carl
|
||||
let outer_loop_state = self.loop_state();
|
||||
self.set_inside_loop(LoopState::InLoop);
|
||||
self.visit_body(body);
|
||||
self.set_inside_loop(outer_loop_state);
|
||||
|
||||
let break_states =
|
||||
std::mem::replace(&mut self.loop_break_states, saved_break_states);
|
||||
@@ -1131,8 +1189,8 @@ where
|
||||
// AST inspection, so we can't simplify here, need to record test expression for
|
||||
// later checking)
|
||||
self.visit_expr(test);
|
||||
let constraint = self.record_expression_constraint(test);
|
||||
let pre_if = self.flow_snapshot();
|
||||
let constraint = self.record_expression_constraint(test);
|
||||
self.visit_expr(body);
|
||||
let post_body = self.flow_snapshot();
|
||||
self.flow_restore(pre_if);
|
||||
|
||||
@@ -83,6 +83,7 @@ pub(crate) enum DefinitionNodeRef<'a> {
|
||||
For(ForStmtDefinitionNodeRef<'a>),
|
||||
Function(&'a ast::StmtFunctionDef),
|
||||
Class(&'a ast::StmtClassDef),
|
||||
TypeAlias(&'a ast::StmtTypeAlias),
|
||||
NamedExpression(&'a ast::ExprNamed),
|
||||
Assignment(AssignmentDefinitionNodeRef<'a>),
|
||||
AnnotatedAssignment(&'a ast::StmtAnnAssign),
|
||||
@@ -109,6 +110,12 @@ impl<'a> From<&'a ast::StmtClassDef> for DefinitionNodeRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtTypeAlias> for DefinitionNodeRef<'a> {
|
||||
fn from(node: &'a ast::StmtTypeAlias) -> Self {
|
||||
Self::TypeAlias(node)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::ExprNamed> for DefinitionNodeRef<'a> {
|
||||
fn from(node: &'a ast::ExprNamed) -> Self {
|
||||
Self::NamedExpression(node)
|
||||
@@ -265,6 +272,9 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
DefinitionNodeRef::Class(class) => {
|
||||
DefinitionKind::Class(AstNodeRef::new(parsed, class))
|
||||
}
|
||||
DefinitionNodeRef::TypeAlias(type_alias) => {
|
||||
DefinitionKind::TypeAlias(AstNodeRef::new(parsed, type_alias))
|
||||
}
|
||||
DefinitionNodeRef::NamedExpression(named) => {
|
||||
DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named))
|
||||
}
|
||||
@@ -358,6 +368,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
}
|
||||
Self::Function(node) => node.into(),
|
||||
Self::Class(node) => node.into(),
|
||||
Self::TypeAlias(node) => node.into(),
|
||||
Self::NamedExpression(node) => node.into(),
|
||||
Self::Assignment(AssignmentDefinitionNodeRef {
|
||||
value: _,
|
||||
@@ -434,6 +445,7 @@ pub enum DefinitionKind<'db> {
|
||||
ImportFrom(ImportFromDefinitionKind),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
Class(AstNodeRef<ast::StmtClassDef>),
|
||||
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
|
||||
NamedExpression(AstNodeRef<ast::ExprNamed>),
|
||||
Assignment(AssignmentDefinitionKind<'db>),
|
||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||
@@ -456,6 +468,7 @@ impl DefinitionKind<'_> {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
DefinitionKind::Function(_)
|
||||
| DefinitionKind::Class(_)
|
||||
| DefinitionKind::TypeAlias(_)
|
||||
| DefinitionKind::Import(_)
|
||||
| DefinitionKind::ImportFrom(_)
|
||||
| DefinitionKind::TypeVar(_)
|
||||
@@ -682,6 +695,12 @@ impl From<&ast::StmtClassDef> for DefinitionNodeKey {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::StmtTypeAlias> for DefinitionNodeKey {
|
||||
fn from(node: &ast::StmtTypeAlias) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::ExprName> for DefinitionNodeKey {
|
||||
fn from(node: &ast::ExprName) -> Self {
|
||||
Self(NodeKey::from_node(node))
|
||||
|
||||
@@ -116,14 +116,11 @@ impl<'db> ScopeId<'db> {
|
||||
// Type parameter scopes behave like function scopes in terms of name resolution; CPython
|
||||
// symbol table also uses the term "function-like" for these scopes.
|
||||
matches!(
|
||||
self.node(db),
|
||||
NodeWithScopeKind::ClassTypeParameters(_)
|
||||
| NodeWithScopeKind::FunctionTypeParameters(_)
|
||||
| NodeWithScopeKind::Function(_)
|
||||
| NodeWithScopeKind::ListComprehension(_)
|
||||
| NodeWithScopeKind::SetComprehension(_)
|
||||
| NodeWithScopeKind::DictComprehension(_)
|
||||
| NodeWithScopeKind::GeneratorExpression(_)
|
||||
self.node(db).scope_kind(),
|
||||
ScopeKind::Annotation
|
||||
| ScopeKind::Function
|
||||
| ScopeKind::TypeAlias
|
||||
| ScopeKind::Comprehension
|
||||
)
|
||||
}
|
||||
|
||||
@@ -144,6 +141,12 @@ impl<'db> ScopeId<'db> {
|
||||
}
|
||||
NodeWithScopeKind::Function(function)
|
||||
| NodeWithScopeKind::FunctionTypeParameters(function) => function.name.as_str(),
|
||||
NodeWithScopeKind::TypeAlias(type_alias)
|
||||
| NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => type_alias
|
||||
.name
|
||||
.as_name_expr()
|
||||
.map(|name| name.id.as_str())
|
||||
.unwrap_or("<type alias>"),
|
||||
NodeWithScopeKind::Lambda(_) => "<lambda>",
|
||||
NodeWithScopeKind::ListComprehension(_) => "<listcomp>",
|
||||
NodeWithScopeKind::SetComprehension(_) => "<setcomp>",
|
||||
@@ -201,6 +204,7 @@ pub enum ScopeKind {
|
||||
Class,
|
||||
Function,
|
||||
Comprehension,
|
||||
TypeAlias,
|
||||
}
|
||||
|
||||
impl ScopeKind {
|
||||
@@ -326,6 +330,8 @@ pub(crate) enum NodeWithScopeRef<'a> {
|
||||
Lambda(&'a ast::ExprLambda),
|
||||
FunctionTypeParameters(&'a ast::StmtFunctionDef),
|
||||
ClassTypeParameters(&'a ast::StmtClassDef),
|
||||
TypeAlias(&'a ast::StmtTypeAlias),
|
||||
TypeAliasTypeParameters(&'a ast::StmtTypeAlias),
|
||||
ListComprehension(&'a ast::ExprListComp),
|
||||
SetComprehension(&'a ast::ExprSetComp),
|
||||
DictComprehension(&'a ast::ExprDictComp),
|
||||
@@ -347,6 +353,12 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::Function(function) => {
|
||||
NodeWithScopeKind::Function(AstNodeRef::new(module, function))
|
||||
}
|
||||
NodeWithScopeRef::TypeAlias(type_alias) => {
|
||||
NodeWithScopeKind::TypeAlias(AstNodeRef::new(module, type_alias))
|
||||
}
|
||||
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
|
||||
NodeWithScopeKind::TypeAliasTypeParameters(AstNodeRef::new(module, type_alias))
|
||||
}
|
||||
NodeWithScopeRef::Lambda(lambda) => {
|
||||
NodeWithScopeKind::Lambda(AstNodeRef::new(module, lambda))
|
||||
}
|
||||
@@ -387,6 +399,12 @@ impl NodeWithScopeRef<'_> {
|
||||
NodeWithScopeRef::ClassTypeParameters(class) => {
|
||||
NodeWithScopeKey::ClassTypeParameters(NodeKey::from_node(class))
|
||||
}
|
||||
NodeWithScopeRef::TypeAlias(type_alias) => {
|
||||
NodeWithScopeKey::TypeAlias(NodeKey::from_node(type_alias))
|
||||
}
|
||||
NodeWithScopeRef::TypeAliasTypeParameters(type_alias) => {
|
||||
NodeWithScopeKey::TypeAliasTypeParameters(NodeKey::from_node(type_alias))
|
||||
}
|
||||
NodeWithScopeRef::ListComprehension(comprehension) => {
|
||||
NodeWithScopeKey::ListComprehension(NodeKey::from_node(comprehension))
|
||||
}
|
||||
@@ -411,6 +429,8 @@ pub enum NodeWithScopeKind {
|
||||
ClassTypeParameters(AstNodeRef<ast::StmtClassDef>),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
FunctionTypeParameters(AstNodeRef<ast::StmtFunctionDef>),
|
||||
TypeAliasTypeParameters(AstNodeRef<ast::StmtTypeAlias>),
|
||||
TypeAlias(AstNodeRef<ast::StmtTypeAlias>),
|
||||
Lambda(AstNodeRef<ast::ExprLambda>),
|
||||
ListComprehension(AstNodeRef<ast::ExprListComp>),
|
||||
SetComprehension(AstNodeRef<ast::ExprSetComp>),
|
||||
@@ -423,9 +443,11 @@ impl NodeWithScopeKind {
|
||||
match self {
|
||||
Self::Module => ScopeKind::Module,
|
||||
Self::Class(_) => ScopeKind::Class,
|
||||
Self::Function(_) => ScopeKind::Function,
|
||||
Self::Lambda(_) => ScopeKind::Function,
|
||||
Self::FunctionTypeParameters(_) | Self::ClassTypeParameters(_) => ScopeKind::Annotation,
|
||||
Self::Function(_) | Self::Lambda(_) => ScopeKind::Function,
|
||||
Self::FunctionTypeParameters(_)
|
||||
| Self::ClassTypeParameters(_)
|
||||
| Self::TypeAliasTypeParameters(_) => ScopeKind::Annotation,
|
||||
Self::TypeAlias(_) => ScopeKind::TypeAlias,
|
||||
Self::ListComprehension(_)
|
||||
| Self::SetComprehension(_)
|
||||
| Self::DictComprehension(_)
|
||||
@@ -446,6 +468,13 @@ impl NodeWithScopeKind {
|
||||
_ => panic!("expected function"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
|
||||
match self {
|
||||
Self::TypeAlias(type_alias) => type_alias.node(),
|
||||
_ => panic!("expected type alias"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
@@ -455,6 +484,8 @@ pub(crate) enum NodeWithScopeKey {
|
||||
ClassTypeParameters(NodeKey),
|
||||
Function(NodeKey),
|
||||
FunctionTypeParameters(NodeKey),
|
||||
TypeAlias(NodeKey),
|
||||
TypeAliasTypeParameters(NodeKey),
|
||||
Lambda(NodeKey),
|
||||
ListComprehension(NodeKey),
|
||||
SetComprehension(NodeKey),
|
||||
|
||||
@@ -324,6 +324,61 @@ fn declarations_ty<'db>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Meta data for `Type::Todo`, which represents a known limitation in red-knot.
|
||||
#[cfg(debug_assertions)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum TodoType {
|
||||
FileAndLine(&'static str, u32),
|
||||
Message(&'static str),
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
impl std::fmt::Display for TodoType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TodoType::FileAndLine(file, line) => write!(f, "[{file}:{line}]"),
|
||||
TodoType::Message(msg) => write!(f, "({msg})"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct TodoType;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
impl std::fmt::Display for TodoType {
|
||||
fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `Type::Todo` variant to represent a known limitation in the type system.
|
||||
///
|
||||
/// It can be used with a custom message (preferred): `todo_type!("PEP 604 not supported")`,
|
||||
/// or simply using `todo_type!()`, which will include information about the file and line.
|
||||
#[cfg(debug_assertions)]
|
||||
macro_rules! todo_type {
|
||||
() => {
|
||||
Type::Todo(crate::types::TodoType::FileAndLine(file!(), line!()))
|
||||
};
|
||||
($message:literal) => {
|
||||
Type::Todo(crate::types::TodoType::Message($message))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
macro_rules! todo_type {
|
||||
() => {
|
||||
Type::Todo(crate::types::TodoType)
|
||||
};
|
||||
($message:literal) => {
|
||||
Type::Todo(crate::types::TodoType)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use todo_type;
|
||||
|
||||
/// Representation of a type: a set of possible values at runtime.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, salsa::Update)]
|
||||
pub enum Type<'db> {
|
||||
@@ -340,7 +395,9 @@ pub enum Type<'db> {
|
||||
/// General rule: `Todo` should only propagate when the presence of the input `Todo` caused the
|
||||
/// output to be unknown. An output should only be `Todo` if fixing all `Todo` inputs to be not
|
||||
/// `Todo` would change the output type.
|
||||
Todo,
|
||||
///
|
||||
/// This variant should be created with the `todo_type!` macro.
|
||||
Todo(TodoType),
|
||||
/// The empty set of values
|
||||
Never,
|
||||
/// A specific function object
|
||||
@@ -384,7 +441,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
|
||||
pub const fn is_todo(&self) -> bool {
|
||||
matches!(self, Type::Todo)
|
||||
matches!(self, Type::Todo(_))
|
||||
}
|
||||
|
||||
pub const fn class_literal(class: Class<'db>) -> Self {
|
||||
@@ -480,6 +537,19 @@ impl<'db> Type<'db> {
|
||||
.expect("Expected a Type::IntLiteral variant")
|
||||
}
|
||||
|
||||
pub const fn into_known_instance(self) -> Option<KnownInstanceType<'db>> {
|
||||
match self {
|
||||
Type::KnownInstance(known_instance) => Some(known_instance),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn expect_known_instance(self) -> KnownInstanceType<'db> {
|
||||
self.into_known_instance()
|
||||
.expect("Expected a Type::KnownInstance variant")
|
||||
}
|
||||
|
||||
pub const fn is_boolean_literal(&self) -> bool {
|
||||
matches!(self, Type::BooleanLiteral(..))
|
||||
}
|
||||
@@ -530,8 +600,8 @@ impl<'db> Type<'db> {
|
||||
return true;
|
||||
}
|
||||
match (self, target) {
|
||||
(Type::Unknown | Type::Any | Type::Todo, _) => false,
|
||||
(_, Type::Unknown | Type::Any | Type::Todo) => false,
|
||||
(Type::Unknown | Type::Any | Type::Todo(_), _) => false,
|
||||
(_, Type::Unknown | Type::Any | Type::Todo(_)) => false,
|
||||
(Type::Never, _) => true,
|
||||
(_, Type::Never) => false,
|
||||
(_, Type::Instance(InstanceType { class }))
|
||||
@@ -666,8 +736,8 @@ impl<'db> Type<'db> {
|
||||
return true;
|
||||
}
|
||||
match (self, target) {
|
||||
(Type::Unknown | Type::Any | Type::Todo, _) => true,
|
||||
(_, Type::Unknown | Type::Any | Type::Todo) => true,
|
||||
(Type::Unknown | Type::Any | Type::Todo(_), _) => true,
|
||||
(_, Type::Unknown | Type::Any | Type::Todo(_)) => true,
|
||||
(Type::Union(union), ty) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
@@ -703,6 +773,7 @@ impl<'db> Type<'db> {
|
||||
// of `NoneType` and `NoDefaultType` in typeshed. This should not be required anymore once
|
||||
// we understand `sys.version_info` branches.
|
||||
self == other
|
||||
|| matches!((self, other), (Type::Todo(_), Type::Todo(_)))
|
||||
|| matches!((self, other),
|
||||
(
|
||||
Type::Instance(InstanceType { class: self_class }),
|
||||
@@ -726,7 +797,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
(Type::Any, _) | (_, Type::Any) => false,
|
||||
(Type::Unknown, _) | (_, Type::Unknown) => false,
|
||||
(Type::Todo, _) | (_, Type::Todo) => false,
|
||||
(Type::Todo(_), _) | (_, Type::Todo(_)) => false,
|
||||
|
||||
(Type::Union(union), other) | (other, Type::Union(union)) => union
|
||||
.elements(db)
|
||||
@@ -931,7 +1002,7 @@ impl<'db> Type<'db> {
|
||||
Type::Any
|
||||
| Type::Never
|
||||
| Type::Unknown
|
||||
| Type::Todo
|
||||
| Type::Todo(_)
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
@@ -1007,7 +1078,10 @@ impl<'db> Type<'db> {
|
||||
|
||||
Type::Instance(InstanceType { class }) => match class.known(db) {
|
||||
Some(
|
||||
KnownClass::NoneType | KnownClass::NoDefaultType | KnownClass::VersionInfo,
|
||||
KnownClass::NoneType
|
||||
| KnownClass::NoDefaultType
|
||||
| KnownClass::VersionInfo
|
||||
| KnownClass::TypeAliasType,
|
||||
) => true,
|
||||
Some(
|
||||
KnownClass::Bool
|
||||
@@ -1034,7 +1108,7 @@ impl<'db> Type<'db> {
|
||||
Type::Any
|
||||
| Type::Never
|
||||
| Type::Unknown
|
||||
| Type::Todo
|
||||
| Type::Todo(_)
|
||||
| Type::Union(..)
|
||||
| Type::Intersection(..)
|
||||
| Type::LiteralString => false,
|
||||
@@ -1052,12 +1126,12 @@ impl<'db> Type<'db> {
|
||||
Type::Any => Type::Any.into(),
|
||||
Type::Never => {
|
||||
// TODO: attribute lookup on Never type
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::Unknown => Type::Unknown.into(),
|
||||
Type::FunctionLiteral(_) => {
|
||||
// TODO: attribute lookup on function type
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::ModuleLiteral(file) => {
|
||||
// `__dict__` is a very special member that is never overridden by module globals;
|
||||
@@ -1107,7 +1181,7 @@ impl<'db> Type<'db> {
|
||||
Type::IntLiteral(Program::get(db).target_version(db).minor.into())
|
||||
}
|
||||
// TODO MRO? get_own_instance_member, get_instance_member
|
||||
_ => Type::Todo,
|
||||
_ => todo_type!("instance attributes"),
|
||||
};
|
||||
ty.into()
|
||||
}
|
||||
@@ -1149,36 +1223,36 @@ impl<'db> Type<'db> {
|
||||
Type::Intersection(_) => {
|
||||
// TODO perform the get_member on each type in the intersection
|
||||
// TODO return the intersection of those results
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::IntLiteral(_) => {
|
||||
// TODO raise error
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::BooleanLiteral(_) => Type::Todo.into(),
|
||||
Type::BooleanLiteral(_) => todo_type!().into(),
|
||||
Type::StringLiteral(_) => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::LiteralString => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::BytesLiteral(_) => {
|
||||
// TODO defer to Type::Instance(<bytes from typeshed>).member
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::SliceLiteral(_) => {
|
||||
// TODO defer to `builtins.slice` methods
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::Tuple(_) => {
|
||||
// TODO: implement tuple methods
|
||||
Type::Todo.into()
|
||||
todo_type!().into()
|
||||
}
|
||||
Type::Todo => Type::Todo.into(),
|
||||
&todo @ Type::Todo(_) => todo.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1188,7 +1262,7 @@ impl<'db> Type<'db> {
|
||||
/// when `bool(x)` is called on an object `x`.
|
||||
fn bool(&self, db: &'db dyn Db) -> Truthiness {
|
||||
match self {
|
||||
Type::Any | Type::Todo | Type::Never | Type::Unknown => Truthiness::Ambiguous,
|
||||
Type::Any | Type::Todo(_) | Type::Never | Type::Unknown => Truthiness::Ambiguous,
|
||||
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
|
||||
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
|
||||
Type::ClassLiteral(_) => {
|
||||
@@ -1329,7 +1403,7 @@ impl<'db> Type<'db> {
|
||||
// `Any` is callable, and its return type is also `Any`.
|
||||
Type::Any => CallOutcome::callable(Type::Any),
|
||||
|
||||
Type::Todo => CallOutcome::callable(Type::Todo),
|
||||
Type::Todo(_) => CallOutcome::callable(todo_type!()),
|
||||
|
||||
Type::Unknown => CallOutcome::callable(Type::Unknown),
|
||||
|
||||
@@ -1342,7 +1416,7 @@ impl<'db> Type<'db> {
|
||||
),
|
||||
|
||||
// TODO: intersection types
|
||||
Type::Intersection(_) => CallOutcome::callable(Type::Todo),
|
||||
Type::Intersection(_) => CallOutcome::callable(todo_type!()),
|
||||
|
||||
_ => CallOutcome::not_callable(self),
|
||||
}
|
||||
@@ -1381,7 +1455,7 @@ impl<'db> Type<'db> {
|
||||
};
|
||||
}
|
||||
|
||||
if matches!(self, Type::Unknown | Type::Any | Type::Todo) {
|
||||
if matches!(self, Type::Unknown | Type::Any | Type::Todo(_)) {
|
||||
// Explicit handling of `Unknown` and `Any` necessary until `type[Unknown]` and
|
||||
// `type[Any]` are not defined as `Todo` anymore.
|
||||
return IterationOutcome::Iterable { element_ty: self };
|
||||
@@ -1440,14 +1514,14 @@ impl<'db> Type<'db> {
|
||||
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Type::Any => Type::Any,
|
||||
Type::Todo => Type::Todo,
|
||||
todo @ Type::Todo(_) => *todo,
|
||||
Type::Unknown => Type::Unknown,
|
||||
Type::Never => Type::Never,
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Type::instance(*class),
|
||||
Type::SubclassOf(SubclassOfType { class }) => Type::instance(*class),
|
||||
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
|
||||
// TODO: we can probably do better here: --Alex
|
||||
Type::Intersection(_) => Type::Todo,
|
||||
Type::Intersection(_) => todo_type!(),
|
||||
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
|
||||
// since they already indicate that the object is an instance of some kind:
|
||||
Type::BooleanLiteral(_)
|
||||
@@ -1478,7 +1552,11 @@ impl<'db> Type<'db> {
|
||||
Type::Unknown => Type::Unknown,
|
||||
// TODO map this to a new `Type::TypeVar` variant
|
||||
Type::KnownInstance(KnownInstanceType::TypeVar(_)) => *self,
|
||||
_ => Type::Todo,
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(alias)) => alias.value_ty(db),
|
||||
Type::KnownInstance(KnownInstanceType::Never | KnownInstanceType::NoReturn) => {
|
||||
Type::Never
|
||||
}
|
||||
_ => todo_type!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1553,8 +1631,8 @@ impl<'db> Type<'db> {
|
||||
// TODO: `type[Unknown]`?
|
||||
Type::Unknown => Type::Unknown,
|
||||
// TODO intersections
|
||||
Type::Intersection(_) => Type::Todo,
|
||||
Type::Todo => Type::Todo,
|
||||
Type::Intersection(_) => todo_type!(),
|
||||
todo @ Type::Todo(_) => *todo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1642,6 +1720,7 @@ pub enum KnownClass {
|
||||
// Typing
|
||||
SpecialForm,
|
||||
TypeVar,
|
||||
TypeAliasType,
|
||||
NoDefaultType,
|
||||
// sys
|
||||
VersionInfo,
|
||||
@@ -1668,6 +1747,7 @@ impl<'db> KnownClass {
|
||||
Self::NoneType => "NoneType",
|
||||
Self::SpecialForm => "_SpecialForm",
|
||||
Self::TypeVar => "TypeVar",
|
||||
Self::TypeAliasType => "TypeAliasType",
|
||||
Self::NoDefaultType => "_NoDefaultType",
|
||||
// This is the name the type of `sys.version_info` has in typeshed,
|
||||
// which is different to what `type(sys.version_info).__name__` is at runtime.
|
||||
@@ -1706,7 +1786,7 @@ impl<'db> KnownClass {
|
||||
Self::VersionInfo => CoreStdlibModule::Sys,
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType => CoreStdlibModule::Types,
|
||||
Self::NoneType => CoreStdlibModule::Typeshed,
|
||||
Self::SpecialForm | Self::TypeVar => CoreStdlibModule::Typing,
|
||||
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType => CoreStdlibModule::Typing,
|
||||
// TODO when we understand sys.version_info, we will need an explicit fallback here,
|
||||
// because typing_extensions has a 3.13+ re-export for the `typing.NoDefault`
|
||||
// singleton, but not for `typing._NoDefaultType`
|
||||
@@ -1720,7 +1800,7 @@ impl<'db> KnownClass {
|
||||
const fn is_singleton(self) -> bool {
|
||||
// TODO there are other singleton types (EllipsisType, NotImplementedType)
|
||||
match self {
|
||||
Self::NoneType | Self::NoDefaultType | Self::VersionInfo => true,
|
||||
Self::NoneType | Self::NoDefaultType | Self::VersionInfo | Self::TypeAliasType => true,
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
@@ -1762,6 +1842,7 @@ impl<'db> KnownClass {
|
||||
"NoneType" => Self::NoneType,
|
||||
"ModuleType" => Self::ModuleType,
|
||||
"FunctionType" => Self::FunctionType,
|
||||
"TypeAliasType" => Self::TypeAliasType,
|
||||
"_SpecialForm" => Self::SpecialForm,
|
||||
"_NoDefaultType" => Self::NoDefaultType,
|
||||
"_version_info" => Self::VersionInfo,
|
||||
@@ -1795,7 +1876,7 @@ impl<'db> KnownClass {
|
||||
| Self::VersionInfo
|
||||
| Self::FunctionType => module.name() == self.canonical_module().as_str(),
|
||||
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
|
||||
Self::SpecialForm | Self::TypeVar | Self::NoDefaultType => {
|
||||
Self::SpecialForm | Self::TypeVar | Self::TypeAliasType | Self::NoDefaultType => {
|
||||
matches!(module.name().as_str(), "typing" | "typing_extensions")
|
||||
}
|
||||
}
|
||||
@@ -1809,24 +1890,42 @@ pub enum KnownInstanceType<'db> {
|
||||
Literal,
|
||||
/// The symbol `typing.Optional` (which can also be found as `typing_extensions.Optional`)
|
||||
Optional,
|
||||
/// The symbol `typing.Union` (which can also be found as `typing_extensions.Union`)
|
||||
Union,
|
||||
/// The symbol `typing.NoReturn` (which can also be found as `typing_extensions.NoReturn`)
|
||||
NoReturn,
|
||||
/// The symbol `typing.Never` available since 3.11 (which can also be found as `typing_extensions.Never`)
|
||||
Never,
|
||||
/// A single instance of `typing.TypeVar`
|
||||
TypeVar(TypeVarInstance<'db>),
|
||||
/// A single instance of `typing.TypeAliasType` (PEP 695 type alias)
|
||||
TypeAliasType(TypeAliasType<'db>),
|
||||
// TODO: fill this enum out with more special forms, etc.
|
||||
}
|
||||
|
||||
impl<'db> KnownInstanceType<'db> {
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
KnownInstanceType::Literal => "Literal",
|
||||
KnownInstanceType::Optional => "Optional",
|
||||
KnownInstanceType::TypeVar(_) => "TypeVar",
|
||||
Self::Literal => "Literal",
|
||||
Self::Optional => "Optional",
|
||||
Self::Union => "Union",
|
||||
Self::TypeVar(_) => "TypeVar",
|
||||
Self::NoReturn => "NoReturn",
|
||||
Self::Never => "Never",
|
||||
Self::TypeAliasType(_) => "TypeAliasType",
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate the known instance in boolean context
|
||||
pub const fn bool(self) -> Truthiness {
|
||||
match self {
|
||||
Self::Literal | Self::Optional | Self::TypeVar(_) => Truthiness::AlwaysTrue,
|
||||
Self::Literal
|
||||
| Self::Optional
|
||||
| Self::TypeVar(_)
|
||||
| Self::Union
|
||||
| Self::NoReturn
|
||||
| Self::Never
|
||||
| Self::TypeAliasType(_) => Truthiness::AlwaysTrue,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1835,7 +1934,11 @@ impl<'db> KnownInstanceType<'db> {
|
||||
match self {
|
||||
Self::Literal => "typing.Literal",
|
||||
Self::Optional => "typing.Optional",
|
||||
Self::Union => "typing.Union",
|
||||
Self::NoReturn => "typing.NoReturn",
|
||||
Self::Never => "typing.Never",
|
||||
Self::TypeVar(typevar) => typevar.name(db),
|
||||
Self::TypeAliasType(_) => "typing.TypeAliasType",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1844,7 +1947,11 @@ impl<'db> KnownInstanceType<'db> {
|
||||
match self {
|
||||
Self::Literal => KnownClass::SpecialForm,
|
||||
Self::Optional => KnownClass::SpecialForm,
|
||||
Self::Union => KnownClass::SpecialForm,
|
||||
Self::NoReturn => KnownClass::SpecialForm,
|
||||
Self::Never => KnownClass::SpecialForm,
|
||||
Self::TypeVar(_) => KnownClass::TypeVar,
|
||||
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1864,6 +1971,9 @@ impl<'db> KnownInstanceType<'db> {
|
||||
match (module.name().as_str(), instance_name) {
|
||||
("typing" | "typing_extensions", "Literal") => Some(Self::Literal),
|
||||
("typing" | "typing_extensions", "Optional") => Some(Self::Optional),
|
||||
("typing" | "typing_extensions", "Union") => Some(Self::Union),
|
||||
("typing" | "typing_extensions", "NoReturn") => Some(Self::NoReturn),
|
||||
("typing" | "typing_extensions", "Never") => Some(Self::Never),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1871,23 +1981,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
fn member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
let ty = match (self, name) {
|
||||
(Self::TypeVar(typevar), "__name__") => Type::string_literal(db, typevar.name(db)),
|
||||
(Self::TypeVar(typevar), "__bound__") => typevar
|
||||
.upper_bound(db)
|
||||
.map(|ty| ty.to_meta_type(db))
|
||||
.unwrap_or_else(|| KnownClass::NoneType.to_instance(db)),
|
||||
(Self::TypeVar(typevar), "__constraints__") => {
|
||||
let tuple_elements: Vec<Type<'db>> = typevar
|
||||
.constraints(db)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|ty| ty.to_meta_type(db))
|
||||
.collect();
|
||||
Type::tuple(db, &tuple_elements)
|
||||
}
|
||||
(Self::TypeVar(typevar), "__default__") => typevar
|
||||
.default_ty(db)
|
||||
.map(|ty| ty.to_meta_type(db))
|
||||
.unwrap_or_else(|| KnownClass::NoDefaultType.to_instance(db)),
|
||||
(Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)),
|
||||
_ => return self.instance_fallback(db).member(db, name),
|
||||
};
|
||||
ty.into()
|
||||
@@ -1919,6 +2013,7 @@ pub struct TypeVarInstance<'db> {
|
||||
}
|
||||
|
||||
impl<'db> TypeVarInstance<'db> {
|
||||
#[allow(unused)]
|
||||
pub(crate) fn upper_bound(self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
if let Some(TypeVarBoundOrConstraints::UpperBound(ty)) = self.bound_or_constraints(db) {
|
||||
Some(ty)
|
||||
@@ -1927,6 +2022,7 @@ impl<'db> TypeVarInstance<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn constraints(self, db: &'db dyn Db) -> Option<&[Type<'db>]> {
|
||||
if let Some(TypeVarBoundOrConstraints::Constraints(tuple)) = self.bound_or_constraints(db) {
|
||||
Some(tuple.elements(db))
|
||||
@@ -2599,7 +2695,7 @@ impl<'db> Class<'db> {
|
||||
// TODO: If the metaclass is not a class, we should verify that it's a callable
|
||||
// which accepts the same arguments as `type.__new__` (otherwise error), and return
|
||||
// the meta-type of its return type. (And validate that is a class type?)
|
||||
return Ok(Type::Todo);
|
||||
return Ok(todo_type!("metaclass not a class"));
|
||||
};
|
||||
|
||||
// Reconcile all base classes' metaclasses with the candidate metaclass.
|
||||
@@ -2713,6 +2809,27 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct TypeAliasType<'db> {
|
||||
#[return_ref]
|
||||
pub name: ast::name::Name,
|
||||
|
||||
rhs_scope: ScopeId<'db>,
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
impl<'db> TypeAliasType<'db> {
|
||||
#[salsa::tracked]
|
||||
pub fn value_ty(self, db: &'db dyn Db) -> Type<'db> {
|
||||
let scope = self.rhs_scope(db);
|
||||
|
||||
let type_alias_stmt_node = scope.node(db).expect_type_alias();
|
||||
let definition = semantic_index(db, scope.file(db)).definition(type_alias_stmt_node);
|
||||
|
||||
definition_expression_ty(db, definition, &type_alias_stmt_node.value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(super) struct MetaclassCandidate<'db> {
|
||||
@@ -2899,6 +3016,11 @@ impl<'db> TupleType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure that the `Type` enum does not grow unexpectedly.
|
||||
#[cfg(not(debug_assertions))]
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
static_assertions::assert_eq_size!(Type, [u8; 16]);
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
@@ -2914,13 +3036,6 @@ pub(crate) mod tests {
|
||||
use ruff_python_ast as ast;
|
||||
use test_case::test_case;
|
||||
|
||||
#[cfg(target_pointer_width = "64")]
|
||||
#[test]
|
||||
fn no_bloat_enum_sizes() {
|
||||
use std::mem::size_of;
|
||||
assert_eq!(size_of::<Type>(), 16);
|
||||
}
|
||||
|
||||
pub(crate) fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
@@ -2974,7 +3089,7 @@ pub(crate) mod tests {
|
||||
Ty::Unknown => Type::Unknown,
|
||||
Ty::None => Type::none(db),
|
||||
Ty::Any => Type::Any,
|
||||
Ty::Todo => Type::Todo,
|
||||
Ty::Todo => todo_type!("Ty::Todo"),
|
||||
Ty::IntLiteral(n) => Type::IntLiteral(n),
|
||||
Ty::StringLiteral(s) => Type::string_literal(db, s),
|
||||
Ty::BooleanLiteral(b) => Type::BooleanLiteral(b),
|
||||
@@ -3565,4 +3680,95 @@ pub(crate) mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_alias_types() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/mod.py",
|
||||
r#"
|
||||
type Alias1 = int
|
||||
type Alias2 = int
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let mod_py = system_path_to_file(&db, "src/mod.py")?;
|
||||
let ty_alias1 = global_symbol(&db, mod_py, "Alias1").expect_type();
|
||||
let ty_alias2 = global_symbol(&db, mod_py, "Alias2").expect_type();
|
||||
|
||||
let Type::KnownInstance(KnownInstanceType::TypeAliasType(alias1)) = ty_alias1 else {
|
||||
panic!("Expected TypeAliasType, got {ty_alias1:?}");
|
||||
};
|
||||
assert_eq!(alias1.name(&db), "Alias1");
|
||||
assert_eq!(alias1.value_ty(&db), KnownClass::Int.to_instance(&db));
|
||||
|
||||
// Two type aliases are distinct and disjoint, even if they refer to the same type
|
||||
assert!(!ty_alias1.is_equivalent_to(&db, ty_alias2));
|
||||
assert!(ty_alias1.is_disjoint_from(&db, ty_alias2));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// All other tests also make sure that `Type::Todo` works as expected. This particular
|
||||
/// test makes sure that we handle `Todo` types correctly, even if they originate from
|
||||
/// different sources.
|
||||
#[test]
|
||||
fn todo_types() {
|
||||
let db = setup_db();
|
||||
|
||||
let todo1 = todo_type!("1");
|
||||
let todo2 = todo_type!("2");
|
||||
let todo3 = todo_type!();
|
||||
let todo4 = todo_type!();
|
||||
|
||||
assert!(todo1.is_equivalent_to(&db, todo2));
|
||||
assert!(todo3.is_equivalent_to(&db, todo4));
|
||||
assert!(todo1.is_equivalent_to(&db, todo3));
|
||||
|
||||
assert!(todo1.is_subtype_of(&db, todo2));
|
||||
assert!(todo2.is_subtype_of(&db, todo1));
|
||||
|
||||
assert!(todo3.is_subtype_of(&db, todo4));
|
||||
assert!(todo4.is_subtype_of(&db, todo3));
|
||||
|
||||
assert!(todo1.is_subtype_of(&db, todo3));
|
||||
assert!(todo3.is_subtype_of(&db, todo1));
|
||||
|
||||
let int = KnownClass::Int.to_instance(&db);
|
||||
|
||||
assert!(int.is_assignable_to(&db, todo1));
|
||||
assert!(int.is_assignable_to(&db, todo3));
|
||||
|
||||
assert!(todo1.is_assignable_to(&db, int));
|
||||
assert!(todo3.is_assignable_to(&db, int));
|
||||
|
||||
// We lose information when combining several `Todo` types. This is an
|
||||
// acknowledged limitation of the current implementation. We can not
|
||||
// easily store the meta information of several `Todo`s in a single
|
||||
// variant, as `TodoType` needs to implement `Copy`, meaning it can't
|
||||
// contain `Vec`/`Box`/etc., and can't be boxed itself.
|
||||
//
|
||||
// Lifting this restriction would require us to intern `TodoType` in
|
||||
// salsa, but that would mean we would have to pass in `db` everywhere.
|
||||
|
||||
// A union of several `Todo` types collapses to a single `Todo` type:
|
||||
assert!(UnionType::from_elements(&db, vec![todo1, todo2, todo3, todo4]).is_todo());
|
||||
|
||||
// And similar for intersection types:
|
||||
assert!(IntersectionBuilder::new(&db)
|
||||
.add_positive(todo1)
|
||||
.add_positive(todo2)
|
||||
.add_positive(todo3)
|
||||
.add_positive(todo4)
|
||||
.build()
|
||||
.is_todo());
|
||||
assert!(IntersectionBuilder::new(&db)
|
||||
.add_positive(todo1)
|
||||
.add_negative(todo2)
|
||||
.add_positive(todo3)
|
||||
.add_negative(todo4)
|
||||
.build()
|
||||
.is_todo());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: SmallVec<[Type<'db>; 1]>,
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db) -> Self {
|
||||
Self {
|
||||
db,
|
||||
elements: SmallVec::new(),
|
||||
elements: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,22 +48,13 @@ impl<'db> UnionBuilder<'db> {
|
||||
match ty {
|
||||
Type::Union(union) => {
|
||||
let new_elements = union.elements(self.db);
|
||||
if self.elements.is_empty() {
|
||||
self.elements.extend_from_slice(new_elements);
|
||||
} else {
|
||||
self.elements.reserve(new_elements.len());
|
||||
for element in new_elements {
|
||||
self = self.add(*element);
|
||||
}
|
||||
self.elements.reserve(new_elements.len());
|
||||
for element in new_elements {
|
||||
self = self.add(*element);
|
||||
}
|
||||
}
|
||||
Type::Never => {}
|
||||
_ => {
|
||||
if self.elements.is_empty() {
|
||||
self.elements.push(ty);
|
||||
return self;
|
||||
}
|
||||
|
||||
let bool_pair = if let Type::BooleanLiteral(b) = ty {
|
||||
Some(Type::BooleanLiteral(!b))
|
||||
} else {
|
||||
@@ -318,7 +309,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
self.add_positive(db, *neg);
|
||||
}
|
||||
}
|
||||
ty @ (Type::Any | Type::Unknown | Type::Todo) => {
|
||||
ty @ (Type::Any | Type::Unknown | Type::Todo(_)) => {
|
||||
// Adding any of these types to the negative side of an intersection
|
||||
// is equivalent to adding it to the positive side. We do this to
|
||||
// simplify the representation.
|
||||
@@ -388,7 +379,7 @@ mod tests {
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::stdlib::typing_symbol;
|
||||
use crate::types::{global_symbol, KnownClass, UnionBuilder};
|
||||
use crate::types::{global_symbol, todo_type, KnownClass, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
@@ -996,7 +987,7 @@ mod tests {
|
||||
|
||||
#[test_case(Type::Any)]
|
||||
#[test_case(Type::Unknown)]
|
||||
#[test_case(Type::Todo)]
|
||||
#[test_case(todo_type!())]
|
||||
fn build_intersection_t_and_negative_t_does_not_simplify(ty: Type) {
|
||||
let db = setup_db();
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ impl Display for DisplayRepresentation<'_> {
|
||||
}
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
Type::Todo => f.write_str("@Todo"),
|
||||
Type::Todo(todo) => write!(f, "@Todo{todo}"),
|
||||
Type::ModuleLiteral(file) => {
|
||||
write!(f, "<module '{:?}'>", file.path(self.db))
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ use std::num::NonZeroU32;
|
||||
use itertools::Itertools;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_python_ast::visitor::{self, Visitor};
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef, Expr, ExprContext, UnaryOp};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use salsa;
|
||||
@@ -52,11 +53,12 @@ use crate::types::diagnostic::{TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder
|
||||
use crate::types::mro::MroErrorKind;
|
||||
use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, typing_extensions_symbol,
|
||||
Boundness, Class, ClassLiteralType, FunctionType, InstanceType, IntersectionBuilder,
|
||||
IntersectionType, IterationOutcome, KnownClass, KnownFunction, KnownInstanceType,
|
||||
MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol, Truthiness, TupleType, Type,
|
||||
TypeArrayDisplay, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
|
||||
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, todo_type,
|
||||
typing_extensions_symbol, Boundness, Class, ClassLiteralType, FunctionType, InstanceType,
|
||||
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
|
||||
KnownInstanceType, MetaclassCandidate, MetaclassErrorKind, SliceLiteralType, Symbol,
|
||||
Truthiness, TupleType, Type, TypeAliasType, TypeArrayDisplay, TypeVarBoundOrConstraints,
|
||||
TypeVarInstance, UnionBuilder, UnionType,
|
||||
};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
@@ -438,6 +440,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
NodeWithScopeKind::FunctionTypeParameters(function) => {
|
||||
self.infer_function_type_params(function.node());
|
||||
}
|
||||
NodeWithScopeKind::TypeAliasTypeParameters(type_alias) => {
|
||||
self.infer_type_alias_type_params(type_alias.node());
|
||||
}
|
||||
NodeWithScopeKind::TypeAlias(type_alias) => {
|
||||
self.infer_type_alias(type_alias.node());
|
||||
}
|
||||
NodeWithScopeKind::ListComprehension(comprehension) => {
|
||||
self.infer_list_comprehension_expression_scope(comprehension.node());
|
||||
}
|
||||
@@ -605,6 +613,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_function_definition(function.node(), definition);
|
||||
}
|
||||
DefinitionKind::Class(class) => self.infer_class_definition(class.node(), definition),
|
||||
DefinitionKind::TypeAlias(type_alias) => {
|
||||
self.infer_type_alias_definition(type_alias.node(), definition);
|
||||
}
|
||||
DefinitionKind::Import(import) => {
|
||||
self.infer_import_definition(import.node(), definition);
|
||||
}
|
||||
@@ -847,6 +858,19 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_parameters(&function.parameters);
|
||||
}
|
||||
|
||||
fn infer_type_alias_type_params(&mut self, type_alias: &ast::StmtTypeAlias) {
|
||||
let type_params = type_alias
|
||||
.type_params
|
||||
.as_ref()
|
||||
.expect("type alias type params scope without type params");
|
||||
|
||||
self.infer_type_parameters(type_params);
|
||||
}
|
||||
|
||||
fn infer_type_alias(&mut self, type_alias: &ast::StmtTypeAlias) {
|
||||
self.infer_annotation_expression(&type_alias.value, DeferredExpressionState::Deferred);
|
||||
}
|
||||
|
||||
fn infer_function_body(&mut self, function: &ast::StmtFunctionDef) {
|
||||
self.infer_body(&function.body);
|
||||
}
|
||||
@@ -1027,7 +1051,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) {
|
||||
// TODO(dhruvmanila): Annotation expression is resolved at the enclosing scope, infer the
|
||||
// parameter type from there
|
||||
let annotated_ty = Type::Todo;
|
||||
let annotated_ty = todo_type!("function parameter type");
|
||||
if parameter.annotation.is_some() {
|
||||
self.add_declaration_with_binding(
|
||||
parameter.into(),
|
||||
@@ -1107,6 +1131,33 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_type_alias_definition(
|
||||
&mut self,
|
||||
type_alias: &ast::StmtTypeAlias,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
self.infer_expression(&type_alias.name);
|
||||
|
||||
let rhs_scope = self
|
||||
.index
|
||||
.node_scope(NodeWithScopeRef::TypeAlias(type_alias))
|
||||
.to_scope_id(self.db, self.file);
|
||||
|
||||
let type_alias_ty =
|
||||
Type::KnownInstance(KnownInstanceType::TypeAliasType(TypeAliasType::new(
|
||||
self.db,
|
||||
&type_alias.name.as_name_expr().unwrap().id,
|
||||
rhs_scope,
|
||||
)));
|
||||
|
||||
self.add_declaration_with_binding(
|
||||
type_alias.into(),
|
||||
definition,
|
||||
type_alias_ty,
|
||||
type_alias_ty,
|
||||
);
|
||||
}
|
||||
|
||||
fn infer_if_statement(&mut self, if_statement: &ast::StmtIf) {
|
||||
let ast::StmtIf {
|
||||
range: _,
|
||||
@@ -1236,7 +1287,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
) -> Type<'db> {
|
||||
// TODO: Handle async with statements (they use `aenter` and `aexit`)
|
||||
if is_async {
|
||||
return Type::Todo;
|
||||
return todo_type!("async with statement");
|
||||
}
|
||||
|
||||
let context_manager_ty = context_expression_ty.to_meta_type(self.db);
|
||||
@@ -1386,12 +1437,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.db,
|
||||
tuple.elements(self.db).iter().map(|ty| {
|
||||
ty.into_class_literal()
|
||||
.map_or(Type::Todo, |ClassLiteralType { class }| {
|
||||
.map_or(todo_type!(), |ClassLiteralType { class }| {
|
||||
Type::instance(class)
|
||||
})
|
||||
}),
|
||||
),
|
||||
_ => Type::Todo,
|
||||
_ => todo_type!("exception type"),
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1461,7 +1512,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
default,
|
||||
} = node;
|
||||
self.infer_optional_expression(default.as_deref());
|
||||
self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo);
|
||||
self.add_declaration_with_binding(node.into(), definition, todo_type!(), todo_type!());
|
||||
}
|
||||
|
||||
fn infer_typevartuple_definition(
|
||||
@@ -1475,7 +1526,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
default,
|
||||
} = node;
|
||||
self.infer_optional_expression(default.as_deref());
|
||||
self.add_declaration_with_binding(node.into(), definition, Type::Todo, Type::Todo);
|
||||
self.add_declaration_with_binding(node.into(), definition, todo_type!(), todo_type!());
|
||||
}
|
||||
|
||||
fn infer_match_statement(&mut self, match_statement: &ast::StmtMatch) {
|
||||
@@ -1510,7 +1561,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// against the subject expression type (which we can query via `infer_expression_types`)
|
||||
// and extract the type at the `index` position if the pattern matches. This will be
|
||||
// similar to the logic in `self.infer_assignment_definition`.
|
||||
self.add_binding(pattern.into(), definition, Type::Todo);
|
||||
self.add_binding(pattern.into(), definition, todo_type!());
|
||||
}
|
||||
|
||||
fn infer_match_pattern(&mut self, pattern: &ast::Pattern) {
|
||||
@@ -1829,17 +1880,8 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_augmented_op(assignment, target_type, value_type)
|
||||
}
|
||||
|
||||
fn infer_type_alias_statement(&mut self, type_alias_statement: &ast::StmtTypeAlias) {
|
||||
let ast::StmtTypeAlias {
|
||||
range: _,
|
||||
name,
|
||||
type_params: _,
|
||||
value,
|
||||
} = type_alias_statement;
|
||||
self.infer_expression(value);
|
||||
self.infer_expression(name);
|
||||
|
||||
// TODO: properly handle generic type aliases, which need their own annotation scope
|
||||
fn infer_type_alias_statement(&mut self, node: &ast::StmtTypeAlias) {
|
||||
self.infer_definition(node);
|
||||
}
|
||||
|
||||
fn infer_for_statement(&mut self, for_statement: &ast::StmtFor) {
|
||||
@@ -1874,8 +1916,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let iterable_ty = self.infer_standalone_expression(iterable);
|
||||
|
||||
let loop_var_value_ty = if is_async {
|
||||
// TODO(Alex): async iterables/iterators!
|
||||
Type::Todo
|
||||
todo_type!("async iterables/iterators")
|
||||
} else {
|
||||
iterable_ty
|
||||
.iterate(self.db)
|
||||
@@ -2203,7 +2244,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
ast::Expr::Await(await_expression) => self.infer_await_expression(await_expression),
|
||||
ast::Expr::IpyEscapeCommand(_) => {
|
||||
// TODO Implement Ipy escape command support
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2397,7 +2438,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
// TODO generator type
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_list_comprehension_expression(&mut self, listcomp: &ast::ExprListComp) -> Type<'db> {
|
||||
@@ -2410,7 +2451,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
// TODO list type
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_dict_comprehension_expression(&mut self, dictcomp: &ast::ExprDictComp) -> Type<'db> {
|
||||
@@ -2424,7 +2465,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
// TODO dict type
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_set_comprehension_expression(&mut self, setcomp: &ast::ExprSetComp) -> Type<'db> {
|
||||
@@ -2437,7 +2478,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_first_comprehension_iter(generators);
|
||||
|
||||
// TODO set type
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_generator_expression_scope(&mut self, generator: &ast::ExprGenerator) {
|
||||
@@ -2552,7 +2593,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
let target_ty = if is_async {
|
||||
// TODO: async iterables/iterators! -- Alex
|
||||
Type::Todo
|
||||
todo_type!("async iterables/iterators")
|
||||
} else {
|
||||
iterable_ty
|
||||
.iterate(self.db)
|
||||
@@ -2642,7 +2683,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
// TODO function type
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_call_expression(&mut self, call_expression: &ast::ExprCall) -> Type<'db> {
|
||||
@@ -2673,7 +2714,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics);
|
||||
|
||||
// TODO
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_yield_expression(&mut self, yield_expression: &ast::ExprYield) -> Type<'db> {
|
||||
@@ -2682,7 +2723,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_optional_expression(value.as_deref());
|
||||
|
||||
// TODO awaitable type
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_yield_from_expression(&mut self, yield_from: &ast::ExprYieldFrom) -> Type<'db> {
|
||||
@@ -2694,7 +2735,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.unwrap_with_diagnostic(value.as_ref().into(), &mut self.diagnostics);
|
||||
|
||||
// TODO get type from `ReturnType` of generator
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
fn infer_await_expression(&mut self, await_expression: &ast::ExprAwait) -> Type<'db> {
|
||||
@@ -2703,7 +2744,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_expression(value);
|
||||
|
||||
// TODO awaitable type
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
/// Look up a name reference that isn't bound in the local scope.
|
||||
@@ -2979,7 +3020,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Type::Unknown
|
||||
}
|
||||
}
|
||||
_ => Type::Todo, // TODO other unary op types
|
||||
_ => todo_type!(), // TODO other unary op types
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3227,7 +3268,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
(left, Type::BooleanLiteral(bool_value), op) => {
|
||||
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
|
||||
}
|
||||
_ => Some(Type::Todo), // TODO
|
||||
_ => Some(todo_type!()), // TODO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3665,7 +3706,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
).expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
|
||||
|
||||
match eq_result {
|
||||
Type::Todo => return Ok(Type::Todo),
|
||||
todo @ Type::Todo(_) => return Ok(todo),
|
||||
ty => match ty.bool(self.db) {
|
||||
Truthiness::AlwaysTrue => eq_count += 1,
|
||||
Truthiness::AlwaysFalse => not_eq_count += 1,
|
||||
@@ -3690,7 +3731,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
|
||||
Ok(match eq_result {
|
||||
Type::Todo => Type::Todo,
|
||||
todo @ Type::Todo(_) => todo,
|
||||
ty => match ty.bool(self.db) {
|
||||
Truthiness::AlwaysFalse => Type::BooleanLiteral(op.is_is_not()),
|
||||
_ => KnownClass::Bool.to_instance(self.db),
|
||||
@@ -3745,7 +3786,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO: handle more types
|
||||
_ => match op {
|
||||
ast::CmpOp::Is | ast::CmpOp::IsNot => Ok(KnownClass::Bool.to_instance(self.db)),
|
||||
_ => Ok(Type::Todo),
|
||||
_ => Ok(todo_type!()),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -3773,7 +3814,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
match pairwise_eq_result {
|
||||
// If propagation is required, return the result as is
|
||||
Type::Todo => return Ok(Type::Todo),
|
||||
todo @ Type::Todo(_) => return Ok(todo),
|
||||
ty => match ty.bool(self.db) {
|
||||
// - AlwaysTrue : Continue to the next pair for lexicographic comparison
|
||||
Truthiness::AlwaysTrue => continue,
|
||||
@@ -4174,24 +4215,6 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// Annotation expressions also get special handling for `*args` and `**kwargs`.
|
||||
ast::Expr::Starred(starred) => self.infer_starred_expression(starred),
|
||||
|
||||
ast::Expr::BytesLiteral(bytes) => {
|
||||
self.diagnostics.add(
|
||||
bytes.into(),
|
||||
"annotation-byte-string",
|
||||
format_args!("Type expressions cannot use bytes literal"),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
|
||||
ast::Expr::FString(fstring) => {
|
||||
self.diagnostics.add(
|
||||
fstring.into(),
|
||||
"annotation-f-string",
|
||||
format_args!("Type expressions cannot use f-strings"),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
|
||||
// All other annotation expressions are (possibly) valid type expressions, so handle
|
||||
// them there instead.
|
||||
type_expr => self.infer_type_expression_no_store(type_expr),
|
||||
@@ -4202,6 +4225,72 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
annotation_ty
|
||||
}
|
||||
|
||||
/// Walk child expressions of the given AST node and store the given type for each of them.
|
||||
/// Does not store a type for the root expression. Resets to normal type inference for all
|
||||
/// expressions with a nested scope (lambda, named expression, comprehensions, generators).
|
||||
fn store_type_for_sub_expressions_of(&mut self, expression: &ast::Expr, ty: Type<'db>) {
|
||||
struct StoreTypeVisitor<'a, 'db> {
|
||||
builder: &'a mut TypeInferenceBuilder<'db>,
|
||||
ty: Type<'db>,
|
||||
is_root_node: bool,
|
||||
}
|
||||
|
||||
impl<'a, 'db> StoreTypeVisitor<'a, 'db> {
|
||||
fn new(builder: &'a mut TypeInferenceBuilder<'db>, ty: Type<'db>) -> Self {
|
||||
Self {
|
||||
builder,
|
||||
ty,
|
||||
is_root_node: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn store(&mut self, expr: &ast::Expr) {
|
||||
if self.is_root_node {
|
||||
self.is_root_node = false;
|
||||
} else {
|
||||
self.builder.store_expression_type(expr, self.ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'db, 'ast> Visitor<'ast> for StoreTypeVisitor<'a, 'db> {
|
||||
fn visit_expr(&mut self, expr: &'ast Expr) {
|
||||
match expr {
|
||||
ast::Expr::Lambda(lambda) => {
|
||||
self.builder.infer_lambda_expression(lambda);
|
||||
self.store(expr);
|
||||
}
|
||||
ast::Expr::Named(named) => {
|
||||
self.builder.infer_named_expression(named);
|
||||
self.store(expr);
|
||||
}
|
||||
ast::Expr::ListComp(list_comp) => {
|
||||
self.builder.infer_list_comprehension_expression(list_comp);
|
||||
self.store(expr);
|
||||
}
|
||||
ast::Expr::SetComp(set_comp) => {
|
||||
self.builder.infer_set_comprehension_expression(set_comp);
|
||||
self.store(expr);
|
||||
}
|
||||
ast::Expr::DictComp(dict_comp) => {
|
||||
self.builder.infer_dict_comprehension_expression(dict_comp);
|
||||
self.store(expr);
|
||||
}
|
||||
ast::Expr::Generator(generator) => {
|
||||
self.builder.infer_generator_expression(generator);
|
||||
self.store(expr);
|
||||
}
|
||||
_ => {
|
||||
self.store(expr);
|
||||
visitor::walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StoreTypeVisitor::new(self, ty).visit_expr(expression);
|
||||
}
|
||||
|
||||
/// Infer the type of a string annotation expression.
|
||||
fn infer_string_annotation_expression(&mut self, string: &ast::ExprStringLiteral) -> Type<'db> {
|
||||
match parse_string_annotation(self.db, self.file, string) {
|
||||
@@ -4263,7 +4352,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_name_expression(name).in_type_expression(self.db)
|
||||
}
|
||||
ast::ExprContext::Invalid => Type::Unknown,
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo,
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
|
||||
},
|
||||
|
||||
ast::Expr::Attribute(attribute_expression) => match attribute_expression.ctx {
|
||||
@@ -4271,7 +4360,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.infer_attribute_expression(attribute_expression)
|
||||
.in_type_expression(self.db),
|
||||
ast::ExprContext::Invalid => Type::Unknown,
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => Type::Todo,
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!(),
|
||||
},
|
||||
|
||||
ast::Expr::NoneLiteral(_literal) => Type::none(self.db),
|
||||
@@ -4281,14 +4370,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
// TODO: an Ellipsis literal *on its own* does not have any meaning in annotation
|
||||
// expressions, but is meaningful in the context of a number of special forms.
|
||||
ast::Expr::EllipsisLiteral(_literal) => Type::Todo,
|
||||
|
||||
// Other literals do not have meaningful values in the annotation expression context.
|
||||
// However, we will we want to handle these differently when working with special forms,
|
||||
// since (e.g.) `123` is not valid in an annotation expression but `Literal[123]` is.
|
||||
ast::Expr::BytesLiteral(_literal) => Type::Todo,
|
||||
ast::Expr::NumberLiteral(_literal) => Type::Todo,
|
||||
ast::Expr::BooleanLiteral(_literal) => Type::Todo,
|
||||
ast::Expr::EllipsisLiteral(_literal) => todo_type!(),
|
||||
|
||||
ast::Expr::Subscript(subscript) => {
|
||||
let ast::ExprSubscript {
|
||||
@@ -4331,94 +4413,69 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO PEP 646
|
||||
ast::Expr::Starred(starred) => {
|
||||
self.infer_starred_expression(starred);
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
|
||||
// Forms which are invalid in the context of annotation expressions: we infer their
|
||||
// nested expressions as normal expressions, but the type of the top-level expression is
|
||||
// always `Type::Unknown` in these cases.
|
||||
ast::Expr::BoolOp(bool_op) => {
|
||||
self.infer_boolean_expression(bool_op);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Named(named) => {
|
||||
self.infer_named_expression(named);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::UnaryOp(unary) => {
|
||||
self.infer_unary_expression(unary);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Lambda(lambda_expression) => {
|
||||
self.infer_lambda_expression(lambda_expression);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::If(if_expression) => {
|
||||
self.infer_if_expression(if_expression);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Dict(dict) => {
|
||||
self.infer_dict_expression(dict);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Set(set) => {
|
||||
self.infer_set_expression(set);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::ListComp(listcomp) => {
|
||||
self.infer_list_comprehension_expression(listcomp);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::SetComp(setcomp) => {
|
||||
self.infer_set_comprehension_expression(setcomp);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::DictComp(dictcomp) => {
|
||||
self.infer_dict_comprehension_expression(dictcomp);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Generator(generator) => {
|
||||
self.infer_generator_expression(generator);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Await(await_expression) => {
|
||||
self.infer_await_expression(await_expression);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Yield(yield_expression) => {
|
||||
self.infer_yield_expression(yield_expression);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::YieldFrom(yield_from) => {
|
||||
self.infer_yield_from_expression(yield_from);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Compare(compare) => {
|
||||
self.infer_compare_expression(compare);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Call(call_expr) => {
|
||||
self.infer_call_expression(call_expr);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::FString(fstring) => {
|
||||
self.infer_fstring_expression(fstring);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::List(list) => {
|
||||
self.infer_list_expression(list);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Tuple(tuple) => {
|
||||
self.infer_tuple_expression(tuple);
|
||||
Type::Unknown
|
||||
}
|
||||
ast::Expr::Slice(slice) => {
|
||||
self.infer_slice_expression(slice);
|
||||
Type::Unknown
|
||||
}
|
||||
// Avoid inferring the types of invalid type expressions that have been parsed from a
|
||||
// string annotation, as they are not present in the semantic index.
|
||||
_ if self.deferred_state.in_string_annotation() => Type::Unknown,
|
||||
|
||||
ast::Expr::IpyEscapeCommand(_) => todo!("Implement Ipy escape command support"),
|
||||
// Forms which are invalid in the context of annotation expressions: we store a type of
|
||||
// `Type::Unknown` for these expressions (and their sub-expressions) to avoid problems
|
||||
// with invalid-syntax examples like `x: f"{x}"` or `x: lambda y: x`, for which we can
|
||||
// not infer a meaningful type for the inner `x` expression. The top-level expression
|
||||
// is also `Type::Unknown` in these cases.
|
||||
ast::Expr::BoolOp(_)
|
||||
| ast::Expr::Named(_)
|
||||
| ast::Expr::UnaryOp(_)
|
||||
| ast::Expr::Lambda(_)
|
||||
| ast::Expr::If(_)
|
||||
| ast::Expr::Dict(_)
|
||||
| ast::Expr::Set(_)
|
||||
| ast::Expr::ListComp(_)
|
||||
| ast::Expr::SetComp(_)
|
||||
| ast::Expr::DictComp(_)
|
||||
| ast::Expr::Generator(_)
|
||||
| ast::Expr::Await(_)
|
||||
| ast::Expr::Yield(_)
|
||||
| ast::Expr::YieldFrom(_)
|
||||
| ast::Expr::Compare(_)
|
||||
| ast::Expr::Call(_)
|
||||
| ast::Expr::FString(_)
|
||||
| ast::Expr::List(_)
|
||||
| ast::Expr::Tuple(_)
|
||||
| ast::Expr::Slice(_)
|
||||
| ast::Expr::IpyEscapeCommand(_)
|
||||
| ast::Expr::BytesLiteral(_)
|
||||
| ast::Expr::NumberLiteral(_)
|
||||
| ast::Expr::BooleanLiteral(_) => {
|
||||
match expression {
|
||||
ast::Expr::BytesLiteral(bytes) => {
|
||||
self.diagnostics.add(
|
||||
bytes.into(),
|
||||
"annotation-byte-string",
|
||||
format_args!("Type expressions cannot use bytes literal"),
|
||||
);
|
||||
}
|
||||
ast::Expr::FString(fstring) => {
|
||||
self.diagnostics.add(
|
||||
fstring.into(),
|
||||
"annotation-f-string",
|
||||
format_args!("Type expressions cannot use f-strings"),
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
self.diagnostics.add(
|
||||
expression.into(),
|
||||
"annotation-with-invalid-expression",
|
||||
format_args!("Invalid expression in type expression"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.store_type_for_sub_expressions_of(expression, Type::Unknown);
|
||||
Type::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4476,7 +4533,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
let ty = if return_todo {
|
||||
Type::Todo
|
||||
todo_type!("full tuple[...] support")
|
||||
} else {
|
||||
Type::tuple(self.db, &element_types)
|
||||
};
|
||||
@@ -4491,7 +4548,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
single_element => {
|
||||
let single_element_ty = self.infer_type_expression(single_element);
|
||||
if element_could_alter_type_of_whole_tuple(single_element, single_element_ty) {
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
} else {
|
||||
Type::tuple(self.db, &[single_element_ty])
|
||||
}
|
||||
@@ -4507,13 +4564,13 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if let Some(ClassLiteralType { class }) = name_ty.into_class_literal() {
|
||||
Type::subclass_of(class)
|
||||
} else {
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
}
|
||||
// TODO: attributes, unions, subscripts, etc.
|
||||
_ => {
|
||||
self.infer_type_expression(slice);
|
||||
Type::Todo
|
||||
todo_type!()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4532,20 +4589,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
match value_ty {
|
||||
Type::KnownInstance(known_instance) => {
|
||||
self.infer_parameterized_known_instance_type_expression(known_instance, slice)
|
||||
self.infer_parameterized_known_instance_type_expression(subscript, known_instance)
|
||||
}
|
||||
_ => {
|
||||
self.infer_type_expression(slice);
|
||||
Type::Todo // TODO: generics
|
||||
todo_type!("generics")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_parameterized_known_instance_type_expression(
|
||||
&mut self,
|
||||
subscript: &ast::ExprSubscript,
|
||||
known_instance: KnownInstanceType,
|
||||
parameters: &ast::Expr,
|
||||
) -> Type<'db> {
|
||||
let parameters = &*subscript.slice;
|
||||
match known_instance {
|
||||
KnownInstanceType::Literal => match self.infer_literal_parameter_type(parameters) {
|
||||
Ok(ty) => ty,
|
||||
@@ -4567,7 +4625,36 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
let param_type = self.infer_type_expression(parameters);
|
||||
UnionType::from_elements(self.db, [param_type, Type::none(self.db)])
|
||||
}
|
||||
KnownInstanceType::TypeVar(_) => Type::Todo,
|
||||
KnownInstanceType::Union => match parameters {
|
||||
ast::Expr::Tuple(t) => {
|
||||
let union_ty = UnionType::from_elements(
|
||||
self.db,
|
||||
t.iter().map(|elt| self.infer_type_expression(elt)),
|
||||
);
|
||||
self.store_expression_type(parameters, union_ty);
|
||||
union_ty
|
||||
}
|
||||
_ => self.infer_type_expression(parameters),
|
||||
},
|
||||
KnownInstanceType::TypeVar(_) => {
|
||||
self.infer_type_expression(parameters);
|
||||
todo_type!()
|
||||
}
|
||||
KnownInstanceType::TypeAliasType(_) => {
|
||||
self.infer_type_expression(parameters);
|
||||
todo_type!("generic type alias")
|
||||
}
|
||||
KnownInstanceType::NoReturn | KnownInstanceType::Never => {
|
||||
self.diagnostics.add(
|
||||
subscript.into(),
|
||||
"invalid-type-parameter",
|
||||
format_args!(
|
||||
"Type `{}` expected no type parameter",
|
||||
known_instance.repr(self.db)
|
||||
),
|
||||
);
|
||||
Type::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4935,8 +5022,8 @@ fn perform_membership_test_comparison<'db>(
|
||||
|
||||
compare_result_opt
|
||||
.map(|ty| {
|
||||
if matches!(ty, Type::Todo) {
|
||||
return Type::Todo;
|
||||
if matches!(ty, Type::Todo(_)) {
|
||||
return ty;
|
||||
}
|
||||
|
||||
match op {
|
||||
@@ -5907,7 +5994,17 @@ mod tests {
|
||||
|
||||
// We currently return `Todo` for all async comprehensions,
|
||||
// including comprehensions that have invalid syntax
|
||||
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "x", "@Todo");
|
||||
assert_scope_ty(
|
||||
&db,
|
||||
"src/a.py",
|
||||
&["foo", "<listcomp>"],
|
||||
"x",
|
||||
if cfg!(debug_assertions) {
|
||||
"@Todo(async iterables/iterators)"
|
||||
} else {
|
||||
"@Todo"
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5931,7 +6028,17 @@ mod tests {
|
||||
)?;
|
||||
|
||||
// TODO async iterables/iterators! --Alex
|
||||
assert_scope_ty(&db, "src/a.py", &["foo", "<listcomp>"], "x", "@Todo");
|
||||
assert_scope_ty(
|
||||
&db,
|
||||
"src/a.py",
|
||||
&["foo", "<listcomp>"],
|
||||
"x",
|
||||
if cfg!(debug_assertions) {
|
||||
"@Todo(async iterables/iterators)"
|
||||
} else {
|
||||
"@Todo"
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -5965,6 +6072,72 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pep695_type_params() {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
"
|
||||
def f[T, U: A, V: (A, B), W = A, X: A = A1, Y: (int,)]():
|
||||
pass
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class A1(A): ...
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let check_typevar = |var: &'static str,
|
||||
upper_bound: Option<&'static str>,
|
||||
constraints: Option<&[&'static str]>,
|
||||
default: Option<&'static str>| {
|
||||
let var_ty = get_symbol(&db, "src/a.py", &["f"], var).expect_type();
|
||||
assert_eq!(var_ty.display(&db).to_string(), var);
|
||||
|
||||
let expected_name_ty = format!(r#"Literal["{var}"]"#);
|
||||
let name_ty = var_ty.member(&db, "__name__").expect_type();
|
||||
assert_eq!(name_ty.display(&db).to_string(), expected_name_ty);
|
||||
|
||||
let KnownInstanceType::TypeVar(typevar) = var_ty.expect_known_instance() else {
|
||||
panic!("expected TypeVar");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
typevar
|
||||
.upper_bound(&db)
|
||||
.map(|ty| ty.display(&db).to_string()),
|
||||
upper_bound.map(std::borrow::ToOwned::to_owned)
|
||||
);
|
||||
assert_eq!(
|
||||
typevar.constraints(&db).map(|tys| tys
|
||||
.iter()
|
||||
.map(|ty| ty.display(&db).to_string())
|
||||
.collect::<Vec<_>>()),
|
||||
constraints.map(|strings| strings
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect::<Vec<_>>())
|
||||
);
|
||||
assert_eq!(
|
||||
typevar
|
||||
.default_ty(&db)
|
||||
.map(|ty| ty.display(&db).to_string()),
|
||||
default.map(std::borrow::ToOwned::to_owned)
|
||||
);
|
||||
};
|
||||
|
||||
check_typevar("T", None, None, None);
|
||||
check_typevar("U", Some("A"), None, None);
|
||||
check_typevar("V", None, Some(&["A", "B"]), None);
|
||||
check_typevar("W", None, None, Some("A"));
|
||||
check_typevar("X", Some("A"), None, Some("A1"));
|
||||
|
||||
// a typevar with less than two constraints is treated as unconstrained
|
||||
check_typevar("Y", None, None, None);
|
||||
}
|
||||
|
||||
// Incremental inference tests
|
||||
|
||||
fn first_public_binding<'db>(db: &'db TestDb, file: File, name: &str) -> Definition<'db> {
|
||||
|
||||
@@ -5,7 +5,7 @@ use itertools::Either;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type};
|
||||
use crate::Db;
|
||||
use crate::{types::todo_type, Db};
|
||||
|
||||
/// The inferred method resolution order of a given class.
|
||||
///
|
||||
@@ -354,7 +354,7 @@ impl<'db> ClassBase<'db> {
|
||||
match ty {
|
||||
Type::Any => Some(Self::Any),
|
||||
Type::Unknown => Some(Self::Unknown),
|
||||
Type::Todo => Some(Self::Todo),
|
||||
Type::Todo(_) => Some(Self::Todo),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Some(Self::Class(class)),
|
||||
Type::Union(_) => None, // TODO -- forces consideration of multiple possible MROs?
|
||||
Type::Intersection(_) => None, // TODO -- probably incorrect?
|
||||
@@ -372,7 +372,11 @@ impl<'db> ClassBase<'db> {
|
||||
| Type::SubclassOf(_) => None,
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::TypeVar(_)
|
||||
| KnownInstanceType::TypeAliasType(_)
|
||||
| KnownInstanceType::Literal
|
||||
| KnownInstanceType::Union
|
||||
| KnownInstanceType::NoReturn
|
||||
| KnownInstanceType::Never
|
||||
| KnownInstanceType::Optional => None,
|
||||
},
|
||||
}
|
||||
@@ -405,7 +409,7 @@ impl<'db> From<ClassBase<'db>> for Type<'db> {
|
||||
fn from(value: ClassBase<'db>) -> Self {
|
||||
match value {
|
||||
ClassBase::Any => Type::Any,
|
||||
ClassBase::Todo => Type::Todo,
|
||||
ClassBase::Todo => todo_type!(),
|
||||
ClassBase::Unknown => Type::Unknown,
|
||||
ClassBase::Class(class) => Type::class_literal(class),
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#![allow(dead_code)]
|
||||
use super::{definition_expression_ty, Type};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::Db;
|
||||
use crate::{semantic_index::definition::Definition, types::todo_type};
|
||||
use ruff_python_ast::{self as ast, name::Name};
|
||||
|
||||
/// A typed callable signature.
|
||||
@@ -18,7 +18,7 @@ impl<'db> Signature<'db> {
|
||||
pub(crate) fn todo() -> Self {
|
||||
Self {
|
||||
parameters: Parameters::todo(),
|
||||
return_ty: Type::Todo,
|
||||
return_ty: todo_type!("return type"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,7 @@ impl<'db> Signature<'db> {
|
||||
.as_ref()
|
||||
.map(|returns| {
|
||||
if function_node.is_async {
|
||||
// TODO: generic `types.CoroutineType`!
|
||||
Type::Todo
|
||||
todo_type!("generic types.CoroutineType")
|
||||
} else {
|
||||
definition_expression_ty(db, definition, returns.as_ref())
|
||||
}
|
||||
@@ -81,11 +80,11 @@ impl<'db> Parameters<'db> {
|
||||
Self {
|
||||
variadic: Some(Parameter {
|
||||
name: Some(Name::new_static("args")),
|
||||
annotated_ty: Type::Todo,
|
||||
annotated_ty: todo_type!(),
|
||||
}),
|
||||
keywords: Some(Parameter {
|
||||
name: Some(Name::new_static("kwargs")),
|
||||
annotated_ty: Type::Todo,
|
||||
annotated_ty: todo_type!(),
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::types::{todo_type, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::Db;
|
||||
|
||||
/// Unpacks the value expression type to their respective targets.
|
||||
@@ -59,7 +59,7 @@ impl<'db> Unpacker<'db> {
|
||||
// TODO: Combine the types into a list type. If the
|
||||
// starred_element_types is empty, then it should be `List[Any]`.
|
||||
// combine_types(starred_element_types);
|
||||
element_types.push(Type::Todo);
|
||||
element_types.push(todo_type!("starred unpacking"));
|
||||
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
@@ -72,7 +72,7 @@ impl<'db> Unpacker<'db> {
|
||||
// index.
|
||||
element_types.resize(elts.len() - 1, Type::Unknown);
|
||||
// TODO: This should be `list[Unknown]`
|
||||
element_types.insert(starred_index, Type::Todo);
|
||||
element_types.insert(starred_index, todo_type!("starred unpacking"));
|
||||
Cow::Owned(element_types)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -180,6 +180,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Discard `@Todo`-type metadata from expected types, which is not available
|
||||
/// when running in release mode.
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn discard_todo_metadata(ty: &str) -> std::borrow::Cow<'_, str> {
|
||||
static TODO_METADATA_REGEX: std::sync::LazyLock<regex::Regex> =
|
||||
std::sync::LazyLock::new(|| regex::Regex::new(r"@Todo\([^)]*\)").unwrap());
|
||||
|
||||
TODO_METADATA_REGEX.replace_all(ty, "@Todo")
|
||||
}
|
||||
|
||||
struct Matcher {
|
||||
line_index: LineIndex,
|
||||
source: SourceText,
|
||||
@@ -276,6 +286,9 @@ impl Matcher {
|
||||
}
|
||||
}
|
||||
Assertion::Revealed(expected_type) => {
|
||||
#[cfg(not(debug_assertions))]
|
||||
let expected_type = discard_todo_metadata(&expected_type);
|
||||
|
||||
let mut matched_revealed_type = None;
|
||||
let mut matched_undefined_reveal = None;
|
||||
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
while True:
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
break
|
||||
@@ -0,0 +1,6 @@
|
||||
while True:
|
||||
|
||||
def b():
|
||||
x: int
|
||||
|
||||
break
|
||||
@@ -0,0 +1,6 @@
|
||||
for _ in range(1):
|
||||
|
||||
class A:
|
||||
x: int
|
||||
|
||||
break
|
||||
@@ -0,0 +1,6 @@
|
||||
for _ in range(1):
|
||||
|
||||
def b():
|
||||
x: int
|
||||
|
||||
break
|
||||
@@ -0,0 +1 @@
|
||||
../../../../ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py
|
||||
@@ -0,0 +1 @@
|
||||
x: f"Literal[{1 + 2}]" = 3
|
||||
@@ -0,0 +1,3 @@
|
||||
from typing import Union
|
||||
|
||||
x: Union[int, str] = 1
|
||||
@@ -264,26 +264,12 @@ 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)] = &[
|
||||
// Probably related to missing support for type aliases / type params:
|
||||
("crates/ruff_python_parser/resources/inline/err/type_param_invalid_bound_expr.py", true, true),
|
||||
("crates/ruff_python_parser/resources/inline/err/type_param_param_spec_invalid_default_expr.py", true, true),
|
||||
("crates/ruff_python_parser/resources/inline/err/type_param_type_var_invalid_default_expr.py", true, true),
|
||||
("crates/ruff_python_parser/resources/inline/err/type_param_type_var_missing_default.py", true, true),
|
||||
("crates/ruff_python_parser/resources/inline/err/type_param_type_var_tuple_invalid_default_expr.py", true, true),
|
||||
("crates/ruff_python_parser/resources/inline/ok/type_param_param_spec.py", true, true),
|
||||
("crates/ruff_python_parser/resources/inline/ok/type_param_type_var_tuple.py", true, true),
|
||||
("crates/ruff_python_parser/resources/inline/ok/type_param_type_var.py", true, true),
|
||||
("crates/ruff_python_parser/resources/valid/statement/type.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TCH004_15.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F401_19.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_14.py", false, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_15.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_17.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_20.py", true, true),
|
||||
// related to circular references in class definitions
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false),
|
||||
// Fails for unknown reasons:
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F632.py", true, true),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false),
|
||||
("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false),
|
||||
// related to circular references in type aliases (salsa cycle panic):
|
||||
("crates/ruff_python_parser/resources/inline/err/type_alias_invalid_value_expr.py", true, true),
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.7.4"
|
||||
version = "0.8.0"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -1966,3 +1966,75 @@ fn nested_implicit_namespace_package() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flake8_import_convention_invalid_aliases_config_alias_name() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint.flake8-import-conventions.aliases]
|
||||
"module.name" = "invalid.alias"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
, @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Failed to parse [TMP]/ruff.toml
|
||||
Cause: TOML parse error at line 2, column 2
|
||||
|
|
||||
2 | [lint.flake8-import-conventions.aliases]
|
||||
| ^^^^
|
||||
invalid value: string "invalid.alias", expected a Python identifier
|
||||
"###);});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flake8_import_convention_invalid_aliases_config_module_name() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint.flake8-import-conventions.aliases]
|
||||
"module..invalid" = "alias"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
, @r###"
|
||||
success: false
|
||||
exit_code: 2
|
||||
----- stdout -----
|
||||
|
||||
----- stderr -----
|
||||
ruff failed
|
||||
Cause: Failed to parse [TMP]/ruff.toml
|
||||
Cause: TOML parse error at line 2, column 2
|
||||
|
|
||||
2 | [lint.flake8-import-conventions.aliases]
|
||||
| ^^^^
|
||||
invalid value: string "module..invalid", expected a sequence of Python identifiers delimited by periods
|
||||
"###);});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -27,13 +27,17 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
// We don't support `*` imports yet:
|
||||
"error[unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
|
||||
// We don't support terminal statements in control flow yet:
|
||||
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:57:71 Invalid expression in type expression",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
|
||||
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:69:66 Invalid expression in type expression",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
|
||||
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:145:27 Invalid expression in type expression",
|
||||
"error[annotation-with-invalid-expression] /src/tomllib/_parser.py:196:25 Invalid expression in type expression",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use super::{write, Arguments, FormatElement};
|
||||
use crate::format_element::Interned;
|
||||
use crate::prelude::LineMode;
|
||||
use crate::prelude::{LineMode, Tag};
|
||||
use crate::{FormatResult, FormatState};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::fmt::Debug;
|
||||
use std::num::NonZeroUsize;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
/// A trait for writing or formatting into [`FormatElement`]-accepting buffers or streams.
|
||||
@@ -294,10 +295,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// A Buffer that removes any soft line breaks.
|
||||
/// A Buffer that removes any soft line breaks or [`if_group_breaks`](crate::builders::if_group_breaks) elements.
|
||||
///
|
||||
/// - Removes [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::Soft).
|
||||
/// - Replaces [`lines`](FormatElement::Line) with the mode [`Soft`](LineMode::SoftOrSpace) with a [`Space`](FormatElement::Space)
|
||||
/// - Removes [`if_group_breaks`](crate::builders::if_group_breaks) elements.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -350,6 +352,8 @@ pub struct RemoveSoftLinesBuffer<'a, Context> {
|
||||
/// It's fine to not snapshot the cache. The worst that can happen is that it holds on interned elements
|
||||
/// that are now unused. But there's little harm in that and the cache is cleaned when dropping the buffer.
|
||||
interned_cache: FxHashMap<Interned, Interned>,
|
||||
|
||||
state: RemoveSoftLineBreaksState,
|
||||
}
|
||||
|
||||
impl<'a, Context> RemoveSoftLinesBuffer<'a, Context> {
|
||||
@@ -357,6 +361,7 @@ impl<'a, Context> RemoveSoftLinesBuffer<'a, Context> {
|
||||
pub fn new(inner: &'a mut dyn Buffer<Context = Context>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
state: RemoveSoftLineBreaksState::default(),
|
||||
interned_cache: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
@@ -375,6 +380,8 @@ fn clean_interned(
|
||||
if let Some(cleaned) = interned_cache.get(interned) {
|
||||
cleaned.clone()
|
||||
} else {
|
||||
let mut state = RemoveSoftLineBreaksState::default();
|
||||
|
||||
// Find the first soft line break element or interned element that must be changed
|
||||
let result = interned
|
||||
.iter()
|
||||
@@ -382,8 +389,9 @@ fn clean_interned(
|
||||
.find_map(|(index, element)| match element {
|
||||
FormatElement::Line(LineMode::Soft | LineMode::SoftOrSpace) => {
|
||||
let mut cleaned = Vec::new();
|
||||
cleaned.extend_from_slice(&interned[..index]);
|
||||
Some((cleaned, &interned[index..]))
|
||||
let (before, after) = interned.split_at(index);
|
||||
cleaned.extend_from_slice(before);
|
||||
Some((cleaned, &after[1..]))
|
||||
}
|
||||
FormatElement::Interned(inner) => {
|
||||
let cleaned_inner = clean_interned(inner, interned_cache);
|
||||
@@ -398,19 +406,33 @@ fn clean_interned(
|
||||
}
|
||||
}
|
||||
|
||||
_ => None,
|
||||
element => {
|
||||
if state.should_drop(element) {
|
||||
let mut cleaned = Vec::new();
|
||||
let (before, after) = interned.split_at(index);
|
||||
cleaned.extend_from_slice(before);
|
||||
Some((cleaned, &after[1..]))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let result = match result {
|
||||
// Copy the whole interned buffer so that becomes possible to change the necessary elements.
|
||||
Some((mut cleaned, rest)) => {
|
||||
for element in rest {
|
||||
if state.should_drop(element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let element = match element {
|
||||
FormatElement::Line(LineMode::Soft) => continue,
|
||||
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
|
||||
FormatElement::Interned(interned) => {
|
||||
FormatElement::Interned(clean_interned(interned, interned_cache))
|
||||
}
|
||||
|
||||
element => element.clone(),
|
||||
};
|
||||
cleaned.push(element);
|
||||
@@ -431,12 +453,17 @@ impl<Context> Buffer for RemoveSoftLinesBuffer<'_, Context> {
|
||||
type Context = Context;
|
||||
|
||||
fn write_element(&mut self, element: FormatElement) {
|
||||
if self.state.should_drop(&element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let element = match element {
|
||||
FormatElement::Line(LineMode::Soft) => return,
|
||||
FormatElement::Line(LineMode::SoftOrSpace) => FormatElement::Space,
|
||||
FormatElement::Interned(interned) => {
|
||||
FormatElement::Interned(self.clean_interned(&interned))
|
||||
}
|
||||
|
||||
element => element,
|
||||
};
|
||||
|
||||
@@ -456,14 +483,77 @@ impl<Context> Buffer for RemoveSoftLinesBuffer<'_, Context> {
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> BufferSnapshot {
|
||||
self.inner.snapshot()
|
||||
BufferSnapshot::Any(Box::new(RemoveSoftLinebreaksSnapshot {
|
||||
inner: self.inner.snapshot(),
|
||||
state: self.state,
|
||||
}))
|
||||
}
|
||||
|
||||
fn restore_snapshot(&mut self, snapshot: BufferSnapshot) {
|
||||
self.inner.restore_snapshot(snapshot);
|
||||
let RemoveSoftLinebreaksSnapshot { inner, state } = snapshot.unwrap_any();
|
||||
self.inner.restore_snapshot(inner);
|
||||
self.state = state;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
enum RemoveSoftLineBreaksState {
|
||||
#[default]
|
||||
Default,
|
||||
InIfGroupBreaks {
|
||||
conditional_content_level: NonZeroUsize,
|
||||
},
|
||||
}
|
||||
|
||||
impl RemoveSoftLineBreaksState {
|
||||
fn should_drop(&mut self, element: &FormatElement) -> bool {
|
||||
match self {
|
||||
Self::Default => {
|
||||
// Entered the start of an `if_group_breaks`
|
||||
if let FormatElement::Tag(Tag::StartConditionalContent(condition)) = element {
|
||||
if condition.mode.is_expanded() {
|
||||
*self = Self::InIfGroupBreaks {
|
||||
conditional_content_level: NonZeroUsize::new(1).unwrap(),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
Self::InIfGroupBreaks {
|
||||
conditional_content_level,
|
||||
} => {
|
||||
match element {
|
||||
// A nested `if_group_breaks` or `if_group_fits`
|
||||
FormatElement::Tag(Tag::StartConditionalContent(_)) => {
|
||||
*conditional_content_level = conditional_content_level.saturating_add(1);
|
||||
}
|
||||
// The end of an `if_group_breaks` or `if_group_fits`.
|
||||
FormatElement::Tag(Tag::EndConditionalContent) => {
|
||||
if let Some(level) = NonZeroUsize::new(conditional_content_level.get() - 1)
|
||||
{
|
||||
*conditional_content_level = level;
|
||||
} else {
|
||||
// Found the end tag of the initial `if_group_breaks`. Skip this element but retain
|
||||
// the elements coming after
|
||||
*self = RemoveSoftLineBreaksState::Default;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RemoveSoftLinebreaksSnapshot {
|
||||
inner: BufferSnapshot,
|
||||
state: RemoveSoftLineBreaksState,
|
||||
}
|
||||
|
||||
pub trait BufferExtensions: Buffer + Sized {
|
||||
/// Returns a new buffer that calls the passed inspector for every element that gets written to the output
|
||||
#[must_use]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.7.4"
|
||||
version = "0.8.0"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
15
crates/ruff_linter/resources/test/fixtures/airflow/AIR301.py
vendored
Normal file
15
crates/ruff_linter/resources/test/fixtures/airflow/AIR301.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
from airflow import DAG, dag
|
||||
|
||||
DAG(dag_id="class_default_schedule")
|
||||
|
||||
DAG(dag_id="class_schedule", schedule="@hourly")
|
||||
|
||||
|
||||
@dag()
|
||||
def decorator_default_schedule():
|
||||
pass
|
||||
|
||||
|
||||
@dag(schedule="0 * * * *")
|
||||
def decorator_schedule():
|
||||
pass
|
||||
@@ -14,6 +14,8 @@ ContextVar("cv", default=frozenset())
|
||||
ContextVar("cv", default=MappingProxyType({}))
|
||||
ContextVar("cv", default=re.compile("foo"))
|
||||
ContextVar("cv", default=float(1))
|
||||
ContextVar("cv", default=frozenset[str]())
|
||||
ContextVar[frozenset[str]]("cv", default=frozenset[str]())
|
||||
|
||||
# Bad
|
||||
ContextVar("cv", default=[])
|
||||
@@ -25,6 +27,8 @@ ContextVar("cv", default=[char for char in "foo"])
|
||||
ContextVar("cv", default={char for char in "foo"})
|
||||
ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
ContextVar("cv", default=collections.deque())
|
||||
ContextVar("cv", default=set[str]())
|
||||
ContextVar[set[str]]("cv", default=set[str]())
|
||||
|
||||
def bar() -> list[int]:
|
||||
return [1, 2, 3]
|
||||
|
||||
0
crates/ruff_linter/resources/test/fixtures/flake8_builtins/A005/modules/_abc/__init__.py
vendored
Normal file
0
crates/ruff_linter/resources/test/fixtures/flake8_builtins/A005/modules/_abc/__init__.py
vendored
Normal file
@@ -84,3 +84,27 @@ field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union me
|
||||
# duplicates of the outer `int`), but not three times (which would indicate that
|
||||
# we incorrectly re-checked the nested union).
|
||||
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should emit in cases with nested `typing.Union`
|
||||
field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should emit in cases with nested `typing.Union`
|
||||
field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should emit in cases with mixed `typing.Union` and `|`
|
||||
field28: typing.Union[int | int] # Error
|
||||
|
||||
# Should emit twice in cases with multiple nested `typing.Union`
|
||||
field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error
|
||||
|
||||
# Should emit once in cases with multiple nested `typing.Union`
|
||||
field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error
|
||||
|
||||
# Should emit once, and fix to `typing.Union[float, int]`
|
||||
field31: typing.Union[float, typing.Union[int | int]] # Error
|
||||
|
||||
# Should emit once, and fix to `typing.Union[float, int]`
|
||||
field32: typing.Union[float, typing.Union[int | int | int]] # Error
|
||||
|
||||
# Test case for mixed union type fix
|
||||
field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error
|
||||
|
||||
@@ -84,3 +84,27 @@ field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union me
|
||||
# duplicates of the outer `int`), but not three times (which would indicate that
|
||||
# we incorrectly re-checked the nested union).
|
||||
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should emit in cases with nested `typing.Union`
|
||||
field26: typing.Union[typing.Union[int, int]] # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should emit in cases with nested `typing.Union`
|
||||
field27: typing.Union[typing.Union[typing.Union[int, int]]] # PYI016: Duplicate union member `int`
|
||||
|
||||
# Should emit in cases with mixed `typing.Union` and `|`
|
||||
field28: typing.Union[int | int] # Error
|
||||
|
||||
# Should emit twice in cases with multiple nested `typing.Union`
|
||||
field29: typing.Union[int, typing.Union[typing.Union[int, int]]] # Error
|
||||
|
||||
# Should emit once in cases with multiple nested `typing.Union`
|
||||
field30: typing.Union[int, typing.Union[typing.Union[int, str]]] # Error
|
||||
|
||||
# Should emit once, and fix to `typing.Union[float, int]`
|
||||
field31: typing.Union[float, typing.Union[int | int]] # Error
|
||||
|
||||
# Should emit once, and fix to `typing.Union[float, int]`
|
||||
field32: typing.Union[float, typing.Union[int | int | int]] # Error
|
||||
|
||||
# Test case for mixed union type fix
|
||||
field33: typing.Union[typing.Union[int | int] | typing.Union[int | int]] # Error
|
||||
@@ -39,14 +39,30 @@ async def f4(**kwargs: int | int | float) -> None:
|
||||
...
|
||||
|
||||
|
||||
def f5(
|
||||
def f5(arg1: int, *args: Union[int, int, float]) -> None:
|
||||
...
|
||||
|
||||
|
||||
def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None:
|
||||
...
|
||||
|
||||
|
||||
def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None:
|
||||
...
|
||||
|
||||
|
||||
def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None:
|
||||
...
|
||||
|
||||
|
||||
def f9(
|
||||
arg: Union[ # comment
|
||||
float, # another
|
||||
complex, int]
|
||||
) -> None:
|
||||
...
|
||||
|
||||
def f6(
|
||||
def f10(
|
||||
arg: (
|
||||
int | # comment
|
||||
float | # another
|
||||
|
||||
@@ -46,6 +46,18 @@ def f6(
|
||||
)
|
||||
) -> None: ... # PYI041
|
||||
|
||||
def f5(arg1: int, *args: Union[int, int, float]) -> None: ... # PYI041
|
||||
|
||||
|
||||
def f6(arg1: int, *args: Union[Union[int, int, float]]) -> None: ... # PYI041
|
||||
|
||||
|
||||
def f7(arg1: int, *args: Union[Union[Union[int, int, float]]]) -> None: ... # PYI041
|
||||
|
||||
|
||||
def f8(arg1: int, *args: Union[Union[Union[int | int | float]]]) -> None: ... # PYI041
|
||||
|
||||
|
||||
class Foo:
|
||||
def good(self, arg: int) -> None: ...
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ A: str | Literal["foo"]
|
||||
B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||
C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||
D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||
E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||
F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||
G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||
|
||||
def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@ A: str | Literal["foo"]
|
||||
B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
|
||||
C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
|
||||
D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
|
||||
E: TypeAlias = typing.Union[typing.Union[typing.Union[typing.Union[Literal["foo"], str]]]]
|
||||
F: TypeAlias = typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||
G: typing.Union[str, typing.Union[typing.Union[typing.Union[Literal["foo"], int]]]]
|
||||
|
||||
def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import builtins
|
||||
from typing import Union
|
||||
|
||||
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
x: type[int] | type[str] | type[float]
|
||||
y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
z: Union[type[float], type[complex]]
|
||||
z: Union[type[float, int], type[complex]]
|
||||
s: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
t: type[int] | type[str] | type[float]
|
||||
u: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
v: Union[type[float], type[complex]]
|
||||
w: Union[type[float, int], type[complex]]
|
||||
x: Union[Union[type[float, int], type[complex]]]
|
||||
y: Union[Union[Union[type[float, int], type[complex]]]]
|
||||
z: Union[type[complex], Union[Union[type[float, int]]]]
|
||||
|
||||
|
||||
def func(arg: type[int] | str | type[float]) -> None:
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import builtins
|
||||
from typing import Union
|
||||
|
||||
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
x: type[int] | type[str] | type[float]
|
||||
y: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
z: Union[type[float], type[complex]]
|
||||
z: Union[type[float, int], type[complex]]
|
||||
s: builtins.type[int] | builtins.type[str] | builtins.type[complex]
|
||||
t: type[int] | type[str] | type[float]
|
||||
u: builtins.type[int] | type[str] | builtins.type[complex]
|
||||
v: Union[type[float], type[complex]]
|
||||
w: Union[type[float, int], type[complex]]
|
||||
x: Union[Union[type[float, int], type[complex]]]
|
||||
y: Union[Union[Union[type[float, int], type[complex]]]]
|
||||
z: Union[type[complex], Union[Union[type[float, int]]]]
|
||||
|
||||
def func(arg: type[int] | str | type[float]) -> None: ...
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Literal
|
||||
from typing import Literal, Union
|
||||
|
||||
|
||||
def func1(arg1: Literal[None]):
|
||||
@@ -17,7 +17,7 @@ def func4(arg1: Literal[int, None, float]):
|
||||
...
|
||||
|
||||
|
||||
def func5(arg1: Literal[None, None]):
|
||||
def func5(arg1: Literal[None, None]):
|
||||
...
|
||||
|
||||
|
||||
@@ -25,13 +25,21 @@ def func6(arg1: Literal[
|
||||
"hello",
|
||||
None # Comment 1
|
||||
, "world"
|
||||
]):
|
||||
]):
|
||||
...
|
||||
|
||||
|
||||
def func7(arg1: Literal[
|
||||
None # Comment 1
|
||||
]):
|
||||
]):
|
||||
...
|
||||
|
||||
|
||||
def func8(arg1: Literal[None] | None):
|
||||
...
|
||||
|
||||
|
||||
def func9(arg1: Union[Literal[None], None]):
|
||||
...
|
||||
|
||||
|
||||
@@ -58,3 +66,16 @@ Literal[1, None, "foo", None] # Y061 None inside "Literal[]" expression. Replac
|
||||
# and there are no None members in the Literal[] slice,
|
||||
# only emit Y062:
|
||||
Literal[None, True, None, True] # Y062 Duplicate "Literal[]" member "True"
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/14567
|
||||
x: Literal[None] | None
|
||||
y: None | Literal[None]
|
||||
z: Union[Literal[None], None]
|
||||
|
||||
a: int | Literal[None] | None
|
||||
b: None | Literal[None] | None
|
||||
c: (None | Literal[None]) | None
|
||||
d: None | (Literal[None] | None)
|
||||
e: None | ((None | Literal[None]) | None) | None
|
||||
f: Literal[None] | Literal[None]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Literal
|
||||
from typing import Literal, Union
|
||||
|
||||
|
||||
def func1(arg1: Literal[None]): ...
|
||||
@@ -28,6 +28,12 @@ def func7(arg1: Literal[
|
||||
]): ...
|
||||
|
||||
|
||||
def func8(arg1: Literal[None] | None):...
|
||||
|
||||
|
||||
def func9(arg1: Union[Literal[None], None]): ...
|
||||
|
||||
|
||||
# OK
|
||||
def good_func(arg1: Literal[int] | None): ...
|
||||
|
||||
@@ -35,3 +41,16 @@ def good_func(arg1: Literal[int] | None): ...
|
||||
# From flake8-pyi
|
||||
Literal[None] # PYI061 None inside "Literal[]" expression. Replace with "None"
|
||||
Literal[True, None] # PYI061 None inside "Literal[]" expression. Replace with "Literal[True] | None"
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/14567
|
||||
x: Literal[None] | None
|
||||
y: None | Literal[None]
|
||||
z: Union[Literal[None], None]
|
||||
|
||||
a: int | Literal[None] | None
|
||||
b: None | Literal[None] | None
|
||||
c: (None | Literal[None]) | None
|
||||
d: None | (Literal[None] | None)
|
||||
e: None | ((None | Literal[None]) | None) | None
|
||||
f: Literal[None] | Literal[None]
|
||||
|
||||
@@ -25,3 +25,9 @@ Literal[
|
||||
MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
|
||||
n: Literal["No", "duplicates", "here", 1, "1"]
|
||||
|
||||
|
||||
# nested literals, all equivalent to `Literal[1]`
|
||||
Literal[Literal[1]] # no duplicate
|
||||
Literal[Literal[Literal[1], Literal[1]]] # once
|
||||
Literal[Literal[1], Literal[Literal[Literal[1]]]] # once
|
||||
|
||||
@@ -25,3 +25,9 @@ Literal[
|
||||
MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
|
||||
n: Literal["No", "duplicates", "here", 1, "1"]
|
||||
|
||||
|
||||
# nested literals, all equivalent to `Literal[1]`
|
||||
Literal[Literal[1]] # no duplicate
|
||||
Literal[Literal[Literal[1], Literal[1]]] # once
|
||||
Literal[Literal[1], Literal[Literal[Literal[1]]]] # once
|
||||
|
||||
@@ -69,3 +69,15 @@ def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
|
||||
@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
|
||||
def test_csv_with_parens(param1, param2):
|
||||
...
|
||||
|
||||
|
||||
parametrize = pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
|
||||
|
||||
@parametrize
|
||||
def test_not_decorator(param1, param2):
|
||||
...
|
||||
|
||||
|
||||
@pytest.mark.parametrize(argnames=("param1,param2"), argvalues=[(1, 2), (3, 4)])
|
||||
def test_keyword_arguments(param1, param2):
|
||||
...
|
||||
|
||||
62
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py
vendored
Normal file
62
crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC006.py
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
def f():
|
||||
from typing import cast
|
||||
|
||||
cast(int, 3.0) # TC006
|
||||
|
||||
|
||||
def f():
|
||||
from typing import cast
|
||||
|
||||
cast(list[tuple[bool | float | int | str]], 3.0) # TC006
|
||||
|
||||
|
||||
def f():
|
||||
from typing import Union, cast
|
||||
|
||||
cast(list[tuple[Union[bool, float, int, str]]], 3.0) # TC006
|
||||
|
||||
|
||||
def f():
|
||||
from typing import cast
|
||||
|
||||
cast("int", 3.0) # OK
|
||||
|
||||
|
||||
def f():
|
||||
from typing import cast
|
||||
|
||||
cast("list[tuple[bool | float | int | str]]", 3.0) # OK
|
||||
|
||||
|
||||
def f():
|
||||
from typing import Union, cast
|
||||
|
||||
cast("list[tuple[Union[bool, float, int, str]]]", 3.0) # OK
|
||||
|
||||
|
||||
def f():
|
||||
from typing import cast as typecast
|
||||
|
||||
typecast(int, 3.0) # TC006
|
||||
|
||||
|
||||
def f():
|
||||
import typing
|
||||
|
||||
typing.cast(int, 3.0) # TC006
|
||||
|
||||
|
||||
def f():
|
||||
import typing as t
|
||||
|
||||
t.cast(t.Literal["3.0", '3'], 3.0) # TC006
|
||||
|
||||
|
||||
def f():
|
||||
from typing import cast
|
||||
|
||||
cast(
|
||||
int # TC006 (unsafe, because it will get rid of this comment)
|
||||
| None,
|
||||
3.0
|
||||
)
|
||||
21
crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py
vendored
Normal file
21
crates/ruff_linter/resources/test/fixtures/pyflakes/F722_1.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Regression test for #13824.
|
||||
|
||||
Don't report an error when the function being annotated has the
|
||||
`@no_type_check` decorator.
|
||||
|
||||
However, we still want to ignore this annotation on classes. See
|
||||
https://github.com/python/typing/pull/1615/files and the discussion on #14615.
|
||||
"""
|
||||
|
||||
from typing import no_type_check
|
||||
|
||||
|
||||
@no_type_check
|
||||
def f(arg: "this isn't python") -> "this isn't python either":
|
||||
x: "this also isn't python" = 0
|
||||
|
||||
|
||||
@no_type_check
|
||||
class C:
|
||||
def f(arg: "this isn't python") -> "this isn't python either":
|
||||
x: "this also isn't python" = 1
|
||||
21
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_30.py
vendored
Normal file
21
crates/ruff_linter/resources/test/fixtures/pyflakes/F821_30.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
"""Regression test for #13824.
|
||||
|
||||
Don't report an error when the function being annotated has the
|
||||
`@no_type_check` decorator.
|
||||
|
||||
However, we still want to ignore this annotation on classes. See
|
||||
https://github.com/python/typing/pull/1615/files and the discussion on #14615.
|
||||
"""
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
@typing.no_type_check
|
||||
def f(arg: "A") -> "R":
|
||||
x: "A" = 1
|
||||
|
||||
|
||||
@typing.no_type_check
|
||||
class C:
|
||||
def f(self, arg: "B") -> "S":
|
||||
x: "B" = 1
|
||||
@@ -12,3 +12,4 @@ os.getenv("AA", "GOOD %s" % "BAR")
|
||||
os.getenv("B", Z)
|
||||
os.getenv("AA", "GOOD" if Z else "BAR")
|
||||
os.getenv("AA", 1 if Z else "BAR") # [invalid-envvar-default]
|
||||
os.environ.get("TEST", 12) # [invalid-envvar-default]
|
||||
|
||||
234
crates/ruff_linter/resources/test/fixtures/pylint/len_as_condition.py
vendored
Normal file
234
crates/ruff_linter/resources/test/fixtures/pylint/len_as_condition.py
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
if len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
if not len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
z = []
|
||||
if z and len(['T', 'E', 'S', 'T']): # [PLC1802]
|
||||
pass
|
||||
|
||||
if True or len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
if len('TEST') == 0: # Should be fine
|
||||
pass
|
||||
|
||||
if len('TEST') < 1: # Should be fine
|
||||
pass
|
||||
|
||||
if len('TEST') <= 0: # Should be fine
|
||||
pass
|
||||
|
||||
if 1 > len('TEST'): # Should be fine
|
||||
pass
|
||||
|
||||
if 0 >= len('TEST'): # Should be fine
|
||||
pass
|
||||
|
||||
if z and len('TEST') == 0: # Should be fine
|
||||
pass
|
||||
|
||||
if 0 == len('TEST') < 10: # Should be fine
|
||||
pass
|
||||
|
||||
# Should be fine
|
||||
if 0 < 1 <= len('TEST') < 10: # [comparison-of-constants]
|
||||
pass
|
||||
|
||||
if 10 > len('TEST') != 0: # Should be fine
|
||||
pass
|
||||
|
||||
if 10 > len('TEST') > 1 > 0: # Should be fine
|
||||
pass
|
||||
|
||||
if 0 <= len('TEST') < 100: # Should be fine
|
||||
pass
|
||||
|
||||
if z or 10 > len('TEST') != 0: # Should be fine
|
||||
pass
|
||||
|
||||
if z:
|
||||
pass
|
||||
elif len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
if z:
|
||||
pass
|
||||
elif not len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
while len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
while not len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
while z and len('TEST'): # [PLC1802]
|
||||
pass
|
||||
|
||||
while not len('TEST') and z: # [PLC1802]
|
||||
pass
|
||||
|
||||
assert len('TEST') > 0 # Should be fine
|
||||
|
||||
x = 1 if len('TEST') != 0 else 2 # Should be fine
|
||||
|
||||
f_o_o = len('TEST') or 42 # Should be fine
|
||||
|
||||
a = x and len(x) # Should be fine
|
||||
|
||||
def some_func():
|
||||
return len('TEST') > 0 # Should be fine
|
||||
|
||||
def github_issue_1325():
|
||||
l = [1, 2, 3]
|
||||
length = len(l) if l else 0 # Should be fine
|
||||
return length
|
||||
|
||||
def github_issue_1331(*args):
|
||||
assert False, len(args) # Should be fine
|
||||
|
||||
def github_issue_1331_v2(*args):
|
||||
assert len(args), args # [PLC1802]
|
||||
|
||||
def github_issue_1331_v3(*args):
|
||||
assert len(args) or z, args # [PLC1802]
|
||||
|
||||
def github_issue_1331_v4(*args):
|
||||
assert z and len(args), args # [PLC1802]
|
||||
|
||||
def github_issue_1331_v5(**args):
|
||||
assert z and len(args), args # [PLC1802]
|
||||
|
||||
b = bool(len(z)) # [PLC1802]
|
||||
c = bool(len('TEST') or 42) # [PLC1802]
|
||||
|
||||
def github_issue_1879():
|
||||
|
||||
class ClassWithBool(list):
|
||||
def __bool__(self):
|
||||
return True
|
||||
|
||||
class ClassWithoutBool(list):
|
||||
pass
|
||||
|
||||
class ChildClassWithBool(ClassWithBool):
|
||||
pass
|
||||
|
||||
class ChildClassWithoutBool(ClassWithoutBool):
|
||||
pass
|
||||
|
||||
assert len(ClassWithBool())
|
||||
assert len(ChildClassWithBool())
|
||||
assert len(ClassWithoutBool()) # unintuitive?, in pylint: [PLC1802]
|
||||
assert len(ChildClassWithoutBool()) # unintuitive?, in pylint: [PLC1802]
|
||||
assert len(range(0)) # [PLC1802]
|
||||
assert len([t + 1 for t in []]) # [PLC1802]
|
||||
# assert len(u + 1 for u in []) generator has no len
|
||||
assert len({"1":(v + 1) for v in {}}) # [PLC1802]
|
||||
assert len(set((w + 1) for w in set())) # [PLC1802]
|
||||
|
||||
|
||||
import numpy
|
||||
numpy_array = numpy.array([0])
|
||||
if len(numpy_array) > 0:
|
||||
print('numpy_array')
|
||||
if len(numpy_array):
|
||||
print('numpy_array')
|
||||
if numpy_array:
|
||||
print('b')
|
||||
|
||||
import pandas as pd
|
||||
pandas_df = pd.DataFrame()
|
||||
if len(pandas_df):
|
||||
print("this works, but pylint tells me not to use len() without comparison")
|
||||
if len(pandas_df) > 0:
|
||||
print("this works and pylint likes it, but it's not the solution intended by PEP-8")
|
||||
if pandas_df:
|
||||
print("this does not work (truth value of dataframe is ambiguous)")
|
||||
|
||||
def function_returning_list(r):
|
||||
if r==1:
|
||||
return [1]
|
||||
return [2]
|
||||
|
||||
def function_returning_int(r):
|
||||
if r==1:
|
||||
return 1
|
||||
return 2
|
||||
|
||||
def function_returning_generator(r):
|
||||
for i in [r, 1, 2, 3]:
|
||||
yield i
|
||||
|
||||
def function_returning_comprehension(r):
|
||||
return [x+1 for x in [r, 1, 2, 3]]
|
||||
|
||||
def function_returning_function(r):
|
||||
return function_returning_generator(r)
|
||||
|
||||
assert len(function_returning_list(z)) # [PLC1802] differs from pylint
|
||||
assert len(function_returning_int(z))
|
||||
# This should raise a PLC1802 once astroid can infer it
|
||||
# See https://github.com/pylint-dev/pylint/pull/3821#issuecomment-743771514
|
||||
assert len(function_returning_generator(z))
|
||||
assert len(function_returning_comprehension(z))
|
||||
assert len(function_returning_function(z))
|
||||
|
||||
|
||||
def github_issue_4215():
|
||||
# Test undefined variables
|
||||
# https://github.com/pylint-dev/pylint/issues/4215
|
||||
if len(undefined_var): # [undefined-variable]
|
||||
pass
|
||||
if len(undefined_var2[0]): # [undefined-variable]
|
||||
pass
|
||||
|
||||
|
||||
def f(cond:bool):
|
||||
x = [1,2,3]
|
||||
if cond:
|
||||
x = [4,5,6]
|
||||
if len(x): # this should be addressed
|
||||
print(x)
|
||||
|
||||
def g(cond:bool):
|
||||
x = [1,2,3]
|
||||
if cond:
|
||||
x = [4,5,6]
|
||||
if len(x): # this should be addressed
|
||||
print(x)
|
||||
del x
|
||||
|
||||
def h(cond:bool):
|
||||
x = [1,2,3]
|
||||
x = 123
|
||||
if len(x): # ok
|
||||
print(x)
|
||||
|
||||
def outer():
|
||||
x = [1,2,3]
|
||||
def inner(x:int):
|
||||
return x+1
|
||||
if len(x): # [PLC1802]
|
||||
print(x)
|
||||
|
||||
def redefined():
|
||||
x = 123
|
||||
x = [1, 2, 3]
|
||||
if len(x): # this should be addressed
|
||||
print(x)
|
||||
|
||||
global_seq = [1, 2, 3]
|
||||
|
||||
def i():
|
||||
global global_seq
|
||||
if len(global_seq): # ok
|
||||
print(global_seq)
|
||||
|
||||
def j():
|
||||
if False:
|
||||
x = [1, 2, 3]
|
||||
if len(x): # [PLC1802] should be fine
|
||||
print(x)
|
||||
@@ -102,3 +102,6 @@ blah = lambda: {"a": 1}.__delitem__("a") # OK
|
||||
blah = dict[{"a": 1}.__delitem__("a")] # OK
|
||||
|
||||
"abc".__contains__("a")
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/14597
|
||||
assert "abc".__str__() == "abc"
|
||||
|
||||
125
crates/ruff_linter/resources/test/fixtures/ruff/RUF009_attrs_auto_attribs.py
vendored
Normal file
125
crates/ruff_linter/resources/test/fixtures/ruff/RUF009_attrs_auto_attribs.py
vendored
Normal file
@@ -0,0 +1,125 @@
|
||||
import attr
|
||||
from attrs import define, field, frozen, mutable
|
||||
|
||||
|
||||
foo = int
|
||||
|
||||
|
||||
@define # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@define() # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@define(auto_attribs=None) # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@frozen # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@frozen() # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@frozen(auto_attribs=None) # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@mutable # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@mutable() # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@mutable(auto_attribs=None) # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@attr.s # auto_attribs = False
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@attr.s() # auto_attribs = False
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=None) # auto_attribs = None => True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=False) # auto_attribs = False
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True) # auto_attribs = True
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=[1, 2, 3]) # auto_attribs = False
|
||||
class C:
|
||||
a: str = 0
|
||||
b = field()
|
||||
c: int = foo()
|
||||
d = list()
|
||||
@@ -6,3 +6,12 @@ Never | int
|
||||
NoReturn | int
|
||||
Union[Union[Never, int], Union[NoReturn, int]]
|
||||
Union[NoReturn, int, float]
|
||||
|
||||
|
||||
# Regression tests for https://github.com/astral-sh/ruff/issues/14567
|
||||
x: None | Never | None
|
||||
y: (None | Never) | None
|
||||
z: None | (Never | None)
|
||||
|
||||
a: int | Never | None
|
||||
b: Never | Never | None
|
||||
|
||||
5
crates/ruff_linter/resources/test/fixtures/ruff/RUF040.py
vendored
Normal file
5
crates/ruff_linter/resources/test/fixtures/ruff/RUF040.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
fruits = ["apples", "plums", "pear"]
|
||||
fruits.filter(lambda fruit: fruit.startwith("p"))
|
||||
assert len(fruits), 2
|
||||
|
||||
assert True, "always true"
|
||||
@@ -11,8 +11,8 @@ use ruff_text_size::Ranged;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{
|
||||
flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear, flake8_builtins,
|
||||
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
|
||||
airflow, flake8_2020, flake8_async, flake8_bandit, flake8_boolean_trap, flake8_bugbear,
|
||||
flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
|
||||
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
|
||||
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
|
||||
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_use_pathlib, flynt, numpy,
|
||||
@@ -494,6 +494,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SuperWithoutBrackets) {
|
||||
pylint::rules::super_without_brackets(checker, func);
|
||||
}
|
||||
if checker.enabled(Rule::LenTest) {
|
||||
pylint::rules::len_test(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::BitCount) {
|
||||
refurb::rules::bit_count(checker, call);
|
||||
}
|
||||
@@ -856,6 +859,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.settings.preview.is_enabled()
|
||||
&& checker.any_enabled(&[
|
||||
Rule::PytestParametrizeNamesWrongType,
|
||||
Rule::PytestParametrizeValuesWrongType,
|
||||
Rule::PytestDuplicateParametrizeTestCases,
|
||||
])
|
||||
{
|
||||
flake8_pytest_style::rules::parametrize(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::PytestUnittestAssertion) {
|
||||
if let Some(diagnostic) = flake8_pytest_style::rules::unittest_assertion(
|
||||
checker, expr, func, args, keywords,
|
||||
@@ -1061,6 +1073,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UnrawRePattern) {
|
||||
ruff::rules::unraw_re_pattern(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::AirflowDagNoScheduleArgument) {
|
||||
airflow::rules::dag_no_schedule_argument(checker, expr);
|
||||
}
|
||||
}
|
||||
Expr::Dict(dict) => {
|
||||
if checker.any_enabled(&[
|
||||
|
||||
@@ -309,12 +309,20 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
body,
|
||||
);
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::PytestParametrizeNamesWrongType,
|
||||
Rule::PytestParametrizeValuesWrongType,
|
||||
Rule::PytestDuplicateParametrizeTestCases,
|
||||
]) {
|
||||
flake8_pytest_style::rules::parametrize(checker, decorator_list);
|
||||
// In preview mode, calls are analyzed. To avoid duplicate diagnostics,
|
||||
// skip analyzing the decorators.
|
||||
if !checker.settings.preview.is_enabled()
|
||||
&& checker.any_enabled(&[
|
||||
Rule::PytestParametrizeNamesWrongType,
|
||||
Rule::PytestParametrizeValuesWrongType,
|
||||
Rule::PytestDuplicateParametrizeTestCases,
|
||||
])
|
||||
{
|
||||
for decorator in decorator_list {
|
||||
if let Some(call) = decorator.expression.as_call_expr() {
|
||||
flake8_pytest_style::rules::parametrize(checker, call);
|
||||
}
|
||||
}
|
||||
}
|
||||
if checker.any_enabled(&[
|
||||
Rule::PytestIncorrectMarkParenthesesStyle,
|
||||
@@ -1268,6 +1276,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::AssertWithPrintMessage) {
|
||||
ruff::rules::assert_with_print_message(checker, assert_stmt);
|
||||
}
|
||||
if checker.enabled(Rule::InvalidAssertMessageLiteralArgument) {
|
||||
ruff::rules::invalid_assert_message_literal_argument(checker, assert_stmt);
|
||||
}
|
||||
}
|
||||
Stmt::With(
|
||||
with_stmt @ ast::StmtWith {
|
||||
|
||||
@@ -723,6 +723,12 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
// Visit the decorators and arguments, but avoid the body, which will be
|
||||
// deferred.
|
||||
for decorator in decorator_list {
|
||||
if self
|
||||
.semantic
|
||||
.match_typing_expr(&decorator.expression, "no_type_check")
|
||||
{
|
||||
self.semantic.flags |= SemanticModelFlags::NO_TYPE_CHECK;
|
||||
}
|
||||
self.visit_decorator(decorator);
|
||||
}
|
||||
|
||||
@@ -1280,6 +1286,10 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
let mut args = arguments.args.iter();
|
||||
if let Some(arg) = args.next() {
|
||||
self.visit_type_definition(arg);
|
||||
|
||||
if self.enabled(Rule::RuntimeCastValue) {
|
||||
flake8_type_checking::rules::runtime_cast_value(self, arg);
|
||||
}
|
||||
}
|
||||
for arg in args {
|
||||
self.visit_expr(arg);
|
||||
@@ -1847,6 +1857,9 @@ impl<'a> Checker<'a> {
|
||||
|
||||
/// Visit an [`Expr`], and treat it as a type definition.
|
||||
fn visit_type_definition(&mut self, expr: &'a Expr) {
|
||||
if self.semantic.in_no_type_check() {
|
||||
return;
|
||||
}
|
||||
let snapshot = self.semantic.flags;
|
||||
self.semantic.flags |= SemanticModelFlags::TYPE_DEFINITION;
|
||||
self.visit_expr(expr);
|
||||
|
||||
@@ -192,6 +192,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "C0208") => (RuleGroup::Stable, rules::pylint::rules::IterationOverSet),
|
||||
(Pylint, "C0414") => (RuleGroup::Stable, rules::pylint::rules::UselessImportAlias),
|
||||
(Pylint, "C0415") => (RuleGroup::Preview, rules::pylint::rules::ImportOutsideTopLevel),
|
||||
(Pylint, "C1802") => (RuleGroup::Preview, rules::pylint::rules::LenTest),
|
||||
(Pylint, "C1901") => (RuleGroup::Preview, rules::pylint::rules::CompareToEmptyString),
|
||||
(Pylint, "C2401") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiName),
|
||||
(Pylint, "C2403") => (RuleGroup::Stable, rules::pylint::rules::NonAsciiImportName),
|
||||
@@ -856,6 +857,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
|
||||
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
|
||||
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeCastValue),
|
||||
(Flake8TypeChecking, "010") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeStringUnion),
|
||||
|
||||
// tryceratops
|
||||
@@ -977,6 +979,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "038") => (RuleGroup::Preview, rules::ruff::rules::RedundantBoolLiteral),
|
||||
(Ruff, "048") => (RuleGroup::Preview, rules::ruff::rules::MapIntVersionParsing),
|
||||
(Ruff, "039") => (RuleGroup::Preview, rules::ruff::rules::UnrawRePattern),
|
||||
(Ruff, "040") => (RuleGroup::Preview, rules::ruff::rules::InvalidAssertMessageLiteralArgument),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||
|
||||
@@ -1031,6 +1034,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
|
||||
// airflow
|
||||
(Airflow, "001") => (RuleGroup::Stable, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
|
||||
(Airflow, "301") => (RuleGroup::Preview, rules::airflow::rules::AirflowDagNoScheduleArgument),
|
||||
|
||||
// perflint
|
||||
(Perflint, "101") => (RuleGroup::Stable, rules::perflint::rules::UnnecessaryListCast),
|
||||
|
||||
@@ -13,6 +13,7 @@ mod tests {
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::AirflowVariableNameTaskIdMismatch, Path::new("AIR001.py"))]
|
||||
#[test_case(Rule::AirflowDagNoScheduleArgument, Path::new("AIR301.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for a `DAG()` class or `@dag()` decorator without an explicit
|
||||
/// `schedule` parameter.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The default `schedule` value on Airflow 2 is `timedelta(days=1)`, which is
|
||||
/// almost never what a user is looking for. Airflow 3 changes this the default
|
||||
/// to *None*, and would break existing DAGs using the implicit default.
|
||||
///
|
||||
/// If your DAG does not have an explicit `schedule` argument, Airflow 2
|
||||
/// schedules a run for it every day (at the time determined by `start_date`).
|
||||
/// Such a DAG will no longer be scheduled on Airflow 3 at all, without any
|
||||
/// exceptions or other messages visible to the user.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from airflow import DAG
|
||||
///
|
||||
///
|
||||
/// # Using the implicit default schedule.
|
||||
/// dag = DAG(dag_id="my_dag")
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from datetime import timedelta
|
||||
///
|
||||
/// from airflow import DAG
|
||||
///
|
||||
///
|
||||
/// dag = DAG(dag_id="my_dag", schedule=timedelta(days=1))
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct AirflowDagNoScheduleArgument;
|
||||
|
||||
impl Violation for AirflowDagNoScheduleArgument {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"DAG should have an explicit `schedule` argument".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// AIR301
|
||||
pub(crate) fn dag_no_schedule_argument(checker: &mut Checker, expr: &Expr) {
|
||||
if !checker.semantic().seen_module(Modules::AIRFLOW) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't check non-call expressions.
|
||||
let Expr::Call(ast::ExprCall {
|
||||
func, arguments, ..
|
||||
}) = expr
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// We don't do anything unless this is a `DAG` (class) or `dag` (decorator
|
||||
// function) from Airflow.
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func)
|
||||
.is_some_and(|qualname| matches!(qualname.segments(), ["airflow", .., "DAG" | "dag"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's a `schedule` keyword argument, we are good.
|
||||
if arguments.find_keyword("schedule").is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Produce a diagnostic when the `schedule` keyword argument is not found.
|
||||
let diagnostic = Diagnostic::new(AirflowDagNoScheduleArgument, expr.range());
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
pub(crate) use dag_schedule_argument::*;
|
||||
pub(crate) use task_variable_name::*;
|
||||
|
||||
mod dag_schedule_argument;
|
||||
mod task_variable_name;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/airflow/mod.rs
|
||||
---
|
||||
AIR301.py:3:1: AIR301 DAG should have an explicit `schedule` argument
|
||||
|
|
||||
1 | from airflow import DAG, dag
|
||||
2 |
|
||||
3 | DAG(dag_id="class_default_schedule")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR301
|
||||
4 |
|
||||
5 | DAG(dag_id="class_schedule", schedule="@hourly")
|
||||
|
|
||||
|
||||
AIR301.py:8:2: AIR301 DAG should have an explicit `schedule` argument
|
||||
|
|
||||
8 | @dag()
|
||||
| ^^^^^ AIR301
|
||||
9 | def decorator_default_schedule():
|
||||
10 | pass
|
||||
|
|
||||
@@ -311,7 +311,8 @@ pub(crate) fn shell_injection(checker: &mut Checker, call: &ast::ExprCall) {
|
||||
}
|
||||
// S603
|
||||
Some(ShellKeyword {
|
||||
truthiness: Truthiness::False | Truthiness::Falsey | Truthiness::Unknown,
|
||||
truthiness:
|
||||
Truthiness::False | Truthiness::Falsey | Truthiness::None | Truthiness::Unknown,
|
||||
}) => {
|
||||
if checker.enabled(Rule::SubprocessWithoutShellEqualsTrue) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
|
||||
@@ -106,6 +106,10 @@ pub(crate) fn boolean_default_value_positional_argument(
|
||||
decorator_list: &[Decorator],
|
||||
parameters: &Parameters,
|
||||
) {
|
||||
// https://github.com/astral-sh/ruff/issues/14535
|
||||
if checker.source_type.is_stub() {
|
||||
return;
|
||||
}
|
||||
// Allow Boolean defaults in explicitly-allowed functions.
|
||||
if is_allowed_func_def(name) {
|
||||
return;
|
||||
|
||||
@@ -115,6 +115,10 @@ pub(crate) fn boolean_type_hint_positional_argument(
|
||||
decorator_list: &[Decorator],
|
||||
parameters: &Parameters,
|
||||
) {
|
||||
// https://github.com/astral-sh/ruff/issues/14535
|
||||
if checker.source_type.is_stub() {
|
||||
return;
|
||||
}
|
||||
// Allow Boolean type hints in explicitly-allowed functions.
|
||||
if is_allowed_func_def(name) {
|
||||
return;
|
||||
|
||||
@@ -11,12 +11,14 @@ use crate::checkers::ast::Checker;
|
||||
use crate::registry::Rule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for abstract classes without abstract methods.
|
||||
/// Checks for abstract classes without abstract methods or properties.
|
||||
/// Annotated but unassigned class variables are regarded as abstract.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Abstract base classes are used to define interfaces. If an abstract base
|
||||
/// class has no abstract methods, you may have forgotten to add an abstract
|
||||
/// method to the class or omitted an `@abstractmethod` decorator.
|
||||
/// class has no abstract methods or properties, you may have forgotten
|
||||
/// to add an abstract method or property to the class,
|
||||
/// or omitted an `@abstractmethod` decorator.
|
||||
///
|
||||
/// If the class is _not_ meant to be used as an interface, consider removing
|
||||
/// the `ABC` base class from the class definition.
|
||||
@@ -24,9 +26,12 @@ use crate::registry::Rule;
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// from abc import ABC
|
||||
/// from typing import ClassVar
|
||||
///
|
||||
///
|
||||
/// class Foo(ABC):
|
||||
/// class_var: ClassVar[str] = "assigned"
|
||||
///
|
||||
/// def method(self):
|
||||
/// bar()
|
||||
/// ```
|
||||
@@ -34,9 +39,12 @@ use crate::registry::Rule;
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// from abc import ABC, abstractmethod
|
||||
/// from typing import ClassVar
|
||||
///
|
||||
///
|
||||
/// class Foo(ABC):
|
||||
/// class_var: ClassVar[str] # unassigned
|
||||
///
|
||||
/// @abstractmethod
|
||||
/// def method(self):
|
||||
/// bar()
|
||||
@@ -44,6 +52,7 @@ use crate::registry::Rule;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html)
|
||||
/// - [Python documentation: `typing.ClassVar`](https://docs.python.org/3/library/typing.html#typing.ClassVar)
|
||||
#[violation]
|
||||
pub struct AbstractBaseClassWithoutAbstractMethod {
|
||||
name: String,
|
||||
@@ -53,7 +62,7 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let AbstractBaseClassWithoutAbstractMethod { name } = self;
|
||||
format!("`{name}` is an abstract base class, but it has no abstract methods")
|
||||
format!("`{name}` is an abstract base class, but it has no abstract methods or properties")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::map_subscript;
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_semantic::analyze::typing::{is_immutable_func, is_mutable_expr, is_mutable_func};
|
||||
@@ -96,7 +97,7 @@ pub(crate) fn mutable_contextvar_default(checker: &mut Checker, call: &ast::Expr
|
||||
&& !is_immutable_func(func, checker.semantic(), &extend_immutable_calls)))
|
||||
&& checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(&call.func)
|
||||
.resolve_qualified_name(map_subscript(&call.func))
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["contextvars", "ContextVar"])
|
||||
})
|
||||
|
||||
@@ -11,10 +11,11 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `warnings.warn` method uses a `stacklevel` of 1 by default, which
|
||||
/// limits the rendered stack trace to that of the line on which the
|
||||
/// `warn` method is called.
|
||||
/// will output a stack frame of the line on which the "warn" method
|
||||
/// is called. Setting it to a higher number will output a stack frame
|
||||
/// from higher up the stack.
|
||||
///
|
||||
/// It's recommended to use a `stacklevel` of 2 or higher, give the caller
|
||||
/// It's recommended to use a `stacklevel` of 2 or higher, to give the caller
|
||||
/// more context about the warning.
|
||||
///
|
||||
/// ## Example
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
B024.py:18:7: B024 `Base_1` is an abstract base class, but it has no abstract methods
|
||||
B024.py:18:7: B024 `Base_1` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
18 | class Base_1(ABC): # error
|
||||
| ^^^^^^ B024
|
||||
@@ -10,7 +10,7 @@ B024.py:18:7: B024 `Base_1` is an abstract base class, but it has no abstract me
|
||||
20 | foo()
|
||||
|
|
||||
|
||||
B024.py:71:7: B024 `MetaBase_1` is an abstract base class, but it has no abstract methods
|
||||
B024.py:71:7: B024 `MetaBase_1` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
71 | class MetaBase_1(metaclass=ABCMeta): # error
|
||||
| ^^^^^^^^^^ B024
|
||||
@@ -18,7 +18,7 @@ B024.py:71:7: B024 `MetaBase_1` is an abstract base class, but it has no abstrac
|
||||
73 | foo()
|
||||
|
|
||||
|
||||
B024.py:82:7: B024 `abc_Base_1` is an abstract base class, but it has no abstract methods
|
||||
B024.py:82:7: B024 `abc_Base_1` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
82 | class abc_Base_1(abc.ABC): # error
|
||||
| ^^^^^^^^^^ B024
|
||||
@@ -26,7 +26,7 @@ B024.py:82:7: B024 `abc_Base_1` is an abstract base class, but it has no abstrac
|
||||
84 | foo()
|
||||
|
|
||||
|
||||
B024.py:87:7: B024 `abc_Base_2` is an abstract base class, but it has no abstract methods
|
||||
B024.py:87:7: B024 `abc_Base_2` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
87 | class abc_Base_2(metaclass=abc.ABCMeta): # error
|
||||
| ^^^^^^^^^^ B024
|
||||
@@ -34,7 +34,7 @@ B024.py:87:7: B024 `abc_Base_2` is an abstract base class, but it has no abstrac
|
||||
89 | foo()
|
||||
|
|
||||
|
||||
B024.py:92:7: B024 `notabc_Base_1` is an abstract base class, but it has no abstract methods
|
||||
B024.py:92:7: B024 `notabc_Base_1` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
92 | class notabc_Base_1(notabc.ABC): # error
|
||||
| ^^^^^^^^^^^^^ B024
|
||||
@@ -42,21 +42,21 @@ B024.py:92:7: B024 `notabc_Base_1` is an abstract base class, but it has no abst
|
||||
94 | foo()
|
||||
|
|
||||
|
||||
B024.py:132:7: B024 `abc_set_class_variable_2` is an abstract base class, but it has no abstract methods
|
||||
B024.py:132:7: B024 `abc_set_class_variable_2` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
132 | class abc_set_class_variable_2(ABC): # error (not an abstract attribute)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B024
|
||||
133 | foo = 2
|
||||
|
|
||||
|
||||
B024.py:136:7: B024 `abc_set_class_variable_3` is an abstract base class, but it has no abstract methods
|
||||
B024.py:136:7: B024 `abc_set_class_variable_3` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
136 | class abc_set_class_variable_3(ABC): # error (not an abstract attribute)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B024
|
||||
137 | foo: int = 2
|
||||
|
|
||||
|
||||
B024.py:141:7: B024 `abc_set_class_variable_4` is an abstract base class, but it has no abstract methods
|
||||
B024.py:141:7: B024 `abc_set_class_variable_4` is an abstract base class, but it has no abstract methods or properties
|
||||
|
|
||||
140 | # this doesn't actually declare a class variable, it's just an expression
|
||||
141 | class abc_set_class_variable_4(ABC): # error
|
||||
|
||||
@@ -2,127 +2,149 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
B039.py:19:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
18 | # Bad
|
||||
19 | ContextVar("cv", default=[])
|
||||
| ^^ B039
|
||||
20 | ContextVar("cv", default={})
|
||||
21 | ContextVar("cv", default=list())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:20:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
18 | # Bad
|
||||
19 | ContextVar("cv", default=[])
|
||||
20 | ContextVar("cv", default={})
|
||||
| ^^ B039
|
||||
21 | ContextVar("cv", default=list())
|
||||
22 | ContextVar("cv", default=set())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:21:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
19 | ContextVar("cv", default=[])
|
||||
20 | ContextVar("cv", default={})
|
||||
21 | ContextVar("cv", default=list())
|
||||
| ^^^^^^ B039
|
||||
22 | ContextVar("cv", default=set())
|
||||
23 | ContextVar("cv", default=dict())
|
||||
20 | # Bad
|
||||
21 | ContextVar("cv", default=[])
|
||||
| ^^ B039
|
||||
22 | ContextVar("cv", default={})
|
||||
23 | ContextVar("cv", default=list())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:22:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
20 | ContextVar("cv", default={})
|
||||
21 | ContextVar("cv", default=list())
|
||||
22 | ContextVar("cv", default=set())
|
||||
| ^^^^^ B039
|
||||
23 | ContextVar("cv", default=dict())
|
||||
24 | ContextVar("cv", default=[char for char in "foo"])
|
||||
20 | # Bad
|
||||
21 | ContextVar("cv", default=[])
|
||||
22 | ContextVar("cv", default={})
|
||||
| ^^ B039
|
||||
23 | ContextVar("cv", default=list())
|
||||
24 | ContextVar("cv", default=set())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:23:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
21 | ContextVar("cv", default=list())
|
||||
22 | ContextVar("cv", default=set())
|
||||
23 | ContextVar("cv", default=dict())
|
||||
21 | ContextVar("cv", default=[])
|
||||
22 | ContextVar("cv", default={})
|
||||
23 | ContextVar("cv", default=list())
|
||||
| ^^^^^^ B039
|
||||
24 | ContextVar("cv", default=[char for char in "foo"])
|
||||
25 | ContextVar("cv", default={char for char in "foo"})
|
||||
24 | ContextVar("cv", default=set())
|
||||
25 | ContextVar("cv", default=dict())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:24:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
22 | ContextVar("cv", default=set())
|
||||
23 | ContextVar("cv", default=dict())
|
||||
24 | ContextVar("cv", default=[char for char in "foo"])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
|
||||
25 | ContextVar("cv", default={char for char in "foo"})
|
||||
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
22 | ContextVar("cv", default={})
|
||||
23 | ContextVar("cv", default=list())
|
||||
24 | ContextVar("cv", default=set())
|
||||
| ^^^^^ B039
|
||||
25 | ContextVar("cv", default=dict())
|
||||
26 | ContextVar("cv", default=[char for char in "foo"])
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:25:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
23 | ContextVar("cv", default=dict())
|
||||
24 | ContextVar("cv", default=[char for char in "foo"])
|
||||
25 | ContextVar("cv", default={char for char in "foo"})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
|
||||
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
27 | ContextVar("cv", default=collections.deque())
|
||||
23 | ContextVar("cv", default=list())
|
||||
24 | ContextVar("cv", default=set())
|
||||
25 | ContextVar("cv", default=dict())
|
||||
| ^^^^^^ B039
|
||||
26 | ContextVar("cv", default=[char for char in "foo"])
|
||||
27 | ContextVar("cv", default={char for char in "foo"})
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:26:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
24 | ContextVar("cv", default=[char for char in "foo"])
|
||||
25 | ContextVar("cv", default={char for char in "foo"})
|
||||
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B039
|
||||
27 | ContextVar("cv", default=collections.deque())
|
||||
24 | ContextVar("cv", default=set())
|
||||
25 | ContextVar("cv", default=dict())
|
||||
26 | ContextVar("cv", default=[char for char in "foo"])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
|
||||
27 | ContextVar("cv", default={char for char in "foo"})
|
||||
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:27:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
25 | ContextVar("cv", default={char for char in "foo"})
|
||||
26 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
27 | ContextVar("cv", default=collections.deque())
|
||||
25 | ContextVar("cv", default=dict())
|
||||
26 | ContextVar("cv", default=[char for char in "foo"])
|
||||
27 | ContextVar("cv", default={char for char in "foo"})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ B039
|
||||
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
29 | ContextVar("cv", default=collections.deque())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:28:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
26 | ContextVar("cv", default=[char for char in "foo"])
|
||||
27 | ContextVar("cv", default={char for char in "foo"})
|
||||
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B039
|
||||
29 | ContextVar("cv", default=collections.deque())
|
||||
30 | ContextVar("cv", default=set[str]())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:29:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
27 | ContextVar("cv", default={char for char in "foo"})
|
||||
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
29 | ContextVar("cv", default=collections.deque())
|
||||
| ^^^^^^^^^^^^^^^^^^^ B039
|
||||
28 |
|
||||
29 | def bar() -> list[int]:
|
||||
30 | ContextVar("cv", default=set[str]())
|
||||
31 | ContextVar[set[str]]("cv", default=set[str]())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:32:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
B039.py:30:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
30 | return [1, 2, 3]
|
||||
31 |
|
||||
32 | ContextVar("cv", default=bar())
|
||||
| ^^^^^ B039
|
||||
33 | ContextVar("cv", default=time.time())
|
||||
28 | ContextVar("cv", default={char: idx for idx, char in enumerate("foo")})
|
||||
29 | ContextVar("cv", default=collections.deque())
|
||||
30 | ContextVar("cv", default=set[str]())
|
||||
| ^^^^^^^^^^ B039
|
||||
31 | ContextVar[set[str]]("cv", default=set[str]())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:33:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
B039.py:31:36: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
32 | ContextVar("cv", default=bar())
|
||||
33 | ContextVar("cv", default=time.time())
|
||||
| ^^^^^^^^^^^ B039
|
||||
34 |
|
||||
35 | def baz(): ...
|
||||
29 | ContextVar("cv", default=collections.deque())
|
||||
30 | ContextVar("cv", default=set[str]())
|
||||
31 | ContextVar[set[str]]("cv", default=set[str]())
|
||||
| ^^^^^^^^^^ B039
|
||||
32 |
|
||||
33 | def bar() -> list[int]:
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:36:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
35 | def baz(): ...
|
||||
36 | ContextVar("cv", default=baz())
|
||||
34 | return [1, 2, 3]
|
||||
35 |
|
||||
36 | ContextVar("cv", default=bar())
|
||||
| ^^^^^ B039
|
||||
37 | ContextVar("cv", default=time.time())
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:37:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
36 | ContextVar("cv", default=bar())
|
||||
37 | ContextVar("cv", default=time.time())
|
||||
| ^^^^^^^^^^^ B039
|
||||
38 |
|
||||
39 | def baz(): ...
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
B039.py:40:26: B039 Do not use mutable data structures for `ContextVar` defaults
|
||||
|
|
||||
39 | def baz(): ...
|
||||
40 | ContextVar("cv", default=baz())
|
||||
| ^^^^^ B039
|
||||
|
|
||||
= help: Replace with `None`; initialize with `.set()``
|
||||
|
||||
@@ -36,6 +36,10 @@ mod tests {
|
||||
Rule::BuiltinModuleShadowing,
|
||||
Path::new("A005/modules/package/bisect.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::BuiltinModuleShadowing,
|
||||
Path::new("A005/modules/_abc/__init__.py")
|
||||
)]
|
||||
#[test_case(Rule::BuiltinModuleShadowing, Path::new("A005/modules/package/xml.py"))]
|
||||
#[test_case(Rule::BuiltinLambdaArgumentShadowing, Path::new("A006.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
@@ -91,6 +95,10 @@ mod tests {
|
||||
Rule::BuiltinModuleShadowing,
|
||||
Path::new("A005/modules/package/bisect.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::BuiltinModuleShadowing,
|
||||
Path::new("A005/modules/_abc/__init__.py")
|
||||
)]
|
||||
#[test_case(Rule::BuiltinModuleShadowing, Path::new("A005/modules/package/xml.py"))]
|
||||
fn builtins_allowed_modules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::package::PackageRoot;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::PySourceType;
|
||||
@@ -9,6 +7,9 @@ use ruff_python_stdlib::path::is_module_file;
|
||||
use ruff_python_stdlib::sys::is_known_standard_library;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::package::PackageRoot;
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for modules that use the same names as Python builtin modules.
|
||||
///
|
||||
@@ -47,25 +48,35 @@ pub(crate) fn builtin_module_shadowing(
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(package) = package {
|
||||
let module_name = if is_module_file(path) {
|
||||
package.path().file_name().unwrap().to_string_lossy()
|
||||
} else {
|
||||
path.file_stem().unwrap().to_string_lossy()
|
||||
};
|
||||
let package = package?;
|
||||
|
||||
if is_known_standard_library(target_version.minor(), &module_name)
|
||||
&& allowed_modules
|
||||
.iter()
|
||||
.all(|allowed_module| allowed_module != &module_name)
|
||||
{
|
||||
return Some(Diagnostic::new(
|
||||
BuiltinModuleShadowing {
|
||||
name: module_name.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
));
|
||||
}
|
||||
let module_name = if is_module_file(path) {
|
||||
package.path().file_name().unwrap().to_string_lossy()
|
||||
} else {
|
||||
path.file_stem().unwrap().to_string_lossy()
|
||||
};
|
||||
|
||||
if !is_known_standard_library(target_version.minor(), &module_name) {
|
||||
return None;
|
||||
}
|
||||
None
|
||||
|
||||
// Shadowing private stdlib modules is okay.
|
||||
// https://github.com/astral-sh/ruff/issues/12949
|
||||
if module_name.starts_with('_') && !module_name.starts_with("__") {
|
||||
return None;
|
||||
}
|
||||
|
||||
if allowed_modules
|
||||
.iter()
|
||||
.any(|allowed_module| allowed_module == &module_name)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(Diagnostic::new(
|
||||
BuiltinModuleShadowing {
|
||||
name: module_name.to_string(),
|
||||
},
|
||||
TextRange::default(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user