Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947e1a4e4d | ||
|
|
33945a5517 | ||
|
|
953e862aca | ||
|
|
fbf140a665 | ||
|
|
670f958525 | ||
|
|
fed35a25e8 | ||
|
|
d1ef418bb0 | ||
|
|
272d24bf3e | ||
|
|
2624249219 | ||
|
|
4b08d17088 | ||
|
|
5b6169b02d | ||
|
|
2040e93add | ||
|
|
794eb886e4 | ||
|
|
57ba25caaf | ||
|
|
4f74db5630 | ||
|
|
adc4216afb | ||
|
|
fe8e49de9a | ||
|
|
574eb3f4bd | ||
|
|
311b0bdf9a | ||
|
|
f2546c562c | ||
|
|
59c0dacea0 | ||
|
|
b8188b2262 | ||
|
|
136721e608 | ||
|
|
5b500b838b | ||
|
|
cb003ebe22 | ||
|
|
03a5788aa1 | ||
|
|
626f716de6 | ||
|
|
46c5a13103 | ||
|
|
31681f66c9 | ||
|
|
a56ee9268e | ||
|
|
4ece8e5c1e | ||
|
|
34b6a9b909 | ||
|
|
eead549254 | ||
|
|
abafeb4bee | ||
|
|
2b76fa8fa1 | ||
|
|
239cbc6f33 | ||
|
|
2296627528 | ||
|
|
05687285fe | ||
|
|
05f97bae73 | ||
|
|
4323512a65 | ||
|
|
9dddd73c29 | ||
|
|
6c56a7a868 | ||
|
|
bb25bd9c6c | ||
|
|
b7e32b0a18 | ||
|
|
fb94b71e63 | ||
|
|
bc0586d922 | ||
|
|
a7a78f939c | ||
|
|
6dabf045c3 | ||
|
|
df45a0e3f9 | ||
|
|
88d9bb191b | ||
|
|
e302c2de7c | ||
|
|
012f385f5d | ||
|
|
a6f7f22b27 | ||
|
|
8d7dda9fb7 | ||
|
|
fb0881d836 | ||
|
|
ded2b15e05 | ||
|
|
3133964d8c | ||
|
|
f00039b6f2 | ||
|
|
6ccd0f187b | ||
|
|
de40f6a3ad | ||
|
|
dfbd27dc2f | ||
|
|
1531ca8a1b | ||
|
|
71702bbd48 | ||
|
|
8d9bdb5b92 | ||
|
|
2b73a1c039 | ||
|
|
2b0cdd2338 | ||
|
|
f09dc8b67c | ||
|
|
71a122f060 | ||
|
|
3ca24785ae | ||
|
|
1de36cfe4c | ||
|
|
66872a41fc | ||
|
|
e00594e8d2 | ||
|
|
443fd3b660 | ||
|
|
ae9f08d1e5 | ||
|
|
f69712c11d | ||
|
|
be485602de | ||
|
|
bc7615af0e | ||
|
|
4a3eeeff86 | ||
|
|
35c6dfe481 | ||
|
|
f8374280c0 | ||
|
|
0925513529 | ||
|
|
70bdde4085 | ||
|
|
34a5d7cb7f | ||
|
|
487941ea66 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -16,7 +16,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.11"
|
||||
PYTHON_VERSION: "3.12"
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
|
||||
2
.github/workflows/publish-playground.yml
vendored
2
.github/workflows/publish-playground.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
working-directory: playground
|
||||
- name: "Deploy to Cloudflare Pages"
|
||||
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
|
||||
uses: cloudflare/wrangler-action@v3.9.0
|
||||
uses: cloudflare/wrangler-action@v3.11.0
|
||||
with:
|
||||
apiToken: ${{ secrets.CF_API_TOKEN }}
|
||||
accountId: ${{ secrets.CF_ACCOUNT_ID }}
|
||||
|
||||
@@ -17,7 +17,7 @@ exclude: |
|
||||
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.21
|
||||
rev: v0.22
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -51,11 +51,15 @@ repos:
|
||||
- id: blacken-docs
|
||||
args: ["--pyi", "--line-length", "130"]
|
||||
files: '^crates/.*/resources/mdtest/.*\.md'
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.*?invalid(_.+)_syntax.md
|
||||
)$
|
||||
additional_dependencies:
|
||||
- black==24.10.0
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.26.0
|
||||
rev: v1.27.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -69,7 +73,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.7.0
|
||||
rev: v0.7.2
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- Formatter: Disallow single-line implicit concatenated strings ([#13928](https://github.com/astral-sh/ruff/pull/13928))
|
||||
- \[`flake8-pyi`\] Include all Python file types for `PYI006` and `PYI066` ([#14059](https://github.com/astral-sh/ruff/pull/14059))
|
||||
- \[`flake8-simplify`\] Implement `split-of-static-string` (`SIM905`) ([#14008](https://github.com/astral-sh/ruff/pull/14008))
|
||||
- \[`refurb`\] Implement `subclass-builtin` (`FURB189`) ([#14105](https://github.com/astral-sh/ruff/pull/14105))
|
||||
- \[`ruff`\] Improve diagnostic messages and docs (`RUF031`, `RUF032`, `RUF034`) ([#14068](https://github.com/astral-sh/ruff/pull/14068))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064))
|
||||
- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094))
|
||||
- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150))
|
||||
- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to alawys provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188))
|
||||
- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065))
|
||||
- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid parsing joint rule codes as distinct codes in `# noqa` ([#12809](https://github.com/astral-sh/ruff/pull/12809))
|
||||
- \[`eradicate`\] ignore `# language=` in commented-out-code rule (ERA001) ([#14069](https://github.com/astral-sh/ruff/pull/14069))
|
||||
- \[`flake8-bugbear`\] - do not run `mutable-argument-default` on stubs (`B006`) ([#14058](https://github.com/astral-sh/ruff/pull/14058))
|
||||
- \[`flake8-builtins`\] Skip lambda expressions in `builtin-argument-shadowing (A002)` ([#14144](https://github.com/astral-sh/ruff/pull/14144))
|
||||
- \[`flake8-comprehension`\] Also remove trailing comma while fixing `C409` and `C419` ([#14097](https://github.com/astral-sh/ruff/pull/14097))
|
||||
- \[`flake8-simplify`\] Allow `open` without context manager in `return` statement (`SIM115`) ([#14066](https://github.com/astral-sh/ruff/pull/14066))
|
||||
- \[`pylint`\] Respect hash-equivalent literals in `iteration-over-set` (`PLC0208`) ([#14063](https://github.com/astral-sh/ruff/pull/14063))
|
||||
- \[`pylint`\] Update known dunder methods for Python 3.13 (`PLW3201`) ([#14146](https://github.com/astral-sh/ruff/pull/14146))
|
||||
- \[`pyupgrade`\] - ignore kwarg unpacking for `UP044` ([#14053](https://github.com/astral-sh/ruff/pull/14053))
|
||||
- \[`refurb`\] Parse more exotic decimal strings in `verbose-decimal-constructor` (`FURB157`) ([#14098](https://github.com/astral-sh/ruff/pull/14098))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add links to missing related options within rule documentations ([#13971](https://github.com/astral-sh/ruff/pull/13971))
|
||||
- Add rule short code to mkdocs tags to allow searching via rule codes ([#14040](https://github.com/astral-sh/ruff/pull/14040))
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Preview features
|
||||
|
||||
126
Cargo.lock
generated
126
Cargo.lock
generated
@@ -123,9 +123,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.91"
|
||||
version = "1.0.92"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8"
|
||||
checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13"
|
||||
|
||||
[[package]]
|
||||
name = "append-only-vec"
|
||||
@@ -407,7 +407,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -687,7 +687,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.10.0",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -698,7 +698,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1162,12 +1162,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.14.5",
|
||||
"hashbrown 0.15.0",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1193,9 +1193,9 @@ checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
checksum = "fdd168d97690d0b8c412d6b6c10360277f4d7ee495c5d0d5d5fe0854923255cc"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
@@ -1213,9 +1213,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "insta"
|
||||
version = "1.41.0"
|
||||
version = "1.41.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1f72d3e19488cf7d8ea52d2fc0f8754fc933398b337cd3cbdb28aaeb35159ef"
|
||||
checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8"
|
||||
dependencies = [
|
||||
"console",
|
||||
"globset",
|
||||
@@ -1267,7 +1267,7 @@ dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1393,7 +1393,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1532,14 +1532,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1593,12 +1594,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
@@ -1606,8 +1606,18 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7393c226621f817964ffb3dc5704f9509e107a8b024b489cc2c1b217378785df"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1786,9 +1796,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pep440_rs"
|
||||
version = "0.7.1"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c8ee724d21f351f9d47276614ac9710975db827ba9fe2ca5a517ba648193307"
|
||||
checksum = "0922a442c78611fa8c5ed6065d2d898a820cf12fa90604217fdb2d01675efec7"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"unicode-width 0.2.0",
|
||||
@@ -1848,7 +1858,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2102,6 +2112,7 @@ dependencies = [
|
||||
"countme",
|
||||
"dir-test",
|
||||
"hashbrown 0.15.0",
|
||||
"indexmap",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"memchr",
|
||||
@@ -2306,7 +2317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2523,7 +2534,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2546,7 +2557,7 @@ dependencies = [
|
||||
"natord",
|
||||
"path-absolutize",
|
||||
"pathdiff",
|
||||
"pep440_rs 0.7.1",
|
||||
"pep440_rs 0.7.2",
|
||||
"pyproject-toml",
|
||||
"quick-junit",
|
||||
"regex",
|
||||
@@ -2590,7 +2601,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"ruff_python_trivia",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2627,6 +2638,7 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
@@ -2837,7 +2849,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -2876,7 +2888,7 @@ dependencies = [
|
||||
"matchit",
|
||||
"path-absolutize",
|
||||
"path-slash",
|
||||
"pep440_rs 0.7.1",
|
||||
"pep440_rs 0.7.2",
|
||||
"regex",
|
||||
"ruff_cache",
|
||||
"ruff_formatter",
|
||||
@@ -3008,7 +3020,7 @@ dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -3042,7 +3054,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3065,9 +3077,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.213"
|
||||
version = "1.0.214"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
|
||||
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -3085,13 +3097,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.213"
|
||||
version = "1.0.214"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
|
||||
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3102,7 +3114,7 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3125,7 +3137,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3166,7 +3178,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3268,7 +3280,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3290,9 +3302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.85"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3307,7 +3319,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3370,7 +3382,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3381,28 +3393,28 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.65"
|
||||
version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
|
||||
checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.65"
|
||||
version = "1.0.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
|
||||
checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3514,7 +3526,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3772,7 +3784,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3858,7 +3870,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -3892,7 +3904,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
@@ -3926,7 +3938,7 @@ checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4214,7 +4226,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.85",
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -81,6 +81,7 @@ hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
indexmap = {version = "2.6.0" }
|
||||
indicatif = { version = "0.17.8" }
|
||||
indoc = { version = "2.0.4" }
|
||||
insta = { version = "1.35.1" }
|
||||
@@ -101,7 +102,7 @@ matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
mimalloc = { version = "0.1.39" }
|
||||
natord = { version = "1.0.9" }
|
||||
notify = { version = "6.1.1" }
|
||||
notify = { version = "7.0.0" }
|
||||
ordermap = { version = "0.5.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
|
||||
@@ -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.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.7.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.3/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.2
|
||||
rev: v0.7.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -5,8 +5,6 @@ use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
@@ -14,7 +12,9 @@ use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use target_version::TargetVersion;
|
||||
|
||||
use crate::logging::{setup_tracing, Verbosity};
|
||||
@@ -318,8 +318,9 @@ impl MainLoop {
|
||||
} => {
|
||||
let has_diagnostics = !result.is_empty();
|
||||
if check_revision == revision {
|
||||
#[allow(clippy::print_stdout)]
|
||||
for diagnostic in result {
|
||||
tracing::error!("{}", diagnostic);
|
||||
println!("{}", diagnostic.display(db));
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -378,7 +379,10 @@ impl MainLoopCancellationToken {
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted { result: Vec<String>, revision: u64 },
|
||||
CheckCompleted {
|
||||
result: Vec<Box<dyn Diagnostic>>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ license = { workspace = true }
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["salsa"] }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
@@ -24,7 +24,8 @@ bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
itertools = { workspace = true}
|
||||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -43,10 +44,9 @@ red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
dir-test = {workspace = true}
|
||||
dir-test = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Starred expression annotations
|
||||
|
||||
Type annotations for `*args` can be starred expressions themselves:
|
||||
|
||||
```py
|
||||
from typing_extensions import TypeVarTuple
|
||||
|
||||
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
|
||||
|
||||
return (*args, 1)
|
||||
|
||||
# TODO should be tuple[Literal[True], Literal["a"], int]
|
||||
reveal_type(append_int(True, "a")) # revealed: @Todo
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# String annotations
|
||||
|
||||
```py
|
||||
def f() -> "int":
|
||||
return 1
|
||||
|
||||
# TODO: We do not support string annotations, but we should not panic if we encounter them
|
||||
reveal_type(f()) # revealed: @Todo
|
||||
```
|
||||
@@ -85,7 +85,7 @@ f = Foo()
|
||||
# that `Foo.__iadd__` may be unbound as additional context.
|
||||
f += "Hello, world!"
|
||||
|
||||
reveal_type(f) # revealed: int
|
||||
reveal_type(f) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Partially bound with `__add__`
|
||||
@@ -104,8 +104,7 @@ class Foo:
|
||||
f = Foo()
|
||||
f += "Hello, world!"
|
||||
|
||||
# TODO(charlie): This should be `int | str`, since `__iadd__` may be unbound.
|
||||
reveal_type(f) # revealed: int
|
||||
reveal_type(f) # revealed: int | str
|
||||
```
|
||||
|
||||
## Partially bound target union
|
||||
@@ -127,8 +126,7 @@ else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
# TODO(charlie): This should be `str | int | float`
|
||||
reveal_type(f) # revealed: @Todo
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
## Target union
|
||||
@@ -149,6 +147,36 @@ else:
|
||||
f = 42.0
|
||||
f += 12
|
||||
|
||||
# TODO(charlie): This should be `str | float`.
|
||||
reveal_type(f) # revealed: @Todo
|
||||
reveal_type(f) # revealed: str | float
|
||||
```
|
||||
|
||||
## Partially bound target union with `__add__`
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
flag = bool_instance()
|
||||
|
||||
class Foo:
|
||||
def __add__(self, other: int) -> str:
|
||||
return "Hello, world!"
|
||||
if bool_instance():
|
||||
def __iadd__(self, other: int) -> int:
|
||||
return 42
|
||||
|
||||
class Bar:
|
||||
def __add__(self, other: int) -> bytes:
|
||||
return b"Hello, world!"
|
||||
|
||||
def __iadd__(self, other: int) -> float:
|
||||
return 42.0
|
||||
|
||||
if flag:
|
||||
f = Foo()
|
||||
else:
|
||||
f = Bar()
|
||||
f += 12
|
||||
|
||||
reveal_type(f) # revealed: int | str | float
|
||||
```
|
||||
|
||||
@@ -18,3 +18,38 @@ else:
|
||||
|
||||
reveal_type(C.x) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Inherited attributes
|
||||
|
||||
```py
|
||||
class A:
|
||||
X = "foo"
|
||||
|
||||
class B(A): ...
|
||||
class C(B): ...
|
||||
|
||||
reveal_type(C.X) # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
## Inherited attributes (multiple inheritance)
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
|
||||
class F(O):
|
||||
X = 56
|
||||
|
||||
class E(O):
|
||||
X = 42
|
||||
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
|
||||
# `E` is earlier in the MRO than `F`, so we should use the type of `E.X`
|
||||
reveal_type(A.X) # revealed: Literal[42]
|
||||
```
|
||||
|
||||
@@ -202,11 +202,7 @@ reveal_type(A() + B()) # revealed: MyString
|
||||
# N.B. Still a subtype of `A`, even though `A` does not appear directly in the class's `__bases__`
|
||||
class C(B): ...
|
||||
|
||||
# TODO: we currently only understand direct subclasses as subtypes of the superclass.
|
||||
# We need to iterate through the full MRO rather than just the class's bases;
|
||||
# if we do, we'll understand `C` as a subtype of `A`, and correctly understand this as being
|
||||
# `MyString` rather than `str`
|
||||
reveal_type(A() + C()) # revealed: str
|
||||
reveal_type(A() + C()) # revealed: MyString
|
||||
```
|
||||
|
||||
## Reflected precedence 2
|
||||
|
||||
@@ -18,3 +18,58 @@ class Unit: ...
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly unbound `__call__` method
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class PossiblyNotCallable:
|
||||
if flag():
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound callable
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-callable `__call__`
|
||||
|
||||
```py
|
||||
class NonCallable:
|
||||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `NonCallable` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly non-callable `__call__`
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class NonCallable:
|
||||
if flag():
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
@@ -44,3 +44,16 @@ reveal_type(bar()) # revealed: @Todo
|
||||
nonsense = 123
|
||||
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
```
|
||||
|
||||
## Potentially unbound function
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Identity tests
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_a() -> A: ...
|
||||
def get_object() -> object: ...
|
||||
|
||||
a1 = get_a()
|
||||
a2 = get_a()
|
||||
|
||||
n1 = None
|
||||
n2 = None
|
||||
|
||||
o = get_object()
|
||||
|
||||
reveal_type(a1 is a1) # revealed: bool
|
||||
reveal_type(a1 is a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is n2) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is a1) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is o) # revealed: bool
|
||||
reveal_type(n1 is o) # revealed: bool
|
||||
|
||||
reveal_type(a1 is not a1) # revealed: bool
|
||||
reveal_type(a1 is not a2) # revealed: bool
|
||||
|
||||
reveal_type(n1 is not n1) # revealed: Literal[False]
|
||||
reveal_type(n1 is not n2) # revealed: Literal[False]
|
||||
|
||||
reveal_type(a1 is not n1) # revealed: Literal[True]
|
||||
reveal_type(n1 is not a1) # revealed: Literal[True]
|
||||
|
||||
reveal_type(a1 is not o) # revealed: bool
|
||||
reveal_type(n1 is not o) # revealed: bool
|
||||
```
|
||||
@@ -93,13 +93,11 @@ class AlwaysFalse:
|
||||
def __contains__(self, item: int) -> Literal[""]:
|
||||
return ""
|
||||
|
||||
# TODO: it should be Literal[True] and Literal[False]
|
||||
reveal_type(42 in AlwaysTrue()) # revealed: @Todo
|
||||
reveal_type(42 not in AlwaysTrue()) # revealed: @Todo
|
||||
reveal_type(42 in AlwaysTrue()) # revealed: Literal[True]
|
||||
reveal_type(42 not in AlwaysTrue()) # revealed: Literal[False]
|
||||
|
||||
# TODO: it should be Literal[False] and Literal[True]
|
||||
reveal_type(42 in AlwaysFalse()) # revealed: @Todo
|
||||
reveal_type(42 not in AlwaysFalse()) # revealed: @Todo
|
||||
reveal_type(42 in AlwaysFalse()) # revealed: Literal[False]
|
||||
reveal_type(42 not in AlwaysFalse()) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## No Fallback for `__contains__`
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# Comparison: Intersections
|
||||
|
||||
## Positive contributions
|
||||
|
||||
If we have an intersection type `A & B` and we get a definitive true/false answer for one of the
|
||||
types, we can infer that the result for the intersection type is also true/false:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
class Child1(Base):
|
||||
def __eq__(self, other) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class Child2(Base): ...
|
||||
|
||||
def get_base() -> Base: ...
|
||||
|
||||
x = get_base()
|
||||
c1 = Child1()
|
||||
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
```
|
||||
|
||||
## Negative contributions
|
||||
|
||||
Negative contributions to the intersection type only allow simplifications in a few special cases
|
||||
(equality and identity comparisons).
|
||||
|
||||
### Equality comparisons
|
||||
|
||||
#### Literal strings
|
||||
|
||||
```py
|
||||
x = "x" * 1_000_000_000
|
||||
y = "y" * 1_000_000_000
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
|
||||
if x != "abc":
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
||||
|
||||
reveal_type(x == "abc") # revealed: Literal[False]
|
||||
reveal_type("abc" == x) # revealed: Literal[False]
|
||||
reveal_type(x == "something else") # revealed: bool
|
||||
reveal_type("something else" == x) # revealed: bool
|
||||
|
||||
reveal_type(x != "abc") # revealed: Literal[True]
|
||||
reveal_type("abc" != x) # revealed: Literal[True]
|
||||
reveal_type(x != "something else") # revealed: bool
|
||||
reveal_type("something else" != x) # revealed: bool
|
||||
|
||||
reveal_type(x == y) # revealed: bool
|
||||
reveal_type(y == x) # revealed: bool
|
||||
reveal_type(x != y) # revealed: bool
|
||||
reveal_type(y != x) # revealed: bool
|
||||
|
||||
reveal_type(x >= "abc") # revealed: bool
|
||||
reveal_type("abc" >= x) # revealed: bool
|
||||
|
||||
reveal_type(x in "abc") # revealed: bool
|
||||
reveal_type("abc" in x) # revealed: bool
|
||||
```
|
||||
|
||||
#### Integers
|
||||
|
||||
```py
|
||||
def get_int() -> int: ...
|
||||
|
||||
x = get_int()
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity comparisons
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
o = object()
|
||||
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
|
||||
### Unsupported operators for positive contributions
|
||||
|
||||
Raise an error if any of the positive contributions to the intersection type are unsupported for the
|
||||
given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
|
||||
Do *not* raise an error if any of the negative contributions to the intersection type are
|
||||
unsupported for the given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# Exception Handling
|
||||
|
||||
## Invalid syntax
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
try:
|
||||
print
|
||||
except as e: # error: [invalid-syntax]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
```
|
||||
@@ -0,0 +1,24 @@
|
||||
# If expression
|
||||
|
||||
## Union
|
||||
|
||||
```py
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
reveal_type(1 if bool_instance() else 2) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Statically known branches
|
||||
|
||||
```py
|
||||
reveal_type(1 if True else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if "not empty" else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if (1,) else 2) # revealed: Literal[1]
|
||||
reveal_type(1 if 1 else 2) # revealed: Literal[1]
|
||||
|
||||
reveal_type(1 if False else 2) # revealed: Literal[2]
|
||||
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]
|
||||
```
|
||||
@@ -0,0 +1,91 @@
|
||||
# Literal
|
||||
|
||||
<https://typing.readthedocs.io/en/latest/spec/literal.html#literals>
|
||||
|
||||
## Parameterization
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
from enum import Enum
|
||||
|
||||
mode: Literal["w", "r"]
|
||||
mode2: Literal["w"] | Literal["r"]
|
||||
union_var: Literal[Literal[Literal[1, 2, 3], "foo"], 5, None]
|
||||
a1: Literal[26]
|
||||
a2: Literal[0x1A]
|
||||
a3: Literal[-4]
|
||||
a4: Literal["hello world"]
|
||||
a5: Literal[b"hello world"]
|
||||
a6: Literal[True]
|
||||
a7: Literal[None]
|
||||
a8: Literal[Literal[1]]
|
||||
a9: Literal[Literal["w"], Literal["r"], Literal[Literal["w+"]]]
|
||||
|
||||
class Color(Enum):
|
||||
RED = 0
|
||||
GREEN = 1
|
||||
BLUE = 2
|
||||
|
||||
b1: Literal[Color.RED]
|
||||
|
||||
def f():
|
||||
reveal_type(mode) # revealed: Literal["w", "r"]
|
||||
reveal_type(mode2) # revealed: Literal["w", "r"]
|
||||
# TODO: should be revealed: Literal[1, 2, 3, "foo", 5] | None
|
||||
reveal_type(union_var) # revealed: Literal[1, 2, 3, 5] | Literal["foo"] | None
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
reveal_type(a2) # revealed: Literal[26]
|
||||
reveal_type(a3) # revealed: Literal[-4]
|
||||
reveal_type(a4) # revealed: Literal["hello world"]
|
||||
reveal_type(a5) # revealed: Literal[b"hello world"]
|
||||
reveal_type(a6) # revealed: Literal[True]
|
||||
reveal_type(a7) # revealed: None
|
||||
reveal_type(a8) # revealed: Literal[1]
|
||||
reveal_type(a9) # revealed: Literal["w", "r", "w+"]
|
||||
# TODO: This should be Color.RED
|
||||
reveal_type(b1) # revealed: Literal[0]
|
||||
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid1: Literal[3 + 4]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid2: Literal[4 + 3j]
|
||||
# error: [invalid-literal-parameter]
|
||||
invalid3: Literal[(3, 4)]
|
||||
invalid4: Literal[
|
||||
1 + 2, # error: [invalid-literal-parameter]
|
||||
"foo",
|
||||
hello, # error: [invalid-literal-parameter]
|
||||
(1, 2, 3), # error: [invalid-literal-parameter]
|
||||
]
|
||||
```
|
||||
|
||||
## Detecting Literal outside typing and typing_extensions
|
||||
|
||||
Only Literal that is defined in typing and typing_extension modules is detected as the special
|
||||
Literal.
|
||||
|
||||
```pyi path=other.pyi
|
||||
from typing import _SpecialForm
|
||||
|
||||
Literal: _SpecialForm
|
||||
```
|
||||
|
||||
```py
|
||||
from other import Literal
|
||||
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Detecting typing_extensions.Literal
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
a1: Literal[26]
|
||||
|
||||
def f():
|
||||
reveal_type(a1) # revealed: Literal[26]
|
||||
```
|
||||
@@ -238,7 +238,7 @@ class Test:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# TODO: we should emit a diagnostic here (it might not be iterable)
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
for x in Test() if coinflip() else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
196
crates/red_knot_python_semantic/resources/mdtest/metaclass.md
Normal file
196
crates/red_knot_python_semantic/resources/mdtest/metaclass.md
Normal file
@@ -0,0 +1,196 @@
|
||||
## Default
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
|
||||
reveal_type(M.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `object`
|
||||
|
||||
```py
|
||||
reveal_type(object.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `type`
|
||||
|
||||
```py
|
||||
reveal_type(type.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class B(metaclass=M): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Invalid metaclass
|
||||
|
||||
A class which doesn't inherit `type` (and/or doesn't implement a custom `__new__` accepting the same
|
||||
arguments as `type.__new__`) isn't a valid metaclass.
|
||||
|
||||
```py
|
||||
class M: ...
|
||||
class A(metaclass=M): ...
|
||||
|
||||
# TODO: emit a diagnostic for the invalid metaclass
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Linear inheritance
|
||||
|
||||
If a class is a subclass of a class with a custom metaclass, then the subclass will also have that
|
||||
metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Conflict (1)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Conflict (2)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` (metaclass of `B`) and `M1` (metaclass of base class `A`) have no subclass relationship"
|
||||
class B(A, metaclass=M2): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Common metaclass
|
||||
|
||||
A class has two explicit bases, both of which have the same metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(metaclass=M): ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Metaclass metaclass
|
||||
|
||||
A class has an explicit base with a custom metaclass. That metaclass itself has a custom metaclass.
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type, metaclass=M1): ...
|
||||
class M3(M2): ...
|
||||
class A(metaclass=M3): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M3]
|
||||
```
|
||||
|
||||
## Diamond inheritance
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class M1(M): ...
|
||||
class M2(M): ...
|
||||
class M12(M1, M2): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
class C(metaclass=M12): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` (metaclass of base class `A`) and `M2` (metaclass of base class `B`) have no subclass relationship"
|
||||
class D(A, B, C): ...
|
||||
|
||||
reveal_type(D.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unknown
|
||||
|
||||
```py
|
||||
from nonexistent_module import UnknownClass # error: [unresolved-import]
|
||||
|
||||
class C(UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[type] & Unknown`
|
||||
reveal_type(C.__class__) # revealed: Literal[type]
|
||||
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[M] & Unknown`
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Duplicate
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`"
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Non-class
|
||||
|
||||
When a class has an explicit `metaclass` that is not a class, but is a callable that accepts
|
||||
`type.__new__` arguments, we should return the meta type of its return type.
|
||||
|
||||
```py
|
||||
def f(*args, **kwargs) -> int: ...
|
||||
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO should be `type[int]`
|
||||
reveal_type(A.__class__) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-def]
|
||||
class B(C): ... # error: [cyclic-class-def]
|
||||
class C(A): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## PEP 695 generic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A[T: str](metaclass=M): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
409
crates/red_knot_python_semantic/resources/mdtest/mro.md
Normal file
409
crates/red_knot_python_semantic/resources/mdtest/mro.md
Normal file
@@ -0,0 +1,409 @@
|
||||
# Method Resolution Order tests
|
||||
|
||||
Tests that assert that we can infer the correct type for a class's `__mro__` attribute.
|
||||
|
||||
This attribute is rarely accessed directly at runtime. However, it's extremely important for *us* to
|
||||
know the precise possible values of a class's Method Resolution Order, or we won't be able to infer
|
||||
the correct type of attributes accessed from instances.
|
||||
|
||||
For documentation on method resolution orders, see:
|
||||
|
||||
- <https://docs.python.org/3/glossary.html#term-method-resolution-order>
|
||||
- <https://docs.python.org/3/howto/mro.html#python-2-3-mro>
|
||||
|
||||
## No bases
|
||||
|
||||
```py
|
||||
class C: ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
```
|
||||
|
||||
## The special case: `object` itself
|
||||
|
||||
```py
|
||||
reveal_type(object.__mro__) # revealed: tuple[Literal[object]]
|
||||
```
|
||||
|
||||
## Explicit inheritance from `object`
|
||||
|
||||
```py
|
||||
class C(object): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[object]]
|
||||
```
|
||||
|
||||
## Explicit inheritance from non-`object` single base
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[A], Literal[object]]
|
||||
```
|
||||
|
||||
## Linearization of multiple bases
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[A], Literal[B], Literal[object]]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (1)
|
||||
|
||||
This is "ex_2" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (2)
|
||||
|
||||
This is "ex_5" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class F(O): ...
|
||||
class E(O): ...
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(D, E): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[D], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[C], Literal[D], Literal[E], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (3)
|
||||
|
||||
This is "ex_6" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class F(O): ...
|
||||
class E(O): ...
|
||||
class D(O): ...
|
||||
class C(D, F): ...
|
||||
class B(E, D): ...
|
||||
class A(B, C): ...
|
||||
|
||||
# revealed: tuple[Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(C.__mro__)
|
||||
# revealed: tuple[Literal[B], Literal[E], Literal[D], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
# revealed: tuple[Literal[A], Literal[B], Literal[E], Literal[C], Literal[D], Literal[F], Literal[O], Literal[object]]
|
||||
reveal_type(A.__mro__)
|
||||
```
|
||||
|
||||
## Complex diamond inheritance (4)
|
||||
|
||||
This is "ex_9" from <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
|
||||
```py
|
||||
class O: ...
|
||||
class A(O): ...
|
||||
class B(O): ...
|
||||
class C(O): ...
|
||||
class D(O): ...
|
||||
class E(O): ...
|
||||
class K1(A, B, C): ...
|
||||
class K2(D, B, E): ...
|
||||
class K3(D, A): ...
|
||||
class Z(K1, K2, K3): ...
|
||||
|
||||
# revealed: tuple[Literal[K1], Literal[A], Literal[B], Literal[C], Literal[O], Literal[object]]
|
||||
reveal_type(K1.__mro__)
|
||||
# revealed: tuple[Literal[K2], Literal[D], Literal[B], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(K2.__mro__)
|
||||
# revealed: tuple[Literal[K3], Literal[D], Literal[A], Literal[O], Literal[object]]
|
||||
reveal_type(K3.__mro__)
|
||||
# revealed: tuple[Literal[Z], Literal[K1], Literal[K2], Literal[K3], Literal[D], Literal[A], Literal[B], Literal[C], Literal[E], Literal[O], Literal[object]]
|
||||
reveal_type(Z.__mro__)
|
||||
```
|
||||
|
||||
## Inheritance from `Unknown`
|
||||
|
||||
```py
|
||||
from does_not_exist import DoesNotExist # error: [unresolved-import]
|
||||
|
||||
class A(DoesNotExist): ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D(A, B, C): ...
|
||||
class E(B, C): ...
|
||||
class F(E, A): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Unknown, Literal[object]]
|
||||
reveal_type(D.__mro__) # revealed: tuple[Literal[D], Literal[A], Unknown, Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(E.__mro__) # revealed: tuple[Literal[E], Literal[B], Literal[C], Literal[object]]
|
||||
reveal_type(F.__mro__) # revealed: tuple[Literal[F], Literal[E], Literal[B], Literal[C], Literal[A], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors at runtime
|
||||
|
||||
If the class's `__bases__` cause an exception to be raised at runtime and therefore the class
|
||||
creation to fail, we infer the class's `__mro__` as being `[<class>, Unknown, object]`:
|
||||
|
||||
```py
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[<class 'object'>, <class 'int'>]`"
|
||||
class Foo(object, int): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar(Foo): ...
|
||||
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
# This is the `TypeError` at the bottom of "ex_2"
|
||||
# in the examples at <https://docs.python.org/3/howto/mro.html#the-end>
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
class A(X, Y): ...
|
||||
class B(Y, X): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__) # revealed: tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Z` with bases list `[<class 'A'>, <class 'B'>]`"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
|
||||
class AA(Z): ...
|
||||
|
||||
reveal_type(AA.__mro__) # revealed: tuple[Literal[AA], Literal[Z], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` includes a `Union`
|
||||
|
||||
We don't support union types in a class's bases; a base must resolve to a single `ClassLiteralType`.
|
||||
If we find a union type in a class's bases, we infer the class's `__mro__` as being
|
||||
`[<class>, Unknown, object]`, the same as for MROs that cause errors at runtime.
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
if returns_bool():
|
||||
x = A
|
||||
else:
|
||||
x = B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` includes multiple `Union`s
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
class D: ...
|
||||
|
||||
if returns_bool():
|
||||
x = A
|
||||
else:
|
||||
x = B
|
||||
|
||||
if returns_bool():
|
||||
y = C
|
||||
else:
|
||||
y = D
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(y) # revealed: Literal[C, D]
|
||||
|
||||
# error: 11 [invalid-base] "Invalid class base with type `Literal[A, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
# error: 14 [invalid-base] "Invalid class base with type `Literal[C, D]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Foo(x, y): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists that cause errors... now with `Union`s
|
||||
|
||||
```py
|
||||
def returns_bool() -> bool:
|
||||
return True
|
||||
|
||||
class O: ...
|
||||
class X(O): ...
|
||||
class Y(O): ...
|
||||
|
||||
if bool():
|
||||
foo = Y
|
||||
else:
|
||||
foo = object
|
||||
|
||||
# error: 21 [invalid-base] "Invalid class base with type `Literal[Y, object]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class PossibleError(foo, X): ...
|
||||
|
||||
reveal_type(PossibleError.__mro__) # revealed: tuple[Literal[PossibleError], Unknown, Literal[object]]
|
||||
|
||||
class A(X, Y): ...
|
||||
|
||||
reveal_type(A.__mro__) # revealed: tuple[Literal[A], Literal[X], Literal[Y], Literal[O], Literal[object]]
|
||||
|
||||
if returns_bool():
|
||||
class B(X, Y): ...
|
||||
|
||||
else:
|
||||
class B(Y, X): ...
|
||||
|
||||
# revealed: tuple[Literal[B], Literal[X], Literal[Y], Literal[O], Literal[object]] | tuple[Literal[B], Literal[Y], Literal[X], Literal[O], Literal[object]]
|
||||
reveal_type(B.__mro__)
|
||||
|
||||
# error: 12 [invalid-base] "Invalid class base with type `Literal[B, B]` (all bases must be a class, `Any`, `Unknown` or `Todo`)"
|
||||
class Z(A, B): ...
|
||||
|
||||
reveal_type(Z.__mro__) # revealed: tuple[Literal[Z], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate bases
|
||||
|
||||
```py
|
||||
class Foo(str, str): ... # error: 16 [duplicate-base] "Duplicate base class `str`"
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Spam: ...
|
||||
class Eggs: ...
|
||||
class Ham(
|
||||
Spam,
|
||||
Eggs,
|
||||
Spam, # error: [duplicate-base] "Duplicate base class `Spam`"
|
||||
Eggs, # error: [duplicate-base] "Duplicate base class `Eggs`"
|
||||
): ...
|
||||
|
||||
reveal_type(Ham.__mro__) # revealed: tuple[Literal[Ham], Unknown, Literal[object]]
|
||||
|
||||
class Mushrooms: ...
|
||||
class Omelette(Spam, Eggs, Mushrooms, Mushrooms): ... # error: [duplicate-base]
|
||||
|
||||
reveal_type(Omelette.__mro__) # revealed: tuple[Literal[Omelette], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## `__bases__` lists with duplicate `Unknown` bases
|
||||
|
||||
```py
|
||||
# error: [unresolved-import]
|
||||
# error: [unresolved-import]
|
||||
from does_not_exist import unknown_object_1, unknown_object_2
|
||||
|
||||
reveal_type(unknown_object_1) # revealed: Unknown
|
||||
reveal_type(unknown_object_2) # revealed: Unknown
|
||||
|
||||
# We *should* emit an error here to warn the user that we have no idea
|
||||
# what the MRO of this class should really be.
|
||||
# However, we don't complain about "duplicate base classes" here,
|
||||
# even though two classes are both inferred as being `Unknown`.
|
||||
#
|
||||
# (TODO: should we revisit this? Does it violate the gradual guarantee?
|
||||
# Should we just silently infer `[Foo, Unknown, object]` as the MRO here
|
||||
# without emitting any error at all? Not sure...)
|
||||
#
|
||||
# error: [inconsistent-mro] "Cannot create a consistent method resolution order (MRO) for class `Foo` with bases list `[Unknown, Unknown]`"
|
||||
class Foo(unknown_object_1, unknown_object_2): ...
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Unrelated objects inferred as `Any`/`Unknown` do not have special `__mro__` attributes
|
||||
|
||||
```py
|
||||
from does_not_exist import unknown_object # error: [unresolved-import]
|
||||
|
||||
reveal_type(unknown_object) # revealed: Unknown
|
||||
reveal_type(unknown_object.__mro__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Classes that inherit from themselves
|
||||
|
||||
These are invalid, but we need to be able to handle them gracefully without panicking.
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Foo): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Foo) # revealed: Literal[Foo]
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
|
||||
class Bar: ...
|
||||
class Baz: ...
|
||||
class Boz(Bar, Baz, Boz): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Boz) # revealed: Literal[Boz]
|
||||
reveal_type(Boz.__mro__) # revealed: tuple[Literal[Boz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with indirect cycles in their MROs
|
||||
|
||||
These are similarly unlikely, but we still shouldn't crash:
|
||||
|
||||
```py path=a.pyi
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MROs, and multiple inheritance
|
||||
|
||||
```py path=a.pyi
|
||||
class Spam: ...
|
||||
class Foo(Bar): ... # error: [cyclic-class-def]
|
||||
class Bar(Baz): ... # error: [cyclic-class-def]
|
||||
class Baz(Foo, Spam): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(Foo.__mro__) # revealed: tuple[Literal[Foo], Unknown, Literal[object]]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
## Classes with cycles in their MRO, and a sub-graph
|
||||
|
||||
```py path=a.pyi
|
||||
class FooCycle(BarCycle): ... # error: [cyclic-class-def]
|
||||
class Foo: ...
|
||||
class BarCycle(FooCycle): ... # error: [cyclic-class-def]
|
||||
class Bar(Foo): ...
|
||||
|
||||
# TODO: can we avoid emitting the errors for these?
|
||||
# The classes have cyclic superclasses,
|
||||
# but are not themselves cyclic...
|
||||
class Baz(Bar, BarCycle): ... # error: [cyclic-class-def]
|
||||
class Spam(Baz): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(FooCycle.__mro__) # revealed: tuple[Literal[FooCycle], Unknown, Literal[object]]
|
||||
reveal_type(BarCycle.__mro__) # revealed: tuple[Literal[BarCycle], Unknown, Literal[object]]
|
||||
reveal_type(Baz.__mro__) # revealed: tuple[Literal[Baz], Unknown, Literal[object]]
|
||||
reveal_type(Spam.__mro__) # revealed: tuple[Literal[Spam], Unknown, Literal[object]]
|
||||
```
|
||||
@@ -0,0 +1,282 @@
|
||||
# Narrowing for conditionals with boolean expressions
|
||||
|
||||
## Narrowing in `and` conditional
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(x, B):
|
||||
reveal_type(x) # revealed: A & B
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | A & ~B
|
||||
```
|
||||
|
||||
## Arms might not add narrowing constraints
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and bool_instance():
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if bool_instance() and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## Statically known arms
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and True:
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True and isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if False and isinstance(x, A):
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B
|
||||
|
||||
if False or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
if True or isinstance(x, A):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
# TODO: should emit an `unreachable code` diagnostic
|
||||
reveal_type(x) # revealed: B & ~A
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
```
|
||||
|
||||
## The type of multiple symbols can be narrowed down
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def instance() -> A | B:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) and isinstance(y, B):
|
||||
reveal_type(x) # revealed: A
|
||||
reveal_type(y) # revealed: B
|
||||
else:
|
||||
# No narrowing: Only-one or both checks might have failed
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
```
|
||||
|
||||
## Narrowing in `or` conditional
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B):
|
||||
reveal_type(x) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## In `or`, all arms should add constraint in order to narrow
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(x, B) or bool_instance():
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~A & ~B
|
||||
```
|
||||
|
||||
## in `or`, all arms should narrow the same set of symbols
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
y = instance()
|
||||
|
||||
if isinstance(x, A) or isinstance(y, A):
|
||||
# The predicate might be satisfied by the right side, so the type of `x` can’t be narrowed down here.
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
# The same for `y`
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
else:
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A
|
||||
reveal_type(y) # revealed: B & ~A | C & ~A
|
||||
|
||||
if (isinstance(x, A) and isinstance(y, A)) or (isinstance(x, B) and isinstance(y, B)):
|
||||
# Here, types of `x` and `y` can be narrowd since all `or` arms constraint them.
|
||||
reveal_type(x) # revealed: A | B
|
||||
reveal_type(y) # revealed: A | B
|
||||
else:
|
||||
reveal_type(x) # revealed: A | B | C
|
||||
reveal_type(y) # revealed: A | B | C
|
||||
```
|
||||
|
||||
## mixing `and` and `not`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) and not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B & ~C
|
||||
else:
|
||||
# ~(B & ~C) -> ~B | C -> (A & ~B) | (C & ~B) | C -> (A & ~B) | C
|
||||
reveal_type(x) # revealed: A & ~B | C
|
||||
```
|
||||
|
||||
## mixing `or` and `not`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, B) or not isinstance(x, C):
|
||||
reveal_type(x) # revealed: B | A & ~C
|
||||
else:
|
||||
reveal_type(x) # revealed: C & ~B
|
||||
```
|
||||
|
||||
## `or` with nested `and`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) or (isinstance(x, B) and not isinstance(x, C)):
|
||||
reveal_type(x) # revealed: A | B & ~C
|
||||
else:
|
||||
# ~(A | (B & ~C)) -> ~A & ~(B & ~C) -> ~A & (~B | C) -> (~A & C) | (~A ~ B)
|
||||
reveal_type(x) # revealed: C & ~A
|
||||
```
|
||||
|
||||
## `and` with nested `or`
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
class C: ...
|
||||
|
||||
def instance() -> A | B | C:
|
||||
return A()
|
||||
|
||||
x = instance()
|
||||
|
||||
if isinstance(x, A) and (isinstance(x, B) or not isinstance(x, C)):
|
||||
# A & (B | ~C) -> (A & B) | (A & ~C)
|
||||
reveal_type(x) # revealed: A & B | A & ~C
|
||||
else:
|
||||
# ~((A & B) | (A & ~C)) ->
|
||||
# ~(A & B) & ~(A & ~C) ->
|
||||
# (~A | ~B) & (~A | C) ->
|
||||
# [(~A | ~B) & ~A] | [(~A | ~B) & C] ->
|
||||
# ~A | (~A & C) | (~B & C) ->
|
||||
# ~A | (C & ~B) ->
|
||||
# ~A | (C & ~B) The positive side of ~A is A | B | C ->
|
||||
reveal_type(x) # revealed: B & ~A | C & ~A | C & ~B
|
||||
```
|
||||
|
||||
## Boolean expression internal narrowing
|
||||
|
||||
```py
|
||||
def optional_string() -> str | None:
|
||||
return None
|
||||
|
||||
x = optional_string()
|
||||
y = optional_string()
|
||||
|
||||
if x is None and y is not x:
|
||||
reveal_type(y) # revealed: str
|
||||
|
||||
# Neither of the conditions alone is sufficient for narrowing y's type:
|
||||
if x is None:
|
||||
reveal_type(y) # revealed: str | None
|
||||
|
||||
if y is not x:
|
||||
reveal_type(y) # revealed: str | None
|
||||
```
|
||||
@@ -0,0 +1,244 @@
|
||||
# Narrowing for `issubclass` checks
|
||||
|
||||
Narrowing for `issubclass(class, classinfo)` expressions.
|
||||
|
||||
## `classinfo` is a single type
|
||||
|
||||
### Basic example
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Proper narrowing in `elif` and `else` branches
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
```
|
||||
|
||||
### Multiple derived classes
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
class Derived1(Base): ...
|
||||
class Derived2(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t1 = Derived1 if flag() else Derived2
|
||||
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
|
||||
t2 = Derived1 if flag() else Base
|
||||
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
|
||||
t3 = Derived1 if flag() else Unrelated
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
```
|
||||
|
||||
### Narrowing for non-literals
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_class() -> type[object]: ...
|
||||
|
||||
t = get_class()
|
||||
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
```
|
||||
|
||||
### Handling of `None`
|
||||
|
||||
```py
|
||||
from types import NoneType
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else NoneType
|
||||
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
|
||||
### (Nested) tuples of types
|
||||
|
||||
```py
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
## Special cases
|
||||
|
||||
### Emit a diagnostic if the first argument is of wrong type
|
||||
|
||||
#### Too wide
|
||||
|
||||
`type[object]` is a subtype of `object`, but not every `object` can be passed as the first argument
|
||||
to `issubclass`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
t = get_object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
```
|
||||
|
||||
#### Wrong
|
||||
|
||||
`Literal[1]` and `type` are entirely disjoint, so the inferred type of `Literal[1] & type[int]` is
|
||||
eagerly simplified to `Never` as a result of the type narrowing in the `if issubclass(t, int)`
|
||||
branch:
|
||||
|
||||
```py
|
||||
t = 1
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Do not use custom `issubclass` for narrowing
|
||||
|
||||
```py
|
||||
def issubclass(c, ci):
|
||||
return True
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is aliased
|
||||
|
||||
```py
|
||||
issubclass_alias = issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass_alias(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is imported
|
||||
|
||||
```py
|
||||
from builtins import issubclass as imported_issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if imported_issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do not narrow if second argument is not a proper `classinfo` argument
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, "str"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, (bytes, "str")):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, Any):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`issubclass` has no `foo` parameter)
|
||||
if issubclass(t, int, foo="bar"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
@@ -58,11 +58,9 @@ reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
# TODO: we don't currently understand `types.ModuleType` as inheriting from `object`;
|
||||
# these should not reveal `Unknown`:
|
||||
reveal_type(typing.__eq__) # revealed: Unknown
|
||||
reveal_type(typing.__class__) # revealed: Unknown
|
||||
reveal_type(typing.__module__) # revealed: Unknown
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[type]
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
|
||||
@@ -6,7 +6,12 @@ In type stubs, classes can reference themselves in their base class definitions.
|
||||
`typeshed`, we have `class str(Sequence[str]): ...`.
|
||||
|
||||
```py path=a.pyi
|
||||
class C(C): ...
|
||||
class Foo[T]: ...
|
||||
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
# TODO: actually is subscriptable
|
||||
# error: [non-subscriptable]
|
||||
class Bar(Foo[Bar]): ...
|
||||
|
||||
reveal_type(Bar) # revealed: Literal[Bar]
|
||||
reveal_type(Bar.__mro__) # revealed: tuple[Literal[Bar], Unknown, Literal[object]]
|
||||
```
|
||||
|
||||
@@ -39,7 +39,8 @@ reveal_type(UnionClassGetItem[0]) # revealed: str | int
|
||||
## Class getitem with class union
|
||||
|
||||
```py
|
||||
flag = True
|
||||
def bool_instance() -> bool:
|
||||
return True
|
||||
|
||||
class A:
|
||||
def __class_getitem__(cls, item: int) -> str:
|
||||
@@ -49,7 +50,7 @@ class B:
|
||||
def __class_getitem__(cls, item: int) -> int:
|
||||
return item
|
||||
|
||||
x = A if flag else B
|
||||
x = A if bool_instance() else B
|
||||
|
||||
reveal_type(x) # revealed: Literal[A, B]
|
||||
reveal_type(x[0]) # revealed: str | int
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Unary Operations
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Number:
|
||||
def __init__(self, value: int):
|
||||
self.value = 1
|
||||
@@ -18,7 +20,7 @@ a = Number()
|
||||
|
||||
reveal_type(+a) # revealed: int
|
||||
reveal_type(-a) # revealed: int
|
||||
reveal_type(~a) # revealed: @Todo
|
||||
reveal_type(~a) # revealed: Literal[True]
|
||||
|
||||
class NoDunder: ...
|
||||
|
||||
|
||||
@@ -145,13 +145,8 @@ reveal_type(f) # revealed: Unknown
|
||||
|
||||
### Non-iterable unpacking
|
||||
|
||||
TODO: Remove duplicate diagnostics. This is happening because for a sequence-like assignment target,
|
||||
multiple definitions are created and the inference engine runs on each of them which results in
|
||||
duplicate diagnostics.
|
||||
|
||||
```py
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
a, b = 1
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
@@ -22,6 +22,7 @@ pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
pub(crate) mod symbol;
|
||||
pub mod types;
|
||||
mod unpack;
|
||||
mod util;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
@@ -125,6 +125,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
///
|
||||
/// Use the Salsa cached [`symbol_table()`] query if you only need the
|
||||
/// symbol table for a single scope.
|
||||
#[track_caller]
|
||||
pub(super) fn symbol_table(&self, scope_id: FileScopeId) -> Arc<SymbolTable> {
|
||||
self.symbol_tables[scope_id].clone()
|
||||
}
|
||||
@@ -133,15 +134,18 @@ impl<'db> SemanticIndex<'db> {
|
||||
///
|
||||
/// Use the Salsa cached [`use_def_map()`] query if you only need the
|
||||
/// use-def map for a single scope.
|
||||
#[track_caller]
|
||||
pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> Arc<UseDefMap> {
|
||||
self.use_def_maps[scope_id].clone()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn ast_ids(&self, scope_id: FileScopeId) -> &AstIds {
|
||||
&self.ast_ids[scope_id]
|
||||
}
|
||||
|
||||
/// Returns the ID of the `expression`'s enclosing scope.
|
||||
#[track_caller]
|
||||
pub(crate) fn expression_scope_id(
|
||||
&self,
|
||||
expression: impl Into<ExpressionNodeKey>,
|
||||
@@ -151,11 +155,13 @@ impl<'db> SemanticIndex<'db> {
|
||||
|
||||
/// Returns the [`Scope`] of the `expression`'s enclosing scope.
|
||||
#[allow(unused)]
|
||||
#[track_caller]
|
||||
pub(crate) fn expression_scope(&self, expression: impl Into<ExpressionNodeKey>) -> &Scope {
|
||||
&self.scopes[self.expression_scope_id(expression)]
|
||||
}
|
||||
|
||||
/// Returns the [`Scope`] with the given id.
|
||||
#[track_caller]
|
||||
pub(crate) fn scope(&self, id: FileScopeId) -> &Scope {
|
||||
&self.scopes[id]
|
||||
}
|
||||
@@ -172,6 +178,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
|
||||
/// Returns the parent scope of `scope_id`.
|
||||
#[allow(unused)]
|
||||
#[track_caller]
|
||||
pub(crate) fn parent_scope(&self, scope_id: FileScopeId) -> Option<&Scope> {
|
||||
Some(&self.scopes[self.parent_scope_id(scope_id)?])
|
||||
}
|
||||
@@ -195,6 +202,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
}
|
||||
|
||||
/// Returns the [`Definition`] salsa ingredient for `definition_key`.
|
||||
#[track_caller]
|
||||
pub(crate) fn definition(
|
||||
&self,
|
||||
definition_key: impl Into<DefinitionNodeKey>,
|
||||
@@ -206,6 +214,7 @@ impl<'db> SemanticIndex<'db> {
|
||||
/// Panics if we have no expression ingredient for that node. We can only call this method for
|
||||
/// standalone-inferable expressions, which we call `add_standalone_expression` for in
|
||||
/// [`SemanticIndexBuilder`].
|
||||
#[track_caller]
|
||||
pub(crate) fn expression(
|
||||
&self,
|
||||
expression_key: impl Into<ExpressionNodeKey>,
|
||||
@@ -213,8 +222,18 @@ impl<'db> SemanticIndex<'db> {
|
||||
self.expressions_by_node[&expression_key.into()]
|
||||
}
|
||||
|
||||
pub(crate) fn try_expression(
|
||||
&self,
|
||||
expression_key: impl Into<ExpressionNodeKey>,
|
||||
) -> Option<Expression<'db>> {
|
||||
self.expressions_by_node
|
||||
.get(&expression_key.into())
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Returns the id of the scope that `node` creates. This is different from [`Definition::scope`] which
|
||||
/// returns the scope in which that definition is defined in.
|
||||
#[track_caller]
|
||||
pub(crate) fn node_scope(&self, node: NodeWithScopeRef) -> FileScopeId {
|
||||
self.scopes_by_node[&node.node_key()]
|
||||
}
|
||||
|
||||
@@ -87,6 +87,14 @@ pub trait HasScopedAstId {
|
||||
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id;
|
||||
}
|
||||
|
||||
impl<T: HasScopedAstId> HasScopedAstId for Box<T> {
|
||||
type Id = <T as HasScopedAstId>::Id;
|
||||
|
||||
fn scoped_ast_id(&self, db: &dyn Db, scope: ScopeId) -> Self::Id {
|
||||
self.as_ref().scoped_ast_id(db, scope)
|
||||
}
|
||||
}
|
||||
|
||||
/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`].
|
||||
#[newtype_index]
|
||||
pub struct ScopedExpressionId;
|
||||
|
||||
@@ -25,12 +25,13 @@ use crate::semantic_index::symbol::{
|
||||
};
|
||||
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
|
||||
use super::definition::{
|
||||
AssignmentKind, DefinitionCategory, ExceptHandlerDefinitionNodeRef,
|
||||
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef,
|
||||
WithItemDefinitionNodeRef,
|
||||
};
|
||||
|
||||
mod except_handlers;
|
||||
@@ -46,6 +47,7 @@ pub(super) struct SemanticIndexBuilder<'db> {
|
||||
current_assignments: Vec<CurrentAssignment<'db>>,
|
||||
/// The match case we're currently visiting.
|
||||
current_match_case: Option<CurrentMatchCase<'db>>,
|
||||
|
||||
/// Flow states at each `break` in the current loop.
|
||||
loop_break_states: Vec<FlowSnapshot>,
|
||||
/// Per-scope contexts regarding nested `try`/`except` statements
|
||||
@@ -112,9 +114,11 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
fn push_scope_with_parent(&mut self, node: NodeWithScopeRef, parent: Option<FileScopeId>) {
|
||||
let children_start = self.scopes.next_index() + 1;
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
let scope = Scope {
|
||||
parent,
|
||||
kind: node.scope_kind(),
|
||||
// SAFETY: `node` is guaranteed to be a child of `self.module`
|
||||
node: unsafe { node.to_kind(self.module.clone()) },
|
||||
descendents: children_start..children_start,
|
||||
};
|
||||
self.try_node_context_stack_manager.enter_nested_scope();
|
||||
@@ -124,15 +128,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.use_def_maps.push(UseDefMapBuilder::new());
|
||||
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::new());
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
// SAFETY: `node` is guaranteed to be a child of `self.module`
|
||||
let scope_id = ScopeId::new(
|
||||
self.db,
|
||||
self.file,
|
||||
file_scope_id,
|
||||
unsafe { node.to_kind(self.module.clone()) },
|
||||
countme::Count::default(),
|
||||
);
|
||||
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);
|
||||
@@ -203,10 +199,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.current_symbol_table().mark_symbol_used(id);
|
||||
}
|
||||
|
||||
fn add_definition<'a>(
|
||||
fn add_definition(
|
||||
&mut self,
|
||||
symbol: ScopedSymbolId,
|
||||
definition_node: impl Into<DefinitionNodeRef<'a>>,
|
||||
definition_node: impl Into<DefinitionNodeRef<'db>>,
|
||||
) -> Definition<'db> {
|
||||
let definition_node: DefinitionNodeRef<'_> = definition_node.into();
|
||||
#[allow(unsafe_code)]
|
||||
@@ -285,8 +281,12 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
debug_assert!(popped_assignment.is_some());
|
||||
}
|
||||
|
||||
fn current_assignment(&self) -> Option<&CurrentAssignment<'db>> {
|
||||
self.current_assignments.last()
|
||||
fn current_assignment(&self) -> Option<CurrentAssignment<'db>> {
|
||||
self.current_assignments.last().copied()
|
||||
}
|
||||
|
||||
fn current_assignment_mut(&mut self) -> Option<&mut CurrentAssignment<'db>> {
|
||||
self.current_assignments.last_mut()
|
||||
}
|
||||
|
||||
fn add_pattern_constraint(
|
||||
@@ -445,7 +445,7 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
self.pop_scope();
|
||||
}
|
||||
|
||||
fn declare_parameter(&mut self, parameter: AnyParameterRef) {
|
||||
fn declare_parameter(&mut self, parameter: AnyParameterRef<'db>) {
|
||||
let symbol = self.add_symbol(parameter.name().id().clone());
|
||||
|
||||
let definition = self.add_definition(symbol, parameter);
|
||||
@@ -619,24 +619,48 @@ where
|
||||
}
|
||||
ast::Stmt::Assign(node) => {
|
||||
debug_assert_eq!(&self.current_assignments, &[]);
|
||||
|
||||
self.visit_expr(&node.value);
|
||||
self.add_standalone_expression(&node.value);
|
||||
for (target_index, target) in node.targets.iter().enumerate() {
|
||||
let kind = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => Some(AssignmentKind::Sequence),
|
||||
ast::Expr::Name(_) => Some(AssignmentKind::Name),
|
||||
let value = self.add_standalone_expression(&node.value);
|
||||
|
||||
for target in &node.targets {
|
||||
// We only handle assignments to names and unpackings here, other targets like
|
||||
// attribute and subscript are handled separately as they don't create a new
|
||||
// definition.
|
||||
let current_assignment = match target {
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first: true,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
#[allow(unsafe_code)]
|
||||
unsafe {
|
||||
AstNodeRef::new(self.module.clone(), target)
|
||||
},
|
||||
value,
|
||||
countme::Count::default(),
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(kind) = kind {
|
||||
self.push_assignment(CurrentAssignment::Assign {
|
||||
assignment: node,
|
||||
target_index,
|
||||
kind,
|
||||
});
|
||||
|
||||
if let Some(current_assignment) = current_assignment {
|
||||
self.push_assignment(current_assignment);
|
||||
}
|
||||
|
||||
self.visit_expr(target);
|
||||
if kind.is_some() {
|
||||
// only need to pop in the case where we pushed something
|
||||
|
||||
if current_assignment.is_some() {
|
||||
// Only need to pop in the case where we pushed something
|
||||
self.pop_assignment();
|
||||
}
|
||||
}
|
||||
@@ -970,19 +994,19 @@ where
|
||||
}
|
||||
|
||||
if is_definition {
|
||||
match self.current_assignment().copied() {
|
||||
match self.current_assignment() {
|
||||
Some(CurrentAssignment::Assign {
|
||||
assignment,
|
||||
target_index,
|
||||
kind,
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
AssignmentDefinitionNodeRef {
|
||||
assignment,
|
||||
target_index,
|
||||
unpack,
|
||||
value: &node.value,
|
||||
name: name_node,
|
||||
kind,
|
||||
first,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1033,6 +1057,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CurrentAssignment::Assign { first, .. }) = self.current_assignment_mut()
|
||||
{
|
||||
*first = false;
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
ast::Expr::Named(node) => {
|
||||
@@ -1073,10 +1102,13 @@ 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();
|
||||
self.visit_expr(body);
|
||||
let post_body = self.flow_snapshot();
|
||||
self.flow_restore(pre_if);
|
||||
|
||||
self.record_negated_constraint(constraint);
|
||||
self.visit_expr(orelse);
|
||||
self.flow_merge(post_body);
|
||||
}
|
||||
@@ -1229,9 +1261,9 @@ where
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign {
|
||||
assignment: &'a ast::StmtAssign,
|
||||
target_index: usize,
|
||||
kind: AssignmentKind,
|
||||
node: &'a ast::StmtAssign,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
AugAssign(&'a ast::StmtAugAssign),
|
||||
|
||||
@@ -6,8 +6,22 @@ use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::node_key::NodeKey;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId, ScopedSymbolId};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::Db;
|
||||
|
||||
/// A definition of a symbol.
|
||||
///
|
||||
/// ## Module-local type
|
||||
/// This type should not be used as part of any cross-module API because
|
||||
/// it holds a reference to the AST node. Range-offset changes
|
||||
/// then propagate through all usages, and deserialization requires
|
||||
/// reparsing the entire module.
|
||||
///
|
||||
/// E.g. don't use this type in:
|
||||
///
|
||||
/// * a return type of a cross-module query
|
||||
/// * a field of a type that is a return type of a cross-module query
|
||||
/// * an argument of a cross-module query
|
||||
#[salsa::tracked]
|
||||
pub struct Definition<'db> {
|
||||
/// The file in which the definition occurs.
|
||||
@@ -24,7 +38,7 @@ pub struct Definition<'db> {
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) kind: DefinitionKind,
|
||||
pub(crate) kind: DefinitionKind<'db>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Definition<'static>>,
|
||||
@@ -166,10 +180,10 @@ pub(crate) struct ImportFromDefinitionNodeRef<'a> {
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
pub(crate) assignment: &'a ast::StmtAssign,
|
||||
pub(crate) target_index: usize,
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) value: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) kind: AssignmentKind,
|
||||
pub(crate) first: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -211,9 +225,9 @@ pub(crate) struct MatchPatternDefinitionNodeRef<'a> {
|
||||
pub(crate) index: u32,
|
||||
}
|
||||
|
||||
impl DefinitionNodeRef<'_> {
|
||||
impl<'db> DefinitionNodeRef<'db> {
|
||||
#[allow(unsafe_code)]
|
||||
pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind {
|
||||
pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind<'db> {
|
||||
match self {
|
||||
DefinitionNodeRef::Import(alias) => {
|
||||
DefinitionKind::Import(AstNodeRef::new(parsed, alias))
|
||||
@@ -234,15 +248,15 @@ impl DefinitionNodeRef<'_> {
|
||||
DefinitionKind::NamedExpression(AstNodeRef::new(parsed, named))
|
||||
}
|
||||
DefinitionNodeRef::Assignment(AssignmentDefinitionNodeRef {
|
||||
assignment,
|
||||
target_index,
|
||||
unpack,
|
||||
value,
|
||||
name,
|
||||
kind,
|
||||
first,
|
||||
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef::new(parsed.clone(), assignment),
|
||||
target_index,
|
||||
target: TargetKind::from(unpack),
|
||||
value: AstNodeRef::new(parsed.clone(), value),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
kind,
|
||||
first,
|
||||
}),
|
||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||
@@ -316,10 +330,10 @@ impl DefinitionNodeRef<'_> {
|
||||
Self::Class(node) => node.into(),
|
||||
Self::NamedExpression(node) => node.into(),
|
||||
Self::Assignment(AssignmentDefinitionNodeRef {
|
||||
assignment: _,
|
||||
target_index: _,
|
||||
value: _,
|
||||
unpack: _,
|
||||
name,
|
||||
kind: _,
|
||||
first: _,
|
||||
}) => name.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
@@ -382,13 +396,13 @@ impl DefinitionCategory {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum DefinitionKind {
|
||||
pub enum DefinitionKind<'db> {
|
||||
Import(AstNodeRef<ast::Alias>),
|
||||
ImportFrom(ImportFromDefinitionKind),
|
||||
Function(AstNodeRef<ast::StmtFunctionDef>),
|
||||
Class(AstNodeRef<ast::StmtClassDef>),
|
||||
NamedExpression(AstNodeRef<ast::ExprNamed>),
|
||||
Assignment(AssignmentDefinitionKind),
|
||||
Assignment(AssignmentDefinitionKind<'db>),
|
||||
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
|
||||
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
|
||||
For(ForStmtDefinitionKind),
|
||||
@@ -400,7 +414,7 @@ pub enum DefinitionKind {
|
||||
ExceptHandler(ExceptHandlerDefinitionKind),
|
||||
}
|
||||
|
||||
impl DefinitionKind {
|
||||
impl DefinitionKind<'_> {
|
||||
pub(crate) fn category(&self) -> DefinitionCategory {
|
||||
match self {
|
||||
// functions, classes, and imports always bind, and we consider them declarations
|
||||
@@ -445,6 +459,21 @@ impl DefinitionKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum TargetKind<'db> {
|
||||
Sequence(Unpack<'db>),
|
||||
Name,
|
||||
}
|
||||
|
||||
impl<'db> From<Option<Unpack<'db>>> for TargetKind<'db> {
|
||||
fn from(value: Option<Unpack<'db>>) -> Self {
|
||||
match value {
|
||||
Some(unpack) => TargetKind::Sequence(unpack),
|
||||
None => TargetKind::Name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[allow(dead_code)]
|
||||
pub struct MatchPatternDefinitionKind {
|
||||
@@ -506,38 +535,31 @@ impl ImportFromDefinitionKind {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AssignmentDefinitionKind {
|
||||
assignment: AstNodeRef<ast::StmtAssign>,
|
||||
target_index: usize,
|
||||
pub struct AssignmentDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
value: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
kind: AssignmentKind,
|
||||
first: bool,
|
||||
}
|
||||
|
||||
impl AssignmentDefinitionKind {
|
||||
pub(crate) fn value(&self) -> &ast::Expr {
|
||||
&self.assignment.node().value
|
||||
impl<'db> AssignmentDefinitionKind<'db> {
|
||||
pub(crate) fn target(&self) -> TargetKind<'db> {
|
||||
self.target
|
||||
}
|
||||
|
||||
pub(crate) fn target(&self) -> &ast::Expr {
|
||||
&self.assignment.node().targets[self.target_index]
|
||||
pub(crate) fn value(&self) -> &ast::Expr {
|
||||
self.value.node()
|
||||
}
|
||||
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) fn kind(&self) -> AssignmentKind {
|
||||
self.kind
|
||||
pub(crate) fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of assignment target expression.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum AssignmentKind {
|
||||
Sequence,
|
||||
Name,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WithItemDefinitionKind {
|
||||
node: AstNodeRef<ast::WithItem>,
|
||||
|
||||
@@ -8,6 +8,18 @@ use salsa;
|
||||
/// An independently type-inferable expression.
|
||||
///
|
||||
/// Includes constraint expressions (e.g. if tests) and the RHS of an unpacking assignment.
|
||||
///
|
||||
/// ## Module-local type
|
||||
/// This type should not be used as part of any cross-module API because
|
||||
/// it holds a reference to the AST node. Range-offset changes
|
||||
/// then propagate through all usages, and deserialization requires
|
||||
/// reparsing the entire module.
|
||||
///
|
||||
/// E.g. don't use this type in:
|
||||
///
|
||||
/// * a return type of a cross-module query
|
||||
/// * a field of a type that is a return type of a cross-module query
|
||||
/// * an argument of a cross-module query
|
||||
#[salsa::tracked]
|
||||
pub(crate) struct Expression<'db> {
|
||||
/// The file in which the expression occurs.
|
||||
|
||||
@@ -103,14 +103,10 @@ pub struct ScopedSymbolId;
|
||||
pub struct ScopeId<'db> {
|
||||
#[id]
|
||||
pub file: File,
|
||||
|
||||
#[id]
|
||||
pub file_scope_id: FileScopeId,
|
||||
|
||||
/// The node that introduces this scope.
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub node: NodeWithScopeKind,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<ScopeId<'static>>,
|
||||
}
|
||||
@@ -131,6 +127,14 @@ impl<'db> ScopeId<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn node(self, db: &dyn Db) -> &NodeWithScopeKind {
|
||||
self.scope(db).node()
|
||||
}
|
||||
|
||||
pub(crate) fn scope(self, db: &dyn Db) -> &Scope {
|
||||
semantic_index(db, self.file(db)).scope(self.file_scope_id(db))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn name(self, db: &'db dyn Db) -> &'db str {
|
||||
match self.node(db) {
|
||||
@@ -169,10 +173,10 @@ impl FileScopeId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug)]
|
||||
pub struct Scope {
|
||||
pub(super) parent: Option<FileScopeId>,
|
||||
pub(super) kind: ScopeKind,
|
||||
pub(super) node: NodeWithScopeKind,
|
||||
pub(super) descendents: Range<FileScopeId>,
|
||||
}
|
||||
|
||||
@@ -181,8 +185,12 @@ impl Scope {
|
||||
self.parent
|
||||
}
|
||||
|
||||
pub fn node(&self) -> &NodeWithScopeKind {
|
||||
&self.node
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> ScopeKind {
|
||||
self.kind
|
||||
self.node().scope_kind()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,21 +384,6 @@ impl NodeWithScopeRef<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn scope_kind(self) -> ScopeKind {
|
||||
match self {
|
||||
NodeWithScopeRef::Module => ScopeKind::Module,
|
||||
NodeWithScopeRef::Class(_) => ScopeKind::Class,
|
||||
NodeWithScopeRef::Function(_) => ScopeKind::Function,
|
||||
NodeWithScopeRef::Lambda(_) => ScopeKind::Function,
|
||||
NodeWithScopeRef::FunctionTypeParameters(_)
|
||||
| NodeWithScopeRef::ClassTypeParameters(_) => ScopeKind::Annotation,
|
||||
NodeWithScopeRef::ListComprehension(_)
|
||||
| NodeWithScopeRef::SetComprehension(_)
|
||||
| NodeWithScopeRef::DictComprehension(_)
|
||||
| NodeWithScopeRef::GeneratorExpression(_) => ScopeKind::Comprehension,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn node_key(self) -> NodeWithScopeKey {
|
||||
match self {
|
||||
NodeWithScopeRef::Module => NodeWithScopeKey::Module,
|
||||
@@ -438,6 +431,36 @@ pub enum NodeWithScopeKind {
|
||||
GeneratorExpression(AstNodeRef<ast::ExprGenerator>),
|
||||
}
|
||||
|
||||
impl NodeWithScopeKind {
|
||||
pub(super) const fn scope_kind(&self) -> ScopeKind {
|
||||
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::ListComprehension(_)
|
||||
| Self::SetComprehension(_)
|
||||
| Self::DictComprehension(_)
|
||||
| Self::GeneratorExpression(_) => ScopeKind::Comprehension,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_class(&self) -> &ast::StmtClassDef {
|
||||
match self {
|
||||
Self::Class(class) => class.node(),
|
||||
_ => panic!("expected class"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expect_function(&self) -> &ast::StmtFunctionDef {
|
||||
match self {
|
||||
Self::Function(function) => function.node(),
|
||||
_ => panic!("expected function"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum NodeWithScopeKey {
|
||||
Module,
|
||||
|
||||
@@ -11,11 +11,9 @@ use crate::Db;
|
||||
enum CoreStdlibModule {
|
||||
Builtins,
|
||||
Types,
|
||||
// the Typing enum is currently only used in tests
|
||||
#[allow(dead_code)]
|
||||
Typing,
|
||||
Typeshed,
|
||||
TypingExtensions,
|
||||
Typing,
|
||||
}
|
||||
|
||||
impl CoreStdlibModule {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
Db,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Boundness {
|
||||
Bound,
|
||||
MayBeUnbound,
|
||||
@@ -44,17 +44,13 @@ impl<'db> Symbol<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_or(&self, other: Type<'db>) -> Type<'db> {
|
||||
pub(crate) fn unwrap_or_unknown(&self) -> Type<'db> {
|
||||
match self {
|
||||
Symbol::Type(ty, _) => *ty,
|
||||
Symbol::Unbound => other,
|
||||
Symbol::Unbound => Type::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_or_unknown(&self) -> Type<'db> {
|
||||
self.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
pub(crate) fn as_type(&self) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Symbol::Type(ty, _) => Some(*ty),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,12 +25,11 @@
|
||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
||||
//! eliminate the supertype from the intersection).
|
||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
|
||||
use crate::types::{InstanceType, IntersectionType, KnownClass, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::KnownClass;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
@@ -80,7 +79,6 @@ impl<'db> UnionBuilder<'db> {
|
||||
to_remove.push(index);
|
||||
}
|
||||
}
|
||||
|
||||
match to_remove[..] {
|
||||
[] => self.elements.push(to_add),
|
||||
[index] => self.elements[index] = to_add,
|
||||
@@ -103,7 +101,6 @@ impl<'db> UnionBuilder<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
@@ -249,8 +246,8 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
}
|
||||
} else {
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
if let Type::Instance(class_type) = new_positive {
|
||||
if class_type.is_known(db, KnownClass::Bool) {
|
||||
if let Type::Instance(InstanceType { class }) = new_positive {
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
if let Some(&Type::BooleanLiteral(value)) = self
|
||||
.negative
|
||||
.iter()
|
||||
@@ -320,7 +317,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
// 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.
|
||||
self.positive.insert(ty);
|
||||
self.add_positive(db, ty);
|
||||
}
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
Type::BooleanLiteral(bool)
|
||||
@@ -386,8 +383,9 @@ mod tests {
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::stdlib::typing_symbol;
|
||||
use crate::types::{KnownClass, StringLiteralType, UnionBuilder};
|
||||
use crate::types::{global_symbol, KnownClass, StringLiteralType, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use test_case::test_case;
|
||||
|
||||
@@ -594,6 +592,22 @@ mod tests {
|
||||
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_any() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Any);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Never)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
@@ -675,8 +689,8 @@ mod tests {
|
||||
fn build_intersection_self_negation() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::None)
|
||||
.add_negative(Type::None)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
@@ -686,18 +700,18 @@ mod tests {
|
||||
fn build_intersection_simplify_negative_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::None)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_negative(Type::Never)
|
||||
.build();
|
||||
|
||||
assert_eq!(ty, Type::None);
|
||||
assert_eq!(ty, Type::none(&db));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_positive_never() {
|
||||
let db = setup_db();
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::None)
|
||||
.add_positive(Type::none(&db))
|
||||
.add_positive(Type::Never)
|
||||
.build();
|
||||
|
||||
@@ -709,14 +723,14 @@ mod tests {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::None)
|
||||
.add_negative(Type::none(&db))
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::IntLiteral(1))
|
||||
.add_negative(Type::None)
|
||||
.add_negative(Type::none(&db))
|
||||
.build();
|
||||
assert_eq!(ty, Type::IntLiteral(1));
|
||||
}
|
||||
@@ -875,7 +889,7 @@ mod tests {
|
||||
let db = setup_db();
|
||||
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::None;
|
||||
let t2 = Type::none(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(t1)
|
||||
@@ -993,4 +1007,66 @@ mod tests {
|
||||
.build();
|
||||
assert_eq!(result, ty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_of_two_unions_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let union = UnionBuilder::new(&db).add(a).add(b).build();
|
||||
assert_eq!(union.display(&db).to_string(), "A | B");
|
||||
let reversed_union = UnionBuilder::new(&db).add(b).add(a).build();
|
||||
assert_eq!(reversed_union.display(&db).to_string(), "B | A");
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(union)
|
||||
.add_positive(reversed_union)
|
||||
.build();
|
||||
assert_eq!(intersection.display(&db).to_string(), "B | A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_of_two_intersections_simplify() {
|
||||
let mut db = setup_db();
|
||||
db.write_dedented(
|
||||
"/src/module.py",
|
||||
"
|
||||
class A: ...
|
||||
class B: ...
|
||||
a = A()
|
||||
b = B()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "src/module.py").expect("file to exist");
|
||||
|
||||
let a = global_symbol(&db, file, "a").expect_type();
|
||||
let b = global_symbol(&db, file, "b").expect_type();
|
||||
let intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(a)
|
||||
.add_positive(b)
|
||||
.build();
|
||||
let reversed_intersection = IntersectionBuilder::new(&db)
|
||||
.add_positive(b)
|
||||
.add_positive(a)
|
||||
.build();
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(intersection)
|
||||
.add(reversed_intersection)
|
||||
.build();
|
||||
assert_eq!(union.display(&db).to_string(), "A & B");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::types::{ClassLiteralType, Type};
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::types::Type;
|
||||
use crate::Db;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
// TODO: Don't use string keys for rules
|
||||
pub(super) rule: String,
|
||||
@@ -31,6 +32,28 @@ impl TypeCheckDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for TypeCheckDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
TypeCheckDiagnostic::rule(self)
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
TypeCheckDiagnostic::message(self).into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
TypeCheckDiagnostic::file(self)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(Ranged::range(self))
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TypeCheckDiagnostic {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
@@ -141,6 +164,23 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
|
||||
/// because its `__iter__` method is possibly unbound.
|
||||
pub(super) fn add_not_iterable_possibly_unbound(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
element_ty: Type<'db>,
|
||||
) {
|
||||
self.add(
|
||||
node,
|
||||
"not-iterable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
|
||||
element_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that an index is out of bounds for a tuple.
|
||||
pub(super) fn add_index_out_of_bounds(
|
||||
&mut self,
|
||||
@@ -209,7 +249,7 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
assigned_ty: Type<'db>,
|
||||
) {
|
||||
match declared_ty {
|
||||
Type::ClassLiteral(class) => {
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
self.add(node, "invalid-assignment", format_args!(
|
||||
"Implicit shadowing of class `{}`; annotate to make it explicit if this is intentional",
|
||||
class.name(self.db)));
|
||||
|
||||
@@ -6,7 +6,9 @@ use ruff_db::display::FormatterJoinExtension;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
use crate::types::{
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, SubclassOfType, Type, UnionType,
|
||||
};
|
||||
use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -64,7 +66,11 @@ impl Display for DisplayRepresentation<'_> {
|
||||
Type::Any => f.write_str("Any"),
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::None => f.write_str("None"),
|
||||
Type::Instance(InstanceType { class })
|
||||
if class.is_known(self.db, KnownClass::NoneType) =>
|
||||
{
|
||||
f.write_str("None")
|
||||
}
|
||||
// `[Type::Todo]`'s display should be explicit that is not a valid display of
|
||||
// any other type
|
||||
Type::Todo => f.write_str("@Todo"),
|
||||
@@ -72,8 +78,12 @@ impl Display for DisplayRepresentation<'_> {
|
||||
write!(f, "<module '{:?}'>", file.path(self.db))
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::ClassLiteral(class) => f.write_str(class.name(self.db)),
|
||||
Type::Instance(class) => f.write_str(class.name(self.db)),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
|
||||
Type::SubclassOf(SubclassOfType { class }) => {
|
||||
write!(f, "type[{}]", class.name(self.db))
|
||||
}
|
||||
Type::Instance(InstanceType { class }) => f.write_str(class.name(self.db)),
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.as_str()),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
@@ -380,7 +390,7 @@ mod tests {
|
||||
global_symbol(&db, mod_file, "bar").expect_type(),
|
||||
global_symbol(&db, mod_file, "B").expect_type(),
|
||||
Type::BooleanLiteral(true),
|
||||
Type::None,
|
||||
Type::none(&db),
|
||||
];
|
||||
let union = UnionType::from_elements(&db, union_elements).expect_union();
|
||||
let display = format!("{}", union.display(&db));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
505
crates/red_knot_python_semantic/src/types/mro.rs
Normal file
505
crates/red_knot_python_semantic/src/types/mro.rs
Normal file
@@ -0,0 +1,505 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::ops::Deref;
|
||||
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Either;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type};
|
||||
use crate::Db;
|
||||
|
||||
/// The inferred method resolution order of a given class.
|
||||
///
|
||||
/// See [`Class::iter_mro`] for more details.
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>);
|
||||
|
||||
impl<'db> Mro<'db> {
|
||||
/// Attempt to resolve the MRO of a given class
|
||||
///
|
||||
/// In the event that a possible list of bases would (or could) lead to a
|
||||
/// `TypeError` being raised at runtime due to an unresolvable MRO, we infer
|
||||
/// the MRO of the class as being `[<the class in question>, Unknown, object]`.
|
||||
/// This seems most likely to reduce the possibility of cascading errors
|
||||
/// elsewhere.
|
||||
///
|
||||
/// (We emit a diagnostic warning about the runtime `TypeError` in
|
||||
/// [`super::infer::TypeInferenceBuilder::infer_region_scope`].)
|
||||
pub(super) fn of_class(db: &'db dyn Db, class: Class<'db>) -> Result<Self, MroError<'db>> {
|
||||
Self::of_class_impl(db, class).map_err(|error_kind| {
|
||||
let fallback_mro = Self::from([
|
||||
ClassBase::Class(class),
|
||||
ClassBase::Unknown,
|
||||
ClassBase::object(db),
|
||||
]);
|
||||
MroError {
|
||||
kind: error_kind,
|
||||
fallback_mro,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn of_class_impl(db: &'db dyn Db, class: Class<'db>) -> Result<Self, MroErrorKind<'db>> {
|
||||
let class_bases = class.explicit_bases(db);
|
||||
|
||||
match class_bases {
|
||||
// `builtins.object` is the special case:
|
||||
// the only class in Python that has an MRO with length <2
|
||||
[] if class.is_known(db, KnownClass::Object) => {
|
||||
Ok(Self::from([ClassBase::Class(class)]))
|
||||
}
|
||||
|
||||
// All other classes in Python have an MRO with length >=2.
|
||||
// Even if a class has no explicit base classes,
|
||||
// it will implicitly inherit from `object` at runtime;
|
||||
// `object` will appear in the class's `__bases__` list and `__mro__`:
|
||||
//
|
||||
// ```pycon
|
||||
// >>> class Foo: ...
|
||||
// ...
|
||||
// >>> Foo.__bases__
|
||||
// (<class 'object'>,)
|
||||
// >>> Foo.__mro__
|
||||
// (<class '__main__.Foo'>, <class 'object'>)
|
||||
// ```
|
||||
[] => Ok(Self::from([ClassBase::Class(class), ClassBase::object(db)])),
|
||||
|
||||
// Fast path for a class that has only a single explicit base.
|
||||
//
|
||||
// This *could* theoretically be handled by the final branch below,
|
||||
// but it's a common case (i.e., worth optimizing for),
|
||||
// and the `c3_merge` function requires lots of allocations.
|
||||
[single_base] => {
|
||||
let single_base = ClassBase::try_from_ty(*single_base).ok_or(*single_base);
|
||||
single_base.map_or_else(
|
||||
|invalid_base_ty| {
|
||||
let bases_info = Box::from([(0, invalid_base_ty)]);
|
||||
Err(MroErrorKind::InvalidBases(bases_info))
|
||||
},
|
||||
|single_base| {
|
||||
if let ClassBase::Class(class_base) = single_base {
|
||||
if class_is_cyclically_defined(db, class_base) {
|
||||
return Err(MroErrorKind::CyclicClassDefinition);
|
||||
}
|
||||
}
|
||||
let mro = std::iter::once(ClassBase::Class(class))
|
||||
.chain(single_base.mro(db))
|
||||
.collect();
|
||||
Ok(mro)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// The class has multiple explicit bases.
|
||||
//
|
||||
// We'll fallback to a full implementation of the C3-merge algorithm to determine
|
||||
// what MRO Python will give this class at runtime
|
||||
// (if an MRO is indeed resolvable at all!)
|
||||
multiple_bases => {
|
||||
if class_is_cyclically_defined(db, class) {
|
||||
return Err(MroErrorKind::CyclicClassDefinition);
|
||||
}
|
||||
|
||||
let mut valid_bases = vec![];
|
||||
let mut invalid_bases = vec![];
|
||||
|
||||
for (i, base) in multiple_bases.iter().enumerate() {
|
||||
match ClassBase::try_from_ty(*base).ok_or(*base) {
|
||||
Ok(valid_base) => valid_bases.push(valid_base),
|
||||
Err(invalid_base) => invalid_bases.push((i, invalid_base)),
|
||||
}
|
||||
}
|
||||
|
||||
if !invalid_bases.is_empty() {
|
||||
return Err(MroErrorKind::InvalidBases(invalid_bases.into_boxed_slice()));
|
||||
}
|
||||
|
||||
let mut seqs = vec![VecDeque::from([ClassBase::Class(class)])];
|
||||
for base in &valid_bases {
|
||||
seqs.push(base.mro(db).collect());
|
||||
}
|
||||
seqs.push(valid_bases.iter().copied().collect());
|
||||
|
||||
c3_merge(seqs).ok_or_else(|| {
|
||||
let mut seen_bases = FxHashSet::default();
|
||||
let mut duplicate_bases = vec![];
|
||||
for (index, base) in valid_bases
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, base)| Some((index, base.into_class_literal_type()?)))
|
||||
{
|
||||
if !seen_bases.insert(base) {
|
||||
duplicate_bases.push((index, base));
|
||||
}
|
||||
}
|
||||
|
||||
if duplicate_bases.is_empty() {
|
||||
MroErrorKind::UnresolvableMro {
|
||||
bases_list: valid_bases.into_boxed_slice(),
|
||||
}
|
||||
} else {
|
||||
MroErrorKind::DuplicateBases(duplicate_bases.into_boxed_slice())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db, const N: usize> From<[ClassBase<'db>; N]> for Mro<'db> {
|
||||
fn from(value: [ClassBase<'db>; N]) -> Self {
|
||||
Self(Box::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Vec<ClassBase<'db>>> for Mro<'db> {
|
||||
fn from(value: Vec<ClassBase<'db>>) -> Self {
|
||||
Self(value.into_boxed_slice())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Deref for Mro<'db> {
|
||||
type Target = [ClassBase<'db>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> FromIterator<ClassBase<'db>> for Mro<'db> {
|
||||
fn from_iter<T: IntoIterator<Item = ClassBase<'db>>>(iter: T) -> Self {
|
||||
Self(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that yields elements of a class's MRO.
|
||||
///
|
||||
/// We avoid materialising the *full* MRO unless it is actually necessary:
|
||||
/// - Materialising the full MRO is expensive
|
||||
/// - We need to do it for every class in the code that we're checking, as we need to make sure
|
||||
/// that there are no class definitions in the code we're checking that would cause an
|
||||
/// exception to be raised at runtime. But the same does *not* necessarily apply for every class
|
||||
/// in third-party and stdlib dependencies: we never emit diagnostics about non-first-party code.
|
||||
/// - However, we *do* need to resolve attribute accesses on classes/instances from
|
||||
/// third-party and stdlib dependencies. That requires iterating over the MRO of third-party/stdlib
|
||||
/// classes, but not necessarily the *whole* MRO: often just the first element is enough.
|
||||
/// Luckily we know that for any class `X`, the first element of `X`'s MRO will always be `X` itself.
|
||||
/// We can therefore avoid resolving the full MRO for many third-party/stdlib classes while still
|
||||
/// being faithful to the runtime semantics.
|
||||
///
|
||||
/// Even for first-party code, where we will have to resolve the MRO for every class we encounter,
|
||||
/// loading the cached MRO comes with a certain amount of overhead, so it's best to avoid calling the
|
||||
/// Salsa-tracked [`Class::try_mro`] method unless it's absolutely necessary.
|
||||
pub(super) struct MroIterator<'db> {
|
||||
db: &'db dyn Db,
|
||||
|
||||
/// The class whose MRO we're iterating over
|
||||
class: Class<'db>,
|
||||
|
||||
/// Whether or not we've already yielded the first element of the MRO
|
||||
first_element_yielded: bool,
|
||||
|
||||
/// Iterator over all elements of the MRO except the first.
|
||||
///
|
||||
/// The full MRO is expensive to materialize, so this field is `None`
|
||||
/// unless we actually *need* to iterate past the first element of the MRO,
|
||||
/// at which point it is lazily materialized.
|
||||
subsequent_elements: Option<std::slice::Iter<'db, ClassBase<'db>>>,
|
||||
}
|
||||
|
||||
impl<'db> MroIterator<'db> {
|
||||
pub(super) fn new(db: &'db dyn Db, class: Class<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
class,
|
||||
first_element_yielded: false,
|
||||
subsequent_elements: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize the full MRO of the class.
|
||||
/// Return an iterator over that MRO which skips the first element of the MRO.
|
||||
fn full_mro_except_first_element(&mut self) -> impl Iterator<Item = ClassBase<'db>> + '_ {
|
||||
self.subsequent_elements
|
||||
.get_or_insert_with(|| {
|
||||
let mut full_mro_iter = match self.class.try_mro(self.db) {
|
||||
Ok(mro) => mro.iter(),
|
||||
Err(error) => error.fallback_mro().iter(),
|
||||
};
|
||||
full_mro_iter.next();
|
||||
full_mro_iter
|
||||
})
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> Iterator for MroIterator<'db> {
|
||||
type Item = ClassBase<'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if !self.first_element_yielded {
|
||||
self.first_element_yielded = true;
|
||||
return Some(ClassBase::Class(self.class));
|
||||
}
|
||||
self.full_mro_except_first_element().next()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for MroIterator<'_> {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) struct MroError<'db> {
|
||||
kind: MroErrorKind<'db>,
|
||||
fallback_mro: Mro<'db>,
|
||||
}
|
||||
|
||||
impl<'db> MroError<'db> {
|
||||
/// Return an [`MroErrorKind`] variant describing why we could not resolve the MRO for this class.
|
||||
pub(super) fn reason(&self) -> &MroErrorKind<'db> {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
/// Return the fallback MRO we should infer for this class during type inference
|
||||
/// (since accurate resolution of its "true" MRO was impossible)
|
||||
pub(super) fn fallback_mro(&self) -> &Mro<'db> {
|
||||
&self.fallback_mro
|
||||
}
|
||||
}
|
||||
|
||||
/// Possible ways in which attempting to resolve the MRO of a class might fail.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(super) enum MroErrorKind<'db> {
|
||||
/// The class inherits from one or more invalid bases.
|
||||
///
|
||||
/// To avoid excessive complexity in our implementation,
|
||||
/// we only permit classes to inherit from class-literal types,
|
||||
/// `Todo`, `Unknown` or `Any`. Anything else results in us
|
||||
/// emitting a diagnostic.
|
||||
///
|
||||
/// This variant records the indices and types of class bases
|
||||
/// that we deem to be invalid. The indices are the indices of nodes
|
||||
/// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node.
|
||||
/// Each index is the index of a node representing an invalid base.
|
||||
InvalidBases(Box<[(usize, Type<'db>)]>),
|
||||
|
||||
/// The class inherits from itself!
|
||||
///
|
||||
/// This is very unlikely to happen in working real-world code,
|
||||
/// but it's important to explicitly account for it.
|
||||
/// If we don't, there's a possibility of an infinite loop and a panic.
|
||||
CyclicClassDefinition,
|
||||
|
||||
/// The class has one or more duplicate bases.
|
||||
///
|
||||
/// This variant records the indices and [`Class`]es
|
||||
/// of the duplicate bases. The indices are the indices of nodes
|
||||
/// in the bases list of the class's [`StmtClassDef`](ruff_python_ast::StmtClassDef) node.
|
||||
/// Each index is the index of a node representing a duplicate base.
|
||||
DuplicateBases(Box<[(usize, Class<'db>)]>),
|
||||
|
||||
/// The MRO is otherwise unresolvable through the C3-merge algorithm.
|
||||
///
|
||||
/// See [`c3_merge`] for more details.
|
||||
UnresolvableMro { bases_list: Box<[ClassBase<'db>]> },
|
||||
}
|
||||
|
||||
/// Enumeration of the possible kinds of types we allow in class bases.
|
||||
///
|
||||
/// This is much more limited than the [`Type`] enum:
|
||||
/// all types that would be invalid to have as a class base are
|
||||
/// transformed into [`ClassBase::Unknown`]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(super) enum ClassBase<'db> {
|
||||
Any,
|
||||
Unknown,
|
||||
Todo,
|
||||
Class(Class<'db>),
|
||||
}
|
||||
|
||||
impl<'db> ClassBase<'db> {
|
||||
pub(super) fn display(self, db: &'db dyn Db) -> impl std::fmt::Display + 'db {
|
||||
struct Display<'db> {
|
||||
base: ClassBase<'db>,
|
||||
db: &'db dyn Db,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Display<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.base {
|
||||
ClassBase::Any => f.write_str("Any"),
|
||||
ClassBase::Todo => f.write_str("Todo"),
|
||||
ClassBase::Unknown => f.write_str("Unknown"),
|
||||
ClassBase::Class(class) => write!(f, "<class '{}'>", class.name(self.db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Display { base: self, db }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[track_caller]
|
||||
pub(super) fn expect_class_base(self) -> Class<'db> {
|
||||
match self {
|
||||
ClassBase::Class(class) => class,
|
||||
_ => panic!("Expected a `ClassBase::Class()` variant"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a `ClassBase` representing the class `builtins.object`
|
||||
fn object(db: &'db dyn Db) -> Self {
|
||||
KnownClass::Object
|
||||
.to_class(db)
|
||||
.into_class_literal()
|
||||
.map_or(Self::Unknown, |ClassLiteralType { class }| {
|
||||
Self::Class(class)
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to resolve `ty` into a `ClassBase`.
|
||||
///
|
||||
/// Return `None` if `ty` is not an acceptable type for a class base.
|
||||
fn try_from_ty(ty: Type<'db>) -> Option<Self> {
|
||||
match ty {
|
||||
Type::Any => Some(Self::Any),
|
||||
Type::Unknown => Some(Self::Unknown),
|
||||
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?
|
||||
Type::Instance(_) => None, // TODO -- handle `__mro_entries__`?
|
||||
Type::Never
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::FunctionLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::LiteralString
|
||||
| Type::Tuple(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SubclassOf(_) => None,
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::Literal => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn into_class_literal_type(self) -> Option<Class<'db>> {
|
||||
match self {
|
||||
Self::Class(class) => Some(class),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over the MRO of this base
|
||||
fn mro(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
) -> Either<impl Iterator<Item = ClassBase<'db>>, impl Iterator<Item = ClassBase<'db>>> {
|
||||
match self {
|
||||
ClassBase::Any => Either::Left([ClassBase::Any, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Unknown => {
|
||||
Either::Left([ClassBase::Unknown, ClassBase::object(db)].into_iter())
|
||||
}
|
||||
ClassBase::Todo => Either::Left([ClassBase::Todo, ClassBase::object(db)].into_iter()),
|
||||
ClassBase::Class(class) => Either::Right(class.iter_mro(db)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::Unknown => Type::Unknown,
|
||||
ClassBase::Class(class) => Type::ClassLiteral(ClassLiteralType { class }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of the [C3-merge algorithm] for calculating a Python class's
|
||||
/// [method resolution order].
|
||||
///
|
||||
/// [C3-merge algorithm]: https://docs.python.org/3/howto/mro.html#python-2-3-mro
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
fn c3_merge(mut sequences: Vec<VecDeque<ClassBase>>) -> Option<Mro> {
|
||||
// Most MROs aren't that long...
|
||||
let mut mro = Vec::with_capacity(8);
|
||||
|
||||
loop {
|
||||
sequences.retain(|sequence| !sequence.is_empty());
|
||||
|
||||
if sequences.is_empty() {
|
||||
return Some(Mro::from(mro));
|
||||
}
|
||||
|
||||
// If the candidate exists "deeper down" in the inheritance hierarchy,
|
||||
// we should refrain from adding it to the MRO for now. Add the first candidate
|
||||
// for which this does not hold true. If this holds true for all candidates,
|
||||
// return `None`; it will be impossible to find a consistent MRO for the class
|
||||
// with the given bases.
|
||||
let mro_entry = sequences.iter().find_map(|outer_sequence| {
|
||||
let candidate = outer_sequence[0];
|
||||
|
||||
let not_head = sequences
|
||||
.iter()
|
||||
.all(|sequence| sequence.iter().skip(1).all(|base| base != &candidate));
|
||||
|
||||
not_head.then_some(candidate)
|
||||
})?;
|
||||
|
||||
mro.push(mro_entry);
|
||||
|
||||
// Make sure we don't try to add the candidate to the MRO twice:
|
||||
for sequence in &mut sequences {
|
||||
if sequence[0] == mro_entry {
|
||||
sequence.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if this class appears to be a cyclic definition,
|
||||
/// i.e., it inherits either directly or indirectly from itself.
|
||||
///
|
||||
/// A class definition like this will fail at runtime,
|
||||
/// but we must be resilient to it or we could panic.
|
||||
fn class_is_cyclically_defined(db: &dyn Db, class: Class) -> bool {
|
||||
fn is_cyclically_defined_recursive<'db>(
|
||||
db: &'db dyn Db,
|
||||
class: Class<'db>,
|
||||
classes_to_watch: &mut IndexSet<Class<'db>>,
|
||||
) -> bool {
|
||||
if !classes_to_watch.insert(class) {
|
||||
return true;
|
||||
}
|
||||
for explicit_base_class in class
|
||||
.explicit_bases(db)
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(Type::into_class_literal)
|
||||
.map(|ClassLiteralType { class }| class)
|
||||
{
|
||||
// Each base must be considered in isolation.
|
||||
// This is due to the fact that if a class uses multiple inheritance,
|
||||
// there could easily be a situation where two bases have the same class in their MROs;
|
||||
// that isn't enough to constitute the class being cyclically defined.
|
||||
let classes_to_watch_len = classes_to_watch.len();
|
||||
if is_cyclically_defined_recursive(db, explicit_base_class, classes_to_watch) {
|
||||
return true;
|
||||
}
|
||||
classes_to_watch.truncate(classes_to_watch_len);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
class
|
||||
.explicit_bases(db)
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(Type::into_class_literal)
|
||||
.map(|ClassLiteralType { class }| class)
|
||||
.any(|base_class| is_cyclically_defined_recursive(db, base_class, &mut IndexSet::default()))
|
||||
}
|
||||
@@ -5,12 +5,15 @@ use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
|
||||
use crate::semantic_index::symbol_table;
|
||||
use crate::types::{
|
||||
infer_expression_types, IntersectionBuilder, KnownFunction, Type, UnionBuilder,
|
||||
infer_expression_types, ClassLiteralType, InstanceType, IntersectionBuilder, KnownClass,
|
||||
KnownConstraintFunction, KnownFunction, Truthiness, Type, UnionBuilder,
|
||||
};
|
||||
use crate::Db;
|
||||
use itertools::Itertools;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{BoolOp, ExprBoolOp};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Return the type constraint that `test` (if true) would place on `definition`, if any.
|
||||
@@ -34,21 +37,20 @@ pub(crate) fn narrowing_constraint<'db>(
|
||||
constraint: Constraint<'db>,
|
||||
definition: Definition<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
match constraint.node {
|
||||
let constraints = match constraint.node {
|
||||
ConstraintNode::Expression(expression) => {
|
||||
if constraint.is_positive {
|
||||
all_narrowing_constraints_for_expression(db, expression)
|
||||
.get(&definition.symbol(db))
|
||||
.copied()
|
||||
} else {
|
||||
all_negative_narrowing_constraints_for_expression(db, expression)
|
||||
.get(&definition.symbol(db))
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
ConstraintNode::Pattern(pattern) => all_narrowing_constraints_for_pattern(db, pattern)
|
||||
.get(&definition.symbol(db))
|
||||
.copied(),
|
||||
ConstraintNode::Pattern(pattern) => all_narrowing_constraints_for_pattern(db, pattern),
|
||||
};
|
||||
if let Some(constraints) = constraints {
|
||||
constraints.get(&definition.symbol(db)).copied()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +58,7 @@ pub(crate) fn narrowing_constraint<'db>(
|
||||
fn all_narrowing_constraints_for_pattern<'db>(
|
||||
db: &'db dyn Db,
|
||||
pattern: PatternConstraint<'db>,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
NarrowingConstraintsBuilder::new(db, ConstraintNode::Pattern(pattern), true).finish()
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ fn all_narrowing_constraints_for_pattern<'db>(
|
||||
fn all_narrowing_constraints_for_expression<'db>(
|
||||
db: &'db dyn Db,
|
||||
expression: Expression<'db>,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
NarrowingConstraintsBuilder::new(db, ConstraintNode::Expression(expression), true).finish()
|
||||
}
|
||||
|
||||
@@ -72,39 +74,83 @@ fn all_narrowing_constraints_for_expression<'db>(
|
||||
fn all_negative_narrowing_constraints_for_expression<'db>(
|
||||
db: &'db dyn Db,
|
||||
expression: Expression<'db>,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
NarrowingConstraintsBuilder::new(db, ConstraintNode::Expression(expression), false).finish()
|
||||
}
|
||||
|
||||
/// Generate a constraint from the *type* of the second argument of an `isinstance` call.
|
||||
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
|
||||
///
|
||||
/// Example: for `isinstance(…, str)`, we would infer `Type::ClassLiteral(str)` from the
|
||||
/// second argument, but we need to generate a `Type::Instance(str)` constraint that can
|
||||
/// be used to narrow down the type of the first argument.
|
||||
fn generate_isinstance_constraint<'db>(
|
||||
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
|
||||
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
|
||||
fn generate_classinfo_constraint<'db, F>(
|
||||
db: &'db dyn Db,
|
||||
classinfo: &Type<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
to_constraint: F,
|
||||
) -> Option<Type<'db>>
|
||||
where
|
||||
F: Fn(ClassLiteralType<'db>) -> Type<'db> + Copy,
|
||||
{
|
||||
match classinfo {
|
||||
Type::ClassLiteral(class) => Some(Type::Instance(*class)),
|
||||
Type::Tuple(tuple) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in tuple.elements(db) {
|
||||
builder = builder.add(generate_isinstance_constraint(db, element)?);
|
||||
builder = builder.add(generate_classinfo_constraint(db, element, to_constraint)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
Type::ClassLiteral(class_literal_type) => Some(to_constraint(*class_literal_type)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
type NarrowingConstraints<'db> = FxHashMap<ScopedSymbolId, Type<'db>>;
|
||||
|
||||
fn merge_constraints_and<'db>(
|
||||
into: &mut NarrowingConstraints<'db>,
|
||||
from: NarrowingConstraints<'db>,
|
||||
db: &'db dyn Db,
|
||||
) {
|
||||
for (key, value) in from {
|
||||
match into.entry(key) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
*entry.get_mut() = IntersectionBuilder::new(db)
|
||||
.add_positive(*entry.get())
|
||||
.add_positive(value)
|
||||
.build();
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_constraints_or<'db>(
|
||||
into: &mut NarrowingConstraints<'db>,
|
||||
from: &NarrowingConstraints<'db>,
|
||||
db: &'db dyn Db,
|
||||
) {
|
||||
for (key, value) in from {
|
||||
match into.entry(*key) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
*entry.get_mut() = UnionBuilder::new(db).add(*entry.get()).add(*value).build();
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(KnownClass::Object.to_instance(db));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, value) in into.iter_mut() {
|
||||
if !from.contains_key(key) {
|
||||
*value = KnownClass::Object.to_instance(db);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NarrowingConstraintsBuilder<'db> {
|
||||
db: &'db dyn Db,
|
||||
constraint: ConstraintNode<'db>,
|
||||
is_positive: bool,
|
||||
constraints: NarrowingConstraints<'db>,
|
||||
}
|
||||
|
||||
impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
@@ -113,24 +159,31 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
db,
|
||||
constraint,
|
||||
is_positive,
|
||||
constraints: NarrowingConstraints::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> NarrowingConstraints<'db> {
|
||||
match self.constraint {
|
||||
fn finish(mut self) -> Option<NarrowingConstraints<'db>> {
|
||||
let constraints: Option<NarrowingConstraints<'db>> = match self.constraint {
|
||||
ConstraintNode::Expression(expression) => {
|
||||
self.evaluate_expression_constraint(expression, self.is_positive);
|
||||
self.evaluate_expression_constraint(expression, self.is_positive)
|
||||
}
|
||||
ConstraintNode::Pattern(pattern) => self.evaluate_pattern_constraint(pattern),
|
||||
};
|
||||
if let Some(mut constraints) = constraints {
|
||||
constraints.shrink_to_fit();
|
||||
Some(constraints)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.constraints.shrink_to_fit();
|
||||
self.constraints
|
||||
}
|
||||
|
||||
fn evaluate_expression_constraint(&mut self, expression: Expression<'db>, is_positive: bool) {
|
||||
fn evaluate_expression_constraint(
|
||||
&mut self,
|
||||
expression: Expression<'db>,
|
||||
is_positive: bool,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let expression_node = expression.node_ref(self.db).node();
|
||||
self.evaluate_expression_node_constraint(expression_node, expression, is_positive);
|
||||
self.evaluate_expression_node_constraint(expression_node, expression, is_positive)
|
||||
}
|
||||
|
||||
fn evaluate_expression_node_constraint(
|
||||
@@ -138,52 +191,51 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
expression_node: &ruff_python_ast::Expr,
|
||||
expression: Expression<'db>,
|
||||
is_positive: bool,
|
||||
) {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
match expression_node {
|
||||
ast::Expr::Compare(expr_compare) => {
|
||||
self.add_expr_compare(expr_compare, expression, is_positive);
|
||||
self.evaluate_expr_compare(expr_compare, expression, is_positive)
|
||||
}
|
||||
ast::Expr::Call(expr_call) => {
|
||||
self.add_expr_call(expr_call, expression, is_positive);
|
||||
self.evaluate_expr_call(expr_call, expression, is_positive)
|
||||
}
|
||||
ast::Expr::UnaryOp(unary_op) if unary_op.op == ast::UnaryOp::Not => {
|
||||
self.evaluate_expression_node_constraint(
|
||||
&unary_op.operand,
|
||||
expression,
|
||||
!is_positive,
|
||||
);
|
||||
}
|
||||
_ => {} // TODO other test expression kinds
|
||||
ast::Expr::UnaryOp(unary_op) if unary_op.op == ast::UnaryOp::Not => self
|
||||
.evaluate_expression_node_constraint(&unary_op.operand, expression, !is_positive),
|
||||
ast::Expr::BoolOp(bool_op) => self.evaluate_bool_op(bool_op, expression, is_positive),
|
||||
_ => None, // TODO other test expression kinds
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_pattern_constraint(&mut self, pattern: PatternConstraint<'db>) {
|
||||
fn evaluate_pattern_constraint(
|
||||
&mut self,
|
||||
pattern: PatternConstraint<'db>,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let subject = pattern.subject(self.db);
|
||||
|
||||
match pattern.pattern(self.db).node() {
|
||||
ast::Pattern::MatchValue(_) => {
|
||||
// TODO
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton_pattern) => {
|
||||
self.add_match_pattern_singleton(subject, singleton_pattern);
|
||||
self.evaluate_match_pattern_singleton(subject, singleton_pattern)
|
||||
}
|
||||
ast::Pattern::MatchSequence(_) => {
|
||||
// TODO
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchMapping(_) => {
|
||||
// TODO
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchClass(_) => {
|
||||
// TODO
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchStar(_) => {
|
||||
// TODO
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchAs(_) => {
|
||||
// TODO
|
||||
None // TODO
|
||||
}
|
||||
ast::Pattern::MatchOr(_) => {
|
||||
// TODO
|
||||
None // TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -199,12 +251,12 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
fn add_expr_compare(
|
||||
fn evaluate_expr_compare(
|
||||
&mut self,
|
||||
expr_compare: &ast::ExprCompare,
|
||||
expression: Expression<'db>,
|
||||
is_positive: bool,
|
||||
) {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let ast::ExprCompare {
|
||||
range: _,
|
||||
left,
|
||||
@@ -214,14 +266,14 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
if !left.is_name_expr() && comparators.iter().all(|c| !c.is_name_expr()) {
|
||||
// If none of the comparators are name expressions,
|
||||
// we have no symbol to narrow down the type of.
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
if !is_positive && comparators.len() > 1 {
|
||||
// We can't negate a constraint made by a multi-comparator expression, since we can't
|
||||
// know which comparison part is the one being negated.
|
||||
// For example, the negation of `x is 1 is y is 2`, would be `(x is not 1) or (y is not 1) or (y is not 2)`
|
||||
// and that requires cross-symbol constraints, which we don't support yet.
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
@@ -229,6 +281,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let comparator_tuples = std::iter::once(&**left)
|
||||
.chain(comparators)
|
||||
.tuple_windows::<(&ruff_python_ast::Expr, &ruff_python_ast::Expr)>();
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
for (op, (left, right)) in std::iter::zip(&**ops, comparator_tuples) {
|
||||
if let ast::Expr::Name(ast::ExprName {
|
||||
range: _,
|
||||
@@ -242,24 +295,24 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
|
||||
match if is_positive { *op } else { op.negate() } {
|
||||
ast::CmpOp::IsNot => {
|
||||
if rhs_ty.is_singleton() {
|
||||
if rhs_ty.is_singleton(self.db) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(rhs_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
constraints.insert(symbol, ty);
|
||||
} else {
|
||||
// Non-singletons cannot be safely narrowed using `is not`
|
||||
}
|
||||
}
|
||||
ast::CmpOp::Is => {
|
||||
self.constraints.insert(symbol, rhs_ty);
|
||||
constraints.insert(symbol, rhs_ty);
|
||||
}
|
||||
ast::CmpOp::NotEq => {
|
||||
if rhs_ty.is_single_valued(self.db) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(rhs_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
constraints.insert(symbol, ty);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
@@ -268,59 +321,137 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(constraints)
|
||||
}
|
||||
|
||||
fn add_expr_call(
|
||||
fn evaluate_expr_call(
|
||||
&mut self,
|
||||
expr_call: &ast::ExprCall,
|
||||
expression: Expression<'db>,
|
||||
is_positive: bool,
|
||||
) {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
|
||||
if let Some(func_type) = inference
|
||||
// TODO: add support for PEP 604 union types on the right hand side of `isinstance`
|
||||
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
|
||||
match inference
|
||||
.expression_ty(expr_call.func.scoped_ast_id(self.db, scope))
|
||||
.into_function_literal_type()
|
||||
.into_function_literal()
|
||||
.and_then(|f| f.known(self.db))
|
||||
.and_then(KnownFunction::constraint_function)
|
||||
{
|
||||
if func_type.is_known(self.db, KnownFunction::IsInstance)
|
||||
&& expr_call.arguments.keywords.is_empty()
|
||||
{
|
||||
if let [ast::Expr::Name(ast::ExprName { id, .. }), rhs] = &*expr_call.arguments.args
|
||||
Some(function) if expr_call.arguments.keywords.is_empty() => {
|
||||
if let [ast::Expr::Name(ast::ExprName { id, .. }), class_info] =
|
||||
&*expr_call.arguments.args
|
||||
{
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let rhs_type = inference.expression_ty(rhs.scoped_ast_id(self.db, scope));
|
||||
let class_info_ty =
|
||||
inference.expression_ty(class_info.scoped_ast_id(self.db, scope));
|
||||
|
||||
// TODO: add support for PEP 604 union types on the right hand side:
|
||||
// isinstance(x, str | (int | float))
|
||||
if let Some(mut constraint) = generate_isinstance_constraint(self.db, &rhs_type)
|
||||
{
|
||||
if !is_positive {
|
||||
constraint = constraint.negate(self.db);
|
||||
let to_constraint = match function {
|
||||
KnownConstraintFunction::IsInstance => {
|
||||
|class_literal: ClassLiteralType<'db>| {
|
||||
Type::Instance(InstanceType {
|
||||
class: class_literal.class,
|
||||
})
|
||||
}
|
||||
}
|
||||
self.constraints.insert(symbol, constraint);
|
||||
}
|
||||
KnownConstraintFunction::IsSubclass => {
|
||||
|class_literal: ClassLiteralType<'db>| {
|
||||
Type::SubclassOf(class_literal.to_subclass_of_type())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generate_classinfo_constraint(self.db, &class_info_ty, to_constraint).map(
|
||||
|constraint| {
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, constraint.negate_if(self.db, !is_positive));
|
||||
constraints
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_match_pattern_singleton(
|
||||
fn evaluate_match_pattern_singleton(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
pattern: &ast::PatternMatchSingleton,
|
||||
) {
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
if let Some(ast::ExprName { id, .. }) = subject.as_name_expr() {
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let ty = match pattern.value {
|
||||
ast::Singleton::None => Type::None,
|
||||
ast::Singleton::None => Type::none(self.db),
|
||||
ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
};
|
||||
self.constraints.insert(symbol, ty);
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, ty);
|
||||
Some(constraints)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_bool_op(
|
||||
&mut self,
|
||||
expr_bool_op: &ExprBoolOp,
|
||||
expression: Expression<'db>,
|
||||
is_positive: bool,
|
||||
) -> Option<NarrowingConstraints<'db>> {
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
let scope = self.scope();
|
||||
let mut sub_constraints = expr_bool_op
|
||||
.values
|
||||
.iter()
|
||||
// filter our arms with statically known truthiness
|
||||
.filter(|expr| {
|
||||
inference
|
||||
.expression_ty(expr.scoped_ast_id(self.db, scope))
|
||||
.bool(self.db)
|
||||
!= match expr_bool_op.op {
|
||||
BoolOp::And => Truthiness::AlwaysTrue,
|
||||
BoolOp::Or => Truthiness::AlwaysFalse,
|
||||
}
|
||||
})
|
||||
.map(|sub_expr| {
|
||||
self.evaluate_expression_node_constraint(sub_expr, expression, is_positive)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
match (expr_bool_op.op, is_positive) {
|
||||
(BoolOp::And, true) | (BoolOp::Or, false) => {
|
||||
let mut aggregation: Option<NarrowingConstraints> = None;
|
||||
for sub_constraint in sub_constraints.into_iter().flatten() {
|
||||
if let Some(ref mut some_aggregation) = aggregation {
|
||||
merge_constraints_and(some_aggregation, sub_constraint, self.db);
|
||||
} else {
|
||||
aggregation = Some(sub_constraint);
|
||||
}
|
||||
}
|
||||
aggregation
|
||||
}
|
||||
(BoolOp::Or, true) | (BoolOp::And, false) => {
|
||||
let (first, rest) = sub_constraints.split_first_mut()?;
|
||||
if let Some(ref mut first) = first {
|
||||
for rest_constraint in rest {
|
||||
if let Some(rest_constraint) = rest_constraint {
|
||||
merge_constraints_or(first, rest_constraint, self.db);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
first.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
143
crates/red_knot_python_semantic/src/types/unpacker.rs
Normal file
143
crates/red_knot_python_semantic/src/types/unpacker.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::semantic_index::ast_ids::{HasScopedAstId, ScopedExpressionId};
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{TupleType, Type, TypeCheckDiagnostics, TypeCheckDiagnosticsBuilder};
|
||||
use crate::Db;
|
||||
|
||||
/// Unpacks the value expression type to their respective targets.
|
||||
pub(crate) struct Unpacker<'db> {
|
||||
db: &'db dyn Db,
|
||||
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
|
||||
diagnostics: TypeCheckDiagnosticsBuilder<'db>,
|
||||
}
|
||||
|
||||
impl<'db> Unpacker<'db> {
|
||||
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
|
||||
Self {
|
||||
db,
|
||||
targets: FxHashMap::default(),
|
||||
diagnostics: TypeCheckDiagnosticsBuilder::new(db, file),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unpack(&mut self, target: &ast::Expr, value_ty: Type<'db>, scope: ScopeId<'db>) {
|
||||
match target {
|
||||
ast::Expr::Name(target_name) => {
|
||||
self.targets
|
||||
.insert(target_name.scoped_ast_id(self.db, scope), value_ty);
|
||||
}
|
||||
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
|
||||
self.unpack(value, value_ty, scope);
|
||||
}
|
||||
ast::Expr::List(ast::ExprList { elts, .. })
|
||||
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => match value_ty {
|
||||
Type::Tuple(tuple_ty) => {
|
||||
let starred_index = elts.iter().position(ast::Expr::is_starred_expr);
|
||||
|
||||
let element_types = if let Some(starred_index) = starred_index {
|
||||
if tuple_ty.len(self.db) >= elts.len() - 1 {
|
||||
let mut element_types = Vec::with_capacity(elts.len());
|
||||
element_types.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db)[..starred_index],
|
||||
);
|
||||
|
||||
// E.g., in `(a, *b, c, d) = ...`, the index of starred element `b`
|
||||
// is 1 and the remaining elements after that are 2.
|
||||
let remaining = elts.len() - (starred_index + 1);
|
||||
// This index represents the type of the last element that belongs
|
||||
// to the starred expression, in an exclusive manner.
|
||||
let starred_end_index = tuple_ty.len(self.db) - remaining;
|
||||
// SAFETY: Safe because of the length check above.
|
||||
let _starred_element_types =
|
||||
&tuple_ty.elements(self.db)[starred_index..starred_end_index];
|
||||
// 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.extend_from_slice(
|
||||
// SAFETY: Safe because of the length check above.
|
||||
&tuple_ty.elements(self.db)[starred_end_index..],
|
||||
);
|
||||
Cow::Owned(element_types)
|
||||
} else {
|
||||
let mut element_types = tuple_ty.elements(self.db).to_vec();
|
||||
// Subtract 1 to insert the starred expression type at the correct
|
||||
// index.
|
||||
element_types.resize(elts.len() - 1, Type::Unknown);
|
||||
// TODO: This should be `list[Unknown]`
|
||||
element_types.insert(starred_index, Type::Todo);
|
||||
Cow::Owned(element_types)
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(tuple_ty.elements(self.db).as_ref())
|
||||
};
|
||||
|
||||
for (index, element) in elts.iter().enumerate() {
|
||||
self.unpack(
|
||||
element,
|
||||
element_types.get(index).copied().unwrap_or(Type::Unknown),
|
||||
scope,
|
||||
);
|
||||
}
|
||||
}
|
||||
Type::StringLiteral(string_literal_ty) => {
|
||||
// Deconstruct the string literal to delegate the inference back to the
|
||||
// tuple type for correct handling of starred expressions. We could go
|
||||
// further and deconstruct to an array of `StringLiteral` with each
|
||||
// individual character, instead of just an array of `LiteralString`, but
|
||||
// there would be a cost and it's not clear that it's worth it.
|
||||
let value_ty = Type::Tuple(TupleType::new(
|
||||
self.db,
|
||||
vec![Type::LiteralString; string_literal_ty.len(self.db)]
|
||||
.into_boxed_slice(),
|
||||
));
|
||||
self.unpack(target, value_ty, scope);
|
||||
}
|
||||
_ => {
|
||||
let value_ty = if value_ty.is_literal_string() {
|
||||
Type::LiteralString
|
||||
} else {
|
||||
value_ty
|
||||
.iterate(self.db)
|
||||
.unwrap_with_diagnostic(AnyNodeRef::from(target), &mut self.diagnostics)
|
||||
};
|
||||
for element in elts {
|
||||
self.unpack(element, value_ty, scope);
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn finish(mut self) -> UnpackResult<'db> {
|
||||
self.targets.shrink_to_fit();
|
||||
UnpackResult {
|
||||
diagnostics: self.diagnostics.finish(),
|
||||
targets: self.targets,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct UnpackResult<'db> {
|
||||
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
|
||||
diagnostics: TypeCheckDiagnostics,
|
||||
}
|
||||
|
||||
impl<'db> UnpackResult<'db> {
|
||||
pub(crate) fn get(&self, expr_id: ScopedExpressionId) -> Option<Type<'db>> {
|
||||
self.targets.get(&expr_id).copied()
|
||||
}
|
||||
|
||||
pub(crate) fn diagnostics(&self) -> &TypeCheckDiagnostics {
|
||||
&self.diagnostics
|
||||
}
|
||||
}
|
||||
55
crates/red_knot_python_semantic/src/unpack.rs
Normal file
55
crates/red_knot_python_semantic/src/unpack.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast};
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
use crate::Db;
|
||||
|
||||
/// This ingredient represents a single unpacking.
|
||||
///
|
||||
/// This is required to make use of salsa to cache the complete unpacking of multiple variables
|
||||
/// involved. It allows us to:
|
||||
/// 1. Avoid doing structural match multiple times for each definition
|
||||
/// 2. Avoid highlighting the same error multiple times
|
||||
///
|
||||
/// ## Module-local type
|
||||
/// This type should not be used as part of any cross-module API because
|
||||
/// it holds a reference to the AST node. Range-offset changes
|
||||
/// then propagate through all usages, and deserialization requires
|
||||
/// reparsing the entire module.
|
||||
///
|
||||
/// E.g. don't use this type in:
|
||||
///
|
||||
/// * a return type of a cross-module query
|
||||
/// * a field of a type that is a return type of a cross-module query
|
||||
/// * an argument of a cross-module query
|
||||
#[salsa::tracked]
|
||||
pub(crate) struct Unpack<'db> {
|
||||
#[id]
|
||||
pub(crate) file: File,
|
||||
|
||||
#[id]
|
||||
pub(crate) file_scope: FileScopeId,
|
||||
|
||||
/// The target expression that is being unpacked. For example, in `(a, b) = (1, 2)`, the target
|
||||
/// expression is `(a, b)`.
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) target: AstNodeRef<ast::Expr>,
|
||||
|
||||
/// The ingredient representing the value expression of the unpacking. For example, in
|
||||
/// `(a, b) = (1, 2)`, the value expression is `(1, 2)`.
|
||||
#[no_eq]
|
||||
pub(crate) value: Expression<'db>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Unpack<'static>>,
|
||||
}
|
||||
|
||||
impl<'db> Unpack<'db> {
|
||||
/// Returns the scope where the unpacking is happening.
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
self.file_scope(db).to_scope_id(db, self.file(db))
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ mod text_document;
|
||||
|
||||
use lsp_types::{PositionEncodingKind, Url};
|
||||
pub use notebook::NotebookDocument;
|
||||
pub(crate) use range::RangeExt;
|
||||
pub(crate) use range::{RangeExt, ToRangeExt};
|
||||
pub(crate) use text_document::DocumentVersion;
|
||||
pub use text_document::TextDocument;
|
||||
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
use super::notebook;
|
||||
use super::PositionEncoding;
|
||||
use ruff_source_file::LineIndex;
|
||||
use lsp_types as types;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_source_file::{LineIndex, SourceLocation};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
pub(crate) struct NotebookRange {
|
||||
pub(crate) cell: notebook::CellId,
|
||||
pub(crate) range: types::Range,
|
||||
}
|
||||
|
||||
pub(crate) trait RangeExt {
|
||||
fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding)
|
||||
-> TextRange;
|
||||
}
|
||||
|
||||
pub(crate) trait ToRangeExt {
|
||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range;
|
||||
fn to_notebook_range(
|
||||
&self,
|
||||
text: &str,
|
||||
source_index: &LineIndex,
|
||||
notebook_index: &NotebookIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> NotebookRange;
|
||||
}
|
||||
|
||||
fn u32_index_to_usize(index: u32) -> usize {
|
||||
usize::try_from(index).expect("u32 fits in usize")
|
||||
}
|
||||
@@ -75,6 +94,61 @@ impl RangeExt for lsp_types::Range {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRangeExt for TextRange {
|
||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range {
|
||||
types::Range {
|
||||
start: source_location_to_position(&offset_to_source_location(
|
||||
self.start(),
|
||||
text,
|
||||
index,
|
||||
encoding,
|
||||
)),
|
||||
end: source_location_to_position(&offset_to_source_location(
|
||||
self.end(),
|
||||
text,
|
||||
index,
|
||||
encoding,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_notebook_range(
|
||||
&self,
|
||||
text: &str,
|
||||
source_index: &LineIndex,
|
||||
notebook_index: &NotebookIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> NotebookRange {
|
||||
let start = offset_to_source_location(self.start(), text, source_index, encoding);
|
||||
let mut end = offset_to_source_location(self.end(), text, source_index, encoding);
|
||||
let starting_cell = notebook_index.cell(start.row);
|
||||
|
||||
// weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds')
|
||||
// we need to move it one character back (which should place it at the end of the last line).
|
||||
// we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset.
|
||||
if notebook_index.cell(end.row) != starting_cell {
|
||||
end.row = end.row.saturating_sub(1);
|
||||
end.column = offset_to_source_location(
|
||||
self.end().checked_sub(1.into()).unwrap_or_default(),
|
||||
text,
|
||||
source_index,
|
||||
encoding,
|
||||
)
|
||||
.column;
|
||||
}
|
||||
|
||||
let start = source_location_to_position(¬ebook_index.translate_location(&start));
|
||||
let end = source_location_to_position(¬ebook_index.translate_location(&end));
|
||||
|
||||
NotebookRange {
|
||||
cell: starting_cell
|
||||
.map(OneIndexed::to_zero_indexed)
|
||||
.unwrap_or_default(),
|
||||
range: types::Range { start, end },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number.
|
||||
fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize {
|
||||
let mut utf8_code_unit_offset = TextSize::new(0);
|
||||
@@ -96,3 +170,46 @@ fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize {
|
||||
|
||||
utf8_code_unit_offset
|
||||
}
|
||||
|
||||
fn offset_to_source_location(
|
||||
offset: TextSize,
|
||||
text: &str,
|
||||
index: &LineIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> SourceLocation {
|
||||
match encoding {
|
||||
PositionEncoding::UTF8 => {
|
||||
let row = index.line_index(offset);
|
||||
let column = offset - index.line_start(row, text);
|
||||
|
||||
SourceLocation {
|
||||
column: OneIndexed::from_zero_indexed(column.to_usize()),
|
||||
row,
|
||||
}
|
||||
}
|
||||
PositionEncoding::UTF16 => {
|
||||
let row = index.line_index(offset);
|
||||
|
||||
let column = if index.is_ascii() {
|
||||
(offset - index.line_start(row, text)).to_usize()
|
||||
} else {
|
||||
let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)];
|
||||
up_to_line.encode_utf16().count()
|
||||
};
|
||||
|
||||
SourceLocation {
|
||||
column: OneIndexed::from_zero_indexed(column),
|
||||
row,
|
||||
}
|
||||
}
|
||||
PositionEncoding::UTF32 => index.source_location(offset, text),
|
||||
}
|
||||
}
|
||||
|
||||
fn source_location_to_position(location: &SourceLocation) -> types::Position {
|
||||
types::Position {
|
||||
line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"),
|
||||
character: u32::try_from(location.column.to_zero_indexed())
|
||||
.expect("character usize fits in u32"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ use std::borrow::Cow;
|
||||
use lsp_types::request::DocumentDiagnosticRequest;
|
||||
use lsp_types::{
|
||||
Diagnostic, DiagnosticSeverity, DocumentDiagnosticParams, DocumentDiagnosticReport,
|
||||
DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, Position, Range,
|
||||
DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, NumberOrString, Range,
|
||||
RelatedFullDocumentDiagnosticReport, Url,
|
||||
};
|
||||
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
|
||||
use crate::edit::ToRangeExt;
|
||||
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
|
||||
use crate::server::{client::Notifier, Result};
|
||||
use crate::session::DocumentSnapshot;
|
||||
use red_knot_workspace::db::{Db, RootDatabase};
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
|
||||
pub(crate) struct DocumentDiagnosticRequestHandler;
|
||||
|
||||
@@ -64,36 +66,37 @@ fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &RootDatabase) -> Vec<Di
|
||||
diagnostics
|
||||
.as_slice()
|
||||
.iter()
|
||||
.map(|message| to_lsp_diagnostic(message))
|
||||
.map(|message| to_lsp_diagnostic(db, message, snapshot.encoding()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_lsp_diagnostic(message: &str) -> Diagnostic {
|
||||
let words = message.split(':').collect::<Vec<_>>();
|
||||
fn to_lsp_diagnostic(
|
||||
db: &dyn Db,
|
||||
diagnostic: &dyn ruff_db::diagnostic::Diagnostic,
|
||||
encoding: crate::PositionEncoding,
|
||||
) -> Diagnostic {
|
||||
let range = if let Some(range) = diagnostic.range() {
|
||||
let index = line_index(db.upcast(), diagnostic.file());
|
||||
let source = source_text(db.upcast(), diagnostic.file());
|
||||
|
||||
let (range, message) = match words.as_slice() {
|
||||
[_, _, line, column, message] | [_, line, column, message] => {
|
||||
let line = line.parse::<u32>().unwrap_or_default().saturating_sub(1);
|
||||
let column = column.parse::<u32>().unwrap_or_default();
|
||||
(
|
||||
Range::new(
|
||||
Position::new(line, column.saturating_sub(1)),
|
||||
Position::new(line, column),
|
||||
),
|
||||
message.trim(),
|
||||
)
|
||||
}
|
||||
_ => (Range::default(), message),
|
||||
range.to_range(&source, &index, encoding)
|
||||
} else {
|
||||
Range::default()
|
||||
};
|
||||
|
||||
let severity = match diagnostic.severity() {
|
||||
Severity::Info => DiagnosticSeverity::INFORMATION,
|
||||
Severity::Error => DiagnosticSeverity::ERROR,
|
||||
};
|
||||
|
||||
Diagnostic {
|
||||
range,
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
severity: Some(severity),
|
||||
tags: None,
|
||||
code: None,
|
||||
code: Some(NumberOrString::String(diagnostic.rule().to_string())),
|
||||
code_description: None,
|
||||
source: Some("red-knot".into()),
|
||||
message: message.to_string(),
|
||||
message: diagnostic.message().into_owned(),
|
||||
related_information: None,
|
||||
data: None,
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! We don't assume that we will get the diagnostics in source order.
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::ops::{Deref, Range};
|
||||
|
||||
/// All diagnostics for one embedded Python file, sorted and grouped by start line number.
|
||||
@@ -19,13 +19,17 @@ pub(crate) struct SortedDiagnostics<T> {
|
||||
|
||||
impl<T> SortedDiagnostics<T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
T: Diagnostic,
|
||||
{
|
||||
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
|
||||
let mut diagnostics: Vec<_> = diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| DiagnosticWithLine {
|
||||
line_number: line_index.line_index(diagnostic.start()),
|
||||
line_number: diagnostic
|
||||
.range()
|
||||
.map_or(OneIndexed::from_zero_indexed(0), |range| {
|
||||
line_index.line_index(range.start())
|
||||
}),
|
||||
diagnostic,
|
||||
})
|
||||
.collect();
|
||||
@@ -94,7 +98,7 @@ pub(crate) struct LineDiagnosticsIterator<'a, T> {
|
||||
|
||||
impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
T: Diagnostic,
|
||||
{
|
||||
type Item = LineDiagnostics<'a, T>;
|
||||
|
||||
@@ -110,7 +114,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Clone + Ranged {}
|
||||
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Diagnostic {}
|
||||
|
||||
/// All diagnostics that start on a single line of source code in one embedded Python file.
|
||||
#[derive(Debug)]
|
||||
@@ -139,11 +143,14 @@ struct DiagnosticWithLine<T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[test]
|
||||
fn sort_and_group() {
|
||||
@@ -152,13 +159,18 @@ mod tests {
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
let lines = line_index(&db, file);
|
||||
|
||||
let ranges = vec![
|
||||
let ranges = [
|
||||
TextRange::new(TextSize::new(0), TextSize::new(1)),
|
||||
TextRange::new(TextSize::new(5), TextSize::new(10)),
|
||||
TextRange::new(TextSize::new(1), TextSize::new(7)),
|
||||
];
|
||||
|
||||
let sorted = super::SortedDiagnostics::new(&ranges, &lines);
|
||||
let diagnostics: Vec<_> = ranges
|
||||
.into_iter()
|
||||
.map(|range| DummyDiagnostic { range, file })
|
||||
.collect();
|
||||
|
||||
let sorted = super::SortedDiagnostics::new(diagnostics, &lines);
|
||||
let grouped = sorted.iter_lines().collect::<Vec<_>>();
|
||||
|
||||
let [line1, line2] = &grouped[..] else {
|
||||
@@ -170,4 +182,32 @@ mod tests {
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
|
||||
assert_eq!(line2.diagnostics.len(), 1);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DummyDiagnostic {
|
||||
range: TextRange,
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl Diagnostic for DummyDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"dummy"
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
"dummy".into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.range)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
@@ -87,16 +88,24 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
.filter_map(|test_file| {
|
||||
let parsed = parsed_module(db, test_file.file);
|
||||
|
||||
// TODO allow testing against code with syntax errors
|
||||
assert!(
|
||||
parsed.errors().is_empty(),
|
||||
"Python syntax errors in {}, {}: {:?}",
|
||||
test.name(),
|
||||
test_file.file.path(db),
|
||||
parsed.errors()
|
||||
);
|
||||
let mut diagnostics: Vec<Box<_>> = parsed
|
||||
.errors()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|error| {
|
||||
let diagnostic: Box<dyn Diagnostic> =
|
||||
Box::new(ParseDiagnostic::new(test_file.file, error));
|
||||
diagnostic
|
||||
})
|
||||
.collect();
|
||||
|
||||
match matcher::match_file(db, test_file.file, check_types(db, test_file.file)) {
|
||||
let type_diagnostics = check_types(db, test_file.file);
|
||||
diagnostics.extend(type_diagnostics.into_iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new((*diagnostic).clone());
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
match matcher::match_file(db, test_file.file, diagnostics) {
|
||||
Ok(()) => None,
|
||||
Err(line_failures) => Some(FileFailures {
|
||||
backtick_offset: test_file.backtick_offset,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
//! Match [`TypeCheckDiagnostic`]s against [`Assertion`]s and produce test failure messages for any
|
||||
//! Match [`Diagnostic`]s against [`Assertion`]s and produce test failure messages for any
|
||||
//! mismatches.
|
||||
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::SortedDiagnostics;
|
||||
use colored::Colorize;
|
||||
use red_knot_python_semantic::types::TypeCheckDiagnostic;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct FailuresByLine {
|
||||
@@ -55,7 +53,7 @@ pub(super) fn match_file<T>(
|
||||
diagnostics: impl IntoIterator<Item = T>,
|
||||
) -> Result<(), FailuresByLine>
|
||||
where
|
||||
T: Diagnostic + Clone,
|
||||
T: Diagnostic,
|
||||
{
|
||||
// Parse assertions from comments in the file, and get diagnostics from the file; both
|
||||
// ordered by line number.
|
||||
@@ -126,22 +124,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait Diagnostic: Ranged {
|
||||
fn rule(&self) -> &str;
|
||||
|
||||
fn message(&self) -> &str;
|
||||
}
|
||||
|
||||
impl Diagnostic for Arc<TypeCheckDiagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
self.as_ref().rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.as_ref().message()
|
||||
}
|
||||
}
|
||||
|
||||
trait Unmatched {
|
||||
fn unmatched(&self) -> String;
|
||||
}
|
||||
@@ -253,10 +235,15 @@ impl Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn column<T: Ranged>(&self, ranged: &T) -> OneIndexed {
|
||||
self.line_index
|
||||
.source_location(ranged.start(), &self.source)
|
||||
.column
|
||||
fn column<T: Diagnostic>(&self, diagnostic: &T) -> OneIndexed {
|
||||
diagnostic
|
||||
.range()
|
||||
.map(|range| {
|
||||
self.line_index
|
||||
.source_location(range.start(), &self.source)
|
||||
.column
|
||||
})
|
||||
.unwrap_or(OneIndexed::from_zero_indexed(0))
|
||||
}
|
||||
|
||||
/// Check if `assertion` matches any [`Diagnostic`]s in `unmatched`.
|
||||
@@ -323,20 +310,21 @@ impl Matcher {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_text_size::TextRange;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TestDiagnostic {
|
||||
struct ExpectedDiagnostic {
|
||||
rule: &'static str,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl TestDiagnostic {
|
||||
impl ExpectedDiagnostic {
|
||||
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self {
|
||||
let offset: u32 = offset.try_into().unwrap();
|
||||
Self {
|
||||
@@ -345,32 +333,64 @@ mod tests {
|
||||
range: TextRange::new(offset.into(), (offset + 1).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_diagnostic(self, file: File) -> TestDiagnostic {
|
||||
TestDiagnostic {
|
||||
rule: self.rule,
|
||||
message: self.message,
|
||||
range: self.range,
|
||||
file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Diagnostic for TestDiagnostic {
|
||||
#[derive(Debug)]
|
||||
struct TestDiagnostic {
|
||||
rule: &'static str,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl Diagnostic for TestDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
self.rule
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.message
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.message.into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.range)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TestDiagnostic {
|
||||
fn range(&self) -> ruff_text_size::TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
fn get_result(source: &str, diagnostics: Vec<TestDiagnostic>) -> Result<(), FailuresByLine> {
|
||||
fn get_result(
|
||||
source: &str,
|
||||
diagnostics: Vec<ExpectedDiagnostic>,
|
||||
) -> Result<(), FailuresByLine> {
|
||||
colored::control::set_override(false);
|
||||
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
|
||||
super::match_file(&db, file, diagnostics)
|
||||
super::match_file(
|
||||
&db,
|
||||
file,
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.into_diagnostic(file)),
|
||||
)
|
||||
}
|
||||
|
||||
fn assert_fail(result: Result<(), FailuresByLine>, messages: &[(usize, &[&str])]) {
|
||||
@@ -403,7 +423,7 @@ mod tests {
|
||||
fn revealed_match() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
@@ -417,7 +437,7 @@ mod tests {
|
||||
fn revealed_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"not-revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
@@ -440,7 +460,11 @@ mod tests {
|
||||
fn revealed_wrong_message() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("revealed-type", "Something else", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
"Something else",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -467,8 +491,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -479,7 +503,11 @@ mod tests {
|
||||
fn revealed_match_with_only_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"undefined-reveal",
|
||||
"Doesn't matter",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
@@ -490,8 +518,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -512,8 +540,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1)",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -535,8 +563,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1) # error: [something-else]",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -565,7 +593,7 @@ mod tests {
|
||||
fn error_match_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -575,7 +603,7 @@ mod tests {
|
||||
fn error_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -594,7 +622,11 @@ mod tests {
|
||||
fn error_match_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -604,7 +636,7 @@ mod tests {
|
||||
fn error_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -623,7 +655,7 @@ mod tests {
|
||||
fn error_match_column_and_rule() {
|
||||
let result = get_result(
|
||||
"x # error: 1 [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -633,7 +665,7 @@ mod tests {
|
||||
fn error_wrong_column() {
|
||||
let result = get_result(
|
||||
"x # error: 2 [rule]",
|
||||
vec![TestDiagnostic::new("rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -652,7 +684,11 @@ mod tests {
|
||||
fn error_match_column_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -662,7 +698,11 @@ mod tests {
|
||||
fn error_match_rule_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -672,7 +712,11 @@ mod tests {
|
||||
fn error_match_all() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -682,7 +726,11 @@ mod tests {
|
||||
fn error_match_all_wrong_column() {
|
||||
let result = get_result(
|
||||
r#"x # error: 2 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"some-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -701,7 +749,7 @@ mod tests {
|
||||
fn error_match_all_wrong_rule() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new(
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"other-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
@@ -724,7 +772,7 @@ mod tests {
|
||||
fn error_match_all_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -757,9 +805,9 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
TestDiagnostic::new("line-three", "msg", three),
|
||||
TestDiagnostic::new("line-five", "msg", five),
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new("line-three", "msg", three),
|
||||
ExpectedDiagnostic::new("line-five", "msg", five),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -788,8 +836,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-one", "msg", one),
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new("line-one", "msg", one),
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -809,8 +857,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -830,8 +878,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -851,9 +899,9 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
TestDiagnostic::new("third-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("third-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -877,8 +925,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -891,7 +939,7 @@ mod tests {
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -912,7 +960,7 @@ mod tests {
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
|
||||
@@ -6,6 +6,7 @@ use wasm_bindgen::prelude::*;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
|
||||
use ruff_db::system::{
|
||||
@@ -110,14 +111,20 @@ impl Workspace {
|
||||
pub fn check_file(&self, file_id: &FileHandle) -> Result<Vec<String>, Error> {
|
||||
let result = self.db.check_file(file_id.file).map_err(into_error)?;
|
||||
|
||||
Ok(result)
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&self.db).to_string())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Checks all open files
|
||||
pub fn check(&self) -> Result<Vec<String>, Error> {
|
||||
let result = self.db.check().map_err(into_error)?;
|
||||
|
||||
Ok(result)
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&self.db).to_string())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns the parsed AST for `path`
|
||||
|
||||
@@ -19,6 +19,6 @@ fn check() {
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec!["/test.py:1:8: Cannot resolve import `random22`"]
|
||||
vec!["error[unresolved-import] /test.py:1:8 Cannot resolve import `random22`"]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Regression test for https://github.com/astral-sh/ruff/issues/14115
|
||||
#
|
||||
# This is invalid syntax, but should not lead to a crash.
|
||||
|
||||
def f() -> *int: ...
|
||||
|
||||
|
||||
f()
|
||||
@@ -4,14 +4,14 @@ use std::sync::Arc;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Cancelled, Event};
|
||||
|
||||
use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
|
||||
|
||||
mod changes;
|
||||
|
||||
#[salsa::db]
|
||||
@@ -51,11 +51,11 @@ impl RootDatabase {
|
||||
}
|
||||
|
||||
/// Checks all open files in the workspace and its dependencies.
|
||||
pub fn check(&self) -> Result<Vec<String>, Cancelled> {
|
||||
pub fn check(&self) -> Result<Vec<Box<dyn Diagnostic>>, Cancelled> {
|
||||
self.with_db(|db| db.workspace().check(db))
|
||||
}
|
||||
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<String>, Cancelled> {
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn Diagnostic>>, Cancelled> {
|
||||
let _span = tracing::debug_span!("check_file", file=%file.path(self)).entered();
|
||||
|
||||
self.with_db(|db| check_file(db, file))
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use salsa::{Durability, Setter as _};
|
||||
use std::borrow::Cow;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::db::RootDatabase;
|
||||
use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
pub use metadata::{PackageMetadata, WorkspaceMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::SearchPathSettings;
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic, Severity};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{line_index, source_text, SourceDiagnostic};
|
||||
use ruff_db::source::{source_text, SourceTextError};
|
||||
use ruff_db::{
|
||||
files::{system_path_to_file, File},
|
||||
system::{walk_directory::WalkState, SystemPath, SystemPathBuf},
|
||||
};
|
||||
use ruff_python_ast::{name::Name, PySourceType};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::db::RootDatabase;
|
||||
use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
mod files;
|
||||
mod metadata;
|
||||
@@ -188,7 +188,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
/// Checks all open files in the workspace and its dependencies.
|
||||
pub fn check(self, db: &RootDatabase) -> Vec<String> {
|
||||
pub fn check(self, db: &RootDatabase) -> Vec<Box<dyn Diagnostic>> {
|
||||
let workspace_span = tracing::debug_span!("check_workspace");
|
||||
let _span = workspace_span.enter();
|
||||
|
||||
@@ -378,47 +378,31 @@ impl Package {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<String> {
|
||||
tracing::debug!("Checking file '{path}'", path = file.path(db));
|
||||
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let source_diagnostics = source_text::accumulated::<SourceDiagnostic>(db.upcast(), file);
|
||||
// TODO(micha): Consider using a single accumulator for all diagnostics
|
||||
diagnostics.extend(
|
||||
source_diagnostics
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string),
|
||||
);
|
||||
|
||||
pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
if source.has_read_error() {
|
||||
if let Some(read_error) = source.read_error() {
|
||||
diagnostics.push(Box::new(IOErrorDiagnostic {
|
||||
file,
|
||||
error: read_error.clone(),
|
||||
}));
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
diagnostics.extend(parsed.errors().iter().map(|error| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(ParseDiagnostic::new(file, error.clone()));
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
if !parsed.errors().is_empty() {
|
||||
let path = file.path(db);
|
||||
let line_index = line_index(db.upcast(), file);
|
||||
diagnostics.extend(parsed.errors().iter().map(|err| {
|
||||
let source_location = line_index.source_location(err.location.start(), source.as_str());
|
||||
format!("{path}:{source_location}: {message}", message = err.error)
|
||||
}));
|
||||
}
|
||||
diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
|
||||
let boxed: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
boxed
|
||||
}));
|
||||
|
||||
for diagnostic in check_types(db.upcast(), file) {
|
||||
let index = line_index(db.upcast(), diagnostic.file());
|
||||
let location = index.source_location(diagnostic.start(), source.as_str());
|
||||
diagnostics.push(format!(
|
||||
"{path}:{location}: {message}",
|
||||
path = file.path(db),
|
||||
message = diagnostic.message()
|
||||
));
|
||||
}
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start());
|
||||
|
||||
diagnostics
|
||||
}
|
||||
@@ -533,17 +517,45 @@ impl Iterator for WorkspaceFilesIter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IOErrorDiagnostic {
|
||||
file: File,
|
||||
error: SourceTextError,
|
||||
}
|
||||
|
||||
impl Diagnostic for IOErrorDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"io"
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
None
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::workspace::check_file;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::workspace::check_file;
|
||||
|
||||
#[test]
|
||||
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
@@ -558,7 +570,10 @@ mod tests {
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file(&db, file),
|
||||
check_file(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Failed to read file: No such file or directory".to_string()]
|
||||
);
|
||||
|
||||
@@ -570,7 +585,13 @@ mod tests {
|
||||
db.write_file(path, "").unwrap();
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(check_file(&db, file), vec![] as Vec<String>);
|
||||
assert_eq!(
|
||||
check_file(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![] as Vec<String>
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -85,6 +85,7 @@ else:
|
||||
|
||||
## Options
|
||||
- `lint.ignore-init-module-imports`
|
||||
- `lint.pyflakes.allowed-unused-imports`
|
||||
|
||||
## References
|
||||
- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)
|
||||
|
||||
@@ -8,6 +8,7 @@ use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_benchmark::criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use ruff_benchmark::TestFile;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem};
|
||||
@@ -24,31 +25,32 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
|
||||
|
||||
static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
// We don't support `*` imports yet:
|
||||
"/src/tomllib/_parser.py:7:29: Module `collections.abc` has no member `Iterable`",
|
||||
"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:
|
||||
"/src/tomllib/_parser.py:246:15: Method `__class_getitem__` of type `Literal[frozenset]` is possibly unbound",
|
||||
"/src/tomllib/_parser.py:66:18: Name `s` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:98:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:101:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:108:17: Conflicting declared types for `second_char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:126:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:267:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:348:20: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:353:5: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:364:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:381:13: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:395:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:453:24: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:455:9: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:482:16: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:566:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:573:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:579:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:580:63: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:590:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:629:38: Name `datetime_obj` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
|
||||
"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[call-possibly-unbound-method] /src/tomllib/_parser.py:246:15 Method `__class_getitem__` of type `Literal[frozenset]` is possibly unbound",
|
||||
"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",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
|
||||
"error[invalid-base] /src/tomllib/_parser.py:692:8354 Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
];
|
||||
|
||||
fn get_test_file(name: &str) -> TestFile {
|
||||
@@ -122,7 +124,14 @@ fn setup_rayon() {
|
||||
fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
fn setup() -> Case {
|
||||
let case = setup_case();
|
||||
let result = case.db.check().unwrap();
|
||||
|
||||
let result: Vec<_> = case
|
||||
.db
|
||||
.check()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&case.db).to_string())
|
||||
.collect();
|
||||
|
||||
assert_eq!(result, EXPECTED_DIAGNOSTICS);
|
||||
|
||||
@@ -149,7 +158,7 @@ fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
|
||||
let result = db.check().unwrap();
|
||||
|
||||
assert_eq!(result, EXPECTED_DIAGNOSTICS);
|
||||
assert_eq!(result.len(), EXPECTED_DIAGNOSTICS.len());
|
||||
}
|
||||
|
||||
setup_rayon();
|
||||
@@ -167,7 +176,12 @@ fn benchmark_cold(criterion: &mut Criterion) {
|
||||
setup_case,
|
||||
|case| {
|
||||
let Case { db, .. } = case;
|
||||
let result = db.check().unwrap();
|
||||
let result: Vec<_> = db
|
||||
.check()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(db).to_string())
|
||||
.collect();
|
||||
|
||||
assert_eq!(result, EXPECTED_DIAGNOSTICS);
|
||||
},
|
||||
|
||||
180
crates/ruff_db/src/diagnostic.rs
Normal file
180
crates/ruff_db/src/diagnostic.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use crate::{
|
||||
files::File,
|
||||
source::{line_index, source_text},
|
||||
Db,
|
||||
};
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_text_size::TextRange;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub trait Diagnostic: Send + Sync + std::fmt::Debug {
|
||||
fn rule(&self) -> &str;
|
||||
|
||||
fn message(&self) -> std::borrow::Cow<str>;
|
||||
|
||||
fn file(&self) -> File;
|
||||
|
||||
fn range(&self) -> Option<TextRange>;
|
||||
|
||||
fn severity(&self) -> Severity;
|
||||
|
||||
fn display<'a>(&'a self, db: &'a dyn Db) -> DisplayDiagnostic<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
DisplayDiagnostic {
|
||||
db,
|
||||
diagnostic: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct DisplayDiagnostic<'db> {
|
||||
db: &'db dyn Db,
|
||||
diagnostic: &'db dyn Diagnostic,
|
||||
}
|
||||
|
||||
impl<'db> DisplayDiagnostic<'db> {
|
||||
pub fn new(db: &'db dyn Db, diagnostic: &'db dyn Diagnostic) -> Self {
|
||||
Self { db, diagnostic }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.diagnostic.severity() {
|
||||
Severity::Info => f.write_str("info")?,
|
||||
Severity::Error => f.write_str("error")?,
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"[{rule}] {path}",
|
||||
rule = self.diagnostic.rule(),
|
||||
path = self.diagnostic.file().path(self.db)
|
||||
)?;
|
||||
|
||||
if let Some(range) = self.diagnostic.range() {
|
||||
let index = line_index(self.db, self.diagnostic.file());
|
||||
let source = source_text(self.db, self.diagnostic.file());
|
||||
|
||||
let start = index.source_location(range.start(), &source);
|
||||
|
||||
write!(f, ":{line}:{col}", line = start.row, col = start.column)?;
|
||||
}
|
||||
|
||||
write!(f, " {message}", message = self.diagnostic.message())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Diagnostic for Box<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Diagnostic for std::sync::Arc<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> std::borrow::Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for Box<dyn Diagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseDiagnostic {
|
||||
file: File,
|
||||
error: ParseError,
|
||||
}
|
||||
|
||||
impl ParseDiagnostic {
|
||||
pub fn new(file: File, error: ParseError) -> Self {
|
||||
Self { file, error }
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for ParseDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"invalid-syntax"
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.error.location)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use crate::files::Files;
|
||||
use crate::system::System;
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
|
||||
pub mod diagnostic;
|
||||
pub mod display;
|
||||
pub mod file_revision;
|
||||
pub mod files;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
use salsa::Accumulator;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
@@ -17,16 +15,14 @@ use crate::Db;
|
||||
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
let path = file.path(db);
|
||||
let _span = tracing::trace_span!("source_text", file = %path).entered();
|
||||
let mut has_read_error = false;
|
||||
let mut read_error = None;
|
||||
|
||||
let kind = if is_notebook(file.path(db)) {
|
||||
file.read_to_notebook(db)
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::debug!("Failed to read notebook '{path}': {error}");
|
||||
|
||||
has_read_error = true;
|
||||
SourceDiagnostic(Arc::new(SourceTextError::FailedToReadNotebook(error)))
|
||||
.accumulate(db);
|
||||
read_error = Some(SourceTextError::FailedToReadNotebook(error.to_string()));
|
||||
Notebook::empty()
|
||||
})
|
||||
.into()
|
||||
@@ -35,8 +31,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::debug!("Failed to read file '{path}': {error}");
|
||||
|
||||
has_read_error = true;
|
||||
SourceDiagnostic(Arc::new(SourceTextError::FailedToReadFile(error))).accumulate(db);
|
||||
read_error = Some(SourceTextError::FailedToReadFile(error.to_string()));
|
||||
String::new()
|
||||
})
|
||||
.into()
|
||||
@@ -45,7 +40,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
SourceText {
|
||||
inner: Arc::new(SourceTextInner {
|
||||
kind,
|
||||
has_read_error,
|
||||
read_error,
|
||||
count: Count::new(),
|
||||
}),
|
||||
}
|
||||
@@ -98,8 +93,8 @@ impl SourceText {
|
||||
}
|
||||
|
||||
/// Returns `true` if there was an error when reading the content of the file.
|
||||
pub fn has_read_error(&self) -> bool {
|
||||
self.inner.has_read_error
|
||||
pub fn read_error(&self) -> Option<&SourceTextError> {
|
||||
self.inner.read_error.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +127,7 @@ impl std::fmt::Debug for SourceText {
|
||||
struct SourceTextInner {
|
||||
count: Count<SourceText>,
|
||||
kind: SourceTextKind,
|
||||
has_read_error: bool,
|
||||
read_error: Option<SourceTextError>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
@@ -153,21 +148,12 @@ impl From<Notebook> for SourceTextKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::accumulator]
|
||||
pub struct SourceDiagnostic(Arc<SourceTextError>);
|
||||
|
||||
impl std::fmt::Display for SourceDiagnostic {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
|
||||
pub enum SourceTextError {
|
||||
#[error("Failed to read notebook: {0}`")]
|
||||
FailedToReadNotebook(#[from] ruff_notebook::NotebookError),
|
||||
FailedToReadNotebook(String),
|
||||
#[error("Failed to read file: {0}")]
|
||||
FailedToReadFile(#[from] std::io::Error),
|
||||
FailedToReadFile(String),
|
||||
}
|
||||
|
||||
/// Computes the [`LineIndex`] for `file`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
10
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_1.pyi
vendored
Normal file
10
crates/ruff_linter/resources/test/fixtures/flake8_bugbear/B006_1.pyi
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# we disable this rule for pyi files
|
||||
|
||||
def mquantiles(
|
||||
a: _ArrayLikeFloat_co,
|
||||
prob: _ArrayLikeFloat_co = [0.25, 0.5, 0.75],
|
||||
alphap: AnyReal = 0.4,
|
||||
betap: AnyReal = 0.4,
|
||||
axis: CanIndex | None = None,
|
||||
limit: tuple[AnyReal, AnyReal] | tuple[()] = (),
|
||||
) -> _MArrayND: ...
|
||||
@@ -19,6 +19,7 @@ incorrect_set = {
|
||||
1,
|
||||
1,
|
||||
}
|
||||
incorrect_set = {False, 1, 0}
|
||||
|
||||
###
|
||||
# Non-errors.
|
||||
|
||||
@@ -10,6 +10,8 @@ async def func3(id, dir):
|
||||
pass
|
||||
|
||||
|
||||
# this is Ok for A002 (trigger A005 instead)
|
||||
# https://github.com/astral-sh/ruff/issues/14135
|
||||
map([], lambda float: ...)
|
||||
|
||||
from typing import override, overload
|
||||
|
||||
@@ -3,3 +3,8 @@ lambda x, float, y: x + y
|
||||
lambda min, max: min
|
||||
lambda id: id
|
||||
lambda dir: dir
|
||||
|
||||
# Ok for A006 - should trigger A002 instead
|
||||
# https://github.com/astral-sh/ruff/issues/14135
|
||||
def func1(str, /, type, *complex, Exception, **getattr):
|
||||
pass
|
||||
@@ -7,8 +7,14 @@ if sys.version_info >= (3, 9): ... # OK
|
||||
|
||||
if sys.version_info == (3, 9): ... # OK
|
||||
|
||||
if sys.version_info <= (3, 10): ... # OK
|
||||
if sys.version_info == (3, 9): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
if sys.version_info > (3, 10): ... # OK
|
||||
if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
if python_version > (3, 10): ... # OK
|
||||
if sys.version_info <= (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
if sys.version_info > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
elif sys.version_info > (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
if python_version > (3, 10): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
elif python_version == (3, 11): ... # Error: PYI006 Use only `<` and `>=` for version info comparisons
|
||||
|
||||
@@ -6,6 +6,19 @@ def foo():
|
||||
class Bar:
|
||||
"""bar""" # ERROR PYI021
|
||||
|
||||
class Qux:
|
||||
"""qux""" # ERROR PYI021
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
class Baz:
|
||||
"""Multiline docstring
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
"""
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def bar():
|
||||
x = 1
|
||||
"""foo""" # OK, not a doc string
|
||||
|
||||
@@ -256,3 +256,11 @@ dbm.sqlite3.open("foo.db").close()
|
||||
# SIM115
|
||||
f = dbm.sqlite3.open("foo.db")
|
||||
f.close()
|
||||
|
||||
# OK
|
||||
def func(filepath, encoding):
|
||||
return open(filepath, mode="rt", encoding=encoding)
|
||||
|
||||
# OK
|
||||
def func(filepath, encoding):
|
||||
return f(open(filepath, mode="rt", encoding=encoding))
|
||||
|
||||
99
crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py
vendored
Normal file
99
crates/ruff_linter/resources/test/fixtures/flake8_simplify/SIM905.py
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
# setup
|
||||
sep = ","
|
||||
no_sep = None
|
||||
|
||||
# positives
|
||||
"""
|
||||
itemA
|
||||
itemB
|
||||
itemC
|
||||
""".split()
|
||||
|
||||
"a,b,c,d".split(",")
|
||||
"a,b,c,d".split(None)
|
||||
"a,b,c,d".split(",", 1)
|
||||
"a,b,c,d".split(None, 1)
|
||||
"a,b,c,d".split(sep=",")
|
||||
"a,b,c,d".split(sep=None)
|
||||
"a,b,c,d".split(sep=",", maxsplit=1)
|
||||
"a,b,c,d".split(sep=None, maxsplit=1)
|
||||
"a,b,c,d".split(maxsplit=1, sep=",")
|
||||
"a,b,c,d".split(maxsplit=1, sep=None)
|
||||
"a,b,c,d".split(",", maxsplit=1)
|
||||
"a,b,c,d".split(None, maxsplit=1)
|
||||
"a,b,c,d".split(maxsplit=1)
|
||||
"a,b,c,d".split(maxsplit=1.0)
|
||||
"a,b,c,d".split(maxsplit=1)
|
||||
"a,b,c,d".split(maxsplit=0)
|
||||
"VERB AUX PRON ADP DET".split(" ")
|
||||
' 1 2 3 '.split()
|
||||
'1<>2<>3<4'.split('<>')
|
||||
|
||||
" a*a a*a a ".split("*", -1) # [' a', 'a a', 'a a ']
|
||||
"".split() # []
|
||||
"""
|
||||
""".split() # []
|
||||
" ".split() # []
|
||||
"/abc/".split() # ['/abc/']
|
||||
("a,b,c"
|
||||
# comment
|
||||
.split()
|
||||
) # ['a,b,c']
|
||||
("a,b,c"
|
||||
# comment1
|
||||
.split(",")
|
||||
) # ['a', 'b', 'c']
|
||||
("a,"
|
||||
# comment
|
||||
"b,"
|
||||
"c"
|
||||
.split(",")
|
||||
) # ['a', 'b', 'c']
|
||||
|
||||
"hello "\
|
||||
"world".split()
|
||||
# ['hello', 'world']
|
||||
|
||||
# prefixes and isc
|
||||
u"a b".split() # ['a', 'b']
|
||||
r"a \n b".split() # ['a', '\\n', 'b']
|
||||
("a " "b").split() # ['a', 'b']
|
||||
"a " "b".split() # ['a', 'b']
|
||||
u"a " "b".split() # ['a', 'b']
|
||||
"a " u"b".split() # ['a', 'b']
|
||||
u"a " r"\n".split() # ['a', '\\n']
|
||||
r"\n " u"\n".split() # ['\\n']
|
||||
r"\n " "\n".split() # ['\\n']
|
||||
"a " r"\n".split() # ['a', '\\n']
|
||||
|
||||
"a,b,c".split(',', maxsplit=0) # ['a,b,c']
|
||||
"a,b,c".split(',', maxsplit=-1) # ['a', 'b', 'c']
|
||||
"a,b,c".split(',', maxsplit=-2) # ['a', 'b', 'c']
|
||||
"a,b,c".split(',', maxsplit=-0) # ['a,b,c']
|
||||
|
||||
# negatives
|
||||
|
||||
# invalid values should not cause panic
|
||||
"a,b,c,d".split(maxsplit="hello")
|
||||
"a,b,c,d".split(maxsplit=-"hello")
|
||||
|
||||
# variable names not implemented
|
||||
"a,b,c,d".split(sep)
|
||||
"a,b,c,d".split(no_sep)
|
||||
for n in range(3):
|
||||
"a,b,c,d".split(",", maxsplit=n)
|
||||
|
||||
# f-strings not yet implemented
|
||||
world = "world"
|
||||
_ = f"{world}_hello_world".split("_")
|
||||
|
||||
hello = "hello_world"
|
||||
_ = f"{hello}_world".split("_")
|
||||
|
||||
# split on bytes not yet implemented, much less frequent
|
||||
b"TesT.WwW.ExamplE.CoM".split(b".")
|
||||
|
||||
# str.splitlines not yet implemented
|
||||
"hello\nworld".splitlines()
|
||||
"hello\nworld".splitlines(keepends=True)
|
||||
"hello\nworld".splitlines(keepends=False)
|
||||
@@ -58,3 +58,13 @@ x = {
|
||||
t={"x":"test123", "x":("test123")}
|
||||
|
||||
t={"x":("test123"), "x":"test123"}
|
||||
|
||||
# Regression test for: https://github.com/astral-sh/ruff/issues/12772
|
||||
x = {
|
||||
1: "abc",
|
||||
1: "def",
|
||||
True: "ghi",
|
||||
0: "foo",
|
||||
0: "bar",
|
||||
False: "baz",
|
||||
}
|
||||
|
||||
@@ -60,3 +60,6 @@ for item in {1, 2, 3, 4, 5, 6, int("7")}: # calls in set literals are fine
|
||||
for item in {1, 2, 2}: # duplicate literals will be ignored
|
||||
# B033 catches this
|
||||
print(f"I like {item}.")
|
||||
|
||||
for item in {False, 0, 0.0, 0j, True, 1, 1.0}:
|
||||
print(item)
|
||||
|
||||
@@ -17,3 +17,11 @@ def f(*args: Unpack[other.Type]): pass
|
||||
def foo(*args: Unpack[int | str]) -> None: pass
|
||||
def foo(*args: Unpack[int and str]) -> None: pass
|
||||
def foo(*args: Unpack[int > str]) -> None: pass
|
||||
|
||||
# We do not use the shorthand unpacking syntax in the following cases
|
||||
from typing import TypedDict
|
||||
class KwargsDict(TypedDict):
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def foo(name: str, /, **kwargs: Unpack[KwargsDict]) -> None: pass
|
||||
|
||||
@@ -15,3 +15,23 @@ decimal.Decimal("0")
|
||||
Decimal(0)
|
||||
Decimal("Infinity")
|
||||
decimal.Decimal(0)
|
||||
|
||||
# Handle Python's Decimal parsing
|
||||
# See https://github.com/astral-sh/ruff/issues/13807
|
||||
|
||||
# Errors
|
||||
Decimal("1_000")
|
||||
Decimal("__1____000")
|
||||
|
||||
# Ok
|
||||
Decimal("2e-4")
|
||||
Decimal("2E-4")
|
||||
Decimal("_1.234__")
|
||||
Decimal("2e4")
|
||||
Decimal("2e+4")
|
||||
Decimal("2E4")
|
||||
Decimal("1.2")
|
||||
# Ok: even though this is equal to `Decimal(123)`,
|
||||
# we assume that a developer would
|
||||
# only write it this way if they meant to.
|
||||
Decimal("١٢٣")
|
||||
42
crates/ruff_linter/resources/test/fixtures/refurb/FURB189.py
vendored
Normal file
42
crates/ruff_linter/resources/test/fixtures/refurb/FURB189.py
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# setup
|
||||
from enum import Enum, EnumMeta
|
||||
from collections import UserList as UL
|
||||
|
||||
class SetOnceMappingMixin:
|
||||
__slots__ = ()
|
||||
def __setitem__(self, key, value):
|
||||
if key in self:
|
||||
raise KeyError(str(key) + ' already set')
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
|
||||
class CaseInsensitiveEnumMeta(EnumMeta):
|
||||
pass
|
||||
|
||||
# positives
|
||||
class D(dict):
|
||||
pass
|
||||
|
||||
class L(list):
|
||||
pass
|
||||
|
||||
class S(str):
|
||||
pass
|
||||
|
||||
# currently not detected
|
||||
class SetOnceDict(SetOnceMappingMixin, dict):
|
||||
pass
|
||||
|
||||
# negatives
|
||||
class C:
|
||||
pass
|
||||
|
||||
class I(int):
|
||||
pass
|
||||
|
||||
class ActivityState(str, Enum, metaclass=CaseInsensitiveEnumMeta):
|
||||
"""Activity state. This is an optional property and if not provided, the state will be Active by
|
||||
default.
|
||||
"""
|
||||
ACTIVE = "Active"
|
||||
INACTIVE = "Inactive"
|
||||
@@ -87,3 +87,17 @@ def match_case_and_elif():
|
||||
pass
|
||||
elif string == "Hello": # fmt: skip
|
||||
pass
|
||||
|
||||
|
||||
# Regression test for decorators
|
||||
import pytest
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_input,expected",
|
||||
[
|
||||
("3+5", 8 ),
|
||||
("17+2", 19),
|
||||
],
|
||||
) # fmt: skip
|
||||
def test_eval(test_input, expected):
|
||||
assert eval(test_input) == expected
|
||||
|
||||
@@ -23,6 +23,9 @@ print(a) # noqa: E501, F821 # comment
|
||||
print(a) # noqa: E501, F821 # comment
|
||||
print(a) # noqa: E501, F821 comment
|
||||
print(a) # noqa: E501, F821 comment
|
||||
print(a) # noqa: E501,,F821 comment
|
||||
print(a) # noqa: E501, ,F821 comment
|
||||
print(a) # noqa: E501 F821 comment
|
||||
|
||||
print(a) # comment with unicode µ # noqa: E501
|
||||
print(a) # comment with unicode µ # noqa: E501, F821
|
||||
|
||||
@@ -155,7 +155,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
|
||||
// flake8-pyi
|
||||
if enforce_stubs {
|
||||
flake8_pyi::rules::docstring_in_stubs(checker, docstring);
|
||||
flake8_pyi::rules::docstring_in_stubs(checker, definition, docstring);
|
||||
}
|
||||
if enforce_stubs_and_runtime {
|
||||
flake8_pyi::rules::iter_method_return_iterable(checker, definition);
|
||||
|
||||
@@ -388,6 +388,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
Rule::StaticJoinToFString,
|
||||
// refurb
|
||||
Rule::HashlibDigestHex,
|
||||
// flake8-simplify
|
||||
Rule::SplitStaticString,
|
||||
]) {
|
||||
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
|
||||
let attr = attr.as_str();
|
||||
@@ -405,6 +407,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
string_value.to_str(),
|
||||
);
|
||||
}
|
||||
} else if matches!(attr, "split" | "rsplit") {
|
||||
// "...".split(...) call
|
||||
if checker.enabled(Rule::SplitStaticString) {
|
||||
flake8_simplify::rules::split_static_string(
|
||||
checker,
|
||||
attr,
|
||||
call,
|
||||
string_value.to_str(),
|
||||
);
|
||||
}
|
||||
} else if attr == "format" {
|
||||
// "...".format(...) call
|
||||
let location = expr.range();
|
||||
|
||||
@@ -549,6 +549,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::WhitespaceAfterDecorator) {
|
||||
pycodestyle::rules::whitespace_after_decorator(checker, decorator_list);
|
||||
}
|
||||
if checker.enabled(Rule::SubclassBuiltin) {
|
||||
refurb::rules::subclass_builtin(checker, class_def);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if checker.enabled(Rule::MultipleImportsOnOneLine) {
|
||||
@@ -1213,8 +1216,18 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_pyi::rules::unrecognized_platform(checker, test);
|
||||
}
|
||||
}
|
||||
if checker.any_enabled(&[Rule::BadVersionInfoComparison, Rule::BadVersionInfoOrder])
|
||||
{
|
||||
if checker.enabled(Rule::ComplexIfStatementInStub) {
|
||||
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
|
||||
for value in values {
|
||||
flake8_pyi::rules::complex_if_statement_in_stub(checker, value);
|
||||
}
|
||||
} else {
|
||||
flake8_pyi::rules::complex_if_statement_in_stub(checker, test);
|
||||
}
|
||||
}
|
||||
}
|
||||
if checker.any_enabled(&[Rule::BadVersionInfoComparison, Rule::BadVersionInfoOrder]) {
|
||||
if checker.source_type.is_stub() || checker.settings.preview.is_enabled() {
|
||||
fn bad_version_info_comparison(
|
||||
checker: &mut Checker,
|
||||
test: &Expr,
|
||||
@@ -1247,15 +1260,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::ComplexIfStatementInStub) {
|
||||
if let Expr::BoolOp(ast::ExprBoolOp { values, .. }) = test.as_ref() {
|
||||
for value in values {
|
||||
flake8_pyi::rules::complex_if_statement_in_stub(checker, value);
|
||||
}
|
||||
} else {
|
||||
flake8_pyi::rules::complex_if_statement_in_stub(checker, test);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Stmt::Assert(
|
||||
|
||||
@@ -1535,7 +1535,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
};
|
||||
|
||||
// Step 4: Analysis
|
||||
analyze::expression(expr, self);
|
||||
match expr {
|
||||
Expr::StringLiteral(string_literal) => {
|
||||
analyze::string_like(string_literal.into(), self);
|
||||
@@ -1546,6 +1545,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
}
|
||||
|
||||
self.semantic.flags = flags_snapshot;
|
||||
analyze::expression(expr, self);
|
||||
self.semantic.pop_node();
|
||||
}
|
||||
|
||||
|
||||
@@ -480,6 +480,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Flake8Simplify, "223") => (RuleGroup::Stable, rules::flake8_simplify::rules::ExprAndFalse),
|
||||
(Flake8Simplify, "300") => (RuleGroup::Stable, rules::flake8_simplify::rules::YodaConditions),
|
||||
(Flake8Simplify, "401") => (RuleGroup::Stable, rules::flake8_simplify::rules::IfElseBlockInsteadOfDictGet),
|
||||
(Flake8Simplify, "905") => (RuleGroup::Preview, rules::flake8_simplify::rules::SplitStaticString),
|
||||
(Flake8Simplify, "910") => (RuleGroup::Stable, rules::flake8_simplify::rules::DictGetWithNoneDefault),
|
||||
(Flake8Simplify, "911") => (RuleGroup::Stable, rules::flake8_simplify::rules::ZipDictKeysAndValues),
|
||||
|
||||
@@ -1071,6 +1072,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "181") => (RuleGroup::Stable, rules::refurb::rules::HashlibDigestHex),
|
||||
(Refurb, "187") => (RuleGroup::Stable, rules::refurb::rules::ListReverseCopy),
|
||||
(Refurb, "188") => (RuleGroup::Preview, rules::refurb::rules::SliceToRemovePrefixOrSuffix),
|
||||
(Refurb, "189") => (RuleGroup::Preview, rules::refurb::rules::SubclassBuiltin),
|
||||
(Refurb, "192") => (RuleGroup::Preview, rules::refurb::rules::SortedMinMax),
|
||||
|
||||
// flake8-logging
|
||||
|
||||
@@ -81,7 +81,7 @@ expression: value
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n- `lint.pyflakes.allowed-unused-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
|
||||
@@ -183,7 +183,7 @@ impl<'a> Directive<'a> {
|
||||
// Extract, e.g., the `401` in `F401`.
|
||||
let suffix = line[prefix..]
|
||||
.chars()
|
||||
.take_while(char::is_ascii_digit)
|
||||
.take_while(char::is_ascii_alphanumeric)
|
||||
.count();
|
||||
if prefix > 0 && suffix > 0 {
|
||||
Some(&line[..prefix + suffix])
|
||||
@@ -549,7 +549,7 @@ impl<'a> ParsedFileExemption<'a> {
|
||||
// Extract, e.g., the `401` in `F401`.
|
||||
let suffix = line[prefix..]
|
||||
.chars()
|
||||
.take_while(char::is_ascii_digit)
|
||||
.take_while(char::is_ascii_alphanumeric)
|
||||
.count();
|
||||
if prefix > 0 && suffix > 0 {
|
||||
Some(&line[..prefix + suffix])
|
||||
@@ -895,7 +895,7 @@ pub(crate) struct NoqaDirectiveLine<'a> {
|
||||
pub(crate) directive: Directive<'a>,
|
||||
/// The codes that are ignored by the directive.
|
||||
pub(crate) matches: Vec<NoqaCode>,
|
||||
// Whether the directive applies to range.end
|
||||
/// Whether the directive applies to `range.end`.
|
||||
pub(crate) includes_end: bool,
|
||||
}
|
||||
|
||||
@@ -1191,6 +1191,24 @@ mod tests {
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_squashed_codes() {
|
||||
let source = "# noqa: F401F841";
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_empty_comma() {
|
||||
let source = "# noqa: F401,,F841";
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_empty_comma_space() {
|
||||
let source = "# noqa: F401, ,F841";
|
||||
assert_debug_snapshot!(Directive::try_extract(source, TextSize::default()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn noqa_invalid_suffix() {
|
||||
let source = "# noqa[F401]";
|
||||
|
||||
@@ -16,8 +16,43 @@ static CODE_INDICATORS: LazyLock<AhoCorasick> = LazyLock::new(|| {
|
||||
|
||||
static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"^(?i)(?:pylint|pyright|noqa|nosec|region|endregion|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:|(?:en)?coding[:=][ \t]*([-_.a-zA-Z0-9]+))",
|
||||
).unwrap()
|
||||
r"(?x)
|
||||
^
|
||||
(?:
|
||||
# Case-sensitive
|
||||
pyright
|
||||
| mypy:
|
||||
| type:\s*ignore
|
||||
| SPDX-License-Identifier:
|
||||
| fmt:\s*(on|off|skip)
|
||||
| region|endregion
|
||||
|
||||
# Case-insensitive
|
||||
| (?i:
|
||||
noqa
|
||||
)
|
||||
|
||||
# Unknown case sensitivity
|
||||
| (?i:
|
||||
pylint
|
||||
| nosec
|
||||
| isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)
|
||||
| (?:en)?coding[:=][\x20\t]*([-_.A-Z0-9]+)
|
||||
)
|
||||
|
||||
# IntelliJ language injection comments:
|
||||
# * `language` must be lowercase.
|
||||
# * No spaces around `=`.
|
||||
# * Language IDs as used in comments must have no spaces,
|
||||
# though to IntelliJ they can be anything.
|
||||
# * May optionally contain `prefix=` and/or `suffix=`,
|
||||
# not declared here since we use `.is_match()`.
|
||||
| language=[-_.a-zA-Z0-9]+
|
||||
|
||||
)
|
||||
",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
static HASH_NUMBER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"#\d").unwrap());
|
||||
@@ -297,6 +332,48 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_contains_language_injection() {
|
||||
// `language` with bad casing
|
||||
assert!(comment_contains_code("# Language=C#", &[]));
|
||||
assert!(comment_contains_code("# lAngUAgE=inI", &[]));
|
||||
|
||||
// Unreasonable language IDs, possibly literals
|
||||
assert!(comment_contains_code("# language=\"pt\"", &[]));
|
||||
assert!(comment_contains_code("# language='en'", &[]));
|
||||
|
||||
// Spaces around equal sign
|
||||
assert!(comment_contains_code("# language =xml", &[]));
|
||||
assert!(comment_contains_code("# language= html", &[]));
|
||||
assert!(comment_contains_code("# language = RegExp", &[]));
|
||||
|
||||
// Leading whitespace
|
||||
assert!(!comment_contains_code("#language=CSS", &[]));
|
||||
assert!(!comment_contains_code("# \t language=C++", &[]));
|
||||
|
||||
// Human language false negatives
|
||||
assert!(!comment_contains_code("# language=en", &[]));
|
||||
assert!(!comment_contains_code("# language=en-US", &[]));
|
||||
|
||||
// Casing (fine because such IDs cannot be validated)
|
||||
assert!(!comment_contains_code("# language=PytHoN", &[]));
|
||||
assert!(!comment_contains_code("# language=jaVaScrIpt", &[]));
|
||||
|
||||
// Space within ID (fine because `Shell` is considered the ID)
|
||||
assert!(!comment_contains_code("# language=Shell Script", &[]));
|
||||
|
||||
// With prefix and/or suffix
|
||||
assert!(!comment_contains_code("# language=HTML prefix=<body>", &[]));
|
||||
assert!(!comment_contains_code(
|
||||
r"# language=Requirements suffix=\n",
|
||||
&[]
|
||||
));
|
||||
assert!(!comment_contains_code(
|
||||
"language=javascript prefix=(function(){ suffix=})()",
|
||||
&[]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_contains_todo() {
|
||||
let task_tags = TASK_TAGS
|
||||
|
||||
@@ -37,11 +37,11 @@ impl Violation for CommentedOutCode {
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Found commented-out code")
|
||||
"Found commented-out code".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some(format!("Remove commented-out code"))
|
||||
Some("Remove commented-out code".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ impl Violation for FastApiNonAnnotatedDependency {
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("FastAPI dependency without `Annotated`")
|
||||
"FastAPI dependency without `Annotated`".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
|
||||
@@ -65,7 +65,7 @@ pub struct FastApiRedundantResponseModel;
|
||||
impl AlwaysFixableViolation for FastApiRedundantResponseModel {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("FastAPI route with redundant `response_model` argument")
|
||||
"FastAPI route with redundant `response_model` argument".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
|
||||
@@ -46,7 +46,7 @@ pub struct SysVersionCmpStr3;
|
||||
impl Violation for SysVersionCmpStr3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version` compared to string (python3.10), use `sys.version_info`")
|
||||
"`sys.version` compared to string (python3.10), use `sys.version_info`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ pub struct SysVersionInfo0Eq3;
|
||||
impl Violation for SysVersionInfo0Eq3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version_info[0] == 3` referenced (python4), use `>=`")
|
||||
"`sys.version_info[0] == 3` referenced (python4), use `>=`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,10 +133,9 @@ pub struct SysVersionInfo1CmpInt;
|
||||
impl Violation for SysVersionInfo1CmpInt {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"`sys.version_info[1]` compared to integer (python4), compare `sys.version_info` to \
|
||||
"`sys.version_info[1]` compared to integer (python4), compare `sys.version_info` to \
|
||||
tuple"
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,10 +175,9 @@ pub struct SysVersionInfoMinorCmpInt;
|
||||
impl Violation for SysVersionInfoMinorCmpInt {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"`sys.version_info.minor` compared to integer (python4), compare `sys.version_info` \
|
||||
"`sys.version_info.minor` compared to integer (python4), compare `sys.version_info` \
|
||||
to tuple"
|
||||
)
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,7 +218,7 @@ pub struct SysVersionCmpStr10;
|
||||
impl Violation for SysVersionCmpStr10 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version` compared to string (python10), use `sys.version_info`")
|
||||
"`sys.version` compared to string (python10), use `sys.version_info`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ pub struct SixPY3;
|
||||
impl Violation for SixPY3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`six.PY3` referenced (python4), use `not six.PY2`")
|
||||
"`six.PY3` referenced (python4), use `not six.PY2`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ pub struct SysVersionSlice3;
|
||||
impl Violation for SysVersionSlice3 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[:3]` referenced (python3.10), use `sys.version_info`")
|
||||
"`sys.version[:3]` referenced (python3.10), use `sys.version_info`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ pub struct SysVersion2;
|
||||
impl Violation for SysVersion2 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[2]` referenced (python3.10), use `sys.version_info`")
|
||||
"`sys.version[2]` referenced (python3.10), use `sys.version_info`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ pub struct SysVersion0;
|
||||
impl Violation for SysVersion0 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[0]` referenced (python10), use `sys.version_info`")
|
||||
"`sys.version[0]` referenced (python10), use `sys.version_info`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ pub struct SysVersionSlice1;
|
||||
impl Violation for SysVersionSlice1 {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("`sys.version[:1]` referenced (python10), use `sys.version_info`")
|
||||
"`sys.version[:1]` referenced (python10), use `sys.version_info`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -229,12 +229,11 @@ impl Violation for MissingReturnTypeUndocumentedPublicFunction {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { annotation, .. } = self;
|
||||
if let Some(annotation) = annotation {
|
||||
Some(format!("Add return type annotation: `{annotation}`"))
|
||||
} else {
|
||||
Some(format!("Add return type annotation"))
|
||||
}
|
||||
let title = match &self.annotation {
|
||||
Some(annotation) => format!("Add return type annotation: `{annotation}`"),
|
||||
None => "Add return type annotation".to_string(),
|
||||
};
|
||||
Some(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,12 +272,11 @@ impl Violation for MissingReturnTypePrivateFunction {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { annotation, .. } = self;
|
||||
if let Some(annotation) = annotation {
|
||||
Some(format!("Add return type annotation: `{annotation}`"))
|
||||
} else {
|
||||
Some(format!("Add return type annotation"))
|
||||
}
|
||||
let title = match &self.annotation {
|
||||
Some(annotation) => format!("Add return type annotation: `{annotation}`"),
|
||||
None => "Add return type annotation".to_string(),
|
||||
};
|
||||
Some(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,12 +328,11 @@ impl Violation for MissingReturnTypeSpecialMethod {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { annotation, .. } = self;
|
||||
if let Some(annotation) = annotation {
|
||||
Some(format!("Add return type annotation: `{annotation}`"))
|
||||
} else {
|
||||
Some(format!("Add return type annotation"))
|
||||
}
|
||||
let title = match &self.annotation {
|
||||
Some(annotation) => format!("Add return type annotation: `{annotation}`"),
|
||||
None => "Add return type annotation".to_string(),
|
||||
};
|
||||
Some(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,12 +375,11 @@ impl Violation for MissingReturnTypeStaticMethod {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { annotation, .. } = self;
|
||||
if let Some(annotation) = annotation {
|
||||
Some(format!("Add return type annotation: `{annotation}`"))
|
||||
} else {
|
||||
Some(format!("Add return type annotation"))
|
||||
}
|
||||
let title = match &self.annotation {
|
||||
Some(annotation) => format!("Add return type annotation: `{annotation}`"),
|
||||
None => "Add return type annotation".to_string(),
|
||||
};
|
||||
Some(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,12 +422,11 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { annotation, .. } = self;
|
||||
if let Some(annotation) = annotation {
|
||||
Some(format!("Add return type annotation: `{annotation}`"))
|
||||
} else {
|
||||
Some(format!("Add return type annotation"))
|
||||
}
|
||||
let title = match &self.annotation {
|
||||
Some(annotation) => format!("Add return type annotation: `{annotation}`"),
|
||||
None => "Add return type annotation".to_string(),
|
||||
};
|
||||
Some(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,7 +469,7 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 484](https://www.python.org/dev/peps/pep-0484/#the-any-type)
|
||||
/// - [Typing spec: `Any`](https://typing.readthedocs.io/en/latest/spec/special-types.html#any)
|
||||
/// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
|
||||
/// - [Mypy documentation: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type)
|
||||
#[violation]
|
||||
|
||||
@@ -71,7 +71,7 @@ pub struct AsyncFunctionWithTimeout {
|
||||
impl Violation for AsyncFunctionWithTimeout {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async function definition with a `timeout` parameter")
|
||||
"Async function definition with a `timeout` parameter".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
|
||||
@@ -37,7 +37,7 @@ pub struct BlockingHttpCallInAsyncFunction;
|
||||
impl Violation for BlockingHttpCallInAsyncFunction {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async functions should not call blocking HTTP methods")
|
||||
"Async functions should not call blocking HTTP methods".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user