Compare commits

..

1 Commits

Author SHA1 Message Date
David Peter
7e5358de2b [red-knot] T is disjoint from ~T 2025-02-10 22:15:47 +01:00
319 changed files with 4986 additions and 13536 deletions

View File

@@ -58,12 +58,6 @@
description: "Disable PRs updating GitHub runners (e.g. 'runs-on: macos-14')",
enabled: false,
},
{
// TODO: Remove this once the codebase is upgrade to v4 (https://github.com/astral-sh/ruff/pull/16069)
matchPackageNames: ["tailwindcss"],
matchManagers: ["npm"],
enabled: false,
},
{
// Disable updates of `zip-rs`; intentionally pinned for now due to ownership change
// See: https://github.com/astral-sh/uv/issues/3642

View File

@@ -712,7 +712,7 @@ jobs:
just test
benchmarks:
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
needs: determine_changes
if: ${{ github.repository == 'astral-sh/ruff' && !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
timeout-minutes: 20

View File

@@ -49,7 +49,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@v3.14.0
uses: cloudflare/wrangler-action@v3.13.1
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -60,7 +60,7 @@ repos:
- black==25.1.0
- repo: https://github.com/crate-ci/typos
rev: v1.29.7
rev: v1.29.5
hooks:
- id: typos
@@ -74,7 +74,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
rev: v0.9.4
hooks:
- id: ruff-format
- id: ruff
@@ -84,7 +84,7 @@ repos:
# Prettier
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.5.1
rev: v3.4.2
hooks:
- id: prettier
types: [yaml]
@@ -92,7 +92,7 @@ repos:
# zizmor detects security vulnerabilities in GitHub Actions workflows.
# Additional configuration for the tool is found in `.github/zizmor.yml`
- repo: https://github.com/woodruffw/zizmor-pre-commit
rev: v1.3.1
rev: v1.3.0
hooks:
- id: zizmor

View File

@@ -1,44 +1,5 @@
# Changelog
## 0.9.6
### Preview features
- \[`airflow`\] Add `external_task.{ExternalTaskMarker, ExternalTaskSensor}` for `AIR302` ([#16014](https://github.com/astral-sh/ruff/pull/16014))
- \[`flake8-builtins`\] Make strict module name comparison optional (`A005`) ([#15951](https://github.com/astral-sh/ruff/pull/15951))
- \[`flake8-pyi`\] Extend fix to Python \<= 3.9 for `redundant-none-literal` (`PYI061`) ([#16044](https://github.com/astral-sh/ruff/pull/16044))
- \[`pylint`\] Also report when the object isn't a literal (`PLE1310`) ([#15985](https://github.com/astral-sh/ruff/pull/15985))
- \[`ruff`\] Implement `indented-form-feed` (`RUF054`) ([#16049](https://github.com/astral-sh/ruff/pull/16049))
- \[`ruff`\] Skip type definitions for `missing-f-string-syntax` (`RUF027`) ([#16054](https://github.com/astral-sh/ruff/pull/16054))
### Rule changes
- \[`flake8-annotations`\] Correct syntax for `typing.Union` in suggested return type fixes for `ANN20x` rules ([#16025](https://github.com/astral-sh/ruff/pull/16025))
- \[`flake8-builtins`\] Match upstream module name comparison (`A005`) ([#16006](https://github.com/astral-sh/ruff/pull/16006))
- \[`flake8-comprehensions`\] Detect overshadowed `list`/`set`/`dict`, ignore variadics and named expressions (`C417`) ([#15955](https://github.com/astral-sh/ruff/pull/15955))
- \[`flake8-pie`\] Remove following comma correctly when the unpacked dictionary is empty (`PIE800`) ([#16008](https://github.com/astral-sh/ruff/pull/16008))
- \[`flake8-simplify`\] Only trigger `SIM401` on known dictionaries ([#15995](https://github.com/astral-sh/ruff/pull/15995))
- \[`pylint`\] Do not report calls when object type and argument type mismatch, remove custom escape handling logic (`PLE1310`) ([#15984](https://github.com/astral-sh/ruff/pull/15984))
- \[`pyupgrade`\] Comments within parenthesized value ranges should not affect applicability (`UP040`) ([#16027](https://github.com/astral-sh/ruff/pull/16027))
- \[`pyupgrade`\] Don't introduce invalid syntax when upgrading old-style type aliases with parenthesized multiline values (`UP040`) ([#16026](https://github.com/astral-sh/ruff/pull/16026))
- \[`pyupgrade`\] Ensure we do not rename two type parameters to the same name (`UP049`) ([#16038](https://github.com/astral-sh/ruff/pull/16038))
- \[`pyupgrade`\] \[`ruff`\] Don't apply renamings if the new name is shadowed in a scope of one of the references to the binding (`UP049`, `RUF052`) ([#16032](https://github.com/astral-sh/ruff/pull/16032))
- \[`ruff`\] Update `RUF009` to behave similar to `B008` and ignore attributes with immutable types ([#16048](https://github.com/astral-sh/ruff/pull/16048))
### Server
- Root exclusions in the server to project root ([#16043](https://github.com/astral-sh/ruff/pull/16043))
### Bug fixes
- \[`flake8-datetime`\] Ignore `.replace()` calls while looking for `.astimezone` ([#16050](https://github.com/astral-sh/ruff/pull/16050))
- \[`flake8-type-checking`\] Avoid `TC004` false positive where the runtime definition is provided by `__getattr__` ([#16052](https://github.com/astral-sh/ruff/pull/16052))
### Documentation
- Improve `ruff-lsp` migration document ([#16072](https://github.com/astral-sh/ruff/pull/16072))
- Undeprecate `ruff.nativeServer` ([#16039](https://github.com/astral-sh/ruff/pull/16039))
## 0.9.5
### Preview features

132
Cargo.lock generated
View File

@@ -29,12 +29,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-tzdata"
version = "0.1.1"
@@ -360,9 +354,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.29"
version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acebd8ad879283633b343856142139f2da2317c96b05b4dd6181c61e2480184"
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
dependencies = [
"clap_builder",
"clap_derive",
@@ -370,9 +364,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.29"
version = "4.5.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ba32cbda51c7e1dfd49acc1457ba1a7dec5b64fe360e828acb13ca8dc9c2f9"
checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7"
dependencies = [
"anstream",
"anstyle",
@@ -413,9 +407,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.28"
version = "4.5.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
dependencies = [
"heck",
"proc-macro2",
@@ -444,22 +438,20 @@ dependencies = [
[[package]]
name = "codspeed"
version = "2.8.0"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25d2f5a6570db487f5258e0bded6352fa2034c2aeb46bb5cc3ff060a0fcfba2f"
checksum = "450a0e9df9df1c154156f4344f99d8f6f6e69d0fc4de96ef6e2e68b2ec3bce97"
dependencies = [
"colored 2.2.0",
"libc",
"serde",
"serde_json",
"uuid",
]
[[package]]
name = "codspeed-criterion-compat"
version = "2.8.0"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f53a55558dedec742b14aae3c5fec389361b8b5ca28c1aadf09dd91faf710074"
checksum = "8eb1a6cb9c20e177fde58cdef97c1c7c9264eb1424fe45c4fccedc2fb078a569"
dependencies = [
"codspeed",
"colored 2.2.0",
@@ -905,7 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -1039,8 +1031,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
@@ -1104,7 +1098,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
]
[[package]]
@@ -1482,7 +1475,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
dependencies = [
"hermit-abi 0.4.0",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -2056,7 +2049,7 @@ dependencies = [
"once_cell",
"pep440_rs",
"regex",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"smallvec",
"thiserror 1.0.69",
@@ -2416,7 +2409,6 @@ dependencies = [
"red_knot_server",
"regex",
"ruff_db",
"ruff_python_ast",
"ruff_python_trivia",
"salsa",
"tempfile",
@@ -2445,7 +2437,7 @@ dependencies = [
"ruff_macros",
"ruff_python_ast",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
@@ -2485,7 +2477,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
@@ -2513,7 +2505,7 @@ dependencies = [
"ruff_python_ast",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"shellexpand",
@@ -2539,7 +2531,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"serde",
"smallvec",
@@ -2566,9 +2558,9 @@ dependencies = [
"js-sys",
"log",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_notebook",
"ruff_python_ast",
"wasm-bindgen",
"wasm-bindgen-test",
]
@@ -2650,7 +2642,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.9.6"
version = "0.9.5"
dependencies = [
"anyhow",
"argfile",
@@ -2690,7 +2682,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"shellexpand",
@@ -2729,13 +2721,14 @@ dependencies = [
"mimalloc",
"rayon",
"red_knot_project",
"red_knot_python_semantic",
"ruff_db",
"ruff_linter",
"ruff_python_ast",
"ruff_python_formatter",
"ruff_python_parser",
"ruff_python_trivia",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"tikv-jemallocator",
]
@@ -2757,10 +2750,10 @@ name = "ruff_db"
version = "0.0.0"
dependencies = [
"camino",
"colored 3.0.0",
"countme",
"dashmap 6.1.0",
"dunce",
"etcetera",
"filetime",
"glob",
"ignore",
@@ -2775,7 +2768,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa",
"schemars",
"serde",
@@ -2846,7 +2839,7 @@ dependencies = [
"ruff_cache",
"ruff_macros",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"static_assertions",
@@ -2878,13 +2871,12 @@ name = "ruff_index"
version = "0.0.0"
dependencies = [
"ruff_macros",
"salsa",
"static_assertions",
]
[[package]]
name = "ruff_linter"
version = "0.9.6"
version = "0.9.5"
dependencies = [
"aho-corasick",
"anyhow",
@@ -2926,7 +2918,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"serde_json",
@@ -2988,8 +2980,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"salsa",
"rustc-hash 2.1.0",
"schemars",
"serde",
]
@@ -3036,7 +3027,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"serde_json",
@@ -3083,7 +3074,7 @@ dependencies = [
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"static_assertions",
"unicode-ident",
"unicode-normalization",
@@ -3114,7 +3105,7 @@ dependencies = [
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_text_size",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"smallvec",
@@ -3173,7 +3164,7 @@ dependencies = [
"ruff_source_file",
"ruff_text_size",
"ruff_workspace",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"serde",
"serde_json",
"shellexpand",
@@ -3203,7 +3194,7 @@ dependencies = [
[[package]]
name = "ruff_wasm"
version = "0.9.6"
version = "0.9.5"
dependencies = [
"console_error_panic_hook",
"console_log",
@@ -3255,7 +3246,7 @@ dependencies = [
"ruff_python_semantic",
"ruff_python_stdlib",
"ruff_source_file",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"schemars",
"serde",
"shellexpand",
@@ -3282,9 +3273,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
[[package]]
name = "rustix"
@@ -3296,7 +3287,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3314,19 +3305,17 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
[[package]]
name = "salsa"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
dependencies = [
"append-only-vec",
"arc-swap",
"compact_str",
"crossbeam",
"dashmap 6.1.0",
"hashbrown 0.14.5",
"hashlink",
"indexmap",
"parking_lot",
"rayon",
"rustc-hash 2.1.1",
"rustc-hash 2.1.0",
"salsa-macro-rules",
"salsa-macros",
"smallvec",
@@ -3336,12 +3325,12 @@ dependencies = [
[[package]]
name = "salsa-macro-rules"
version = "0.1.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
[[package]]
name = "salsa-macros"
version = "0.18.0"
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
dependencies = [
"heck",
"proc-macro2",
@@ -3550,9 +3539,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "smallvec"
version = "1.14.0"
version = "1.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
[[package]]
name = "snapbox"
@@ -3612,18 +3601,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.1"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.1"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck",
"proc-macro2",
@@ -3667,16 +3656,16 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.17.0"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a40f762a77d2afa88c2d919489e390a12bdd261ed568e60cfa7e48d4e20f0d33"
checksum = "38c246215d7d24f48ae091a2902398798e05d978b24315d6efbc00ede9a8bb91"
dependencies = [
"cfg-if",
"fastrand",
"getrandom 0.3.1",
"once_cell",
"rustix",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3865,9 +3854,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.8.20"
version = "0.8.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
dependencies = [
"serde",
"serde_spanned",
@@ -4164,22 +4153,21 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.13.1"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
dependencies = [
"getrandom 0.3.1",
"js-sys",
"rand 0.9.0",
"getrandom 0.2.15",
"rand 0.8.5",
"uuid-macro-internal",
"wasm-bindgen",
]
[[package]]
name = "uuid-macro-internal"
version = "1.13.1"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d28dd23acb5f2fa7bd2155ab70b960e770596b3bb6395119b40476c3655dfba4"
checksum = "f8a86d88347b61a0e17b9908a67efcc594130830bf1045653784358dd023e294"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -123,7 +123,7 @@ rayon = { version = "1.10.0" }
regex = { version = "1.10.2" }
rustc-hash = { version = "2.0.0" }
# When updating salsa, make sure to also update the revision in `fuzz/Cargo.toml`
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
schemars = { version = "0.8.16" }
seahash = { version = "4.1.0" }
serde = { version = "1.0.197", features = ["derive"] }
@@ -143,8 +143,8 @@ snapbox = { version = "0.6.0", features = [
"examples",
] }
static_assertions = "1.1.0"
strum = { version = "0.27.0", features = ["strum_macros"] }
strum_macros = { version = "0.27.0" }
strum = { version = "0.26.0", features = ["strum_macros"] }
strum_macros = { version = "0.26.0" }
syn = { version = "2.0.55" }
tempfile = { version = "3.9.0" }
test-case = { version = "3.3.1" }

View File

@@ -149,8 +149,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
# For a specific version.
curl -LsSf https://astral.sh/ruff/0.9.6/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.6/install.ps1 | iex"
curl -LsSf https://astral.sh/ruff/0.9.5/install.sh | sh
powershell -c "irm https://astral.sh/ruff/0.9.5/install.ps1 | iex"
```
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
@@ -183,7 +183,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.9.6
rev: v0.9.5
hooks:
# Run the linter.
- id: ruff

View File

@@ -16,7 +16,6 @@ red_knot_python_semantic = { workspace = true }
red_knot_project = { workspace = true, features = ["zstd"] }
red_knot_server = { workspace = true }
ruff_db = { workspace = true, features = ["os", "cache"] }
ruff_python_ast = { workspace = true }
anyhow = { workspace = true }
chrono = { workspace = true }

View File

@@ -1,7 +1,7 @@
use crate::logging::Verbosity;
use crate::python_version::PythonVersion;
use clap::{ArgAction, ArgMatches, Error, Parser};
use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_python_semantic::lint;
use ruff_db::system::SystemPathBuf;
@@ -67,8 +67,8 @@ pub(crate) struct CheckCommand {
pub(crate) rules: RulesArg,
/// Use exit code 1 if there are any warning-level diagnostics.
#[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)]
pub(crate) error_on_warning: Option<bool>,
#[arg(long, conflicts_with = "exit_zero")]
pub(crate) error_on_warning: bool,
/// Always use exit code 0, even when there are error-level diagnostics.
#[arg(long)]
@@ -107,9 +107,6 @@ impl CheckCommand {
}),
..EnvironmentOptions::default()
}),
terminal: Some(TerminalOptions {
error_on_warning: self.error_on_warning,
}),
rules,
..Default::default()
}

View File

@@ -11,17 +11,18 @@ use clap::Parser;
use colored::Colorize;
use crossbeam::channel as crossbeam_channel;
use red_knot_project::metadata::options::Options;
use red_knot_project::watch;
use red_knot_project::watch::ProjectWatcher;
use red_knot_project::{watch, Db};
use red_knot_project::{ProjectDatabase, ProjectMetadata};
use red_knot_server::run_server;
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, Severity};
use ruff_db::diagnostic::{Diagnostic, Severity};
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
use salsa::plumbing::ZalsaDatabase;
mod args;
mod logging;
mod python_version;
mod verbosity;
mod version;
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
@@ -96,15 +97,19 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
let system = OsSystem::new(cwd);
let watch = args.watch;
let exit_zero = args.exit_zero;
let min_error_severity = if args.error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let cli_options = args.into_options();
let mut project_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
project_metadata.apply_cli_options(cli_options.clone());
project_metadata.apply_configuration_files(&system)?;
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
workspace_metadata.apply_cli_options(cli_options.clone());
let mut db = ProjectDatabase::new(project_metadata, system)?;
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);
// Listen to Ctrl+C and abort the watch mode.
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
@@ -162,10 +167,18 @@ struct MainLoop {
watcher: Option<ProjectWatcher>,
cli_options: Options,
/// The minimum severity to consider an error when deciding the exit status.
///
/// TODO(micha): Get from the terminal settings.
min_error_severity: Severity,
}
impl MainLoop {
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
fn new(
cli_options: Options,
min_error_severity: Severity,
) -> (Self, MainLoopCancellationToken) {
let (sender, receiver) = crossbeam_channel::bounded(10);
(
@@ -174,6 +187,7 @@ impl MainLoop {
receiver,
watcher: None,
cli_options,
min_error_severity,
},
MainLoopCancellationToken { sender },
)
@@ -231,24 +245,14 @@ impl MainLoop {
result,
revision: check_revision,
} => {
let display_config = DisplayDiagnosticConfig::default()
.color(colored::control::SHOULD_COLORIZE.should_colorize());
let min_error_severity =
if db.project().settings(db).terminal().error_on_warning {
Severity::Warning
} else {
Severity::Error
};
let failed = result
.iter()
.any(|diagnostic| diagnostic.severity() >= min_error_severity);
.any(|diagnostic| diagnostic.severity() >= self.min_error_severity);
if check_revision == revision {
#[allow(clippy::print_stdout)]
for diagnostic in result {
println!("{}", diagnostic.display(db, &display_config));
println!("{}", diagnostic.display(db));
}
} else {
tracing::debug!(

View File

@@ -40,7 +40,7 @@ impl std::fmt::Display for PythonVersion {
}
}
impl From<PythonVersion> for ruff_python_ast::python_version::PythonVersion {
impl From<PythonVersion> for red_knot_python_semantic::PythonVersion {
fn from(value: PythonVersion) -> Self {
match value {
PythonVersion::Py37 => Self::PY37,
@@ -61,8 +61,8 @@ mod tests {
#[test]
fn same_default_as_python_version() {
assert_eq!(
ruff_python_ast::python_version::PythonVersion::from(PythonVersion::default()),
ruff_python_ast::python_version::PythonVersion::default()
red_knot_python_semantic::PythonVersion::from(PythonVersion::default()),
red_knot_python_semantic::PythonVersion::default()
);
}
}

View File

@@ -0,0 +1 @@

View File

@@ -98,7 +98,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
])?;
// Make sure that the CLI fails when the `libs` directory is not in the search path.
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r###"
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r###"
success: false
exit_code: 1
----- stdout -----
@@ -115,7 +115,7 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
----- stderr -----
"###);
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")).arg("--extra-search-path").arg("../libs"), @r"
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
success: true
exit_code: 0
----- stdout -----
@@ -167,7 +167,7 @@ fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Re
),
])?;
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
success: true
exit_code: 0
----- stdout -----
@@ -575,37 +575,6 @@ fn exit_code_no_errors_but_error_on_warning_is_true() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exit_code_no_errors_but_error_on_warning_is_enabled_in_configuration() -> anyhow::Result<()> {
let case = TestCase::with_files([
("test.py", r"print(x) # [unresolved-reference]"),
(
"knot.toml",
r#"
[terminal]
error-on-warning = true
"#,
),
])?;
assert_cmd_snapshot!(case.command(), @r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:unresolved-reference
--> <temp_dir>/test.py:1:7
|
1 | print(x) # [unresolved-reference]
| - Name `x` used when not defined
|
----- stderr -----
"###);
Ok(())
}
#[test]
fn exit_code_both_warnings_and_errors() -> anyhow::Result<()> {
let case = TestCase::with_file(
@@ -717,109 +686,6 @@ fn exit_code_exit_zero_is_true() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn user_configuration() -> anyhow::Result<()> {
let case = TestCase::with_files([
(
"project/knot.toml",
r#"
[rules]
division-by-zero = "warn"
"#,
),
(
"project/main.py",
r#"
y = 4 / 0
for a in range(0, y):
x = a
print(x)
"#,
),
])?;
let config_directory = case.root().join("home/.config");
let config_env_var = if cfg!(windows) {
"APPDATA"
} else {
"XDG_CONFIG_HOME"
};
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r###"
success: true
exit_code: 0
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
warning: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| - Name `x` used when possibly not defined
|
----- stderr -----
"###
);
// The user-level configuration promotes `possibly-unresolved-reference` to an error.
// Changing the level for `division-by-zero` has no effect, because the project-level configuration
// has higher precedence.
case.write_file(
config_directory.join("knot/knot.toml"),
r#"
[rules]
division-by-zero = "error"
possibly-unresolved-reference = "error"
"#,
)?;
assert_cmd_snapshot!(
case.command().current_dir(case.root().join("project")).env(config_env_var, config_directory.as_os_str()),
@r###"
success: false
exit_code: 1
----- stdout -----
warning: lint:division-by-zero
--> <temp_dir>/project/main.py:2:5
|
2 | y = 4 / 0
| ----- Cannot divide object of type `Literal[4]` by zero
3 |
4 | for a in range(0, y):
|
error: lint:possibly-unresolved-reference
--> <temp_dir>/project/main.py:7:7
|
5 | x = a
6 |
7 | print(x)
| ^ Name `x` used when possibly not defined
|
----- stderr -----
"###
);
Ok(())
}
struct TestCase {
_temp_dir: TempDir,
_settings_scope: SettingsBindDropGuard,
@@ -887,7 +753,7 @@ impl TestCase {
Ok(())
}
fn root(&self) -> &Path {
fn project_dir(&self) -> &Path {
&self.project_dir
}

View File

@@ -9,14 +9,11 @@ use red_knot_project::metadata::pyproject::{PyProject, Tool};
use red_knot_project::metadata::value::{RangedValue, RelativePathBuf};
use red_knot_project::watch::{directory_watcher, ChangeEvent, ProjectWatcher};
use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform};
use red_knot_python_semantic::{resolve_module, ModuleName, PythonPlatform, PythonVersion};
use ruff_db::files::{system_path_to_file, File, FileError};
use ruff_db::source::source_text;
use ruff_db::system::{
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
};
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf};
use ruff_db::Upcast;
use ruff_python_ast::python_version::PythonVersion;
struct TestCase {
db: ProjectDatabase,
@@ -223,44 +220,17 @@ where
}
trait SetupFiles {
fn setup(self, context: &SetupContext) -> anyhow::Result<()>;
}
struct SetupContext<'a> {
system: &'a OsSystem,
root_path: &'a SystemPath,
}
impl<'a> SetupContext<'a> {
fn system(&self) -> &'a OsSystem {
self.system
}
fn join_project_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
self.project_path().join(relative)
}
fn project_path(&self) -> &SystemPath {
self.system.current_directory()
}
fn root_path(&self) -> &'a SystemPath {
self.root_path
}
fn join_root_path(&self, relative: impl AsRef<SystemPath>) -> SystemPathBuf {
self.root_path().join(relative)
}
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()>;
}
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
where
P: AsRef<SystemPath>,
{
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
fn setup(self, _root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
for (relative_path, content) in self {
let relative_path = relative_path.as_ref();
let absolute_path = context.join_project_path(relative_path);
let absolute_path = project_path.join(relative_path);
if let Some(parent) = absolute_path.parent() {
std::fs::create_dir_all(parent).with_context(|| {
format!("Failed to create parent directory for file `{relative_path}`")
@@ -280,10 +250,10 @@ where
impl<F> SetupFiles for F
where
F: FnOnce(&SetupContext) -> anyhow::Result<()>,
F: FnOnce(&SystemPath, &SystemPath) -> anyhow::Result<()>,
{
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
self(context)
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
self(root_path, project_path)
}
}
@@ -291,12 +261,13 @@ fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
where
F: SetupFiles,
{
setup_with_options(setup_files, |_context| None)
setup_with_options(setup_files, |_root, _project_path| None)
}
// TODO: Replace with configuration?
fn setup_with_options<F>(
setup_files: F,
create_options: impl FnOnce(&SetupContext) -> Option<Options>,
create_options: impl FnOnce(&SystemPath, &SystemPath) -> Option<Options>,
) -> anyhow::Result<TestCase>
where
F: SetupFiles,
@@ -324,17 +295,13 @@ where
std::fs::create_dir_all(project_path.as_std_path())
.with_context(|| format!("Failed to create project directory `{project_path}`"))?;
let system = OsSystem::new(&project_path);
let setup_context = SetupContext {
system: &system,
root_path: &root_path,
};
setup_files
.setup(&setup_context)
.setup(&root_path, &project_path)
.context("Failed to setup test files")?;
if let Some(options) = create_options(&setup_context) {
let system = OsSystem::new(&project_path);
if let Some(options) = create_options(&root_path, &project_path) {
std::fs::write(
project_path.join("pyproject.toml").as_std_path(),
toml::to_string(&PyProject {
@@ -348,9 +315,7 @@ where
.context("Failed to write configuration")?;
}
let mut project = ProjectMetadata::discover(&project_path, &system)?;
project.apply_configuration_files(&system)?;
let project = ProjectMetadata::discover(&project_path, &system)?;
let program_settings = project.to_program_settings(&system);
for path in program_settings
@@ -824,12 +789,10 @@ fn directory_deleted() -> anyhow::Result<()> {
#[test]
fn search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(
context.join_root_path("site_packages"),
)]),
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -890,12 +853,10 @@ fn add_search_path() -> anyhow::Result<()> {
#[test]
fn remove_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(
context.join_root_path("site_packages"),
)]),
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -933,7 +894,7 @@ import os
print(sys.last_exc, os.getegid())
"#,
)],
|_context| {
|_root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
@@ -981,31 +942,21 @@ print(sys.last_exc, os.getegid())
#[test]
fn changed_versions_file() -> anyhow::Result<()> {
let mut case = setup_with_options(
|context: &SetupContext| {
|root_path: &SystemPath, project_path: &SystemPath| {
std::fs::write(project_path.join("bar.py").as_std_path(), "import sub.a")?;
std::fs::create_dir_all(root_path.join("typeshed/stdlib").as_std_path())?;
std::fs::write(root_path.join("typeshed/stdlib/VERSIONS").as_std_path(), "")?;
std::fs::write(
context.join_project_path("bar.py").as_std_path(),
"import sub.a",
)?;
std::fs::create_dir_all(context.join_root_path("typeshed/stdlib").as_std_path())?;
std::fs::write(
context
.join_root_path("typeshed/stdlib/VERSIONS")
.as_std_path(),
"",
)?;
std::fs::write(
context
.join_root_path("typeshed/stdlib/os.pyi")
.as_std_path(),
root_path.join("typeshed/stdlib/os.pyi").as_std_path(),
"# not important",
)?;
Ok(())
},
|context| {
|root_path, _project_path| {
Some(Options {
environment: Some(EnvironmentOptions {
typeshed: Some(RelativePathBuf::cli(context.join_root_path("typeshed"))),
typeshed: Some(RelativePathBuf::cli(root_path.join("typeshed"))),
..EnvironmentOptions::default()
}),
..Options::default()
@@ -1056,12 +1007,12 @@ fn changed_versions_file() -> anyhow::Result<()> {
/// we're seeing is that Windows only emits a single event, similar to Linux.
#[test]
fn hard_links_in_project() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let foo_path = context.join_project_path("foo.py");
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
let foo_path = project.join("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = context.join_project_path("bar.py");
let bar_path = project.join("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
@@ -1127,12 +1078,12 @@ fn hard_links_in_project() -> anyhow::Result<()> {
ignore = "windows doesn't support observing changes to hard linked files."
)]
fn hard_links_to_target_outside_project() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let foo_path = context.join_root_path("foo.py");
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
let foo_path = root.join("foo.py");
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
// Create a hardlink to `foo`
let bar_path = context.join_project_path("bar.py");
let bar_path = project.join("bar.py");
std::fs::hard_link(foo_path.as_std_path(), bar_path.as_std_path())
.context("Failed to create hard link from foo.py -> bar.py")?;
@@ -1235,9 +1186,9 @@ mod unix {
ignore = "FSEvents doesn't emit change events for symlinked directories outside of the watched paths."
)]
fn symlink_target_outside_watched_paths() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let mut case = setup(|root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let link_target = context.join_root_path("bar");
let link_target = root.join("bar");
std::fs::create_dir_all(link_target.as_std_path())
.context("Failed to create link target directory")?;
let baz_original = link_target.join("baz.py");
@@ -1245,7 +1196,7 @@ mod unix {
.context("Failed to write link target file")?;
// Create a symlink inside the project
let bar = context.join_project_path("bar");
let bar = project.join("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
.context("Failed to create symlink to bar package")?;
@@ -1316,9 +1267,9 @@ mod unix {
/// ```
#[test]
fn symlink_inside_project() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let link_target = context.join_project_path("patched/bar");
let link_target = project.join("patched/bar");
std::fs::create_dir_all(link_target.as_std_path())
.context("Failed to create link target directory")?;
let baz_original = link_target.join("baz.py");
@@ -1326,7 +1277,7 @@ mod unix {
.context("Failed to write link target file")?;
// Create a symlink inside site-packages
let bar_in_project = context.join_project_path("bar");
let bar_in_project = project.join("bar");
std::os::unix::fs::symlink(link_target.as_std_path(), bar_in_project.as_std_path())
.context("Failed to create symlink to bar package")?;
@@ -1407,9 +1358,9 @@ mod unix {
#[test]
fn symlinked_module_search_path() -> anyhow::Result<()> {
let mut case = setup_with_options(
|context: &SetupContext| {
|root: &SystemPath, project: &SystemPath| {
// Set up the symlink target.
let site_packages = context.join_root_path("site-packages");
let site_packages = root.join("site-packages");
let bar = site_packages.join("bar");
std::fs::create_dir_all(bar.as_std_path())
.context("Failed to create bar directory")?;
@@ -1418,8 +1369,7 @@ mod unix {
.context("Failed to write baz.py")?;
// Symlink the site packages in the venv to the global site packages
let venv_site_packages =
context.join_project_path(".venv/lib/python3.12/site-packages");
let venv_site_packages = project.join(".venv/lib/python3.12/site-packages");
std::fs::create_dir_all(venv_site_packages.parent().unwrap())
.context("Failed to create .venv directory")?;
std::os::unix::fs::symlink(
@@ -1430,7 +1380,7 @@ mod unix {
Ok(())
},
|_context| {
|_root, _project| {
Some(Options {
environment: Some(EnvironmentOptions {
extra_paths: Some(vec![RelativePathBuf::cli(
@@ -1500,9 +1450,9 @@ mod unix {
#[test]
fn nested_projects_delete_root() -> anyhow::Result<()> {
let mut case = setup(|context: &SetupContext| {
let mut case = setup(|root: &SystemPath, project_root: &SystemPath| {
std::fs::write(
context.join_project_path("pyproject.toml").as_std_path(),
project_root.join("pyproject.toml").as_std_path(),
r#"
[project]
name = "inner"
@@ -1512,7 +1462,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
)?;
std::fs::write(
context.join_root_path("pyproject.toml").as_std_path(),
root.join("pyproject.toml").as_std_path(),
r#"
[project]
name = "outer"
@@ -1537,79 +1487,3 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn changes_to_user_configuration() -> anyhow::Result<()> {
let mut _config_dir_override: Option<UserConfigDirectoryOverrideGuard> = None;
let mut case = setup(|context: &SetupContext| {
std::fs::write(
context.join_project_path("pyproject.toml").as_std_path(),
r#"
[project]
name = "test"
"#,
)?;
std::fs::write(
context.join_project_path("foo.py").as_std_path(),
"a = 10 / 0",
)?;
let config_directory = context.join_root_path("home/.config");
std::fs::create_dir_all(config_directory.join("knot").as_std_path())?;
std::fs::write(
config_directory.join("knot/knot.toml").as_std_path(),
r#"
[rules]
division-by-zero = "ignore"
"#,
)?;
_config_dir_override = Some(
context
.system()
.with_user_config_directory(Some(config_directory)),
);
Ok(())
})?;
let foo = case
.system_file(case.project_path("foo.py"))
.expect("foo.py to exist");
let diagnostics = case
.db()
.check_file(foo)
.context("Failed to check project.")?;
assert!(
diagnostics.is_empty(),
"Expected no diagnostics but got: {diagnostics:#?}"
);
// Enable division-by-zero in the user configuration with warning severity
update_file(
case.root_path().join("home/.config/knot/knot.toml"),
r#"
[rules]
division-by-zero = "warn"
"#,
)?;
let changes = case.stop_watch(event_for_file("knot.toml"));
case.apply_changes(changes);
let diagnostics = case
.db()
.check_file(foo)
.context("Failed to check project.")?;
assert!(
diagnostics.len() == 1,
"Expected exactly one diagnostic but got: {diagnostics:#?}"
);
Ok(())
}

View File

@@ -13,7 +13,7 @@ license.workspace = true
[dependencies]
ruff_cache = { workspace = true }
ruff_db = { workspace = true, features = ["cache", "serde"] }
ruff_db = { workspace = true, features = ["os", "cache", "serde"] }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["serde"] }
ruff_text_size = { workspace = true }
@@ -24,7 +24,7 @@ anyhow = { workspace = true }
crossbeam = { workspace = true }
glob = { workspace = true }
notify = { workspace = true }
pep440_rs = { workspace = true, features = ["version-ranges"] }
pep440_rs = { workspace = true }
rayon = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }

View File

@@ -1,8 +1,7 @@
use std::{collections::HashMap, hash::BuildHasher};
use red_knot_python_semantic::{PythonPlatform, SitePackages};
use red_knot_python_semantic::{PythonPlatform, PythonVersion, SitePackages};
use ruff_db::system::SystemPathBuf;
use ruff_python_ast::python_version::PythonVersion;
/// Combine two values, preferring the values in `self`.
///

View File

@@ -114,8 +114,8 @@ impl SemanticDb for ProjectDatabase {
project.is_file_open(self, file)
}
fn rule_selection(&self) -> Arc<RuleSelection> {
self.project().rules(self)
fn rule_selection(&self) -> &RuleSelection {
self.project().rule_selection(self)
}
fn lint_registry(&self) -> &LintRegistry {
@@ -186,6 +186,7 @@ pub(crate) mod tests {
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
rule_selection: RuleSelection,
project: Option<Project>,
}
@@ -197,6 +198,7 @@ pub(crate) mod tests {
vendored: red_knot_vendored::file_system().clone(),
files: Files::default(),
events: Arc::default(),
rule_selection: RuleSelection::from_registry(&DEFAULT_LINT_REGISTRY),
project: None,
};
@@ -268,8 +270,8 @@ pub(crate) mod tests {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> Arc<RuleSelection> {
self.project().rules(self)
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {

View File

@@ -8,7 +8,6 @@ use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::system::walk_directory::WalkState;
use ruff_db::system::SystemPath;
use ruff_db::Db as _;
use ruff_python_ast::PySourceType;
use rustc_hash::FxHashSet;
impl ProjectDatabase {
@@ -48,7 +47,7 @@ impl ProjectDatabase {
if let Some(path) = change.system_path() {
if matches!(
path.file_name(),
Some(".gitignore" | ".ignore" | "knot.toml" | "pyproject.toml")
Some(".gitignore" | ".ignore" | "ruff.toml" | ".ruff.toml" | "pyproject.toml")
) {
// Changes to ignore files or settings can change the project structure or add/remove files.
project_changed = true;
@@ -145,12 +144,6 @@ impl ProjectDatabase {
metadata.apply_cli_options(cli_options.clone());
}
if let Err(error) = metadata.apply_configuration_files(self.system()) {
tracing::error!(
"Failed to apply configuration files, continuing without applying them: {error}"
);
}
let program_settings = metadata.to_program_settings(self.system());
let program = Program::get(self);
@@ -208,16 +201,9 @@ impl ProjectDatabase {
return WalkState::Continue;
}
if entry
.path()
.extension()
.and_then(PySourceType::try_from_extension)
.is_some()
{
let mut paths = added_paths.lock().unwrap();
let mut paths = added_paths.lock().unwrap();
paths.push(entry.into_path());
}
paths.push(entry.into_path());
WalkState::Continue
})

View File

@@ -3,18 +3,18 @@
use crate::metadata::options::OptionDiagnostic;
pub use db::{Db, ProjectDatabase};
use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings;
pub use metadata::{ProjectDiscoveryError, ProjectMetadata};
use red_knot_python_semantic::lint::{LintRegistry, LintRegistryBuilder, RuleSelection};
use red_knot_python_semantic::register_lints;
use red_knot_python_semantic::types::check_types;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity, Span};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::parsed::parsed_module;
use ruff_db::source::{source_text, SourceTextError};
use ruff_db::system::walk_directory::WalkState;
use ruff_db::system::{FileType, SystemPath};
use ruff_python_ast::PySourceType;
use ruff_text_size::TextRange;
use rustc_hash::{FxBuildHasher, FxHashSet};
use salsa::Durability;
use salsa::Setter;
@@ -66,22 +66,12 @@ pub struct Project {
/// The metadata describing the project, including the unresolved options.
#[return_ref]
pub metadata: ProjectMetadata,
/// The resolved project settings.
#[return_ref]
pub settings: Settings,
/// Diagnostics that were generated when resolving the project settings.
#[return_ref]
settings_diagnostics: Vec<OptionDiagnostic>,
}
#[salsa::tracked]
impl Project {
pub fn from_metadata(db: &dyn Db, metadata: ProjectMetadata) -> Self {
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
Project::builder(metadata, settings, settings_diagnostics)
Project::builder(metadata)
.durability(Durability::MEDIUM)
.open_fileset_durability(Durability::LOW)
.file_set_durability(Durability::LOW)
@@ -96,37 +86,30 @@ impl Project {
self.metadata(db).name()
}
/// Returns the resolved linter rules for the project.
///
/// This is a salsa query to prevent re-computing queries if other, unrelated
/// settings change. For example, we don't want that changing the terminal settings
/// invalidates any type checking queries.
#[salsa::tracked]
pub fn rules(self, db: &dyn Db) -> Arc<RuleSelection> {
self.settings(db).to_rules()
}
pub fn reload(self, db: &mut dyn Db, metadata: ProjectMetadata) {
tracing::debug!("Reloading project");
assert_eq!(self.root(db), metadata.root());
if &metadata != self.metadata(db) {
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
if self.settings(db) != &settings {
self.set_settings(db).to(settings);
}
if self.settings_diagnostics(db) != &settings_diagnostics {
self.set_settings_diagnostics(db).to(settings_diagnostics);
}
self.set_metadata(db).to(metadata);
}
self.reload_files(db);
}
pub fn rule_selection(self, db: &dyn Db) -> &RuleSelection {
let (selection, _) = self.rule_selection_with_diagnostics(db);
selection
}
#[salsa::tracked(return_ref)]
fn rule_selection_with_diagnostics(
self,
db: &dyn Db,
) -> (RuleSelection, Vec<OptionDiagnostic>) {
self.metadata(db).options().to_rule_selection(db)
}
/// Checks all open files in the project and its dependencies.
pub(crate) fn check(self, db: &ProjectDatabase) -> Vec<Box<dyn Diagnostic>> {
let project_span = tracing::debug_span!("Project::check");
@@ -135,7 +118,8 @@ impl Project {
tracing::debug!("Checking project '{name}'", name = self.name(db));
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
diagnostics.extend(options_diagnostics.iter().map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
diagnostic
}));
@@ -167,8 +151,9 @@ impl Project {
}
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
let mut file_diagnostics: Vec<_> = self
.settings_diagnostics(db)
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
let mut file_diagnostics: Vec<_> = options_diagnostics
.iter()
.map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
@@ -344,13 +329,7 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
boxed
}));
diagnostics.sort_unstable_by_key(|diagnostic| {
diagnostic
.span()
.and_then(|span| span.range())
.unwrap_or_default()
.start()
});
diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start());
diagnostics
}
@@ -463,8 +442,12 @@ impl Diagnostic for IOErrorDiagnostic {
self.error.to_string().into()
}
fn span(&self) -> Option<Span> {
Some(Span::from(self.file))
fn file(&self) -> Option<File> {
Some(self.file)
}
fn range(&self) -> Option<TextRange> {
None
}
fn severity(&self) -> Severity {

View File

@@ -1,4 +1,3 @@
use configuration_file::{ConfigurationFile, ConfigurationFileError};
use red_knot_python_semantic::ProgramSettings;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_python_ast::name::Name;
@@ -6,15 +5,13 @@ use std::sync::Arc;
use thiserror::Error;
use crate::combine::Combine;
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
use crate::metadata::pyproject::{Project, PyProject, PyProjectError};
use crate::metadata::value::ValueSource;
use options::KnotTomlError;
use options::Options;
mod configuration_file;
pub mod options;
pub mod pyproject;
pub mod settings;
pub mod value;
#[derive(Debug, PartialEq, Eq)]
@@ -26,15 +23,6 @@ pub struct ProjectMetadata {
/// The raw options
pub(super) options: Options,
/// Paths of configurations other than the project's configuration that were combined into [`Self::options`].
///
/// This field stores the paths of the configuration files, mainly for
/// knowing which files to watch for changes.
///
/// The path ordering doesn't imply precedence.
#[cfg_attr(test, serde(skip_serializing_if = "Vec::is_empty"))]
pub(super) extra_configuration_paths: Vec<SystemPathBuf>,
}
impl ProjectMetadata {
@@ -43,16 +31,12 @@ impl ProjectMetadata {
Self {
name,
root,
extra_configuration_paths: Vec::default(),
options: Options::default(),
}
}
/// Loads a project from a `pyproject.toml` file.
pub(crate) fn from_pyproject(
pyproject: PyProject,
root: SystemPathBuf,
) -> Result<Self, ResolveRequiresPythonError> {
pub(crate) fn from_pyproject(pyproject: PyProject, root: SystemPathBuf) -> Self {
Self::from_options(
pyproject
.tool
@@ -65,37 +49,21 @@ impl ProjectMetadata {
/// Loads a project from a set of options with an optional pyproject-project table.
pub(crate) fn from_options(
mut options: Options,
options: Options,
root: SystemPathBuf,
project: Option<&Project>,
) -> Result<Self, ResolveRequiresPythonError> {
) -> Self {
let name = project
.and_then(|project| project.name.as_deref())
.map(|name| Name::new(&**name))
.and_then(|project| project.name.as_ref())
.map(|name| Name::new(&***name))
.unwrap_or_else(|| Name::new(root.file_name().unwrap_or("root")));
// If the `options` don't specify a python version but the `project.requires-python` field is set,
// use that as a lower bound instead.
if let Some(project) = project {
if !options
.environment
.as_ref()
.is_some_and(|env| env.python_version.is_some())
{
if let Some(requires_python) = project.resolve_requires_python_lower_bound()? {
let mut environment = options.environment.unwrap_or_default();
environment.python_version = Some(requires_python);
options.environment = Some(environment);
}
}
}
Ok(Self {
// TODO(https://github.com/astral-sh/ruff/issues/15491): Respect requires-python
Self {
name,
root,
options,
extra_configuration_paths: Vec::new(),
})
}
}
/// Discovers the closest project at `path` and returns its metadata.
@@ -163,34 +131,19 @@ impl ProjectMetadata {
}
tracing::debug!("Found project at '{}'", project_root);
let metadata = ProjectMetadata::from_options(
return Ok(ProjectMetadata::from_options(
options,
project_root.to_path_buf(),
pyproject
.as_ref()
.and_then(|pyproject| pyproject.project.as_ref()),
)
.map_err(|err| {
ProjectDiscoveryError::InvalidRequiresPythonConstraint {
source: err,
path: pyproject_path,
}
})?;
return Ok(metadata);
));
}
if let Some(pyproject) = pyproject {
let has_knot_section = pyproject.knot().is_some();
let metadata =
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
.map_err(
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
source: err,
path: pyproject_path,
},
)?;
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf());
if has_knot_section {
tracing::debug!("Found project at '{}'", project_root);
@@ -238,10 +191,6 @@ impl ProjectMetadata {
&self.options
}
pub fn extra_configuration_paths(&self) -> &[SystemPathBuf] {
&self.extra_configuration_paths
}
pub fn to_program_settings(&self, system: &dyn System) -> ProgramSettings {
self.options.to_program_settings(self.root(), system)
}
@@ -251,31 +200,9 @@ impl ProjectMetadata {
self.options = options.combine(std::mem::take(&mut self.options));
}
/// Applies the options from the configuration files to the project's options.
///
/// This includes:
///
/// * The user-level configuration
pub fn apply_configuration_files(
&mut self,
system: &dyn System,
) -> Result<(), ConfigurationFileError> {
if let Some(user) = ConfigurationFile::user(system)? {
tracing::debug!(
"Applying user-level configuration loaded from `{path}`.",
path = user.path()
);
self.apply_configuration_file(user);
}
Ok(())
}
/// Applies a lower-precedence configuration files to the project's options.
fn apply_configuration_file(&mut self, options: ConfigurationFile) {
self.extra_configuration_paths
.push(options.path().to_owned());
self.options.combine_with(options.into_options());
/// Combine the project options with the user options where project options take precedence.
pub fn apply_user_options(&mut self, options: Options) {
self.options.combine_with(options);
}
}
@@ -295,22 +222,16 @@ pub enum ProjectDiscoveryError {
source: Box<KnotTomlError>,
path: SystemPathBuf,
},
#[error("Invalid `requires-python` version specifier (`{path}`): {source}")]
InvalidRequiresPythonConstraint {
source: ResolveRequiresPythonError,
path: SystemPathBuf,
},
}
#[cfg(test)]
mod tests {
//! Integration tests for project discovery
use crate::snapshot_project;
use anyhow::{anyhow, Context};
use insta::assert_ron_snapshot;
use ruff_db::system::{SystemPathBuf, TestSystem};
use ruff_python_ast::python_version::PythonVersion;
use crate::{ProjectDiscoveryError, ProjectMetadata};
@@ -329,15 +250,7 @@ mod tests {
assert_eq!(project.root(), &*root);
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)
"#);
});
snapshot_project!(project);
Ok(())
}
@@ -366,16 +279,7 @@ mod tests {
ProjectMetadata::discover(&root, &system).context("Failed to discover project")?;
assert_eq!(project.root(), &*root);
with_escaped_paths(|| {
assert_ron_snapshot!(&project, @r#"
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)
"#);
});
snapshot_project!(project);
// Discovering the same package from a subdirectory should give the same result
let from_src = ProjectMetadata::discover(&root.join("db"), &system)
@@ -458,19 +362,7 @@ expected `.`, `]`
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(sub_project, @r#"
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
snapshot_project!(sub_project);
Ok(())
}
@@ -508,19 +400,7 @@ expected `.`, `]`
let root = ProjectMetadata::discover(&root, &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
snapshot_project!(root);
Ok(())
}
@@ -552,15 +432,7 @@ expected `.`, `]`
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(sub_project, @r#"
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(),
)
"#);
});
snapshot_project!(sub_project);
Ok(())
}
@@ -595,19 +467,7 @@ expected `.`, `]`
let root = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.10"),
)),
),
)
"#);
});
snapshot_project!(root);
Ok(())
}
@@ -627,304 +487,27 @@ expected `.`, `]`
(
root.join("pyproject.toml"),
r#"
[project]
name = "super-app"
requires-python = ">=3.12"
[project]
name = "super-app"
requires-python = ">=3.12"
[tool.knot.src]
root = "this_option_is_ignored"
"#,
[tool.knot.src]
root = "this_option_is_ignored"
"#,
),
(
root.join("knot.toml"),
r#"
[src]
root = "src"
"#,
[src]
root = "src"
"#,
),
])
.context("Failed to write files")?;
let root = ProjectMetadata::discover(&root, &system)?;
with_escaped_paths(|| {
assert_ron_snapshot!(root, @r#"
ProjectMetadata(
name: Name("super-app"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.12"),
)),
src: Some(SrcOptions(
root: Some("src"),
)),
),
)
"#);
});
Ok(())
}
#[test]
fn requires_python_major_minor() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
#[test]
fn requires_python_major_only() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::from((3, 0)))
);
Ok(())
}
/// A `requires-python` constraint with major, minor and patch can be simplified
/// to major and minor (e.g. 3.12.1 -> 3.12).
#[test]
fn requires_python_major_minor_patch() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12.8"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
#[test]
fn requires_python_beta_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">= 3.13.0b0"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY313)
);
Ok(())
}
#[test]
fn requires_python_greater_than_major_minor() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
# This is somewhat nonsensical because 3.12.1 > 3.12 is true.
# That's why simplifying the constraint to >= 3.12 is correct
requires-python = ">3.12"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY312)
);
Ok(())
}
/// `python-version` takes precedence if both `requires-python` and `python-version` are configured.
#[test]
fn requires_python_and_python_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=3.12"
[tool.knot.environment]
python-version = "3.10"
"#,
)
.context("Failed to write file")?;
let root = ProjectMetadata::discover(&root, &system)?;
assert_eq!(
root.options
.environment
.unwrap_or_default()
.python_version
.as_deref(),
Some(&PythonVersion::PY310)
);
Ok(())
}
#[test]
fn requires_python_less_than() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = "<3.12"
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because the `requires-python` doesn't specify a lower bound (it only specifies an upper bound)."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `<3.12` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.");
Ok(())
}
#[test]
fn requires_python_no_specifiers() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ""
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because the `requires-python` specifiers are empty and don't define a lower bound."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): value `` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.");
Ok(())
}
#[test]
fn requires_python_too_large_major_version() -> anyhow::Result<()> {
let system = TestSystem::default();
let root = SystemPathBuf::from("/app");
system
.memory_file_system()
.write_file(
root.join("pyproject.toml"),
r#"
[project]
requires-python = ">=999.0"
"#,
)
.context("Failed to write file")?;
let Err(error) = ProjectMetadata::discover(&root, &system) else {
return Err(anyhow!("Expected project discovery to fail because of the requires-python major version that is larger than 255."));
};
assert_error_eq(&error, "Invalid `requires-python` version specifier (`/app/pyproject.toml`): The major version `999` is larger than the maximum supported value 255");
snapshot_project!(root);
Ok(())
}
@@ -934,12 +517,15 @@ expected `.`, `]`
assert_eq!(error.to_string().replace('\\', "/"), message);
}
fn with_escaped_paths<R>(f: impl FnOnce() -> R) -> R {
let mut settings = insta::Settings::clone_current();
settings.add_dynamic_redaction(".root", |content, _path| {
content.as_str().unwrap().replace('\\', "/")
/// Snapshots a project but with all paths using unix separators.
#[macro_export]
macro_rules! snapshot_project {
($project:expr) => {{
assert_ron_snapshot!($project,{
".root" => insta::dynamic_redaction(|content, _content_path| {
content.as_str().unwrap().replace("\\", "/")
}),
});
settings.bind(f)
}
}};
}
}

View File

@@ -1,69 +0,0 @@
use std::sync::Arc;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use thiserror::Error;
use crate::metadata::value::ValueSource;
use super::options::{KnotTomlError, Options};
/// A `knot.toml` configuration file with the options it contains.
pub(crate) struct ConfigurationFile {
path: SystemPathBuf,
options: Options,
}
impl ConfigurationFile {
/// Loads the user-level configuration file if it exists.
///
/// Returns `None` if the file does not exist or if the concept of user-level configurations
/// doesn't exist on `system`.
pub(crate) fn user(system: &dyn System) -> Result<Option<Self>, ConfigurationFileError> {
let Some(configuration_directory) = system.user_config_directory() else {
return Ok(None);
};
let knot_toml_path = configuration_directory.join("knot").join("knot.toml");
tracing::debug!(
"Searching for a user-level configuration at `{path}`",
path = &knot_toml_path
);
let Ok(knot_toml_str) = system.read_to_string(&knot_toml_path) else {
return Ok(None);
};
match Options::from_toml_str(
&knot_toml_str,
ValueSource::File(Arc::new(knot_toml_path.clone())),
) {
Ok(options) => Ok(Some(Self {
path: knot_toml_path,
options,
})),
Err(error) => Err(ConfigurationFileError::InvalidKnotToml {
source: Box::new(error),
path: knot_toml_path,
}),
}
}
/// Returns the path to the configuration file.
pub(crate) fn path(&self) -> &SystemPath {
&self.path
}
pub(crate) fn into_options(self) -> Options {
self.options
}
}
#[derive(Debug, Error)]
pub enum ConfigurationFileError {
#[error("{path} is not a valid `knot.toml`: {source}")]
InvalidKnotToml {
source: Box<KnotTomlError>,
path: SystemPathBuf,
},
}

View File

@@ -1,20 +1,20 @@
use crate::metadata::value::{RangedValue, RelativePathBuf, ValueSource, ValueSourceGuard};
use crate::Db;
use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelection};
use red_knot_python_semantic::{ProgramSettings, PythonPlatform, SearchPathSettings, SitePackages};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::system_path_to_file;
use red_knot_python_semantic::{
ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings, SitePackages,
};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{System, SystemPath};
use ruff_macros::Combine;
use ruff_python_ast::python_version::PythonVersion;
use ruff_text_size::TextRange;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::fmt::Debug;
use thiserror::Error;
use super::settings::{Settings, TerminalSettings};
/// The options for the project.
#[derive(Debug, Default, Clone, PartialEq, Eq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
@@ -30,9 +30,6 @@ pub struct Options {
/// Configures the enabled lints and their severity.
#[serde(skip_serializing_if = "Option::is_none")]
pub rules: Option<Rules>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terminal: Option<TerminalOptions>,
}
impl Options {
@@ -113,22 +110,7 @@ impl Options {
}
#[must_use]
pub(crate) fn to_settings(&self, db: &dyn Db) -> (Settings, Vec<OptionDiagnostic>) {
let (rules, diagnostics) = self.to_rule_selection(db);
let mut settings = Settings::new(rules);
if let Some(terminal) = self.terminal.as_ref() {
settings.set_terminal(TerminalSettings {
error_on_warning: terminal.error_on_warning.unwrap_or_default(),
});
}
(settings, diagnostics)
}
#[must_use]
fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
let registry = db.lint_registry();
let mut diagnostics = Vec::new();
@@ -187,14 +169,7 @@ impl Options {
),
};
let span = file.map(Span::from).map(|span| {
if let Some(range) = rule_name.range() {
span.with_range(range)
} else {
span
}
});
diagnostics.push(diagnostic.with_span(span));
diagnostics.push(diagnostic.with_file(file).with_range(rule_name.range()));
}
}
}
@@ -269,16 +244,6 @@ impl FromIterator<(RangedValue<String>, RangedValue<Level>)> for Rules {
}
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Combine, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TerminalOptions {
/// Use exit code 1 if there are any warning-level diagnostics.
///
/// Defaults to `false`.
pub error_on_warning: Option<bool>,
}
#[cfg(feature = "schemars")]
mod schema {
use crate::DEFAULT_LINT_REGISTRY;
@@ -353,7 +318,8 @@ pub struct OptionDiagnostic {
id: DiagnosticId,
message: String,
severity: Severity,
span: Option<Span>,
file: Option<File>,
range: Option<TextRange>,
}
impl OptionDiagnostic {
@@ -362,13 +328,21 @@ impl OptionDiagnostic {
id,
message,
severity,
span: None,
file: None,
range: None,
}
}
#[must_use]
fn with_span(self, span: Option<Span>) -> Self {
OptionDiagnostic { span, ..self }
fn with_file(mut self, file: Option<File>) -> Self {
self.file = file;
self
}
#[must_use]
fn with_range(mut self, range: Option<TextRange>) -> Self {
self.range = range;
self
}
}
@@ -381,8 +355,12 @@ impl Diagnostic for OptionDiagnostic {
Cow::Borrowed(&self.message)
}
fn span(&self) -> Option<Span> {
self.span.clone()
fn file(&self) -> Option<File> {
self.file
}
fn range(&self) -> Option<TextRange> {
self.range
}
fn severity(&self) -> Severity {

View File

@@ -1,12 +1,11 @@
use crate::metadata::options::Options;
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
use pep440_rs::{release_specifiers_to_ranges, Version, VersionSpecifiers};
use ruff_python_ast::python_version::PythonVersion;
use pep440_rs::{Version, VersionSpecifiers};
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::Bound;
use std::ops::Deref;
use thiserror::Error;
use crate::metadata::options::Options;
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
/// A `pyproject.toml` as specified in PEP 517.
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
#[serde(rename_all = "kebab-case")]
@@ -56,73 +55,6 @@ pub struct Project {
pub requires_python: Option<RangedValue<VersionSpecifiers>>,
}
impl Project {
pub(super) fn resolve_requires_python_lower_bound(
&self,
) -> Result<Option<RangedValue<PythonVersion>>, ResolveRequiresPythonError> {
let Some(requires_python) = self.requires_python.as_ref() else {
return Ok(None);
};
tracing::debug!("Resolving requires-python constraint: `{requires_python}`");
let ranges = release_specifiers_to_ranges((**requires_python).clone());
let Some((lower, _)) = ranges.bounding_range() else {
return Ok(None);
};
let version = match lower {
// Ex) `>=3.10.1` -> `>=3.10`
Bound::Included(version) => version,
// Ex) `>3.10.1` -> `>=3.10` or `>3.10` -> `>=3.10`
// The second example looks obscure at first but it is required because
// `3.10.1 > 3.10` is true but we only have two digits here. So including 3.10 is the
// right move. Overall, using `>` without a patch release is most likely bogus.
Bound::Excluded(version) => version,
// Ex) `<3.10` or ``
Bound::Unbounded => {
return Err(ResolveRequiresPythonError::NoLowerBound(
requires_python.to_string(),
))
}
};
// Take the major and minor version
let mut versions = version.release().iter().take(2);
let Some(major) = versions.next().copied() else {
return Ok(None);
};
let minor = versions.next().copied().unwrap_or_default();
tracing::debug!("Resolved requires-python constraint to: {major}.{minor}");
let major =
u8::try_from(major).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(major))?;
let minor =
u8::try_from(minor).map_err(|_| ResolveRequiresPythonError::TooLargeMajor(minor))?;
Ok(Some(
requires_python
.clone()
.map_value(|_| PythonVersion::from((major, minor))),
))
}
}
#[derive(Debug, Error)]
pub enum ResolveRequiresPythonError {
#[error("The major version `{0}` is larger than the maximum supported value 255")]
TooLargeMajor(u64),
#[error("The minor version `{0}` is larger than the maximum supported value 255")]
TooLargeMinor(u64),
#[error("value `{0}` does not contain a lower bound. Add a lower bound to indicate the minimum compatible Python version (e.g., `>=3.13`) or specify a version in `environment.python-version`.")]
NoLowerBound(String),
}
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub struct Tool {

View File

@@ -1,53 +0,0 @@
use std::sync::Arc;
use red_knot_python_semantic::lint::RuleSelection;
/// The resolved [`super::Options`] for the project.
///
/// Unlike [`super::Options`], the struct has default values filled in and
/// uses representations that are optimized for reads (instead of preserving the source representation).
/// It's also not required that this structure precisely resembles the TOML schema, although
/// it's encouraged to use a similar structure.
///
/// It's worth considering to adding a salsa query for specific settings to
/// limit the blast radius when only some settings change. For example,
/// changing the terminal settings shouldn't invalidate any core type-checking queries.
/// This can be achieved by adding a salsa query for the type checking specific settings.
///
/// Settings that are part of [`red_knot_python_semantic::ProgramSettings`] are not included here.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Settings {
rules: Arc<RuleSelection>,
terminal: TerminalSettings,
}
impl Settings {
pub fn new(rules: RuleSelection) -> Self {
Self {
rules: Arc::new(rules),
terminal: TerminalSettings::default(),
}
}
pub fn rules(&self) -> &RuleSelection {
&self.rules
}
pub fn to_rules(&self) -> Arc<RuleSelection> {
self.rules.clone()
}
pub fn terminal(&self) -> &TerminalSettings {
&self.terminal
}
pub fn set_terminal(&mut self, terminal: TerminalSettings) {
self.terminal = terminal;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct TerminalSettings {
pub error_on_warning: bool,
}

View File

@@ -118,15 +118,6 @@ impl<T> RangedValue<T> {
self
}
#[must_use]
pub fn map_value<R>(self, f: impl FnOnce(T) -> R) -> RangedValue<R> {
RangedValue {
value: f(self.value),
source: self.source,
range: self.range,
}
}
pub fn into_inner(self) -> T {
self.value
}

View File

@@ -0,0 +1,13 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -0,0 +1,13 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: sub_project
---
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -0,0 +1,13 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("project-root"),
root: "/app",
options: Options(
environment: Some(EnvironmentOptions(
r#python-version: Some("3.10"),
)),
),
)

View File

@@ -0,0 +1,9 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: sub_project
---
ProjectMetadata(
name: Name("nested-project"),
root: "/app/packages/a",
options: Options(),
)

View File

@@ -0,0 +1,13 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: root
---
ProjectMetadata(
name: Name("super-app"),
root: "/app",
options: Options(
src: Some(SrcOptions(
root: Some("src"),
)),
),
)

View File

@@ -0,0 +1,9 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: project
---
ProjectMetadata(
name: Name("backend"),
root: "/app",
options: Options(),
)

View File

@@ -0,0 +1,9 @@
---
source: crates/red_knot_project/src/metadata.rs
expression: project
---
ProjectMetadata(
name: Name("app"),
root: "/app",
options: Options(),
)

View File

@@ -73,13 +73,6 @@ impl ProjectWatcher {
.canonicalize_path(&project_path)
.unwrap_or(project_path);
let config_paths = db
.project()
.metadata(db)
.extra_configuration_paths()
.iter()
.cloned();
// Find the non-overlapping module search paths and filter out paths that are already covered by the project.
// Module search paths are already canonicalized.
let unique_module_paths = ruff_db::system::deduplicate_nested_paths(
@@ -90,11 +83,8 @@ impl ProjectWatcher {
.map(SystemPath::to_path_buf);
// Now add the new paths, first starting with the project path and then
// adding the library search paths, and finally the paths for configurations.
for path in std::iter::once(project_path)
.chain(unique_module_paths)
.chain(config_paths)
{
// adding the library search paths.
for path in std::iter::once(project_path).chain(unique_module_paths) {
// Log a warning. It's not worth aborting if registering a single folder fails because
// Ruff otherwise stills works as expected.
if let Err(error) = self.watcher.watch(&path) {

View File

@@ -12,9 +12,9 @@ license = { workspace = true }
[dependencies]
ruff_db = { workspace = true }
ruff_index = { workspace = true, features = ["salsa"] }
ruff_index = { workspace = true }
ruff_macros = { workspace = true }
ruff_python_ast = { workspace = true, features = ["salsa"] }
ruff_python_ast = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_stdlib = { workspace = true }
ruff_source_file = { workspace = true }
@@ -31,7 +31,7 @@ drop_bomb = { workspace = true }
indexmap = { workspace = true }
itertools = { workspace = true }
ordermap = { workspace = true }
salsa = { workspace = true, features = ["compact_str"] }
salsa = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
rustc-hash = { workspace = true }
@@ -44,7 +44,7 @@ test-case = { workspace = true }
memchr = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing", "os"] }
ruff_db = { workspace = true, features = ["os", "testing"] }
ruff_python_parser = { workspace = true }
red_knot_test = { workspace = true }
red_knot_vendored = { workspace = true }
@@ -57,7 +57,7 @@ quickcheck = { version = "1.0.3", default-features = false }
quickcheck_macros = { version = "1.0.0" }
[features]
serde = ["ruff_db/serde", "dep:serde", "ruff_python_ast/serde"]
serde = ["ruff_db/serde", "dep:serde"]
[lints]
workspace = true

View File

@@ -1,90 +0,0 @@
# Special cases for int/float/complex in annotations
In order to support common use cases, an annotation of `float` actually means `int | float`, and an
annotation of `complex` actually means `int | float | complex`. See
[the specification](https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex)
## float
An annotation of `float` means `int | float`, so `int` is assignable to it:
```py
def takes_float(x: float):
pass
def passes_int_to_float(x: int):
# no error!
takes_float(x)
```
It also applies to variable annotations:
```py
def assigns_int_to_float(x: int):
# no error!
y: float = x
```
It doesn't work the other way around:
```py
def takes_int(x: int):
pass
def passes_float_to_int(x: float):
# error: [invalid-argument-type]
takes_int(x)
def assigns_float_to_int(x: float):
# error: [invalid-assignment]
y: int = x
```
Unlike other type checkers, we choose not to obfuscate this special case by displaying `int | float`
as just `float`; we display the actual type:
```py
def f(x: float):
reveal_type(x) # revealed: int | float
```
## complex
An annotation of `complex` means `int | float | complex`, so `int` and `float` are both assignable
to it (but not the other way around):
```py
def takes_complex(x: complex):
pass
def passes_to_complex(x: float, y: int):
# no errors!
takes_complex(x)
takes_complex(y)
def assigns_to_complex(x: float, y: int):
# no errors!
a: complex = x
b: complex = y
def takes_int(x: int):
pass
def takes_float(x: float):
pass
def passes_complex(x: complex):
# error: [invalid-argument-type]
takes_int(x)
# error: [invalid-argument-type]
takes_float(x)
def assigns_complex(x: complex):
# error: [invalid-assignment]
y: int = x
# error: [invalid-assignment]
z: float = x
def f(x: complex):
reveal_type(x) # revealed: int | float | complex
```

View File

@@ -9,9 +9,9 @@ from typing import Union
a: Union[int, str]
a1: Union[int, bool]
a2: Union[int, Union[bytes, str]]
a2: Union[int, Union[float, str]]
a3: Union[int, None]
a4: Union[Union[bytes, str]]
a4: Union[Union[float, str]]
a5: Union[int]
a6: Union[()]
@@ -21,11 +21,11 @@ def f():
# Since bool is a subtype of int we simplify to int here. But we do allow assigning boolean values (see below).
# revealed: int
reveal_type(a1)
# revealed: int | bytes | str
# revealed: int | float | str
reveal_type(a2)
# revealed: int | None
reveal_type(a3)
# revealed: bytes | str
# revealed: float | str
reveal_type(a4)
# revealed: int
reveal_type(a5)

View File

@@ -9,7 +9,7 @@ reveal_type(x) # revealed: Literal[2]
x = 1.0
x /= 2
reveal_type(x) # revealed: int | float
reveal_type(x) # revealed: float
```
## Dunder methods
@@ -24,12 +24,12 @@ x -= 1
reveal_type(x) # revealed: str
class C:
def __iadd__(self, other: str) -> int:
return 1
def __iadd__(self, other: str) -> float:
return 1.0
x = C()
x += "Hello"
reveal_type(x) # revealed: int
reveal_type(x) # revealed: float
```
## Unsupported types
@@ -130,10 +130,10 @@ def _(flag: bool):
if flag:
f = Foo()
else:
f = 42
f = 42.0
f += 12
reveal_type(f) # revealed: str | Literal[54]
reveal_type(f) # revealed: str | float
```
## Partially bound target union with `__add__`

View File

@@ -804,67 +804,6 @@ def _(flag: bool, flag1: bool, flag2: bool):
reveal_type(C.x) # revealed: Unknown | Literal[1, 2, 3]
```
### Attribute possibly unbound on a subclass but not on a superclass
```py
def _(flag: bool):
class Foo:
x = 1
class Bar(Foo):
if flag:
x = 2
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
```
### Attribute possibly unbound on a subclass and on a superclass
```py
def _(flag: bool):
class Foo:
if flag:
x = 1
class Bar(Foo):
if flag:
x = 2
# error: [possibly-unbound-attribute]
reveal_type(Bar.x) # revealed: Unknown | Literal[2, 1]
```
### Attribute access on `Any`
The union of the set of types that `Any` could materialise to is equivalent to `object`. It follows
from this that attribute access on `Any` resolves to `Any` if the attribute does not exist on
`object` -- but if the attribute *does* exist on `object`, the type of the attribute is
`<type as it exists on object> & Any`.
```py
from typing import Any
class Foo(Any): ...
reveal_type(Foo.bar) # revealed: Any
reveal_type(Foo.__repr__) # revealed: Literal[__repr__] & Any
```
Similar principles apply if `Any` appears in the middle of an inheritance hierarchy:
```py
from typing import ClassVar, Literal
class A:
x: ClassVar[Literal[1]] = 1
class B(Any): ...
class C(B, A): ...
reveal_type(C.__mro__) # revealed: tuple[Literal[C], Literal[B], Any, Literal[A], Literal[object]]
reveal_type(C.x) # revealed: Literal[1] & Any
```
### Unions with all paths unbound
If the symbol is unbound in all elements of the union, we detect that:

View File

@@ -56,7 +56,7 @@ def _(a: bool):
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: int | float
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_int(x: int):
@@ -64,7 +64,7 @@ def _(a: bool):
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: int | float
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def lhs_is_bool(x: bool):
@@ -72,7 +72,7 @@ def _(a: bool):
reveal_type(x - a) # revealed: int
reveal_type(x * a) # revealed: int
reveal_type(x // a) # revealed: int
reveal_type(x / a) # revealed: int | float
reveal_type(x / a) # revealed: float
reveal_type(x % a) # revealed: int
def rhs_is_bool(x: bool):
@@ -80,7 +80,7 @@ def _(a: bool):
reveal_type(a - x) # revealed: int
reveal_type(a * x) # revealed: int
reveal_type(a // x) # revealed: int
reveal_type(a / x) # revealed: int | float
reveal_type(a / x) # revealed: float
reveal_type(a % x) # revealed: int
def both_are_bool(x: bool, y: bool):
@@ -88,6 +88,6 @@ def _(a: bool):
reveal_type(x - y) # revealed: int
reveal_type(x * y) # revealed: int
reveal_type(x // y) # revealed: int
reveal_type(x / y) # revealed: int | float
reveal_type(x / y) # revealed: float
reveal_type(x % y) # revealed: int
```

View File

@@ -268,28 +268,23 @@ reveal_type(B() + B()) # revealed: Unknown | int
## Integration test: numbers from typeshed
We get less precise results from binary operations on float/complex literals due to the special case
for annotations of `float` or `complex`, which applies also to return annotations for typeshed
dunder methods. Perhaps we could have a special-case on the special-case, to exclude these typeshed
return annotations from the widening, and preserve a bit more precision here?
```py
reveal_type(3j + 3.14) # revealed: int | float | complex
reveal_type(4.2 + 42) # revealed: int | float
reveal_type(3j + 3) # revealed: int | float | complex
reveal_type(3j + 3.14) # revealed: complex
reveal_type(4.2 + 42) # revealed: float
reveal_type(3j + 3) # revealed: complex
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3.14 + 3j) # revealed: int | float
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3.14 + 3j) # revealed: float
# TODO should be int | float, need to check arg type and fall back to `rhs.__radd__`
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(42 + 4.2) # revealed: int
# TODO should be int | float | complex, need to check arg type and fall back to `rhs.__radd__`
# TODO should be complex, need to check arg type and fall back to `rhs.__radd__`
reveal_type(3 + 3j) # revealed: int
def _(x: bool, y: int):
reveal_type(x + y) # revealed: int
reveal_type(4.2 + x) # revealed: int | float
reveal_type(4.2 + x) # revealed: float
# TODO should be float, need to check arg type and fall back to `rhs.__radd__`
reveal_type(y + 4.12) # revealed: int

View File

@@ -19,7 +19,7 @@ def lhs(x: int):
reveal_type(x - 4) # revealed: int
reveal_type(x * -1) # revealed: int
reveal_type(x // 3) # revealed: int
reveal_type(x / 3) # revealed: int | float
reveal_type(x / 3) # revealed: float
reveal_type(x % 3) # revealed: int
def rhs(x: int):
@@ -27,7 +27,7 @@ def rhs(x: int):
reveal_type(3 - x) # revealed: int
reveal_type(3 * x) # revealed: int
reveal_type(-3 // x) # revealed: int
reveal_type(-3 / x) # revealed: int | float
reveal_type(-3 / x) # revealed: float
reveal_type(5 % x) # revealed: int
def both(x: int):
@@ -35,7 +35,7 @@ def both(x: int):
reveal_type(x - x) # revealed: int
reveal_type(x * x) # revealed: int
reveal_type(x // x) # revealed: int
reveal_type(x / x) # revealed: int | float
reveal_type(x / x) # revealed: float
reveal_type(x % x) # revealed: int
```
@@ -80,20 +80,24 @@ c = 3 % 0 # error: "Cannot reduce object of type `Literal[3]` modulo zero"
reveal_type(c) # revealed: int
# error: "Cannot divide object of type `int` by zero"
reveal_type(int() / 0) # revealed: int | float
# revealed: float
reveal_type(int() / 0)
# error: "Cannot divide object of type `Literal[1]` by zero"
reveal_type(1 / False) # revealed: float
# revealed: float
reveal_type(1 / False)
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
True / False
# error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
bool(1) / False
# error: "Cannot divide object of type `float` by zero"
reveal_type(1.0 / 0) # revealed: int | float
# revealed: float
reveal_type(1.0 / 0)
class MyInt(int): ...
# No error for a subclass of int
reveal_type(MyInt(3) / 0) # revealed: int | float
# revealed: float
reveal_type(MyInt(3) / 0)
```

View File

@@ -4,14 +4,14 @@
```py
class Multiplier:
def __init__(self, factor: int):
def __init__(self, factor: float):
self.factor = factor
def __call__(self, number: int) -> int:
def __call__(self, number: float) -> float:
return number * self.factor
a = Multiplier(2)(3)
reveal_type(a) # revealed: int
a = Multiplier(2.0)(3.0)
reveal_type(a) # revealed: float
class Unit: ...

View File

@@ -20,8 +20,8 @@ class A:
def __eq__(self, other: A) -> int:
return 42
def __ne__(self, other: A) -> bytearray:
return bytearray()
def __ne__(self, other: A) -> float:
return 42.0
def __lt__(self, other: A) -> str:
return "42"
@@ -36,7 +36,7 @@ class A:
return {42}
reveal_type(A() == A()) # revealed: int
reveal_type(A() != A()) # revealed: bytearray
reveal_type(A() != A()) # revealed: float
reveal_type(A() < A()) # revealed: str
reveal_type(A() <= A()) # revealed: bytes
reveal_type(A() > A()) # revealed: list
@@ -55,8 +55,8 @@ class A:
def __eq__(self, other: B) -> int:
return 42
def __ne__(self, other: B) -> bytearray:
return bytearray()
def __ne__(self, other: B) -> float:
return 42.0
def __lt__(self, other: B) -> str:
return "42"
@@ -73,7 +73,7 @@ class A:
class B: ...
reveal_type(A() == B()) # revealed: int
reveal_type(A() != B()) # revealed: bytearray
reveal_type(A() != B()) # revealed: float
reveal_type(A() < B()) # revealed: str
reveal_type(A() <= B()) # revealed: bytes
reveal_type(A() > B()) # revealed: list
@@ -93,8 +93,8 @@ class A:
def __eq__(self, other: B) -> int:
return 42
def __ne__(self, other: B) -> bytearray:
return bytearray()
def __ne__(self, other: B) -> float:
return 42.0
def __lt__(self, other: B) -> str:
return "42"
@@ -117,7 +117,7 @@ class B:
def __ne__(self, other: str) -> B:
return B()
# TODO: should be `int` and `bytearray`.
# TODO: should be `int` and `float`.
# Need to check arg type and fall back to `rhs.__eq__` and `rhs.__ne__`.
#
# Because `object.__eq__` and `object.__ne__` accept `object` in typeshed,
@@ -136,11 +136,11 @@ class C:
def __gt__(self, other: C) -> int:
return 42
def __ge__(self, other: C) -> bytearray:
return bytearray()
def __ge__(self, other: C) -> float:
return 42.0
reveal_type(C() < C()) # revealed: int
reveal_type(C() <= C()) # revealed: bytearray
reveal_type(C() <= C()) # revealed: float
```
## Reflected Comparisons with Subclasses
@@ -175,8 +175,8 @@ class B(A):
def __eq__(self, other: A) -> int:
return 42
def __ne__(self, other: A) -> bytearray:
return bytearray()
def __ne__(self, other: A) -> float:
return 42.0
def __lt__(self, other: A) -> str:
return "42"
@@ -191,7 +191,7 @@ class B(A):
return {42}
reveal_type(A() == B()) # revealed: int
reveal_type(A() != B()) # revealed: bytearray
reveal_type(A() != B()) # revealed: float
reveal_type(A() < B()) # revealed: list
reveal_type(A() <= B()) # revealed: set

View File

@@ -151,11 +151,11 @@ class A:
def __ne__(self, o: object) -> bytes:
return b"world"
def __lt__(self, o: A) -> bytearray:
return bytearray()
def __lt__(self, o: A) -> float:
return 3.14
def __le__(self, o: A) -> memoryview:
return memoryview(b"")
def __le__(self, o: A) -> complex:
return complex(0.5, -0.5)
def __gt__(self, o: A) -> tuple:
return (1, 2, 3)
@@ -167,8 +167,8 @@ a = (A(), A())
reveal_type(a == a) # revealed: bool
reveal_type(a != a) # revealed: bool
reveal_type(a < a) # revealed: bytearray | Literal[False]
reveal_type(a <= a) # revealed: memoryview | Literal[True]
reveal_type(a < a) # revealed: float | Literal[False]
reveal_type(a <= a) # revealed: complex | Literal[True]
reveal_type(a > a) # revealed: tuple | Literal[False]
reveal_type(a >= a) # revealed: list | Literal[True]
@@ -187,7 +187,7 @@ class B:
def __lt__(self, o: B) -> set:
return set()
reveal_type((A(), B()) < (A(), B())) # revealed: bytearray | set | Literal[False]
reveal_type((A(), B()) < (A(), B())) # revealed: float | set | Literal[False]
```
#### Special Handling of Eq and NotEq in Lexicographic Comparisons

View File

@@ -1,2 +0,0 @@
This directory contains user-facing documentation, but also doubles as an extended test suite that
makes sure that our documentation stays up to date.

View File

@@ -1,125 +0,0 @@
# Public type of undeclared symbols
## Summary
One major deviation from the behavior of existing Python type checkers is our handling of 'public'
types for undeclared symbols. This is best illustrated with an example:
```py
class Wrapper:
value = None
wrapper = Wrapper()
reveal_type(wrapper.value) # revealed: Unknown | None
wrapper.value = 1
```
Mypy and Pyright both infer a type of `None` for the type of `wrapper.value`. Consequently, both
tools emit an error when trying to assign `1` to `wrapper.value`. But there is nothing wrong with
this program. Emitting an error here violates the [gradual guarantee] which states that *"Removing
type annotations (making the program more dynamic) should not result in additional static type
errors."*: If `value` were annotated with `int | None` here, Mypy and Pyright would not emit any
errors.
By inferring `Unknown | None` instead, we allow arbitrary values to be assigned to `wrapper.value`.
This is a deliberate choice to prevent false positive errors on untyped code.
More generally, we infer `Unknown | T_inferred` for undeclared symbols, where `T_inferred` is the
inferred type of the right-hand side of the assignment. This gradual type represents an *unknown*
fully-static type that is *at least as large as* `T_inferred`. It accurately describes our static
knowledge about this type. In the example above, we don't know what values `wrapper.value` could
possibly contain, but we *do know* that `None` is a possibility. This allows us to catch errors
where `wrapper.value` is used in a way that is incompatible with `None`:
```py
def accepts_int(i: int) -> None:
pass
def f(w: Wrapper) -> None:
# This is fine
v: int | None = w.value
# This function call is incorrect, because `w.value` could be `None`. We therefore emit the following
# error: "`Unknown | None` cannot be assigned to parameter 1 (`i`) of function `accepts_int`; expected type `int`"
c = accepts_int(w.value)
```
## Explicit lack of knowledge
The following example demonstrates how Mypy and Pyright's type inference of fully-static types in
these situations can lead to false-negatives, even though everything appears to be (statically)
typed. To make this a bit more realistic, imagine that `OptionalInt` is imported from an external,
untyped module:
`optional_int.py`:
```py
class OptionalInt:
value = 10
def reset(o):
o.value = None
```
It is then used like this:
```py
from optional_int import OptionalInt, reset
o = OptionalInt()
reset(o) # Oh no...
# Mypy and Pyright infer a fully-static type of `int` here, which appears to make the
# subsequent division operation safe -- but it is not. We infer the following type:
reveal_type(o.value) # revealed: Unknown | Literal[10]
print(o.value // 2) # Runtime error!
```
We do not catch this mistake either, but we accurately reflect our lack of knowledge about
`o.value`. Together with a possible future type-checker mode that would detect the prevalence of
dynamic types, this could help developers catch such mistakes.
## Stricter behavior
Users can always opt in to stricter behavior by adding type annotations. For the `OptionalInt`
class, this would probably be:
```py
class OptionalInt:
value: int | None = 10
o = OptionalInt()
# The following public type is now
# revealed: int | None
reveal_type(o.value)
# Incompatible assignments are now caught:
# error: "Object of type `Literal["a"]` is not assignable to attribute `value` of type `int | None`"
o.value = "a"
```
## What is meant by 'public' type?
We apply different semantics depending on whether a symbol is accessed from the same scope in which
it was originally defined, or whether it is accessed from an external scope. External scopes will
see the symbol's "public type", which has been discussed above. But within the same scope the symbol
was defined in, we use a narrower type of `T_inferred` for undeclared symbols. This is because, from
the perspective of this scope, there is no way that the value of the symbol could have been
reassigned from external scopes. For example:
```py
class Wrapper:
value = None
# Type as seen from the same scope:
reveal_type(value) # revealed: None
# Type as seen from another scope:
reveal_type(Wrapper.value) # revealed: Unknown | None
```
[gradual guarantee]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-gradual-guarantee

View File

@@ -303,8 +303,8 @@ An example with multiple `except` branches and a `finally` branch:
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_float() -> float:
return 3.14
x = 1
@@ -322,13 +322,13 @@ except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray`
reveal_type(x) # revealed: str | bool | bytearray
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: str | bool | float
reveal_type(x) # revealed: str | bool | bytearray
reveal_type(x) # revealed: str | bool | float
```
## Combining `except`, `else` and `finally` branches
@@ -350,8 +350,8 @@ def could_raise_returns_bool() -> bool:
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_float() -> float:
return 3.14
x = 1
@@ -369,13 +369,13 @@ else:
reveal_type(x) # revealed: str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray`
reveal_type(x) # revealed: bool | bytearray
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float`
reveal_type(x) # revealed: bool | float
reveal_type(x) # revealed: bool | bytearray
reveal_type(x) # revealed: bool | float
```
The same again, this time with multiple `except` branches:
@@ -403,8 +403,8 @@ except ValueError:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
x = could_raise_returns_float()
reveal_type(x) # revealed: float
else:
reveal_type(x) # revealed: str
x = could_raise_returns_range()
@@ -412,10 +412,10 @@ else:
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | bytearray | range | slice`
reveal_type(x) # revealed: bool | bytearray | slice
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
reveal_type(x) # revealed: bool | bytearray | slice
reveal_type(x) # revealed: bool | float | slice
```
## Nested `try`/`except` blocks
@@ -441,8 +441,8 @@ def could_raise_returns_bool() -> bool:
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_property() -> property:
return property()
def could_raise_returns_float() -> float:
return 3.14
def could_raise_returns_range() -> range:
return range(42)
@@ -450,8 +450,8 @@ def could_raise_returns_range() -> range:
def could_raise_returns_slice() -> slice:
return slice(None)
def could_raise_returns_super() -> super:
return super()
def could_raise_returns_complex() -> complex:
return 3j
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
@@ -482,8 +482,8 @@ try:
reveal_type(x) # revealed: Literal[1] | str
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_property()
reveal_type(x) # revealed: property
x = could_raise_returns_float()
reveal_type(x) # revealed: float
else:
reveal_type(x) # revealed: str
x = could_raise_returns_range()
@@ -491,15 +491,15 @@ try:
x = could_raise_returns_slice()
reveal_type(x) # revealed: slice
finally:
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | property | range | slice`
reveal_type(x) # revealed: bool | property | slice
# TODO: should be `Literal[1] | str | bytes | bool | memoryview | float | range | slice`
reveal_type(x) # revealed: bool | float | slice
x = 2
reveal_type(x) # revealed: Literal[2]
reveal_type(x) # revealed: Literal[2]
except:
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice
x = could_raise_returns_super()
reveal_type(x) # revealed: super
reveal_type(x) # revealed: Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice
x = could_raise_returns_complex()
reveal_type(x) # revealed: complex
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
else:
@@ -509,7 +509,7 @@ else:
x = could_raise_returns_Bar()
reveal_type(x) # revealed: Bar
finally:
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | property | range | slice | super | bytearray | Foo | Bar`
# TODO: should be `Literal[1, 2] | str | bytes | bool | memoryview | float | range | slice | complex | bytearray | Foo | Bar`
reveal_type(x) # revealed: bytearray | Bar
# Either one `except` branch or the `else`
@@ -535,8 +535,8 @@ def could_raise_returns_range() -> range:
def could_raise_returns_bytearray() -> bytearray:
return bytearray()
def could_raise_returns_memoryview() -> memoryview:
return memoryview(b"")
def could_raise_returns_float() -> float:
return 3.14
x = 1
@@ -553,12 +553,12 @@ try:
reveal_type(x) # revealed: str | bytes
x = could_raise_returns_bytearray()
reveal_type(x) # revealed: bytearray
x = could_raise_returns_memoryview()
reveal_type(x) # revealed: memoryview
x = could_raise_returns_float()
reveal_type(x) # revealed: float
finally:
# TODO: should be `str | bytes | bytearray | memoryview`
reveal_type(x) # revealed: bytes | memoryview
reveal_type(x) # revealed: bytes | memoryview
# TODO: should be `str | bytes | bytearray | float`
reveal_type(x) # revealed: bytes | float
reveal_type(x) # revealed: bytes | float
x = foo
reveal_type(x) # revealed: Literal[foo]
except:

View File

@@ -1,371 +0,0 @@
# Import conventions
This document describes the conventions for importing symbols.
Reference:
- <https://typing.readthedocs.io/en/latest/spec/distributing.html#import-conventions>
## Builtins scope
When looking up for a name, red knot will fallback to using the builtins scope if the name is not
found in the global scope. The `builtins.pyi` file, that will be used to resolve any symbol in the
builtins scope, contains multiple symbols from other modules (e.g., `typing`) but those are not
re-exported.
```py
# These symbols are being imported in `builtins.pyi` but shouldn't be considered as being
# available in the builtins scope.
# error: "Name `Literal` used when not defined"
reveal_type(Literal) # revealed: Unknown
# error: "Name `sys` used when not defined"
reveal_type(sys) # revealed: Unknown
```
## Builtins import
Similarly, trying to import the symbols from the builtins module which aren't re-exported should
also raise an error.
```py
# error: "Module `builtins` has no member `Literal`"
# error: "Module `builtins` has no member `sys`"
from builtins import Literal, sys
reveal_type(Literal) # revealed: Unknown
reveal_type(sys) # revealed: Unknown
# error: "Module `math` has no member `Iterable`"
from math import Iterable
reveal_type(Iterable) # revealed: Unknown
```
## Re-exported symbols in stub files
When a symbol is re-exported, importing it should not raise an error. This tests both `import ...`
and `from ... import ...` forms.
Note: Submodule imports in `import ...` form doesn't work because it's a syntax error. For example,
in `import os.path as os.path` the `os.path` is not a valid identifier.
```py
from b import Any, Literal, foo
reveal_type(Any) # revealed: typing.Any
reveal_type(Literal) # revealed: typing.Literal
reveal_type(foo) # revealed: <module 'foo'>
```
`b.pyi`:
```pyi
import foo as foo
from typing import Any as Any, Literal as Literal
```
`foo.py`:
```py
```
## Non-exported symbols in stub files
Here, none of the symbols are being re-exported in the stub file.
```py
# error: 15 [unresolved-import] "Module `b` has no member `foo`"
# error: 20 [unresolved-import] "Module `b` has no member `Any`"
# error: 25 [unresolved-import] "Module `b` has no member `Literal`"
from b import foo, Any, Literal
reveal_type(Any) # revealed: Unknown
reveal_type(Literal) # revealed: Unknown
reveal_type(foo) # revealed: Unknown
```
`b.pyi`:
```pyi
import foo
from typing import Any, Literal
```
`foo.pyi`:
```pyi
```
## Nested non-exports
Here, a chain of modules all don't re-export an import.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
# error: "Module `b` has no member `Any`"
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Nested mixed re-export and not
But, if the symbol is being re-exported explicitly in one of the modules in the chain, it should not
raise an error at that step in the chain.
```py
# error: "Module `a` has no member `Any`"
from a import Any
reveal_type(Any) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import Any
reveal_type(Any) # revealed: Unknown
```
`b.pyi`:
```pyi
# error: "Module `c` has no member `Any`"
from c import Any as Any
reveal_type(Any) # revealed: Unknown
```
`c.pyi`:
```pyi
from typing import Any
reveal_type(Any) # revealed: typing.Any
```
## Exported as different name
The re-export convention only works when the aliased name is exactly the same as the original name.
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
from b import AnyFoo as Foo
reveal_type(Foo) # revealed: Literal[AnyFoo]
```
`b.pyi`:
```pyi
class AnyFoo: ...
```
## Exported using `__all__`
Here, the symbol is re-exported using the `__all__` variable.
```py
# TODO: This should *not* be an error but we don't understand `__all__` yet.
# error: "Module `a` has no member `Foo`"
from a import Foo
```
`a.pyi`:
```pyi
from b import Foo
__all__ = ['Foo']
```
`b.pyi`:
```pyi
class Foo: ...
```
## Re-exports in `__init__.pyi`
Similarly, for an `__init__.pyi` (stub) file, importing a non-exported name should raise an error
but the inference would be `Unknown`.
```py
# error: 15 "Module `a` has no member `Foo`"
# error: 20 "Module `a` has no member `c`"
from a import Foo, c, foo
reveal_type(Foo) # revealed: Unknown
reveal_type(c) # revealed: Unknown
reveal_type(foo) # revealed: <module 'a.foo'>
```
`a/__init__.pyi`:
```pyi
from .b import c
from .foo import Foo
```
`a/foo.pyi`:
```pyi
class Foo: ...
```
`a/b/__init__.pyi`:
```pyi
```
`a/b/c.pyi`:
```pyi
```
## Conditional re-export in stub file
The following scenarios are when a re-export happens conditionally in a stub file.
### Global import
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: str
```
`a.pyi`:
```pyi
from b import Foo
def coinflip() -> bool: ...
if coinflip():
Foo: str = ...
reveal_type(Foo) # revealed: Literal[Foo] | str
```
`b.pyi`:
```pyi
class Foo: ...
```
### Both branch is an import
Here, both the branches of the condition are import statements where one of them re-exports while
the other does not.
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
else:
from b import Foo as Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`b.pyi`:
```pyi
class Foo: ...
```
### Re-export in one branch
```py
# error: "Member `Foo` of module `a` is possibly unbound"
from a import Foo
reveal_type(Foo) # revealed: Literal[Foo]
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo as Foo
```
`b.pyi`:
```pyi
class Foo: ...
```
### Non-export in one branch
```py
# error: "Module `a` has no member `Foo`"
from a import Foo
reveal_type(Foo) # revealed: Unknown
```
`a.pyi`:
```pyi
def coinflip() -> bool: ...
if coinflip():
from b import Foo
```
`b.pyi`:
```pyi
class Foo: ...
```

View File

@@ -64,39 +64,3 @@ def _(flag1: bool, flag2: bool):
else:
reveal_type(x) # revealed: Literal[1]
```
## `is` for `EllipsisType` (Python 3.10+)
```toml
[environment]
python-version = "3.10"
```
```py
from types import EllipsisType
def _(x: int | EllipsisType):
if x is ...:
reveal_type(x) # revealed: EllipsisType
else:
reveal_type(x) # revealed: int
```
## `is` for `EllipsisType` (Python 3.9 and below)
```toml
[environment]
python-version = "3.9"
```
```py
def _(flag: bool):
x = ... if flag else 42
reveal_type(x) # revealed: ellipsis | Literal[42]
if x is ...:
reveal_type(x) # revealed: ellipsis
else:
reveal_type(x) # revealed: Literal[42]
```

View File

@@ -132,6 +132,27 @@ static_assert(not is_disjoint_from(Intersection[X, Z], Y))
static_assert(not is_disjoint_from(Intersection[Y, Z], X))
```
## Negation / complement
The complement of a type `T` is disjoint from `T`. In fact, it is disjoint from every subtype of
`T`:
```py
from knot_extensions import Not, Intersection, is_disjoint_from, static_assert
class T: ...
class S(T): ...
static_assert(is_disjoint_from(Not[T], T))
static_assert(is_disjoint_from(Not[T], S))
static_assert(is_disjoint_from(Intersection[T, Any], Not[T]))
static_assert(is_disjoint_from(Not[T], Intersection[T, Any]))
static_assert(is_disjoint_from(Intersection[S, Any], Not[T]))
static_assert(is_disjoint_from(Not[T], Intersection[S, Any]))
```
## Special types
### `Never`
@@ -244,7 +265,7 @@ static_assert(not is_disjoint_from(TypeOf[f], object))
### `AlwaysTruthy` and `AlwaysFalsy`
```py
from knot_extensions import AlwaysFalsy, AlwaysTruthy, is_disjoint_from, static_assert
from knot_extensions import AlwaysFalsy, AlwaysTruthy, Intersection, Not, is_disjoint_from, static_assert
from typing import Literal
static_assert(is_disjoint_from(None, AlwaysTruthy))
@@ -256,6 +277,14 @@ static_assert(not is_disjoint_from(str, AlwaysTruthy))
static_assert(is_disjoint_from(Literal[1, 2], AlwaysFalsy))
static_assert(not is_disjoint_from(Literal[0, 1], AlwaysTruthy))
type Truthy = Not[AlwaysFalsy]
type Falsy = Not[AlwaysTruthy]
type AmbiguousTruthiness = Intersection[Truthy, Falsy]
static_assert(is_disjoint_from(AlwaysTruthy, AmbiguousTruthiness))
static_assert(is_disjoint_from(AlwaysFalsy, AmbiguousTruthiness))
```
### Instance types versus `type[T]` types

View File

@@ -54,41 +54,3 @@ from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(_NoDefaultType))
```
## `builtins.ellipsis`/`types.EllipsisType`
### All Python versions
The type of the builtin symbol `Ellipsis` is the same as the type of an ellipsis literal (`...`).
The type is not actually exposed from the standard library on Python \<3.10, but we still recognise
the type as a singleton on any Python version.
```toml
[environment]
python-version = "3.9"
```
```py
import sys
from knot_extensions import is_singleton, static_assert
static_assert(is_singleton(Ellipsis.__class__))
static_assert(is_singleton((...).__class__))
```
### Python 3.10+
On Python 3.10+, the standard library exposes the type of `...` as `types.EllipsisType`, and we also
recognise this as a singleton type when it is referenced directly:
```toml
[environment]
python-version = "3.10"
```
```py
import types
from knot_extensions import static_assert, is_singleton
static_assert(is_singleton(types.EllipsisType))
```

View File

@@ -11,15 +11,11 @@ See the [typing documentation] for more information.
- `bool` is a subtype of `int`. This is modeled after Python's runtime behavior, where `int` is a
supertype of `bool` (present in `bool`s bases and MRO).
- `int` is not a subtype of `float`/`complex`, although this is muddied by the
[special case for float and complex] where annotations of `float` and `complex` are interpreted
as `int | float` and `int | float | complex`, respectively.
- `int` is not a subtype of `float`/`complex`, even though `float`/`complex` can be used in place of
`int` in some contexts (see [special case for float and complex]).
```py
from knot_extensions import is_subtype_of, static_assert, TypeOf
type JustFloat = TypeOf[1.0]
type JustComplex = TypeOf[1j]
from knot_extensions import is_subtype_of, static_assert
static_assert(is_subtype_of(bool, bool))
static_assert(is_subtype_of(bool, int))
@@ -34,8 +30,8 @@ static_assert(not is_subtype_of(int, bool))
static_assert(not is_subtype_of(int, str))
static_assert(not is_subtype_of(object, int))
static_assert(not is_subtype_of(int, JustFloat))
static_assert(not is_subtype_of(int, JustComplex))
static_assert(not is_subtype_of(int, float))
static_assert(not is_subtype_of(int, complex))
static_assert(is_subtype_of(TypeError, Exception))
static_assert(is_subtype_of(FloatingPointError, Exception))
@@ -83,9 +79,7 @@ static_assert(is_subtype_of(C, object))
```py
from typing_extensions import Literal, LiteralString
from knot_extensions import is_subtype_of, static_assert, TypeOf
type JustFloat = TypeOf[1.0]
from knot_extensions import is_subtype_of, static_assert
# Boolean literals
static_assert(is_subtype_of(Literal[True], bool))
@@ -98,7 +92,8 @@ static_assert(is_subtype_of(Literal[1], object))
static_assert(not is_subtype_of(Literal[1], bool))
static_assert(not is_subtype_of(Literal[1], JustFloat))
# See the note above (or link below) concerning int and float/complex
static_assert(not is_subtype_of(Literal[1], float))
# String literals
static_assert(is_subtype_of(Literal["foo"], LiteralString))

View File

@@ -37,31 +37,6 @@ def noreturn(u1: int | NoReturn, u2: int | NoReturn | str) -> None:
reveal_type(u2) # revealed: int | str
```
## `object` subsumes everything
Unions with `object` can be simplified to `object`:
```py
from typing_extensions import Never, Any
def _(
u1: int | object,
u2: object | int,
u3: Any | object,
u4: object | Any,
u5: object | Never,
u6: Never | object,
u7: int | str | object | bytes | Any,
) -> None:
reveal_type(u1) # revealed: object
reveal_type(u2) # revealed: object
reveal_type(u3) # revealed: object
reveal_type(u4) # revealed: object
reveal_type(u5) # revealed: object
reveal_type(u6) # revealed: object
reveal_type(u7) # revealed: object
```
## Flattening of nested unions
```py
@@ -70,11 +45,11 @@ from typing import Literal
def _(
u1: (int | str) | bytes,
u2: int | (str | bytes),
u3: int | (str | (bytes | bytearray)),
u3: int | (str | (bytes | complex)),
) -> None:
reveal_type(u1) # revealed: int | str | bytes
reveal_type(u2) # revealed: int | str | bytes
reveal_type(u3) # revealed: int | str | bytes | bytearray
reveal_type(u3) # revealed: int | str | bytes | complex
```
## Simplification using subtyping
@@ -145,8 +120,8 @@ Simplifications still apply when `Unknown` is present.
```py
from knot_extensions import Unknown
def _(u1: int | Unknown | bool) -> None:
reveal_type(u1) # revealed: int | Unknown
def _(u1: str | Unknown | int | object):
reveal_type(u1) # revealed: Unknown | object
```
## Union of intersections

View File

@@ -12,32 +12,14 @@ use ruff_db::parsed::ParsedModule;
/// Holding on to any [`AstNodeRef`] prevents the [`ParsedModule`] from being released.
///
/// ## Equality
/// Two `AstNodeRef` are considered equal if their pointer addresses are equal.
///
/// ## Usage in salsa tracked structs
/// It's important that [`AstNodeRef`] fields in salsa tracked structs are tracked fields
/// (attributed with `#[tracked`]). It prevents that the tracked struct gets a new ID
/// everytime the AST changes, which in turn, invalidates the result of any query
/// that takes said tracked struct as a query argument or returns the tracked struct as part of its result.
///
/// For example, marking the [`AstNodeRef`] as tracked on `Expression`
/// has the effect that salsa will consider the expression as "unchanged" for as long as it:
///
/// * belongs to the same file
/// * belongs to the same scope
/// * has the same kind
/// * was created in the same order
///
/// This means that changes to expressions in other scopes don't invalidate the expression's id, giving
/// us some form of scope-stable identity for expressions. Only queries accessing the node field
/// run on every AST change. All other queries only run when the expression's identity changes.
/// Two `AstNodeRef` are considered equal if their wrapped nodes are equal.
#[derive(Clone)]
pub struct AstNodeRef<T> {
/// Owned reference to the node's [`ParsedModule`].
///
/// The node's reference is guaranteed to remain valid as long as it's enclosing
/// [`ParsedModule`] is alive.
parsed: ParsedModule,
_parsed: ParsedModule,
/// Pointer to the referenced node.
node: std::ptr::NonNull<T>,
@@ -55,7 +37,7 @@ impl<T> AstNodeRef<T> {
/// the invariant `node belongs to parsed` is upheld.
pub(super) unsafe fn new(parsed: ParsedModule, node: &T) -> Self {
Self {
parsed,
_parsed: parsed,
node: std::ptr::NonNull::from(node),
}
}
@@ -90,14 +72,7 @@ where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
if self.parsed == other.parsed {
// Comparing the pointer addresses is sufficient to determine equality
// if the parsed are the same.
self.node.eq(&other.node)
} else {
// Otherwise perform a deep comparison.
self.node().eq(other.node())
}
self.node().eq(other.node())
}
}
@@ -112,20 +87,6 @@ where
}
}
#[allow(unsafe_code)]
unsafe impl<T> salsa::Update for AstNodeRef<T> {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
let old_ref = &mut (*old_pointer);
if old_ref.parsed == new_value.parsed && old_ref.node.eq(&new_value.node) {
false
} else {
*old_ref = new_value;
true
}
}
}
#[allow(unsafe_code)]
unsafe impl<T> Send for AstNodeRef<T> where T: Send {}
#[allow(unsafe_code)]

View File

@@ -1,5 +1,3 @@
use std::sync::Arc;
use crate::lint::{LintRegistry, RuleSelection};
use ruff_db::files::File;
use ruff_db::{Db as SourceDb, Upcast};
@@ -9,7 +7,7 @@ use ruff_db::{Db as SourceDb, Upcast};
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
fn is_file_open(&self, file: File) -> bool;
fn rule_selection(&self) -> Arc<RuleSelection>;
fn rule_selection(&self) -> &RuleSelection;
fn lint_registry(&self) -> &LintRegistry;
}
@@ -19,6 +17,7 @@ pub(crate) mod tests {
use std::sync::Arc;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
use super::Db;
@@ -28,7 +27,6 @@ pub(crate) mod tests {
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::python_version::PythonVersion;
#[salsa::db]
#[derive(Clone)]
@@ -113,8 +111,8 @@ pub(crate) mod tests {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> Arc<RuleSelection> {
self.rule_selection.clone()
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {

View File

@@ -9,6 +9,7 @@ pub use module_name::ModuleName;
pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module};
pub use program::{Program, ProgramSettings, SearchPathSettings, SitePackages};
pub use python_platform::PythonPlatform;
pub use python_version::PythonVersion;
pub use semantic_model::{HasType, SemanticModel};
pub mod ast_node_ref;
@@ -19,6 +20,7 @@ mod module_resolver;
mod node_key;
mod program;
mod python_platform;
mod python_version;
pub mod semantic_index;
mod semantic_model;
pub(crate) mod site_packages;

View File

@@ -631,10 +631,10 @@ impl PartialEq<SearchPath> for VendoredPathBuf {
#[cfg(test)]
mod tests {
use ruff_db::Db;
use ruff_python_ast::python_version::PythonVersion;
use crate::db::tests::TestDb;
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::python_version::PythonVersion;
use super::*;

View File

@@ -6,13 +6,12 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_db::files::{File, FilePath, FileRootKind};
use ruff_db::system::{DirectoryEntry, System, SystemPath, SystemPathBuf};
use ruff_db::vendored::{VendoredFileSystem, VendoredPath};
use ruff_python_ast::python_version::PythonVersion;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::module_resolver::typeshed::{vendored_typeshed_versions, TypeshedVersions};
use crate::site_packages::VirtualEnvironment;
use crate::{Program, SearchPathSettings, SitePackages};
use crate::{Program, PythonVersion, SearchPathSettings, SitePackages};
use super::module::{Module, ModuleKind};
use super::path::{ModulePath, SearchPath, SearchPathValidationError};
@@ -134,7 +133,7 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
}
#[derive(Debug, PartialEq, Eq)]
pub struct SearchPaths {
pub(crate) struct SearchPaths {
/// Search paths that have been statically determined purely from reading Ruff's configuration settings.
/// These shouldn't ever change unless the config settings themselves change.
static_paths: Vec<SearchPath>,
@@ -725,12 +724,12 @@ mod tests {
assert_const_function_query_was_not_run, assert_function_query_was_not_run,
};
use ruff_db::Db;
use ruff_python_ast::python_version::PythonVersion;
use crate::db::tests::TestDb;
use crate::module_name::ModuleName;
use crate::module_resolver::module::ModuleKind;
use crate::module_resolver::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use crate::PythonVersion;
use crate::{ProgramSettings, PythonPlatform};
use super::*;

View File

@@ -1,9 +1,9 @@
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
use ruff_db::vendored::VendoredPathBuf;
use ruff_python_ast::python_version::PythonVersion;
use crate::db::tests::TestDb;
use crate::program::{Program, SearchPathSettings};
use crate::python_version::PythonVersion;
use crate::{ProgramSettings, PythonPlatform, SitePackages};
/// A test case for the module resolver.

View File

@@ -4,12 +4,11 @@ use std::num::{NonZeroU16, NonZeroUsize};
use std::ops::{RangeFrom, RangeInclusive};
use std::str::FromStr;
use ruff_python_ast::python_version::PythonVersion;
use rustc_hash::FxHashMap;
use crate::db::Db;
use crate::module_name::ModuleName;
use crate::Program;
use crate::{Program, PythonVersion};
pub(in crate::module_resolver) fn vendored_typeshed_versions(db: &dyn Db) -> TypeshedVersions {
TypeshedVersions::from_str(
@@ -279,12 +278,12 @@ impl FromStr for PyVersionRange {
let mut parts = s.split('-').map(str::trim);
match (parts.next(), parts.next(), parts.next()) {
(Some(lower), Some(""), None) => {
let lower = python_version_from_versions_file_string(lower)?;
let lower = PythonVersion::from_versions_file_string(lower)?;
Ok(Self::AvailableFrom(lower..))
}
(Some(lower), Some(upper), None) => {
let lower = python_version_from_versions_file_string(lower)?;
let upper = python_version_from_versions_file_string(upper)?;
let lower = PythonVersion::from_versions_file_string(lower)?;
let upper = PythonVersion::from_versions_file_string(upper)?;
Ok(Self::AvailableWithin(lower..=upper))
}
_ => Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfHyphens),
@@ -303,21 +302,21 @@ impl fmt::Display for PyVersionRange {
}
}
fn python_version_from_versions_file_string(
s: &str,
) -> Result<PythonVersion, TypeshedVersionsParseErrorKind> {
let mut parts = s.split('.').map(str::trim);
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
s.to_string(),
));
};
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
version: s.to_string(),
err: int_parse_error,
}
})
impl PythonVersion {
fn from_versions_file_string(s: &str) -> Result<Self, TypeshedVersionsParseErrorKind> {
let mut parts = s.split('.').map(str::trim);
let (Some(major), Some(minor), None) = (parts.next(), parts.next(), parts.next()) else {
return Err(TypeshedVersionsParseErrorKind::UnexpectedNumberOfPeriods(
s.to_string(),
));
};
PythonVersion::try_from((major, minor)).map_err(|int_parse_error| {
TypeshedVersionsParseErrorKind::IntegerParsingFailure {
version: s.to_string(),
err: int_parse_error,
}
})
}
}
#[cfg(test)]

View File

@@ -1,10 +1,10 @@
use crate::module_resolver::SearchPaths;
use crate::python_platform::PythonPlatform;
use crate::python_version::PythonVersion;
use crate::Db;
use anyhow::Context;
use ruff_db::system::{SystemPath, SystemPathBuf};
use ruff_python_ast::python_version::PythonVersion;
use salsa::Durability;
use salsa::Setter;

View File

@@ -2,7 +2,8 @@ use std::fmt;
/// Representation of a Python version.
///
/// N.B. This does not necessarily represent a Python version that we actually support.
/// Unlike the `TargetVersion` enums in the CLI crates,
/// this does not necessarily represent a Python version that we actually support.
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct PythonVersion {
pub major: u8,
@@ -40,7 +41,8 @@ impl PythonVersion {
PythonVersion::PY312,
PythonVersion::PY313,
]
.into_iter()
.iter()
.copied()
}
pub fn free_threaded_build_available(self) -> bool {
@@ -82,7 +84,7 @@ impl fmt::Display for PythonVersion {
#[cfg(feature = "serde")]
mod serde {
use super::PythonVersion;
use crate::PythonVersion;
impl<'de> serde::Deserialize<'de> for PythonVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>

View File

@@ -1,14 +1,13 @@
use std::iter::FusedIterator;
use std::sync::Arc;
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_index::{IndexSlice, IndexVec};
use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
use salsa::plumbing::AsId;
use salsa::Update;
use crate::module_name::ModuleName;
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
use crate::semantic_index::ast_ids::AstIds;
@@ -124,7 +123,7 @@ pub(crate) fn global_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
}
/// The symbol tables and use-def maps for all scopes in a file.
#[derive(Debug, Update)]
#[derive(Debug)]
pub(crate) struct SemanticIndex<'db> {
/// List of all symbol tables in this file, indexed by scope.
symbol_tables: IndexVec<FileScopeId, Arc<SymbolTable>>,

View File

@@ -24,7 +24,7 @@ use crate::Db;
///
/// x = foo()
/// ```
#[derive(Debug, salsa::Update)]
#[derive(Debug)]
pub(crate) struct AstIds {
/// Maps expressions to their expression id.
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
@@ -74,7 +74,6 @@ impl HasScopedUseId for ast::ExprRef<'_> {
/// Uniquely identifies an [`ast::Expr`] in a [`crate::semantic_index::symbol::FileScopeId`].
#[newtype_index]
#[derive(salsa::Update)]
pub struct ScopedExpressionId;
pub trait HasScopedExpressionId {
@@ -182,7 +181,7 @@ pub(crate) mod node_key {
use crate::node_key::NodeKey;
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) struct ExpressionNodeKey(NodeKey);
impl From<ast::ExprRef<'_>> for ExpressionNodeKey {

View File

@@ -9,7 +9,7 @@ use rustc_hash::FxHashMap;
/// Describes an (annotated) attribute assignment that we discovered in a method
/// body, typically of the form `self.x: int`, `self.x: int = …` or `self.x = …`.
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum AttributeAssignment<'db> {
/// An attribute assignment with an explicit type annotation, either
/// `self.x: <annotation>` or `self.x: <annotation> = …`.

View File

@@ -33,8 +33,8 @@ use crate::Db;
use super::constraint::{Constraint, ConstraintNode, PatternConstraint};
use super::definition::{
DefinitionCategory, ExceptHandlerDefinitionNodeRef, ImportDefinitionNodeRef,
MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
DefinitionCategory, ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef,
WithItemDefinitionNodeRef,
};
mod except_handlers;
@@ -886,28 +886,22 @@ where
self.imported_modules.extend(module_name.ancestors());
}
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
(asname.id.clone(), asname.id == alias.name.id)
let symbol_name = if let Some(asname) = &alias.asname {
asname.id.clone()
} else {
(Name::new(alias.name.id.split('.').next().unwrap()), false)
Name::new(alias.name.id.split('.').next().unwrap())
};
let symbol = self.add_symbol(symbol_name);
self.add_definition(
symbol,
ImportDefinitionNodeRef {
alias,
is_reexported,
},
);
self.add_definition(symbol, alias);
}
}
ast::Stmt::ImportFrom(node) => {
for (alias_index, alias) in node.names.iter().enumerate() {
let (symbol_name, is_reexported) = if let Some(asname) = &alias.asname {
(&asname.id, asname.id == alias.name.id)
let symbol_name = if let Some(asname) = &alias.asname {
&asname.id
} else {
(&alias.name.id, false)
&alias.name.id
};
// Look for imports `from __future__ import annotations`, ignore `as ...`
@@ -920,14 +914,7 @@ where
let symbol = self.add_symbol(symbol_name.clone());
self.add_definition(
symbol,
ImportFromDefinitionNodeRef {
node,
alias_index,
is_reexported,
},
);
self.add_definition(symbol, ImportFromDefinitionNodeRef { node, alias_index });
}
}
ast::Stmt::Assign(node) => {

View File

@@ -5,20 +5,20 @@ use crate::db::Db;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub(crate) struct Constraint<'db> {
pub(crate) node: ConstraintNode<'db>,
pub(crate) is_positive: bool,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub(crate) enum ConstraintNode<'db> {
Expression(Expression<'db>),
Pattern(PatternConstraint<'db>),
}
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
#[derive(Debug, Clone, Hash, PartialEq)]
pub(crate) enum PatternConstraintKind<'db> {
Singleton(Singleton, Option<Expression<'db>>),
Value(Expression<'db>, Option<Expression<'db>>),
@@ -28,15 +28,21 @@ pub(crate) enum PatternConstraintKind<'db> {
#[salsa::tracked]
pub(crate) struct PatternConstraint<'db> {
#[id]
pub(crate) file: File,
#[id]
pub(crate) file_scope: FileScopeId,
#[no_eq]
#[return_ref]
pub(crate) subject: Expression<'db>,
#[no_eq]
#[return_ref]
pub(crate) kind: PatternConstraintKind<'db>,
#[no_eq]
count: countme::Count<PatternConstraint<'static>>,
}

View File

@@ -25,19 +25,22 @@ use crate::Db;
#[salsa::tracked]
pub struct Definition<'db> {
/// The file in which the definition occurs.
#[id]
pub(crate) file: File,
/// The scope in which the definition occurs.
#[id]
pub(crate) file_scope: FileScopeId,
/// The symbol defined.
#[id]
pub(crate) symbol: ScopedSymbolId,
#[no_eq]
#[return_ref]
#[tracked]
pub(crate) kind: DefinitionKind<'db>,
#[no_eq]
count: countme::Count<Definition<'static>>,
}
@@ -57,15 +60,11 @@ impl<'db> Definition<'db> {
pub(crate) fn is_binding(self, db: &'db dyn Db) -> bool {
self.kind(db).category().is_binding()
}
pub(crate) fn is_reexported(self, db: &'db dyn Db) -> bool {
self.kind(db).is_reexported()
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) enum DefinitionNodeRef<'a> {
Import(ImportDefinitionNodeRef<'a>),
Import(&'a ast::Alias),
ImportFrom(ImportFromDefinitionNodeRef<'a>),
For(ForStmtDefinitionNodeRef<'a>),
Function(&'a ast::StmtFunctionDef),
@@ -123,6 +122,12 @@ impl<'a> From<&'a ast::StmtAugAssign> for DefinitionNodeRef<'a> {
}
}
impl<'a> From<&'a ast::Alias> for DefinitionNodeRef<'a> {
fn from(node_ref: &'a ast::Alias) -> Self {
Self::Import(node_ref)
}
}
impl<'a> From<&'a ast::TypeParamTypeVar> for DefinitionNodeRef<'a> {
fn from(value: &'a ast::TypeParamTypeVar) -> Self {
Self::TypeVar(value)
@@ -141,12 +146,6 @@ impl<'a> From<&'a ast::TypeParamTypeVarTuple> for DefinitionNodeRef<'a> {
}
}
impl<'a> From<ImportDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
fn from(node_ref: ImportDefinitionNodeRef<'a>) -> Self {
Self::Import(node_ref)
}
}
impl<'a> From<ImportFromDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
fn from(node_ref: ImportFromDefinitionNodeRef<'a>) -> Self {
Self::ImportFrom(node_ref)
@@ -189,17 +188,10 @@ impl<'a> From<MatchPatternDefinitionNodeRef<'a>> for DefinitionNodeRef<'a> {
}
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ImportDefinitionNodeRef<'a> {
pub(crate) alias: &'a ast::Alias,
pub(crate) is_reexported: bool,
}
#[derive(Copy, Clone, Debug)]
pub(crate) struct ImportFromDefinitionNodeRef<'a> {
pub(crate) node: &'a ast::StmtImportFrom,
pub(crate) alias_index: usize,
pub(crate) is_reexported: bool,
}
#[derive(Copy, Clone, Debug)]
@@ -255,22 +247,15 @@ impl<'db> DefinitionNodeRef<'db> {
#[allow(unsafe_code)]
pub(super) unsafe fn into_owned(self, parsed: ParsedModule) -> DefinitionKind<'db> {
match self {
DefinitionNodeRef::Import(ImportDefinitionNodeRef {
alias,
is_reexported,
}) => DefinitionKind::Import(ImportDefinitionKind {
alias: AstNodeRef::new(parsed, alias),
is_reexported,
}),
DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef {
node,
alias_index,
is_reexported,
}) => DefinitionKind::ImportFrom(ImportFromDefinitionKind {
node: AstNodeRef::new(parsed, node),
alias_index,
is_reexported,
}),
DefinitionNodeRef::Import(alias) => {
DefinitionKind::Import(AstNodeRef::new(parsed, alias))
}
DefinitionNodeRef::ImportFrom(ImportFromDefinitionNodeRef { node, alias_index }) => {
DefinitionKind::ImportFrom(ImportFromDefinitionKind {
node: AstNodeRef::new(parsed, node),
alias_index,
})
}
DefinitionNodeRef::Function(function) => {
DefinitionKind::Function(AstNodeRef::new(parsed, function))
}
@@ -372,15 +357,10 @@ impl<'db> DefinitionNodeRef<'db> {
pub(super) fn key(self) -> DefinitionNodeKey {
match self {
Self::Import(ImportDefinitionNodeRef {
alias,
is_reexported: _,
}) => alias.into(),
Self::ImportFrom(ImportFromDefinitionNodeRef {
node,
alias_index,
is_reexported: _,
}) => (&node.names[alias_index]).into(),
Self::Import(node) => node.into(),
Self::ImportFrom(ImportFromDefinitionNodeRef { node, alias_index }) => {
(&node.names[alias_index]).into()
}
Self::Function(node) => node.into(),
Self::Class(node) => node.into(),
Self::TypeAlias(node) => node.into(),
@@ -455,16 +435,9 @@ impl DefinitionCategory {
}
}
/// The kind of a definition.
///
/// ## Usage in salsa tracked structs
///
/// [`DefinitionKind`] fields in salsa tracked structs should be tracked (attributed with `#[tracked]`)
/// because the kind is a thin wrapper around [`AstNodeRef`]. See the [`AstNodeRef`] documentation
/// for an in-depth explanation of why this is necessary.
#[derive(Clone, Debug)]
pub enum DefinitionKind<'db> {
Import(ImportDefinitionKind),
Import(AstNodeRef<ast::Alias>),
ImportFrom(ImportFromDefinitionKind),
Function(AstNodeRef<ast::StmtFunctionDef>),
Class(AstNodeRef<ast::StmtClassDef>),
@@ -487,14 +460,6 @@ pub enum DefinitionKind<'db> {
}
impl DefinitionKind<'_> {
pub(crate) fn is_reexported(&self) -> bool {
match self {
DefinitionKind::Import(import) => import.is_reexported(),
DefinitionKind::ImportFrom(import) => import.is_reexported(),
_ => true,
}
}
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the symbol being defined i.e.,
@@ -503,7 +468,7 @@ impl DefinitionKind<'_> {
/// This is mainly used for logging and debugging purposes.
pub(crate) fn target_range(&self) -> TextRange {
match self {
DefinitionKind::Import(import) => import.alias().range(),
DefinitionKind::Import(alias) => alias.range(),
DefinitionKind::ImportFrom(import) => import.alias().range(),
DefinitionKind::Function(function) => function.name.range(),
DefinitionKind::Class(class) => class.name.range(),
@@ -575,7 +540,7 @@ impl DefinitionKind<'_> {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) enum TargetKind<'db> {
Sequence(Unpack<'db>),
Name,
@@ -634,27 +599,10 @@ impl ComprehensionDefinitionKind {
}
}
#[derive(Clone, Debug)]
pub struct ImportDefinitionKind {
alias: AstNodeRef<ast::Alias>,
is_reexported: bool,
}
impl ImportDefinitionKind {
pub(crate) fn alias(&self) -> &ast::Alias {
self.alias.node()
}
pub(crate) fn is_reexported(&self) -> bool {
self.is_reexported
}
}
#[derive(Clone, Debug)]
pub struct ImportFromDefinitionKind {
node: AstNodeRef<ast::StmtImportFrom>,
alias_index: usize,
is_reexported: bool,
}
impl ImportFromDefinitionKind {
@@ -665,10 +613,6 @@ impl ImportFromDefinitionKind {
pub(crate) fn alias(&self) -> &ast::Alias {
&self.node.node().names[self.alias_index]
}
pub(crate) fn is_reexported(&self) -> bool {
self.is_reexported
}
}
#[derive(Clone, Debug)]
@@ -769,7 +713,7 @@ impl ExceptHandlerDefinitionKind {
}
}
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)]
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) struct DefinitionNodeKey(NodeKey);
impl From<&ast::Alias> for DefinitionNodeKey {

View File

@@ -33,20 +33,23 @@ pub(crate) enum ExpressionKind {
#[salsa::tracked]
pub(crate) struct Expression<'db> {
/// The file in which the expression occurs.
#[id]
pub(crate) file: File,
/// The scope in which the expression occurs.
#[id]
pub(crate) file_scope: FileScopeId,
/// The expression node.
#[no_eq]
#[tracked]
#[return_ref]
pub(crate) node_ref: AstNodeRef<ast::Expr>,
/// Should this expression be inferred as a normal expression or a type expression?
#[id]
pub(crate) kind: ExpressionKind,
#[no_eq]
count: countme::Count<Expression<'static>>,
}

View File

@@ -96,16 +96,18 @@ impl From<FileSymbolId> for ScopedSymbolId {
/// Symbol ID that uniquely identifies a symbol inside a [`Scope`].
#[newtype_index]
#[derive(salsa::Update)]
pub struct ScopedSymbolId;
/// A cross-module identifier of a scope that can be used as a salsa query parameter.
#[salsa::tracked]
pub struct ScopeId<'db> {
#[id]
pub file: File,
#[id]
pub file_scope_id: FileScopeId,
#[no_eq]
count: countme::Count<ScopeId<'static>>,
}
@@ -157,7 +159,6 @@ impl<'db> ScopeId<'db> {
/// ID that uniquely identifies a scope inside of a module.
#[newtype_index]
#[derive(salsa::Update)]
pub struct FileScopeId;
impl FileScopeId {
@@ -176,7 +177,7 @@ impl FileScopeId {
}
}
#[derive(Debug, salsa::Update)]
#[derive(Debug)]
pub struct Scope {
pub(super) parent: Option<FileScopeId>,
pub(super) node: NodeWithScopeKind,
@@ -215,7 +216,7 @@ impl ScopeKind {
}
/// Symbol table for a specific [`Scope`].
#[derive(Debug, Default, salsa::Update)]
#[derive(Debug, Default)]
pub struct SymbolTable {
/// The symbols in this scope.
symbols: IndexVec<ScopedSymbolId, Symbol>,
@@ -423,7 +424,7 @@ impl NodeWithScopeRef<'_> {
}
/// Node that introduces a new scope.
#[derive(Clone, Debug, salsa::Update)]
#[derive(Clone, Debug)]
pub enum NodeWithScopeKind {
Module,
Class(AstNodeRef<ast::StmtClassDef>),

View File

@@ -278,7 +278,7 @@ mod symbol_state;
type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
/// Applicable definitions and constraints for every use of a name.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct UseDefMap<'db> {
/// Array of [`Definition`] in this scope. Only the first entry should be `None`;
/// this represents the implicit "unbound"/"undeclared" definition of every symbol.
@@ -384,7 +384,7 @@ impl<'db> UseDefMap<'db> {
}
/// Either live bindings or live declarations for a symbol.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
#[derive(Debug, PartialEq, Eq)]
enum SymbolDefinitions {
Bindings(SymbolBindings),
Declarations(SymbolDeclarations),

View File

@@ -14,7 +14,8 @@ use std::num::NonZeroUsize;
use std::ops::Deref;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ruff_python_ast::python_version::PythonVersion;
use crate::PythonVersion;
type SitePackagesDiscoveryResult<T> = Result<T, SitePackagesDiscoveryError>;

View File

@@ -2,7 +2,7 @@ use crate::module_resolver::{resolve_module, KnownModule};
use crate::semantic_index::global_scope;
use crate::semantic_index::symbol::ScopeId;
use crate::symbol::Symbol;
use crate::types::imported_symbol;
use crate::types::global_symbol;
use crate::Db;
/// Lookup the type of `symbol` in a given known module
@@ -14,10 +14,18 @@ pub(crate) fn known_module_symbol<'db>(
symbol: &str,
) -> Symbol<'db> {
resolve_module(db, &known_module.name())
.map(|module| imported_symbol(db, &module, symbol))
.map(|module| global_symbol(db, module.file(), symbol))
.unwrap_or(Symbol::Unbound)
}
/// Lookup the type of `symbol` in the builtins namespace.
///
/// Returns `Symbol::Unbound` if the `builtins` module isn't available for some reason.
#[inline]
pub(crate) fn builtins_symbol<'db>(db: &'db dyn Db, symbol: &str) -> Symbol<'db> {
known_module_symbol(db, KnownModule::Builtins, symbol)
}
/// Lookup the type of `symbol` in the `typing` module namespace.
///
/// Returns `Symbol::Unbound` if the `typing` module isn't available for some reason.

View File

@@ -1,5 +1,5 @@
use crate::{
types::{todo_type, Type, UnionType},
types::{Type, UnionType},
Db,
};
@@ -26,25 +26,13 @@ pub(crate) enum Boundness {
/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
/// non_existent: Symbol::Unbound,
/// ```
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Symbol<'db> {
Type(Type<'db>, Boundness),
Unbound,
}
impl<'db> Symbol<'db> {
/// Constructor that creates a `Symbol` with boundness [`Boundness::Bound`].
pub(crate) fn bound(ty: impl Into<Type<'db>>) -> Self {
Symbol::Type(ty.into(), Boundness::Bound)
}
/// Constructor that creates a [`Symbol`] with a [`crate::types::TodoType`] type
/// and boundness [`Boundness::Bound`].
#[allow(unused_variables)] // Only unused in release builds
pub(crate) fn todo(message: &'static str) -> Self {
Symbol::Type(todo_type!(message), Boundness::Bound)
}
pub(crate) fn is_unbound(&self) -> bool {
matches!(self, Symbol::Unbound)
}
@@ -67,30 +55,6 @@ impl<'db> Symbol<'db> {
.expect("Expected a (possibly unbound) type, not an unbound symbol")
}
/// Transform the symbol into a [`LookupResult`],
/// a [`Result`] type in which the `Ok` variant represents a definitely bound symbol
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
pub(crate) fn into_lookup_result(self) -> LookupResult<'db> {
match self {
Symbol::Type(ty, Boundness::Bound) => Ok(ty),
Symbol::Type(ty, Boundness::PossiblyUnbound) => Err(LookupError::PossiblyUnbound(ty)),
Symbol::Unbound => Err(LookupError::Unbound),
}
}
/// Safely unwrap the symbol into a [`Type`].
///
/// If the symbol is definitely unbound or possibly unbound, it will be transformed into a
/// [`LookupError`] and `diagnostic_fn` will be applied to the error value before returning
/// the result of `diagnostic_fn` (which will be a [`Type`]). This allows the caller to ensure
/// that a diagnostic is emitted if the symbol is possibly or definitely unbound.
pub(crate) fn unwrap_with_diagnostic(
self,
diagnostic_fn: impl FnOnce(LookupError<'db>) -> Type<'db>,
) -> Type<'db> {
self.into_lookup_result().unwrap_or_else(diagnostic_fn)
}
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
///
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
@@ -107,9 +71,17 @@ impl<'db> Symbol<'db> {
db: &'db dyn Db,
fallback_fn: impl FnOnce() -> Self,
) -> Self {
self.into_lookup_result()
.or_else(|lookup_error| lookup_error.or_fall_back_to(db, fallback_fn()))
.into()
match self {
Symbol::Type(_, Boundness::Bound) => self,
Symbol::Unbound => fallback_fn(),
Symbol::Type(self_ty, Boundness::PossiblyUnbound) => match fallback_fn() {
Symbol::Unbound => self,
Symbol::Type(fallback_ty, fallback_boundness) => Symbol::Type(
UnionType::from_elements(db, [self_ty, fallback_ty]),
fallback_boundness,
),
},
}
}
#[must_use]
@@ -121,51 +93,6 @@ impl<'db> Symbol<'db> {
}
}
impl<'db> From<LookupResult<'db>> for Symbol<'db> {
fn from(value: LookupResult<'db>) -> Self {
match value {
Ok(ty) => Symbol::Type(ty, Boundness::Bound),
Err(LookupError::Unbound) => Symbol::Unbound,
Err(LookupError::PossiblyUnbound(ty)) => Symbol::Type(ty, Boundness::PossiblyUnbound),
}
}
}
/// Possible ways in which a symbol lookup can (possibly or definitely) fail.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum LookupError<'db> {
Unbound,
PossiblyUnbound(Type<'db>),
}
impl<'db> LookupError<'db> {
/// Fallback (wholly or partially) to `fallback` to create a new [`LookupResult`].
pub(crate) fn or_fall_back_to(
self,
db: &'db dyn Db,
fallback: Symbol<'db>,
) -> LookupResult<'db> {
let fallback = fallback.into_lookup_result();
match (&self, &fallback) {
(LookupError::Unbound, _) => fallback,
(LookupError::PossiblyUnbound { .. }, Err(LookupError::Unbound)) => Err(self),
(LookupError::PossiblyUnbound(ty), Ok(ty2)) => {
Ok(UnionType::from_elements(db, [ty, ty2]))
}
(LookupError::PossiblyUnbound(ty), Err(LookupError::PossiblyUnbound(ty2))) => Err(
LookupError::PossiblyUnbound(UnionType::from_elements(db, [ty, ty2])),
),
}
}
}
/// A [`Result`] type in which the `Ok` variant represents a definitely bound symbol
/// and the `Err` variant represents a symbol that is either definitely or possibly unbound.
///
/// Note that this type is exactly isomorphic to [`Symbol`].
/// In the future, we could possibly consider removing `Symbol` and using this type everywhere instead.
pub(crate) type LookupResult<'db> = Result<Type<'db>, LookupError<'db>>;
#[cfg(test)]
mod tests {
use super::*;

File diff suppressed because it is too large Load Diff

View File

@@ -43,13 +43,6 @@ impl<'db> UnionBuilder<'db> {
}
}
/// Collapse the union to a single type: `object`.
fn collapse_to_object(mut self) -> Self {
self.elements.clear();
self.elements.push(Type::object(self.db));
self
}
/// Adds a type to this union.
pub(crate) fn add(mut self, ty: Type<'db>) -> Self {
match ty {
@@ -60,12 +53,7 @@ impl<'db> UnionBuilder<'db> {
self = self.add(*element);
}
}
// Adding `Never` to a union is a no-op.
Type::Never => {}
// Adding `object` to a union results in `object`.
ty if ty.is_object(self.db) => {
return self.collapse_to_object();
}
_ => {
let bool_pair = if let Type::BooleanLiteral(b) = ty {
Some(Type::BooleanLiteral(!b))
@@ -88,10 +76,7 @@ impl<'db> UnionBuilder<'db> {
break;
}
if ty.is_same_gradual_form(*element)
|| ty.is_subtype_of(self.db, *element)
|| element.is_object(self.db)
{
if ty.is_same_gradual_form(*element) || ty.is_subtype_of(self.db, *element) {
return self;
} else if element.is_subtype_of(self.db, ty) {
to_remove.push(index);
@@ -103,7 +88,9 @@ impl<'db> UnionBuilder<'db> {
// `element | ty` must be `object` (object has no other supertypes). This means we can simplify
// the whole union to just `object`, since all other potential elements would also be subtypes of
// `object`.
return self.collapse_to_object();
self.elements.clear();
self.elements.push(KnownClass::Object.to_instance(self.db));
return self;
}
}
match to_remove[..] {
@@ -429,7 +416,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
Type::Never => {
// Adding ~Never to an intersection is a no-op.
}
Type::Instance(instance) if instance.class.is_object(db) => {
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
// Adding ~object to an intersection results in Never.
*self = Self::default();
self.positive.insert(Type::Never);
@@ -494,7 +481,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
match (self.positive.len(), self.negative.len()) {
(0, 0) => Type::object(db),
(0, 0) => KnownClass::Object.to_instance(db),
(1, 0) => self.positive[0],
_ => {
self.positive.shrink_to_fit();
@@ -547,7 +534,7 @@ mod tests {
let db = setup_db();
let intersection = IntersectionBuilder::new(&db).build();
assert_eq!(intersection, Type::object(&db));
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
}
#[test_case(Type::BooleanLiteral(true))]
@@ -561,7 +548,7 @@ mod tests {
// We add t_object in various orders (in first or second position) in
// the tests below to ensure that the boolean simplification eliminates
// everything from the intersection, not just `bool`.
let t_object = Type::object(&db);
let t_object = KnownClass::Object.to_instance(&db);
let t_bool = KnownClass::Bool.to_instance(&db);
let ty = IntersectionBuilder::new(&db)

View File

@@ -1,7 +1,9 @@
use super::context::InferContext;
use super::diagnostic::CALL_NON_CALLABLE;
use super::{Signature, Type, TypeArrayDisplay, UnionBuilder};
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
use crate::Db;
use ruff_db::diagnostic::DiagnosticId;
use ruff_python_ast as ast;
mod arguments;
@@ -11,10 +13,22 @@ pub(super) use arguments::{Argument, CallArguments};
pub(super) use bind::{bind_call, CallBinding};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum CallOutcome<'db> {
pub(super) enum StaticAssertionErrorKind<'db> {
ArgumentIsFalse,
ArgumentIsFalsy(Type<'db>),
ArgumentTruthinessIsAmbiguous(Type<'db>),
CustomError(&'db str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum CallOutcome<'db> {
Callable {
binding: CallBinding<'db>,
},
RevealType {
binding: CallBinding<'db>,
revealed_ty: Type<'db>,
},
NotCallable {
not_callable_ty: Type<'db>,
},
@@ -26,6 +40,14 @@ pub(crate) enum CallOutcome<'db> {
called_ty: Type<'db>,
call_outcome: Box<CallOutcome<'db>>,
},
StaticAssertionError {
binding: CallBinding<'db>,
error_kind: StaticAssertionErrorKind<'db>,
},
AssertType {
binding: CallBinding<'db>,
asserted_ty: Type<'db>,
},
}
impl<'db> CallOutcome<'db> {
@@ -39,6 +61,14 @@ impl<'db> CallOutcome<'db> {
CallOutcome::NotCallable { not_callable_ty }
}
/// Create a new `CallOutcome::RevealType` with given revealed and return types.
pub(super) fn revealed(binding: CallBinding<'db>, revealed_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::RevealType {
binding,
revealed_ty,
}
}
/// Create a new `CallOutcome::Union` with given wrapped outcomes.
pub(super) fn union(
called_ty: Type<'db>,
@@ -50,10 +80,22 @@ impl<'db> CallOutcome<'db> {
}
}
/// Create a new `CallOutcome::AssertType` with given asserted and return types.
pub(super) fn asserted(binding: CallBinding<'db>, asserted_ty: Type<'db>) -> CallOutcome<'db> {
CallOutcome::AssertType {
binding,
asserted_ty,
}
}
/// Get the return type of the call, or `None` if not callable.
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
match self {
Self::Callable { binding } => Some(binding.return_type()),
Self::RevealType {
binding,
revealed_ty: _,
} => Some(binding.return_type()),
Self::NotCallable { not_callable_ty: _ } => None,
Self::Union {
outcomes,
@@ -72,6 +114,11 @@ impl<'db> CallOutcome<'db> {
})
.map(UnionBuilder::build),
Self::PossiblyUnboundDunderCall { call_outcome, .. } => call_outcome.return_type(db),
Self::StaticAssertionError { .. } => Some(Type::none(db)),
Self::AssertType {
binding,
asserted_ty: _,
} => Some(binding.return_type()),
}
}
@@ -157,12 +204,22 @@ impl<'db> CallOutcome<'db> {
// only non-callable diagnostics in the union case, which is inconsistent.
match self {
Self::Callable { binding } => {
// TODO: Move this out of the `CallOutcome` and into `TypeInferenceBuilder`?
// This check is required everywhere where we call `return_type_result`
// from the TypeInferenceBuilder.
binding.report_diagnostics(context, node);
Ok(binding.return_type())
}
Self::RevealType {
binding,
revealed_ty,
} => {
binding.report_diagnostics(context, node);
context.report_diagnostic(
node,
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(context.db())),
);
Ok(binding.return_type())
}
Self::NotCallable { not_callable_ty } => Err(NotCallableError::Type {
not_callable_ty: *not_callable_ty,
return_ty: Type::unknown(),
@@ -182,12 +239,24 @@ impl<'db> CallOutcome<'db> {
} => {
let mut not_callable = vec![];
let mut union_builder = UnionBuilder::new(context.db());
let mut revealed = false;
for outcome in outcomes {
let return_ty = match outcome {
Self::NotCallable { not_callable_ty } => {
not_callable.push(*not_callable_ty);
Type::unknown()
}
Self::RevealType {
binding,
revealed_ty: _,
} => {
if revealed {
binding.return_type()
} else {
revealed = true;
outcome.unwrap_with_diagnostic(context, node)
}
}
_ => outcome.unwrap_with_diagnostic(context, node),
};
union_builder = union_builder.add(return_ty);
@@ -211,6 +280,73 @@ impl<'db> CallOutcome<'db> {
}),
}
}
Self::StaticAssertionError {
binding,
error_kind,
} => {
binding.report_diagnostics(context, node);
match error_kind {
StaticAssertionErrorKind::ArgumentIsFalse => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: argument evaluates to `False`"),
);
}
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(parameter_ty) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(context.db())
),
);
}
StaticAssertionErrorKind::CustomError(message) => {
context.report_lint(
&STATIC_ASSERT_ERROR,
node,
format_args!("Static assertion error: {message}"),
);
}
}
Ok(Type::unknown())
}
Self::AssertType {
binding,
asserted_ty,
} => {
let [actual_ty, _asserted] = binding.parameter_types() else {
return Ok(binding.return_type());
};
if !actual_ty.is_gradual_equivalent_to(context.db(), *asserted_ty) {
context.report_lint(
&TYPE_ASSERTION_FAILURE,
node,
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(context.db()),
asserted_ty.display(context.db()),
),
);
}
Ok(binding.return_type())
}
}
}
}

View File

@@ -5,7 +5,7 @@ use crate::types::diagnostic::{
TOO_MANY_POSITIONAL_ARGUMENTS, UNKNOWN_ARGUMENT,
};
use crate::types::signatures::Parameter;
use crate::types::{todo_type, UnionType};
use crate::types::UnionType;
use ruff_python_ast as ast;
/// Bind a [`CallArguments`] against a callable [`Signature`].
@@ -16,7 +16,7 @@ pub(crate) fn bind_call<'db>(
db: &'db dyn Db,
arguments: &CallArguments<'_, 'db>,
signature: &Signature<'db>,
callable_ty: Type<'db>,
callable_ty: Option<Type<'db>>,
) -> CallBinding<'db> {
let parameters = signature.parameters();
// The type assigned to each parameter at this call site.
@@ -138,7 +138,7 @@ pub(crate) fn bind_call<'db>(
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CallBinding<'db> {
/// Type of the callable object (function, class...)
callable_ty: Type<'db>,
callable_ty: Option<Type<'db>>,
/// Return type of the call.
return_ty: Type<'db>,
@@ -154,7 +154,7 @@ impl<'db> CallBinding<'db> {
// TODO remove this constructor and construct always from `bind_call`
pub(crate) fn from_return_type(return_ty: Type<'db>) -> Self {
Self {
callable_ty: todo_type!("CallBinding::from_return_type"),
callable_ty: None,
return_ty,
parameter_tys: Box::default(),
errors: vec![],
@@ -189,8 +189,8 @@ impl<'db> CallBinding<'db> {
fn callable_name(&self, db: &'db dyn Db) -> Option<&str> {
match self.callable_ty {
Type::FunctionLiteral(function) => Some(function.name(db)),
Type::ClassLiteral(class_type) => Some(class_type.class.name(db)),
Some(Type::FunctionLiteral(function)) => Some(function.name(db)),
Some(Type::ClassLiteral(class_type)) => Some(class_type.class.name(db)),
_ => None,
}
}

View File

@@ -8,7 +8,7 @@ use crate::types::string_annotation::{
};
use crate::types::{ClassLiteralType, KnownInstanceType, Type};
use crate::{declare_lint, Db};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
@@ -802,8 +802,12 @@ impl Diagnostic for TypeCheckDiagnostic {
TypeCheckDiagnostic::message(self).into()
}
fn span(&self) -> Option<Span> {
Some(Span::from(self.file).with_range(self.range))
fn file(&self) -> Option<File> {
Some(TypeCheckDiagnostic::file(self))
}
fn range(&self) -> Option<TextRange> {
Some(Ranged::range(self))
}
fn severity(&self) -> Severity {

View File

@@ -29,7 +29,6 @@
use std::num::NonZeroU32;
use itertools::{Either, Itertools};
use ruff_db::diagnostic::{DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
@@ -50,8 +49,7 @@ use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::{NodeWithScopeKind, NodeWithScopeRef, ScopeId};
use crate::semantic_index::SemanticIndex;
use crate::stdlib::builtins_module_scope;
use crate::symbol::LookupError;
use crate::types::call::{Argument, CallArguments, CallOutcome};
use crate::types::call::{Argument, CallArguments};
use crate::types::diagnostic::{
report_invalid_arguments_to_annotated, report_invalid_assignment,
report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics,
@@ -60,19 +58,19 @@ use crate::types::diagnostic::{
INCONSISTENT_MRO, INVALID_ATTRIBUTE_ACCESS, INVALID_BASE, INVALID_CONTEXT_MANAGER,
INVALID_DECLARATION, INVALID_PARAMETER_DEFAULT, INVALID_TYPE_FORM,
INVALID_TYPE_VARIABLE_CONSTRAINTS, POSSIBLY_UNBOUND_ATTRIBUTE, POSSIBLY_UNBOUND_IMPORT,
STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE,
UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
};
use crate::types::mro::MroErrorKind;
use crate::types::statistics::TypeStatistics;
use crate::types::unpacker::{UnpackResult, Unpacker};
use crate::types::{
builtins_symbol, symbol, symbol_from_bindings, symbol_from_declarations, todo_type,
typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType, DynamicType,
FunctionType, InstanceType, IntersectionBuilder, IntersectionType, IterationOutcome,
KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate, MetaclassErrorKind,
RequiresExplicitReExport, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers,
Truthiness, TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay,
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
builtins_symbol, global_symbol, symbol, symbol_from_bindings, symbol_from_declarations,
todo_type, typing_extensions_symbol, Boundness, CallDunderResult, Class, ClassLiteralType,
DynamicType, FunctionType, InstanceType, IntersectionBuilder, IntersectionType,
IterationOutcome, KnownClass, KnownFunction, KnownInstanceType, MetaclassCandidate,
MetaclassErrorKind, SliceLiteralType, SubclassOfType, Symbol, SymbolAndQualifiers, Truthiness,
TupleType, Type, TypeAliasType, TypeAndQualifiers, TypeArrayDisplay, TypeQualifiers,
TypeVarBoundOrConstraints, TypeVarInstance, UnionBuilder, UnionType,
};
use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
@@ -89,7 +87,7 @@ use super::slots::check_class_slots;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
use super::{global_symbol, ParameterExpectation, ParameterExpectations};
use super::{ParameterExpectation, ParameterExpectations};
/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
@@ -119,9 +117,7 @@ fn infer_definition_types_cycle_recovery<'db>(
let mut inference = TypeInference::empty(input.scope(db));
let category = input.category(db);
if category.is_declaration() {
inference
.declarations
.insert(input, TypeAndQualifiers::unknown());
inference.declarations.insert(input, Type::unknown().into());
}
if category.is_binding() {
inference.bindings.insert(input, Type::unknown());
@@ -241,7 +237,7 @@ impl<'db> InferenceRegion<'db> {
}
/// The inferred types for a single region.
#[derive(Debug, Eq, PartialEq, salsa::Update)]
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct TypeInference<'db> {
/// The types of every expression in this region.
expressions: FxHashMap<ScopedExpressionId, Type<'db>>,
@@ -304,6 +300,14 @@ impl<'db> TypeInference<'db> {
self.diagnostics.shrink_to_fit();
self.deferred.shrink_to_fit();
}
pub(super) fn statistics(&self) -> TypeStatistics {
let mut statistics = TypeStatistics::default();
for ty in self.expressions.values() {
statistics.increment(*ty);
}
statistics
}
}
impl WithDiagnostics for TypeInference<'_> {
@@ -738,7 +742,7 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_type_alias_definition(type_alias.node(), definition);
}
DefinitionKind::Import(import) => {
self.infer_import_definition(import.alias(), definition);
self.infer_import_definition(import.node(), definition);
}
DefinitionKind::ImportFrom(import_from) => {
self.infer_import_from_definition(
@@ -874,25 +878,22 @@ impl<'db> TypeInferenceBuilder<'db> {
let use_def = self.index.use_def_map(binding.file_scope(self.db()));
let declarations = use_def.declarations_at_binding(binding);
let mut bound_ty = ty;
let declared_ty =
symbol_from_declarations(self.db(), declarations, RequiresExplicitReExport::No)
.map(|SymbolAndQualifiers(s, _)| {
s.ignore_possibly_unbound().unwrap_or(Type::unknown())
})
.unwrap_or_else(|(ty, conflicting)| {
// TODO point out the conflicting declarations in the diagnostic?
let symbol_table = self.index.symbol_table(binding.file_scope(self.db()));
let symbol_name = symbol_table.symbol(binding.symbol(self.db())).name();
self.context.report_lint(
&CONFLICTING_DECLARATIONS,
node,
format_args!(
"Conflicting declared types for `{symbol_name}`: {}",
conflicting.display(self.db())
),
);
ty.inner_type()
});
let declared_ty = symbol_from_declarations(self.db(), declarations)
.map(|SymbolAndQualifiers(s, _)| s.ignore_possibly_unbound().unwrap_or(Type::unknown()))
.unwrap_or_else(|(ty, conflicting)| {
// TODO point out the conflicting declarations in the diagnostic?
let symbol_table = self.index.symbol_table(binding.file_scope(self.db()));
let symbol_name = symbol_table.symbol(binding.symbol(self.db())).name();
self.context.report_lint(
&CONFLICTING_DECLARATIONS,
node,
format_args!(
"Conflicting declared types for `{symbol_name}`: {}",
conflicting.display(self.db())
),
);
ty.inner_type()
});
if !bound_ty.is_assignable_to(self.db(), declared_ty) {
report_invalid_assignment(&self.context, node, declared_ty, bound_ty);
// allow declarations to override inference in case of invalid assignment
@@ -912,10 +913,9 @@ impl<'db> TypeInferenceBuilder<'db> {
let use_def = self.index.use_def_map(declaration.file_scope(self.db()));
let prior_bindings = use_def.bindings_at_declaration(declaration);
// unbound_ty is Never because for this check we don't care about unbound
let inferred_ty =
symbol_from_bindings(self.db(), prior_bindings, RequiresExplicitReExport::No)
.ignore_possibly_unbound()
.unwrap_or(Type::Never);
let inferred_ty = symbol_from_bindings(self.db(), prior_bindings)
.ignore_possibly_unbound()
.unwrap_or(Type::Never);
let ty = if inferred_ty.is_assignable_to(self.db(), ty.inner_type()) {
ty
} else {
@@ -928,7 +928,7 @@ impl<'db> TypeInferenceBuilder<'db> {
inferred_ty.display(self.db())
),
);
TypeAndQualifiers::unknown()
Type::unknown().into()
};
self.types.declarations.insert(declaration, ty);
}
@@ -2816,15 +2816,17 @@ impl<'db> TypeInferenceBuilder<'db> {
fn infer_number_literal_expression(&mut self, literal: &ast::ExprNumberLiteral) -> Type<'db> {
let ast::ExprNumberLiteral { range: _, value } = literal;
let db = self.db();
match value {
ast::Number::Int(n) => n
.as_i64()
.map(Type::IntLiteral)
.unwrap_or_else(|| KnownClass::Int.to_instance(db)),
ast::Number::Float(_) => KnownClass::Float.to_instance(db),
ast::Number::Complex { .. } => KnownClass::Complex.to_instance(db),
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db())),
ast::Number::Float(_) => KnownClass::Float.to_instance(self.db()),
ast::Number::Complex { .. } => builtins_symbol(self.db(), "complex")
.ignore_possibly_unbound()
.unwrap_or(Type::unknown())
.to_instance(self.db()),
}
}
@@ -2908,7 +2910,9 @@ impl<'db> TypeInferenceBuilder<'db> {
&mut self,
_literal: &ast::ExprEllipsisLiteral,
) -> Type<'db> {
KnownClass::EllipsisType.to_instance(self.db())
builtins_symbol(self.db(), "Ellipsis")
.ignore_possibly_unbound()
.unwrap_or(Type::unknown())
}
fn infer_tuple_expression(&mut self, tuple: &ast::ExprTuple) -> Type<'db> {
@@ -3248,107 +3252,9 @@ impl<'db> TypeInferenceBuilder<'db> {
.unwrap_or_default();
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
let call_outcome = self.check_call(function_type, &call_arguments, call_expression);
call_outcome.unwrap_with_diagnostic(&self.context, call_expression.into())
}
fn check_call(
&self,
callee: Type<'db>,
arguments: &CallArguments<'_, 'db>,
call_expression: &ast::ExprCall,
) -> CallOutcome<'db> {
let call = callee.call(self.db(), arguments);
let Type::FunctionLiteral(function_type) = callee else {
return call;
};
let CallOutcome::Callable { binding } = &call else {
return call;
};
let Some(known) = function_type.known(self.db()) else {
return call;
};
match known {
KnownFunction::RevealType => {
let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown());
self.context.report_diagnostic(
call_expression.into(),
DiagnosticId::RevealedType,
Severity::Info,
format_args!("Revealed type is `{}`", revealed_ty.display(self.db())),
);
}
KnownFunction::AssertType => {
let [actual_ty, asserted_ty] = binding.parameter_types() else {
return call;
};
if !actual_ty.is_gradual_equivalent_to(self.db(), *asserted_ty) {
self.context.report_lint(
&TYPE_ASSERTION_FAILURE,
call_expression.into(),
format_args!(
"Actual type `{}` is not the same as asserted type `{}`",
actual_ty.display(self.db()),
asserted_ty.display(self.db()),
),
);
}
}
KnownFunction::StaticAssert => {
let Some((parameter_ty, message)) = binding.two_parameter_types() else {
return call;
};
let truthiness = parameter_ty.bool(self.db());
if !truthiness.is_always_true() {
if let Some(message) =
message.into_string_literal().map(|s| &**s.value(self.db()))
{
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!("Static assertion error: {message}"),
);
} else if parameter_ty == Type::BooleanLiteral(false) {
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!("Static assertion error: argument evaluates to `False`"),
);
} else if truthiness.is_always_false() {
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!(
"Static assertion error: argument of type `{parameter_ty}` is statically known to be falsy",
parameter_ty=parameter_ty.display(self.db())
),
);
} else {
self.context.report_lint(
&STATIC_ASSERT_ERROR,
call_expression.into(),
format_args!(
"Static assertion error: argument of type `{parameter_ty}` has an ambiguous static truthiness",
parameter_ty=parameter_ty.display(self.db())
),
);
};
}
}
_ => {}
}
call
function_type
.call(self.db(), &call_arguments)
.unwrap_with_diagnostic(&self.context, call_expression.into())
}
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
@@ -3391,84 +3297,38 @@ impl<'db> TypeInferenceBuilder<'db> {
todo_type!("generic `typing.Awaitable` type")
}
/// Infer the type of a [`ast::ExprName`] expression, assuming a load context.
fn infer_name_load(&mut self, name_node: &ast::ExprName) -> Type<'db> {
let ast::ExprName {
range: _,
id: symbol_name,
ctx: _,
} = name_node;
/// Look up a name reference that isn't bound in the local scope.
fn lookup_name(&mut self, name_node: &ast::ExprName) -> Symbol<'db> {
let db = self.db();
let scope = self.scope();
let file_scope_id = scope.file_scope_id(db);
let symbol_table = self.index.symbol_table(file_scope_id);
let use_def = self.index.use_def_map(file_scope_id);
// If we're inferring types of deferred expressions, always treat them as public symbols
let local_scope_symbol = if self.is_deferred() {
if let Some(symbol_id) = symbol_table.symbol_id_by_name(symbol_name) {
symbol_from_bindings(
db,
use_def.public_bindings(symbol_id),
RequiresExplicitReExport::No,
)
let ast::ExprName { id: name, .. } = name_node;
let file_scope_id = self.scope().file_scope_id(db);
let is_bound =
if let Some(symbol) = self.index.symbol_table(file_scope_id).symbol_by_name(name) {
symbol.is_bound()
} else {
assert!(
self.deferred_state.in_string_annotation(),
"Expected the symbol table to create a symbol for every Name node"
);
Symbol::Unbound
}
} else {
let use_id = name_node.scoped_use_id(db, scope);
symbol_from_bindings(
db,
use_def.bindings_at_use(use_id),
RequiresExplicitReExport::No,
)
};
let symbol = local_scope_symbol.or_fall_back_to(db, || {
let has_bindings_in_this_scope = match symbol_table.symbol_by_name(symbol_name) {
Some(symbol) => symbol.is_bound(),
None => {
assert!(
self.deferred_state.in_string_annotation(),
"Expected the symbol table to create a symbol for every Name node"
);
false
}
false
};
// If it's a function-like scope and there is one or more binding in this scope (but
// none of those bindings are visible from where we are in the control flow), we cannot
// fallback to any bindings in enclosing scopes. As such, we can immediately short-circuit
// here and return `Symbol::Unbound`.
//
// This is because Python is very strict in its categorisation of whether a variable is
// a local variable or not in function-like scopes. If a variable has any bindings in a
// function-like scope, it is considered a local variable; it never references another
// scope. (At runtime, it would use the `LOAD_FAST` opcode.)
if has_bindings_in_this_scope && scope.is_function_like(db) {
return Symbol::Unbound;
}
let current_file = self.file();
// In function-like scopes, any local variable (symbol that is bound in this scope) can
// only have a definition in this scope, or error; it never references another scope.
// (At runtime, it would use the `LOAD_FAST` opcode.)
if !is_bound || !self.scope().is_function_like(db) {
// Walk up parent scopes looking for a possible enclosing scope that may have a
// definition of this name visible to us (would be `LOAD_DEREF` at runtime.)
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id) {
// Class scopes are not visible to nested scopes, and we need to handle global
// scope differently (because an unbound name there falls back to builtins), so
// check only function-like scopes.
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_file);
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, self.file());
if !enclosing_scope_id.is_function_like(db) {
continue;
}
let enclosing_symbol_table = self.index.symbol_table(enclosing_scope_file_id);
let Some(enclosing_symbol) = enclosing_symbol_table.symbol_by_name(symbol_name)
else {
let Some(enclosing_symbol) = enclosing_symbol_table.symbol_by_name(name) else {
continue;
};
if enclosing_symbol.is_bound() {
@@ -3477,7 +3337,7 @@ impl<'db> TypeInferenceBuilder<'db> {
// runtime, it is the scope that creates the cell for our closure.) If the name
// isn't bound in that scope, we should get an unbound name, not continue
// falling back to other scopes / globals / builtins.
return symbol(db, enclosing_scope_id, symbol_name);
return symbol(db, enclosing_scope_id, name);
}
}
@@ -3488,7 +3348,7 @@ impl<'db> TypeInferenceBuilder<'db> {
if file_scope_id.is_global() {
Symbol::Unbound
} else {
global_symbol(db, self.file(), symbol_name)
global_symbol(db, self.file(), name)
}
})
// Not found in globals? Fallback to builtins
@@ -3497,12 +3357,12 @@ impl<'db> TypeInferenceBuilder<'db> {
if Some(self.scope()) == builtins_module_scope(db) {
Symbol::Unbound
} else {
builtins_symbol(db, symbol_name)
builtins_symbol(db, name)
}
})
// Still not found? It might be `reveal_type`...
.or_fall_back_to(db, || {
if symbol_name == "reveal_type" {
if name == "reveal_type" {
self.context.report_lint(
&UNDEFINED_REVEAL,
name_node.into(),
@@ -3511,23 +3371,70 @@ impl<'db> TypeInferenceBuilder<'db> {
this is allowed for debugging convenience but will fail at runtime"
),
);
typing_extensions_symbol(db, symbol_name)
typing_extensions_symbol(db, name)
} else {
Symbol::Unbound
}
})
});
} else {
Symbol::Unbound
}
}
symbol.unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound => {
report_unresolved_reference(&self.context, name_node);
Type::unknown()
/// Infer the type of a [`ast::ExprName`] expression, assuming a load context.
fn infer_name_load(&mut self, name: &ast::ExprName) -> Type<'db> {
let ast::ExprName {
range: _,
id,
ctx: _,
} = name;
let file_scope_id = self.scope().file_scope_id(self.db());
let use_def = self.index.use_def_map(file_scope_id);
// If we're inferring types of deferred expressions, always treat them as public symbols
let inferred = if self.is_deferred() {
if let Some(symbol) = self.index.symbol_table(file_scope_id).symbol_id_by_name(id) {
symbol_from_bindings(self.db(), use_def.public_bindings(symbol))
} else {
assert!(
self.deferred_state.in_string_annotation(),
"Expected the symbol table to create a symbol for every Name node"
);
Symbol::Unbound
}
LookupError::PossiblyUnbound(type_when_bound) => {
report_possibly_unresolved_reference(&self.context, name_node);
type_when_bound
} else {
let use_id = name.scoped_use_id(self.db(), self.scope());
symbol_from_bindings(self.db(), use_def.bindings_at_use(use_id))
};
if let Symbol::Type(ty, Boundness::Bound) = inferred {
ty
} else {
match self.lookup_name(name) {
Symbol::Type(looked_up_ty, looked_up_boundness) => {
if looked_up_boundness == Boundness::PossiblyUnbound {
report_possibly_unresolved_reference(&self.context, name);
}
inferred
.ignore_possibly_unbound()
.map(|ty| UnionType::from_elements(self.db(), [ty, looked_up_ty]))
.unwrap_or(looked_up_ty)
}
Symbol::Unbound => match inferred {
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
report_possibly_unresolved_reference(&self.context, name);
ty
}
Symbol::Unbound => {
report_unresolved_reference(&self.context, name);
Type::unknown()
}
Symbol::Type(_, Boundness::Bound) => unreachable!("Handled above"),
},
}
})
}
}
fn infer_name_expression(&mut self, name: &ast::ExprName) -> Type<'db> {
@@ -3547,37 +3454,36 @@ impl<'db> TypeInferenceBuilder<'db> {
ctx: _,
} = attribute;
let value_type = self.infer_expression(value);
let db = self.db();
value_type
.member(db, &attr.id)
.unwrap_with_diagnostic(|lookup_error| match lookup_error {
LookupError::Unbound => {
self.context.report_lint(
&UNRESOLVED_ATTRIBUTE,
attribute.into(),
format_args!(
"Type `{}` has no attribute `{}`",
value_type.display(db),
attr.id
),
);
Type::unknown()
}
LookupError::PossiblyUnbound(type_when_bound) => {
let value_ty = self.infer_expression(value);
match value_ty.member(self.db(), &attr.id) {
Symbol::Type(member_ty, boundness) => {
if boundness == Boundness::PossiblyUnbound {
self.context.report_lint(
&POSSIBLY_UNBOUND_ATTRIBUTE,
attribute.into(),
format_args!(
"Attribute `{}` on type `{}` is possibly unbound",
attr.id,
value_type.display(db),
value_ty.display(self.db()),
),
);
type_when_bound
}
})
member_ty
}
Symbol::Unbound => {
self.context.report_lint(
&UNRESOLVED_ATTRIBUTE,
attribute.into(),
format_args!(
"Type `{}` has no attribute `{}`",
value_ty.display(self.db()),
attr.id
),
);
Type::unknown()
}
}
}
fn infer_attribute_expression(&mut self, attribute: &ast::ExprAttribute) -> Type<'db> {
@@ -3935,8 +3841,6 @@ impl<'db> TypeInferenceBuilder<'db> {
if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) {
let reflected_dunder = op.reflected_dunder();
let rhs_reflected = right_class.member(self.db(), reflected_dunder);
// TODO: if `rhs_reflected` is possibly unbound, we should union the two possible
// CallOutcomes together
if !rhs_reflected.is_unbound()
&& rhs_reflected != left_class.member(self.db(), reflected_dunder)
{
@@ -4958,7 +4862,7 @@ impl<'db> TypeInferenceBuilder<'db> {
bytes.into(),
format_args!("Type expressions cannot use bytes literal"),
);
TypeAndQualifiers::unknown()
Type::unknown().into()
}
ast::Expr::FString(fstring) => {
@@ -4968,7 +4872,7 @@ impl<'db> TypeInferenceBuilder<'db> {
format_args!("Type expressions cannot use f-strings"),
);
self.infer_fstring_expression(fstring);
TypeAndQualifiers::unknown()
Type::unknown().into()
}
ast::Expr::Name(name) => match name.ctx {
@@ -4989,7 +4893,7 @@ impl<'db> TypeInferenceBuilder<'db> {
.into(),
}
}
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
ast::ExprContext::Invalid => Type::unknown().into(),
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!().into(),
},
@@ -5027,7 +4931,7 @@ impl<'db> TypeInferenceBuilder<'db> {
inner_annotation_ty
} else {
self.infer_type_expression(slice);
TypeAndQualifiers::unknown()
Type::unknown().into()
}
} else {
report_invalid_arguments_to_annotated(
@@ -5096,7 +5000,7 @@ impl<'db> TypeInferenceBuilder<'db> {
DeferredExpressionState::InStringAnnotation,
)
}
None => TypeAndQualifiers::unknown(),
None => Type::unknown().into(),
}
}
}
@@ -6189,7 +6093,7 @@ mod tests {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import Literal, assert_type
from typing_extensions import assert_type
assert_type(not "{y}", bool)
assert_type(not 10*"{y}", bool)
@@ -6211,7 +6115,7 @@ mod tests {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import Literal, LiteralString, assert_type
from typing_extensions import assert_type
assert_type(2 * "hello", Literal["hellohello"])
assert_type("goodbye" * 3, Literal["goodbyegoodbyegoodbye"])
@@ -6236,7 +6140,7 @@ mod tests {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import Literal, LiteralString, assert_type
from typing_extensions import assert_type
assert_type("{y}", LiteralString)
assert_type(10*"{y}", LiteralString)
@@ -6258,7 +6162,7 @@ mod tests {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import LiteralString, assert_type
from typing_extensions import assert_type
assert_type("{y}", LiteralString)
assert_type("a" + "{z}", LiteralString)
@@ -6278,7 +6182,7 @@ mod tests {
let mut db = setup_db();
let content = format!(
r#"
from typing_extensions import LiteralString, assert_type
from typing_extensions import assert_type
assert_type("{y}", LiteralString)
assert_type("{y}" + "a", LiteralString)
@@ -6537,7 +6441,6 @@ mod tests {
assert_eq!(attr_ty.display(&db).to_string(), "Unknown | str | None");
db.take_salsa_events()
};
assert_function_query_was_not_run(
&db,
infer_expression_types,

View File

@@ -4,13 +4,13 @@ use std::ops::Deref;
use rustc_hash::FxHashSet;
use crate::types::class_base::ClassBase;
use crate::types::{Class, Type};
use crate::types::{Class, KnownClass, 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, salsa::Update)]
#[derive(PartialEq, Eq, Clone, Debug)]
pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>);
impl<'db> Mro<'db> {
@@ -52,7 +52,9 @@ impl<'db> Mro<'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_object(db) => Ok(Self::from([ClassBase::Class(class)])),
[] 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,
@@ -236,7 +238,7 @@ impl<'db> Iterator for MroIterator<'db> {
impl std::iter::FusedIterator for MroIterator<'_> {}
#[derive(Debug, PartialEq, Eq, salsa::Update)]
#[derive(Debug, PartialEq, Eq)]
pub(super) struct MroError<'db> {
kind: MroErrorKind<'db>,
fallback_mro: Mro<'db>,
@@ -256,7 +258,7 @@ impl<'db> MroError<'db> {
}
/// Possible ways in which attempting to resolve the MRO of a class might fail.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
#[derive(Debug, PartialEq, Eq)]
pub(super) enum MroErrorKind<'db> {
/// The class inherits from one or more invalid bases.
///

View File

@@ -152,13 +152,13 @@ fn merge_constraints_or<'db>(
*entry.get_mut() = UnionBuilder::new(db).add(*entry.get()).add(*value).build();
}
Entry::Vacant(entry) => {
entry.insert(Type::object(db));
entry.insert(KnownClass::Object.to_instance(db));
}
}
}
for (key, value) in into.iter_mut() {
if !from.contains_key(key) {
*value = Type::object(db);
*value = KnownClass::Object.to_instance(db);
}
}
}
@@ -231,10 +231,10 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
match pattern.kind(self.db) {
PatternConstraintKind::Singleton(singleton, _guard) => {
self.evaluate_match_pattern_singleton(subject, *singleton)
self.evaluate_match_pattern_singleton(*subject, *singleton)
}
PatternConstraintKind::Class(cls, _guard) => {
self.evaluate_match_pattern_class(subject, *cls)
self.evaluate_match_pattern_class(*subject, *cls)
}
// TODO: support more pattern kinds
PatternConstraintKind::Value(..) | PatternConstraintKind::Unsupported => None,

View File

@@ -329,7 +329,7 @@ fn union<'db>(db: &'db TestDb, tys: impl IntoIterator<Item = Type<'db>>) -> Type
mod stable {
use super::union;
use crate::types::Type;
use crate::types::{KnownClass, Type};
// Reflexivity: `T` is equivalent to itself.
type_property_test!(
@@ -419,13 +419,13 @@ mod stable {
// All types should be assignable to `object`
type_property_test!(
all_types_assignable_to_object, db,
forall types t. t.is_assignable_to(db, Type::object(db))
forall types t. t.is_assignable_to(db, KnownClass::Object.to_instance(db))
);
// And for fully static types, they should also be subtypes of `object`
type_property_test!(
all_fully_static_types_subtype_of_object, db,
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db))
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, KnownClass::Object.to_instance(db))
);
// Never should be assignable to every type

View File

@@ -4,7 +4,7 @@ use crate::{semantic_index::definition::Definition, types::todo_type};
use ruff_python_ast::{self as ast, name::Name};
/// A typed callable signature.
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Signature<'db> {
/// Parameters, in source order.
///
@@ -60,7 +60,7 @@ impl<'db> Signature<'db> {
}
// TODO: use SmallVec here once invariance bug is fixed
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Parameters<'db>(Vec<Parameter<'db>>);
impl<'db> Parameters<'db> {
@@ -218,7 +218,7 @@ impl<'db> std::ops::Index<usize> for Parameters<'db> {
}
}
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Parameter<'db> {
/// Parameter name.
///
@@ -304,7 +304,7 @@ impl<'db> Parameter<'db> {
}
}
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum ParameterKind<'db> {
/// Positional-only parameter, e.g. `def f(x, /): ...`
PositionalOnly { default_ty: Option<Type<'db>> },
@@ -357,8 +357,6 @@ mod tests {
db.write_dedented(
"/src/a.py",
"
from typing import Literal
def f(a, b: int, c = 1, d: int = 2, /,
e = 3, f: Literal[4] = 4, *args: object,
g = 5, h: Literal[6] = 6, **kwargs: str) -> bytes: ...
@@ -413,7 +411,7 @@ mod tests {
},
Parameter {
name: Some(Name::new_static("args")),
annotated_ty: Some(Type::object(&db)),
annotated_ty: Some(KnownClass::Object.to_instance(&db)),
kind: ParameterKind::Variadic,
},
Parameter {

View File

@@ -0,0 +1,121 @@
use crate::types::{infer_scope_types, semantic_index, Type};
use crate::Db;
use ruff_db::files::File;
use rustc_hash::FxHashMap;
/// Get type-coverage statistics for a file.
#[salsa::tracked(return_ref)]
pub fn type_statistics<'db>(db: &'db dyn Db, file: File) -> TypeStatistics<'db> {
let _span = tracing::trace_span!("type_statistics", file=?file.path(db)).entered();
tracing::debug!(
"Gathering statistics for file '{path}'",
path = file.path(db)
);
let index = semantic_index(db, file);
let mut statistics = TypeStatistics::default();
for scope_id in index.scope_ids() {
let result = infer_scope_types(db, scope_id);
statistics.extend(&result.statistics());
}
statistics
}
/// Map each type to count of expressions with that type.
#[derive(Debug, Default, Eq, PartialEq)]
pub(super) struct TypeStatistics<'db>(FxHashMap<Type<'db>, u32>);
impl<'db> TypeStatistics<'db> {
fn extend(&mut self, other: &TypeStatistics<'db>) {
for (ty, count) in &other.0 {
self.0
.entry(*ty)
.and_modify(|my_count| *my_count += count)
.or_insert(*count);
}
}
pub(super) fn increment(&mut self, ty: Type<'db>) {
self.0
.entry(ty)
.and_modify(|count| *count += 1)
.or_insert(1);
}
#[allow(unused)]
fn expression_count(&self) -> u32 {
self.0.values().sum()
}
#[allow(unused)]
fn todo_count(&self) -> u32 {
self.0
.iter()
.filter(|(key, _)| key.is_todo())
.map(|(_, count)| count)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::tests::{setup_db, TestDb};
use ruff_db::files::system_path_to_file;
use ruff_db::system::DbWithTestSystem;
fn get_stats<'db>(
db: &'db mut TestDb,
filename: &str,
source: &str,
) -> &'db TypeStatistics<'db> {
db.write_dedented(filename, source).unwrap();
type_statistics(db, system_path_to_file(db, filename).unwrap())
}
#[test]
fn all_static() {
let mut db = setup_db();
let stats = get_stats(&mut db, "src/foo.py", "1");
assert_eq!(stats.0, FxHashMap::from_iter([(Type::IntLiteral(1), 1)]));
}
#[test]
fn todo_and_expression_count() {
let mut db = setup_db();
let stats = get_stats(
&mut db,
"src/foo.py",
r#"
x = [x for x in [1]]
"#,
);
assert_eq!(stats.todo_count(), 4);
assert_eq!(stats.expression_count(), 6);
}
#[test]
fn sum() {
let mut db = setup_db();
let stats = get_stats(
&mut db,
"src/foo.py",
r#"
1
def f():
1
"#,
);
assert_eq!(stats.0[&Type::IntLiteral(1)], 2);
}
}

View File

@@ -1,4 +1,5 @@
use ruff_db::source::source_text;
use ruff_python_ast::str::raw_contents;
use ruff_python_ast::{self as ast, ModExpression};
use ruff_python_parser::Parsed;
use ruff_text_size::Ranged;
@@ -137,8 +138,9 @@ pub(crate) fn parse_string_annotation(
let _span = tracing::trace_span!("parse_string_annotation", string=?string_expr.range(), file=%file.path(db)).entered();
let source = source_text(db.upcast(), file);
let node_text = &source[string_expr.range()];
if let Some(string_literal) = string_expr.as_unconcatenated_literal() {
if let [string_literal] = string_expr.value.as_slice() {
let prefix = string_literal.flags.prefix();
if prefix.is_raw() {
context.report_lint(
@@ -148,7 +150,9 @@ pub(crate) fn parse_string_annotation(
);
// Compare the raw contents (without quotes) of the expression with the parsed contents
// contained in the string literal.
} else if &source[string_literal.content_range()] == string_literal.as_str() {
} else if raw_contents(node_text)
.is_some_and(|raw_contents| raw_contents == string_literal.as_str())
{
match ruff_python_parser::parse_string_annotation(source.as_str(), string_literal) {
Ok(parsed) => return Some(parsed),
Err(parse_error) => context.report_lint(

View File

@@ -26,7 +26,7 @@ impl<'db> SubclassOfType<'db> {
ClassBase::Class(class) => {
if class.is_final(db) {
Type::ClassLiteral(ClassLiteralType { class })
} else if class.is_object(db) {
} else if class.is_known(db, KnownClass::Object) {
KnownClass::Type.to_instance(db)
} else {
Type::SubclassOf(Self { subclass_of })

View File

@@ -261,7 +261,7 @@ impl<'db> Unpacker<'db> {
}
}
#[derive(Debug, Default, PartialEq, Eq, salsa::Update)]
#[derive(Debug, Default, PartialEq, Eq)]
pub(crate) struct UnpackResult<'db> {
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
diagnostics: TypeCheckDiagnostics,

View File

@@ -28,15 +28,16 @@ use crate::Db;
/// * 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]
#[tracked]
pub(crate) target: AstNodeRef<ast::Expr>,
/// The ingredient representing the value expression of the unpacking. For example, in
@@ -44,6 +45,7 @@ pub(crate) struct Unpack<'db> {
#[no_eq]
pub(crate) value: UnpackValue<'db>,
#[no_eq]
count: countme::Count<Unpack<'static>>,
}
@@ -60,7 +62,7 @@ impl<'db> Unpack<'db> {
}
/// The expression that is being unpacked.
#[derive(Clone, Copy, Debug, Hash)]
#[derive(Clone, Copy, Debug)]
pub(crate) enum UnpackValue<'db> {
/// An iterable expression like the one in a `for` loop or a comprehension.
Iterable(Expression<'db>),

View File

@@ -338,7 +338,7 @@ const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE;
/// A collection of visibility constraints. This is currently stored in `UseDefMap`, which means we
/// maintain a separate set of visibility constraints for each scope in file.
#[derive(Debug, PartialEq, Eq, salsa::Update)]
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VisibilityConstraints<'db> {
constraints: IndexVec<Atom, Constraint<'db>>,
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
@@ -627,7 +627,7 @@ impl<'db> VisibilityConstraints<'db> {
ConstraintNode::Pattern(inner) => match inner.kind(db) {
PatternConstraintKind::Value(value, guard) => {
let subject_expression = inner.subject(db);
let inference = infer_expression_types(db, subject_expression);
let inference = infer_expression_types(db, *subject_expression);
let scope = subject_expression.scope(db);
let subject_ty = inference.expression_type(
subject_expression

View File

@@ -12,7 +12,7 @@ license = { workspace = true }
[dependencies]
red_knot_project = { workspace = true }
ruff_db = { workspace = true, features = ["os"] }
ruff_db = { workspace = true }
ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_source_file = { workspace = true }

View File

@@ -75,13 +75,11 @@ fn to_lsp_diagnostic(
diagnostic: &dyn ruff_db::diagnostic::Diagnostic,
encoding: crate::PositionEncoding,
) -> Diagnostic {
let range = if let Some(span) = diagnostic.span() {
let index = line_index(db.upcast(), span.file());
let source = source_text(db.upcast(), span.file());
let range = if let (Some(file), Some(range)) = (diagnostic.file(), diagnostic.range()) {
let index = line_index(db.upcast(), file);
let source = source_text(db.upcast(), file);
span.range()
.map(|range| range.to_range(&source, &index, encoding))
.unwrap_or_default()
range.to_range(&source, &index, encoding)
} else {
Range::default()
};

View File

@@ -68,9 +68,7 @@ impl Session {
let system = LSPSystem::new(index.clone());
// TODO(dhruvmanila): Get the values from the client settings
let mut metadata = ProjectMetadata::discover(system_path, &system)?;
metadata.apply_configuration_files(&system)?;
let metadata = ProjectMetadata::discover(system_path, &system)?;
// TODO(micha): Handle the case where the program settings are incorrect more gracefully.
workspaces.insert(path, ProjectDatabase::new(metadata, system)?);
}

View File

@@ -187,10 +187,6 @@ impl System for LSPSystem {
self.os_system.current_directory()
}
fn user_config_directory(&self) -> Option<SystemPathBuf> {
self.os_system.user_config_directory()
}
fn read_directory<'a>(
&'a self,
path: &SystemPath,

View File

@@ -9,8 +9,7 @@
//! ```
use anyhow::Context;
use red_knot_python_semantic::PythonPlatform;
use ruff_python_ast::python_version::PythonVersion;
use red_knot_python_semantic::{PythonPlatform, PythonVersion};
use serde::Deserialize;
#[derive(Deserialize, Debug, Default, Clone)]

View File

@@ -1,15 +1,12 @@
use std::sync::Arc;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::{
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonPlatform,
SearchPathSettings,
PythonVersion, SearchPathSettings,
};
use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPath, SystemPathBuf, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use ruff_db::{Db as SourceDb, Upcast};
use ruff_python_ast::python_version::PythonVersion;
#[salsa::db]
#[derive(Clone)]
@@ -19,7 +16,7 @@ pub(crate) struct Db {
files: Files,
system: TestSystem,
vendored: VendoredFileSystem,
rule_selection: Arc<RuleSelection>,
rule_selection: RuleSelection,
}
impl Db {
@@ -32,7 +29,7 @@ impl Db {
system: TestSystem::default(),
vendored: red_knot_vendored::file_system().clone(),
files: Files::default(),
rule_selection: Arc::new(rule_selection),
rule_selection,
};
db.memory_file_system()
@@ -97,8 +94,8 @@ impl SemanticDb for Db {
!file.path(self).is_vendored_path()
}
fn rule_selection(&self) -> Arc<RuleSelection> {
self.rule_selection.clone()
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {

View File

@@ -26,8 +26,7 @@ where
.into_iter()
.map(|diagnostic| DiagnosticWithLine {
line_number: diagnostic
.span()
.and_then(|span| span.range())
.range()
.map_or(OneIndexed::from_zero_indexed(0), |range| {
line_index.line_index(range.start())
}),
@@ -145,7 +144,7 @@ struct DiagnosticWithLine<T> {
mod tests {
use crate::db::Db;
use crate::diagnostic::Diagnostic;
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity, Span};
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::source::line_index;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
@@ -199,8 +198,12 @@ mod tests {
"dummy".into()
}
fn span(&self) -> Option<Span> {
Some(Span::from(self.file).with_range(self.range))
fn file(&self) -> Option<File> {
Some(self.file)
}
fn range(&self) -> Option<TextRange> {
Some(self.range)
}
fn severity(&self) -> Severity {

View File

@@ -5,7 +5,7 @@ use colored::Colorize;
use parser as test_parser;
use red_knot_python_semantic::types::check_types;
use red_knot_python_semantic::{Program, ProgramSettings, SearchPathSettings, SitePackages};
use ruff_db::diagnostic::{Diagnostic, DisplayDiagnosticConfig, ParseDiagnostic};
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
use ruff_db::files::{system_path_to_file, File, Files};
use ruff_db::panic::catch_unwind;
use ruff_db::parsed::parsed_module;
@@ -300,7 +300,9 @@ fn create_diagnostic_snapshot<D: Diagnostic>(
test: &parser::MarkdownTest,
diagnostics: impl IntoIterator<Item = D>,
) -> String {
let display_config = DisplayDiagnosticConfig::default().color(false);
// TODO(ag): Do something better than requiring this
// global state to be twiddled everywhere.
colored::control::set_override(false);
let mut snapshot = String::new();
writeln!(snapshot).unwrap();
@@ -338,7 +340,7 @@ fn create_diagnostic_snapshot<D: Diagnostic>(
writeln!(snapshot).unwrap();
}
writeln!(snapshot, "```").unwrap();
writeln!(snapshot, "{}", diag.display(db, &display_config)).unwrap();
writeln!(snapshot, "{}", diag.display(db)).unwrap();
writeln!(snapshot, "```").unwrap();
}
snapshot

View File

@@ -257,8 +257,7 @@ impl Matcher {
fn column<T: Diagnostic>(&self, diagnostic: &T) -> OneIndexed {
diagnostic
.span()
.and_then(|span| span.range())
.range()
.map(|range| {
self.line_index
.source_location(range.start(), &self.source)
@@ -335,7 +334,7 @@ impl Matcher {
#[cfg(test)]
mod tests {
use super::FailuresByLine;
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::{system_path_to_file, File};
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_python_trivia::textwrap::dedent;
@@ -386,8 +385,12 @@ mod tests {
self.message.into()
}
fn span(&self) -> Option<Span> {
Some(Span::from(self.file).with_range(self.range))
fn file(&self) -> Option<File> {
Some(self.file)
}
fn range(&self) -> Option<TextRange> {
Some(self.range)
}
fn severity(&self) -> Severity {

Some files were not shown because too many files have changed in this diff Show More