Compare commits
46 Commits
david/comp
...
micha/call
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20cfc116db | ||
|
|
44eaf112f2 | ||
|
|
302b64b139 | ||
|
|
34ef941f0c | ||
|
|
daf19cc40a | ||
|
|
03f08283ad | ||
|
|
ae1b381c06 | ||
|
|
366ae1feaa | ||
|
|
86c5cba472 | ||
|
|
6e34f74c16 | ||
|
|
9c179314ed | ||
|
|
ce31c2693b | ||
|
|
7b487d853a | ||
|
|
df1d430294 | ||
|
|
69d86d1d69 | ||
|
|
7fbd89cb39 | ||
|
|
0019d39f6e | ||
|
|
f30fac6326 | ||
|
|
a4c8c49ac2 | ||
|
|
af832560fc | ||
|
|
f7819e553f | ||
|
|
678b0c2d39 | ||
|
|
524cf6e515 | ||
|
|
857cf0deb0 | ||
|
|
0f1eb1e2fc | ||
|
|
b69eb9099a | ||
|
|
d2f661f795 | ||
|
|
07cf8852a3 | ||
|
|
c08989692b | ||
|
|
869a9543e4 | ||
|
|
cc0a5dd14a | ||
|
|
b54e390cb4 | ||
|
|
5e1403a8a6 | ||
|
|
a6b86e3de2 | ||
|
|
798725ccf9 | ||
|
|
81749164bc | ||
|
|
b3ea17f128 | ||
|
|
8fb69d3b05 | ||
|
|
3b69a8833d | ||
|
|
88b543d73a | ||
|
|
f367aa8367 | ||
|
|
9ae98d4a09 | ||
|
|
0af4b23d9f | ||
|
|
f178ecc2d7 | ||
|
|
a46fbda948 | ||
|
|
fc59e1b17f |
6
.github/renovate.json5
vendored
6
.github/renovate.json5
vendored
@@ -58,6 +58,12 @@
|
||||
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
|
||||
|
||||
@@ -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.4
|
||||
rev: v0.9.5
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
@@ -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.0
|
||||
rev: v1.3.1
|
||||
hooks:
|
||||
- id: zizmor
|
||||
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,5 +1,44 @@
|
||||
# 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
|
||||
|
||||
111
Cargo.lock
generated
111
Cargo.lock
generated
@@ -29,6 +29,12 @@ 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"
|
||||
@@ -354,9 +360,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.27"
|
||||
version = "4.5.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796"
|
||||
checksum = "3e77c3243bd94243c03672cb5154667347c457ca271254724f9f393aee1c05ff"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -407,9 +413,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.24"
|
||||
version = "4.5.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c"
|
||||
checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -471,7 +477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -480,7 +486,7 @@ version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -897,7 +903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1031,10 +1037,8 @@ 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]]
|
||||
@@ -1098,6 +1102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1475,7 +1480,7 @@ checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
|
||||
dependencies = [
|
||||
"hermit-abi 0.4.0",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2049,7 +2054,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"pep440_rs",
|
||||
"regex",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"thiserror 1.0.69",
|
||||
@@ -2437,7 +2442,7 @@ dependencies = [
|
||||
"ruff_macros",
|
||||
"ruff_python_ast",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -2477,7 +2482,7 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -2505,7 +2510,7 @@ dependencies = [
|
||||
"ruff_python_ast",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
@@ -2531,7 +2536,7 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"serde",
|
||||
"smallvec",
|
||||
@@ -2642,7 +2647,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2682,7 +2687,7 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"ruff_workspace",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
@@ -2728,7 +2733,7 @@ dependencies = [
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"tikv-jemallocator",
|
||||
]
|
||||
|
||||
@@ -2754,6 +2759,7 @@ dependencies = [
|
||||
"countme",
|
||||
"dashmap 6.1.0",
|
||||
"dunce",
|
||||
"etcetera",
|
||||
"filetime",
|
||||
"glob",
|
||||
"ignore",
|
||||
@@ -2768,7 +2774,7 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -2839,7 +2845,7 @@ dependencies = [
|
||||
"ruff_cache",
|
||||
"ruff_macros",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
@@ -2871,12 +2877,13 @@ name = "ruff_index"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"ruff_macros",
|
||||
"salsa",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"anyhow",
|
||||
@@ -2918,7 +2925,7 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2980,7 +2987,8 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa",
|
||||
"schemars",
|
||||
"serde",
|
||||
]
|
||||
@@ -3027,7 +3035,7 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3074,7 +3082,7 @@ dependencies = [
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"static_assertions",
|
||||
"unicode-ident",
|
||||
"unicode-normalization",
|
||||
@@ -3105,7 +3113,7 @@ dependencies = [
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
"smallvec",
|
||||
@@ -3164,7 +3172,7 @@ dependencies = [
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"ruff_workspace",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
@@ -3194,7 +3202,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3246,7 +3254,7 @@ dependencies = [
|
||||
"ruff_python_semantic",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_source_file",
|
||||
"rustc-hash 2.1.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"schemars",
|
||||
"serde",
|
||||
"shellexpand",
|
||||
@@ -3273,9 +3281,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497"
|
||||
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
@@ -3287,7 +3295,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3305,17 +3313,19 @@ checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
|
||||
[[package]]
|
||||
name = "salsa"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
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.0",
|
||||
"rustc-hash 2.1.1",
|
||||
"salsa-macro-rules",
|
||||
"salsa-macros",
|
||||
"smallvec",
|
||||
@@ -3325,12 +3335,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "salsa-macro-rules"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
|
||||
[[package]]
|
||||
name = "salsa-macros"
|
||||
version = "0.18.0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=88a1d7774d78f048fbd77d40abca9ebd729fd1f0#88a1d7774d78f048fbd77d40abca9ebd729fd1f0"
|
||||
source = "git+https://github.com/salsa-rs/salsa.git?rev=351d9cf0037be949d17800d0c7b4838e533c2ed6#351d9cf0037be949d17800d0c7b4838e533c2ed6"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3601,18 +3611,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
checksum = "ce1475c515a4f03a8a7129bb5228b81a781a86cb0b3fbbc19e1c556d491a401f"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
checksum = "9688894b43459159c82bfa5a5fa0435c19cbe3c9b427fa1dd7b1ce0c279b18a7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -3665,7 +3675,7 @@ dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3854,9 +3864,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
@@ -4153,21 +4163,22 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.12.1"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b"
|
||||
checksum = "ced87ca4be083373936a67f8de945faa23b6b42384bd5b64434850802c6dccd0"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"rand 0.8.5",
|
||||
"getrandom 0.3.1",
|
||||
"js-sys",
|
||||
"rand 0.9.0",
|
||||
"uuid-macro-internal",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-macro-internal"
|
||||
version = "1.12.1"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8a86d88347b61a0e17b9908a67efcc594130830bf1045653784358dd023e294"
|
||||
checksum = "d28dd23acb5f2fa7bd2155ab70b960e770596b3bb6395119b40476c3655dfba4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4429,7 +4440,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -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 = "88a1d7774d78f048fbd77d40abca9ebd729fd1f0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "351d9cf0037be949d17800d0c7b4838e533c2ed6" }
|
||||
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.26.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.26.0" }
|
||||
strum = { version = "0.27.0", features = ["strum_macros"] }
|
||||
strum_macros = { version = "0.27.0" }
|
||||
syn = { version = "2.0.55" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
test-case = { version = "3.3.1" }
|
||||
|
||||
@@ -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.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.9.5/install.ps1 | iex"
|
||||
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"
|
||||
```
|
||||
|
||||
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.5
|
||||
rev: v0.9.6
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -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};
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options, TerminalOptions};
|
||||
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")]
|
||||
pub(crate) error_on_warning: bool,
|
||||
#[arg(long, conflicts_with = "exit_zero", default_missing_value = "true", num_args=0..1)]
|
||||
pub(crate) error_on_warning: Option<bool>,
|
||||
|
||||
/// Always use exit code 0, even when there are error-level diagnostics.
|
||||
#[arg(long)]
|
||||
@@ -107,6 +107,9 @@ impl CheckCommand {
|
||||
}),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
terminal: Some(TerminalOptions {
|
||||
error_on_warning: self.error_on_warning,
|
||||
}),
|
||||
rules,
|
||||
..Default::default()
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ 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, Severity};
|
||||
@@ -22,7 +22,6 @@ 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)]
|
||||
@@ -97,19 +96,15 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
let system = OsSystem::new(cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
let min_error_severity = if args.error_on_warning {
|
||||
Severity::Warning
|
||||
} else {
|
||||
Severity::Error
|
||||
};
|
||||
|
||||
let cli_options = args.into_options();
|
||||
let mut workspace_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
|
||||
workspace_metadata.apply_cli_options(cli_options.clone());
|
||||
let mut project_metadata = ProjectMetadata::discover(system.current_directory(), &system)?;
|
||||
project_metadata.apply_cli_options(cli_options.clone());
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
let mut db = ProjectDatabase::new(workspace_metadata, system)?;
|
||||
let mut db = ProjectDatabase::new(project_metadata, system)?;
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options, min_error_severity);
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new(cli_options);
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
@@ -167,18 +162,10 @@ 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,
|
||||
min_error_severity: Severity,
|
||||
) -> (Self, MainLoopCancellationToken) {
|
||||
fn new(cli_options: Options) -> (Self, MainLoopCancellationToken) {
|
||||
let (sender, receiver) = crossbeam_channel::bounded(10);
|
||||
|
||||
(
|
||||
@@ -187,7 +174,6 @@ impl MainLoop {
|
||||
receiver,
|
||||
watcher: None,
|
||||
cli_options,
|
||||
min_error_severity,
|
||||
},
|
||||
MainLoopCancellationToken { sender },
|
||||
)
|
||||
@@ -245,9 +231,16 @@ impl MainLoop {
|
||||
result,
|
||||
revision: check_revision,
|
||||
} => {
|
||||
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() >= self.min_error_severity);
|
||||
.any(|diagnostic| diagnostic.severity() >= min_error_severity);
|
||||
|
||||
if check_revision == revision {
|
||||
#[allow(clippy::print_stdout)]
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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.project_dir().join("child")), @r###"
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().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.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().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.project_dir().join("child")), @r"
|
||||
assert_cmd_snapshot!(case.command().current_dir(case.root().join("child")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
@@ -575,6 +575,37 @@ 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(
|
||||
@@ -686,6 +717,109 @@ 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,
|
||||
@@ -753,7 +887,7 @@ impl TestCase {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn project_dir(&self) -> &Path {
|
||||
fn root(&self) -> &Path {
|
||||
&self.project_dir
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,9 @@ use red_knot_project::{Db, ProjectDatabase, ProjectMetadata};
|
||||
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, SystemPath, SystemPathBuf};
|
||||
use ruff_db::system::{
|
||||
OsSystem, System, SystemPath, SystemPathBuf, UserConfigDirectoryOverrideGuard,
|
||||
};
|
||||
use ruff_db::Upcast;
|
||||
|
||||
struct TestCase {
|
||||
@@ -220,17 +222,44 @@ where
|
||||
}
|
||||
|
||||
trait SetupFiles {
|
||||
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()>;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, P> SetupFiles for [(P, &'static str); N]
|
||||
where
|
||||
P: AsRef<SystemPath>,
|
||||
{
|
||||
fn setup(self, _root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
|
||||
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
|
||||
for (relative_path, content) in self {
|
||||
let relative_path = relative_path.as_ref();
|
||||
let absolute_path = project_path.join(relative_path);
|
||||
let absolute_path = context.join_project_path(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}`")
|
||||
@@ -250,10 +279,10 @@ where
|
||||
|
||||
impl<F> SetupFiles for F
|
||||
where
|
||||
F: FnOnce(&SystemPath, &SystemPath) -> anyhow::Result<()>,
|
||||
F: FnOnce(&SetupContext) -> anyhow::Result<()>,
|
||||
{
|
||||
fn setup(self, root_path: &SystemPath, project_path: &SystemPath) -> anyhow::Result<()> {
|
||||
self(root_path, project_path)
|
||||
fn setup(self, context: &SetupContext) -> anyhow::Result<()> {
|
||||
self(context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,13 +290,12 @@ fn setup<F>(setup_files: F) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
{
|
||||
setup_with_options(setup_files, |_root, _project_path| None)
|
||||
setup_with_options(setup_files, |_context| None)
|
||||
}
|
||||
|
||||
// TODO: Replace with configuration?
|
||||
fn setup_with_options<F>(
|
||||
setup_files: F,
|
||||
create_options: impl FnOnce(&SystemPath, &SystemPath) -> Option<Options>,
|
||||
create_options: impl FnOnce(&SetupContext) -> Option<Options>,
|
||||
) -> anyhow::Result<TestCase>
|
||||
where
|
||||
F: SetupFiles,
|
||||
@@ -295,13 +323,17 @@ 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(&root_path, &project_path)
|
||||
.setup(&setup_context)
|
||||
.context("Failed to setup test files")?;
|
||||
|
||||
let system = OsSystem::new(&project_path);
|
||||
|
||||
if let Some(options) = create_options(&root_path, &project_path) {
|
||||
if let Some(options) = create_options(&setup_context) {
|
||||
std::fs::write(
|
||||
project_path.join("pyproject.toml").as_std_path(),
|
||||
toml::to_string(&PyProject {
|
||||
@@ -315,7 +347,9 @@ where
|
||||
.context("Failed to write configuration")?;
|
||||
}
|
||||
|
||||
let project = ProjectMetadata::discover(&project_path, &system)?;
|
||||
let mut project = ProjectMetadata::discover(&project_path, &system)?;
|
||||
project.apply_configuration_files(&system)?;
|
||||
|
||||
let program_settings = project.to_program_settings(&system);
|
||||
|
||||
for path in program_settings
|
||||
@@ -789,10 +823,12 @@ fn directory_deleted() -> anyhow::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
|
||||
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
context.join_root_path("site_packages"),
|
||||
)]),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -853,10 +889,12 @@ fn add_search_path() -> anyhow::Result<()> {
|
||||
|
||||
#[test]
|
||||
fn remove_search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options([("bar.py", "import sub.a")], |root_path, _project_path| {
|
||||
let mut case = setup_with_options([("bar.py", "import sub.a")], |context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(root_path.join("site_packages"))]),
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
context.join_root_path("site_packages"),
|
||||
)]),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -894,7 +932,7 @@ import os
|
||||
print(sys.last_exc, os.getegid())
|
||||
"#,
|
||||
)],
|
||||
|_root_path, _project_path| {
|
||||
|_context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(PythonVersion::PY311)),
|
||||
@@ -942,21 +980,31 @@ print(sys.last_exc, os.getegid())
|
||||
#[test]
|
||||
fn changed_versions_file() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options(
|
||||
|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(), "")?;
|
||||
|context: &SetupContext| {
|
||||
std::fs::write(
|
||||
root_path.join("typeshed/stdlib/os.pyi").as_std_path(),
|
||||
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(),
|
||||
"# not important",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|root_path, _project_path| {
|
||||
|context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
typeshed: Some(RelativePathBuf::cli(root_path.join("typeshed"))),
|
||||
typeshed: Some(RelativePathBuf::cli(context.join_root_path("typeshed"))),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
@@ -1007,12 +1055,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(|_root: &SystemPath, project: &SystemPath| {
|
||||
let foo_path = project.join("foo.py");
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
let foo_path = context.join_project_path("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
// Create a hardlink to `foo`
|
||||
let bar_path = project.join("bar.py");
|
||||
let bar_path = context.join_project_path("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")?;
|
||||
|
||||
@@ -1078,12 +1126,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(|root: &SystemPath, project: &SystemPath| {
|
||||
let foo_path = root.join("foo.py");
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
let foo_path = context.join_root_path("foo.py");
|
||||
std::fs::write(foo_path.as_std_path(), "print('Version 1')")?;
|
||||
|
||||
// Create a hardlink to `foo`
|
||||
let bar_path = project.join("bar.py");
|
||||
let bar_path = context.join_project_path("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")?;
|
||||
|
||||
@@ -1186,9 +1234,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(|root: &SystemPath, project: &SystemPath| {
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
// Set up the symlink target.
|
||||
let link_target = root.join("bar");
|
||||
let link_target = context.join_root_path("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");
|
||||
@@ -1196,7 +1244,7 @@ mod unix {
|
||||
.context("Failed to write link target file")?;
|
||||
|
||||
// Create a symlink inside the project
|
||||
let bar = project.join("bar");
|
||||
let bar = context.join_project_path("bar");
|
||||
std::os::unix::fs::symlink(link_target.as_std_path(), bar.as_std_path())
|
||||
.context("Failed to create symlink to bar package")?;
|
||||
|
||||
@@ -1267,9 +1315,9 @@ mod unix {
|
||||
/// ```
|
||||
#[test]
|
||||
fn symlink_inside_project() -> anyhow::Result<()> {
|
||||
let mut case = setup(|_root: &SystemPath, project: &SystemPath| {
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
// Set up the symlink target.
|
||||
let link_target = project.join("patched/bar");
|
||||
let link_target = context.join_project_path("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");
|
||||
@@ -1277,7 +1325,7 @@ mod unix {
|
||||
.context("Failed to write link target file")?;
|
||||
|
||||
// Create a symlink inside site-packages
|
||||
let bar_in_project = project.join("bar");
|
||||
let bar_in_project = context.join_project_path("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")?;
|
||||
|
||||
@@ -1358,9 +1406,9 @@ mod unix {
|
||||
#[test]
|
||||
fn symlinked_module_search_path() -> anyhow::Result<()> {
|
||||
let mut case = setup_with_options(
|
||||
|root: &SystemPath, project: &SystemPath| {
|
||||
|context: &SetupContext| {
|
||||
// Set up the symlink target.
|
||||
let site_packages = root.join("site-packages");
|
||||
let site_packages = context.join_root_path("site-packages");
|
||||
let bar = site_packages.join("bar");
|
||||
std::fs::create_dir_all(bar.as_std_path())
|
||||
.context("Failed to create bar directory")?;
|
||||
@@ -1369,7 +1417,8 @@ mod unix {
|
||||
.context("Failed to write baz.py")?;
|
||||
|
||||
// Symlink the site packages in the venv to the global site packages
|
||||
let venv_site_packages = project.join(".venv/lib/python3.12/site-packages");
|
||||
let venv_site_packages =
|
||||
context.join_project_path(".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(
|
||||
@@ -1380,7 +1429,7 @@ mod unix {
|
||||
|
||||
Ok(())
|
||||
},
|
||||
|_root, _project| {
|
||||
|_context| {
|
||||
Some(Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
extra_paths: Some(vec![RelativePathBuf::cli(
|
||||
@@ -1450,9 +1499,9 @@ mod unix {
|
||||
|
||||
#[test]
|
||||
fn nested_projects_delete_root() -> anyhow::Result<()> {
|
||||
let mut case = setup(|root: &SystemPath, project_root: &SystemPath| {
|
||||
let mut case = setup(|context: &SetupContext| {
|
||||
std::fs::write(
|
||||
project_root.join("pyproject.toml").as_std_path(),
|
||||
context.join_project_path("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "inner"
|
||||
@@ -1462,7 +1511,7 @@ fn nested_projects_delete_root() -> anyhow::Result<()> {
|
||||
)?;
|
||||
|
||||
std::fs::write(
|
||||
root.join("pyproject.toml").as_std_path(),
|
||||
context.join_root_path("pyproject.toml").as_std_path(),
|
||||
r#"
|
||||
[project]
|
||||
name = "outer"
|
||||
@@ -1487,3 +1536,79 @@ 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(())
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ruff_cache = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os", "cache", "serde"] }
|
||||
ruff_db = { workspace = true, features = ["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 }
|
||||
pep440_rs = { workspace = true, features = ["version-ranges"] }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
|
||||
@@ -114,8 +114,8 @@ impl SemanticDb for ProjectDatabase {
|
||||
project.is_file_open(self, file)
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
self.project().rule_selection(self)
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
@@ -186,7 +186,6 @@ pub(crate) mod tests {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
rule_selection: RuleSelection,
|
||||
project: Option<Project>,
|
||||
}
|
||||
|
||||
@@ -198,7 +197,6 @@ 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,
|
||||
};
|
||||
|
||||
@@ -270,8 +268,8 @@ pub(crate) mod tests {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.project().rules(self)
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 {
|
||||
@@ -47,7 +48,7 @@ impl ProjectDatabase {
|
||||
if let Some(path) = change.system_path() {
|
||||
if matches!(
|
||||
path.file_name(),
|
||||
Some(".gitignore" | ".ignore" | "ruff.toml" | ".ruff.toml" | "pyproject.toml")
|
||||
Some(".gitignore" | ".ignore" | "knot.toml" | "pyproject.toml")
|
||||
) {
|
||||
// Changes to ignore files or settings can change the project structure or add/remove files.
|
||||
project_changed = true;
|
||||
@@ -144,6 +145,12 @@ 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);
|
||||
@@ -201,9 +208,16 @@ impl ProjectDatabase {
|
||||
return WalkState::Continue;
|
||||
}
|
||||
|
||||
let mut paths = added_paths.lock().unwrap();
|
||||
if entry
|
||||
.path()
|
||||
.extension()
|
||||
.and_then(PySourceType::try_from_extension)
|
||||
.is_some()
|
||||
{
|
||||
let mut paths = added_paths.lock().unwrap();
|
||||
|
||||
paths.push(entry.into_path());
|
||||
paths.push(entry.into_path());
|
||||
}
|
||||
|
||||
WalkState::Continue
|
||||
})
|
||||
|
||||
@@ -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};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, ParseDiagnostic, Severity, Span};
|
||||
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,12 +66,22 @@ 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 {
|
||||
Project::builder(metadata)
|
||||
let (settings, settings_diagnostics) = metadata.options().to_settings(db);
|
||||
|
||||
Project::builder(metadata, settings, settings_diagnostics)
|
||||
.durability(Durability::MEDIUM)
|
||||
.open_fileset_durability(Durability::LOW)
|
||||
.file_set_durability(Durability::LOW)
|
||||
@@ -86,30 +96,37 @@ 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");
|
||||
@@ -118,8 +135,7 @@ impl Project {
|
||||
tracing::debug!("Checking project '{name}'", name = self.name(db));
|
||||
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
|
||||
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
|
||||
diagnostics.extend(options_diagnostics.iter().map(|diagnostic| {
|
||||
diagnostics.extend(self.settings_diagnostics(db).iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
diagnostic
|
||||
}));
|
||||
@@ -151,9 +167,8 @@ impl Project {
|
||||
}
|
||||
|
||||
pub(crate) fn check_file(self, db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
let (_, options_diagnostics) = self.rule_selection_with_diagnostics(db);
|
||||
|
||||
let mut file_diagnostics: Vec<_> = options_diagnostics
|
||||
let mut file_diagnostics: Vec<_> = self
|
||||
.settings_diagnostics(db)
|
||||
.iter()
|
||||
.map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
@@ -329,7 +344,13 @@ fn check_file_impl(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
boxed
|
||||
}));
|
||||
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start());
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||
diagnostic
|
||||
.span()
|
||||
.and_then(|span| span.range())
|
||||
.unwrap_or_default()
|
||||
.start()
|
||||
});
|
||||
|
||||
diagnostics
|
||||
}
|
||||
@@ -442,12 +463,8 @@ impl Diagnostic for IOErrorDiagnostic {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
None
|
||||
fn span(&self) -> Option<Span> {
|
||||
Some(Span::from(self.file))
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use configuration_file::{ConfigurationFile, ConfigurationFileError};
|
||||
use red_knot_python_semantic::ProgramSettings;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::name::Name;
|
||||
@@ -5,13 +6,15 @@ use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::combine::Combine;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError};
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
|
||||
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)]
|
||||
@@ -23,6 +26,15 @@ 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 {
|
||||
@@ -31,12 +43,16 @@ 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) -> Self {
|
||||
pub(crate) fn from_pyproject(
|
||||
pyproject: PyProject,
|
||||
root: SystemPathBuf,
|
||||
) -> Result<Self, ResolveRequiresPythonError> {
|
||||
Self::from_options(
|
||||
pyproject
|
||||
.tool
|
||||
@@ -49,21 +65,37 @@ impl ProjectMetadata {
|
||||
|
||||
/// Loads a project from a set of options with an optional pyproject-project table.
|
||||
pub(crate) fn from_options(
|
||||
options: Options,
|
||||
mut options: Options,
|
||||
root: SystemPathBuf,
|
||||
project: Option<&Project>,
|
||||
) -> Self {
|
||||
) -> Result<Self, ResolveRequiresPythonError> {
|
||||
let name = project
|
||||
.and_then(|project| project.name.as_ref())
|
||||
.map(|name| Name::new(&***name))
|
||||
.and_then(|project| project.name.as_deref())
|
||||
.map(|name| Name::new(&**name))
|
||||
.unwrap_or_else(|| Name::new(root.file_name().unwrap_or("root")));
|
||||
|
||||
// TODO(https://github.com/astral-sh/ruff/issues/15491): Respect requires-python
|
||||
Self {
|
||||
// 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 {
|
||||
name,
|
||||
root,
|
||||
options,
|
||||
}
|
||||
extra_configuration_paths: Vec::new(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Discovers the closest project at `path` and returns its metadata.
|
||||
@@ -131,19 +163,34 @@ impl ProjectMetadata {
|
||||
}
|
||||
|
||||
tracing::debug!("Found project at '{}'", project_root);
|
||||
return Ok(ProjectMetadata::from_options(
|
||||
|
||||
let metadata = 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());
|
||||
ProjectMetadata::from_pyproject(pyproject, project_root.to_path_buf())
|
||||
.map_err(
|
||||
|err| ProjectDiscoveryError::InvalidRequiresPythonConstraint {
|
||||
source: err,
|
||||
path: pyproject_path,
|
||||
},
|
||||
)?;
|
||||
|
||||
if has_knot_section {
|
||||
tracing::debug!("Found project at '{}'", project_root);
|
||||
@@ -191,6 +238,10 @@ 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)
|
||||
}
|
||||
@@ -200,9 +251,31 @@ impl ProjectMetadata {
|
||||
self.options = options.combine(std::mem::take(&mut self.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);
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,15 +295,21 @@ 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 red_knot_python_semantic::PythonVersion;
|
||||
use ruff_db::system::{SystemPathBuf, TestSystem};
|
||||
|
||||
use crate::{ProjectDiscoveryError, ProjectMetadata};
|
||||
@@ -250,7 +329,15 @@ mod tests {
|
||||
|
||||
assert_eq!(project.root(), &*root);
|
||||
|
||||
snapshot_project!(project);
|
||||
with_escaped_paths(|| {
|
||||
assert_ron_snapshot!(&project, @r#"
|
||||
ProjectMetadata(
|
||||
name: Name("app"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -279,7 +366,16 @@ mod tests {
|
||||
ProjectMetadata::discover(&root, &system).context("Failed to discover project")?;
|
||||
|
||||
assert_eq!(project.root(), &*root);
|
||||
snapshot_project!(project);
|
||||
|
||||
with_escaped_paths(|| {
|
||||
assert_ron_snapshot!(&project, @r#"
|
||||
ProjectMetadata(
|
||||
name: Name("backend"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
// Discovering the same package from a subdirectory should give the same result
|
||||
let from_src = ProjectMetadata::discover(&root.join("db"), &system)
|
||||
@@ -362,7 +458,19 @@ expected `.`, `]`
|
||||
|
||||
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
|
||||
|
||||
snapshot_project!(sub_project);
|
||||
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"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -400,7 +508,19 @@ expected `.`, `]`
|
||||
|
||||
let root = ProjectMetadata::discover(&root, &system)?;
|
||||
|
||||
snapshot_project!(root);
|
||||
with_escaped_paths(|| {
|
||||
assert_ron_snapshot!(root, @r#"
|
||||
ProjectMetadata(
|
||||
name: Name("project-root"),
|
||||
root: "/app",
|
||||
options: Options(
|
||||
src: Some(SrcOptions(
|
||||
root: Some("src"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -432,7 +552,15 @@ expected `.`, `]`
|
||||
|
||||
let sub_project = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
|
||||
|
||||
snapshot_project!(sub_project);
|
||||
with_escaped_paths(|| {
|
||||
assert_ron_snapshot!(sub_project, @r#"
|
||||
ProjectMetadata(
|
||||
name: Name("nested-project"),
|
||||
root: "/app/packages/a",
|
||||
options: Options(),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -467,7 +595,19 @@ expected `.`, `]`
|
||||
|
||||
let root = ProjectMetadata::discover(&root.join("packages/a"), &system)?;
|
||||
|
||||
snapshot_project!(root);
|
||||
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"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
"#);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -487,27 +627,304 @@ 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)?;
|
||||
|
||||
snapshot_project!(root);
|
||||
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");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -517,15 +934,12 @@ expected `.`, `]`
|
||||
assert_eq!(error.to_string().replace('\\', "/"), message);
|
||||
}
|
||||
|
||||
/// 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("\\", "/")
|
||||
}),
|
||||
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('\\', "/")
|
||||
});
|
||||
}};
|
||||
}
|
||||
|
||||
settings.bind(f)
|
||||
}
|
||||
}
|
||||
|
||||
69
crates/red_knot_project/src/metadata/configuration_file.rs
Normal file
69
crates/red_knot_project/src/metadata/configuration_file.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
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,
|
||||
},
|
||||
}
|
||||
@@ -4,17 +4,18 @@ use red_knot_python_semantic::lint::{GetLintError, Level, LintSource, RuleSelect
|
||||
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::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{System, SystemPath};
|
||||
use ruff_macros::Combine;
|
||||
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,6 +31,9 @@ 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 {
|
||||
@@ -110,7 +114,22 @@ impl Options {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn to_rule_selection(&self, db: &dyn Db) -> (RuleSelection, Vec<OptionDiagnostic>) {
|
||||
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>) {
|
||||
let registry = db.lint_registry();
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
@@ -169,7 +188,14 @@ impl Options {
|
||||
),
|
||||
};
|
||||
|
||||
diagnostics.push(diagnostic.with_file(file).with_range(rule_name.range()));
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -244,6 +270,16 @@ 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;
|
||||
@@ -318,8 +354,7 @@ pub struct OptionDiagnostic {
|
||||
id: DiagnosticId,
|
||||
message: String,
|
||||
severity: Severity,
|
||||
file: Option<File>,
|
||||
range: Option<TextRange>,
|
||||
span: Option<Span>,
|
||||
}
|
||||
|
||||
impl OptionDiagnostic {
|
||||
@@ -328,21 +363,13 @@ impl OptionDiagnostic {
|
||||
id,
|
||||
message,
|
||||
severity,
|
||||
file: None,
|
||||
range: None,
|
||||
span: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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
|
||||
fn with_span(self, span: Option<Span>) -> Self {
|
||||
OptionDiagnostic { span, ..self }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -355,12 +382,8 @@ impl Diagnostic for OptionDiagnostic {
|
||||
Cow::Borrowed(&self.message)
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
self.range
|
||||
fn span(&self) -> Option<Span> {
|
||||
self.span.clone()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use pep440_rs::{Version, VersionSpecifiers};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::ops::Deref;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::metadata::options::Options;
|
||||
use crate::metadata::value::{RangedValue, ValueSource, ValueSourceGuard};
|
||||
use pep440_rs::{release_specifiers_to_ranges, Version, VersionSpecifiers};
|
||||
use red_knot_python_semantic::PythonVersion;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::Bound;
|
||||
use std::ops::Deref;
|
||||
use thiserror::Error;
|
||||
|
||||
/// A `pyproject.toml` as specified in PEP 517.
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone)]
|
||||
@@ -55,6 +56,73 @@ 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 {
|
||||
|
||||
53
crates/red_knot_project/src/metadata/settings.rs
Normal file
53
crates/red_knot_project/src/metadata/settings.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
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,
|
||||
}
|
||||
@@ -118,6 +118,15 @@ 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
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
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"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
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"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
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"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: sub_project
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("nested-project"),
|
||||
root: "/app/packages/a",
|
||||
options: Options(),
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
---
|
||||
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"),
|
||||
)),
|
||||
),
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: project
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("backend"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
@@ -1,9 +0,0 @@
|
||||
---
|
||||
source: crates/red_knot_project/src/metadata.rs
|
||||
expression: project
|
||||
---
|
||||
ProjectMetadata(
|
||||
name: Name("app"),
|
||||
root: "/app",
|
||||
options: Options(),
|
||||
)
|
||||
@@ -73,6 +73,13 @@ 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(
|
||||
@@ -83,8 +90,11 @@ 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.
|
||||
for path in std::iter::once(project_path).chain(unique_module_paths) {
|
||||
// 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)
|
||||
{
|
||||
// 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) {
|
||||
|
||||
@@ -12,9 +12,9 @@ license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_index = { workspace = true, features = ["salsa"] }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["salsa"] }
|
||||
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 }
|
||||
salsa = { workspace = true, features = ["compact_str"] }
|
||||
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 = ["os", "testing"] }
|
||||
ruff_db = { workspace = true, features = ["testing", "os"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
This directory contains user-facing documentation, but also doubles as an extended test suite that
|
||||
makes sure that our documentation stays up to date.
|
||||
@@ -0,0 +1,125 @@
|
||||
# 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
|
||||
@@ -37,6 +37,31 @@ 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
|
||||
@@ -120,8 +145,8 @@ Simplifications still apply when `Unknown` is present.
|
||||
```py
|
||||
from knot_extensions import Unknown
|
||||
|
||||
def _(u1: str | Unknown | int | object):
|
||||
reveal_type(u1) # revealed: Unknown | object
|
||||
def _(u1: int | Unknown | bool) -> None:
|
||||
reveal_type(u1) # revealed: int | Unknown
|
||||
```
|
||||
|
||||
## Union of intersections
|
||||
|
||||
@@ -12,14 +12,32 @@ use ruff_db::parsed::ParsedModule;
|
||||
/// Holding on to any [`AstNodeRef`] prevents the [`ParsedModule`] from being released.
|
||||
///
|
||||
/// ## Equality
|
||||
/// Two `AstNodeRef` are considered equal if their wrapped nodes are equal.
|
||||
/// 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.
|
||||
#[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>,
|
||||
@@ -37,7 +55,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),
|
||||
}
|
||||
}
|
||||
@@ -72,7 +90,14 @@ where
|
||||
T: PartialEq,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.node().eq(other.node())
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +112,20 @@ 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)]
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::lint::{LintRegistry, RuleSelection};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
@@ -7,7 +9,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) -> &RuleSelection;
|
||||
fn rule_selection(&self) -> Arc<RuleSelection>;
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry;
|
||||
}
|
||||
@@ -111,8 +113,8 @@ pub(crate) mod tests {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -133,7 +133,7 @@ pub(crate) fn search_paths(db: &dyn Db) -> SearchPathIterator {
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct SearchPaths {
|
||||
pub 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>,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
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;
|
||||
@@ -123,7 +124,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)]
|
||||
#[derive(Debug, Update)]
|
||||
pub(crate) struct SemanticIndex<'db> {
|
||||
/// List of all symbol tables in this file, indexed by scope.
|
||||
symbol_tables: IndexVec<FileScopeId, Arc<SymbolTable>>,
|
||||
|
||||
@@ -24,7 +24,7 @@ use crate::Db;
|
||||
///
|
||||
/// x = foo()
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, salsa::Update)]
|
||||
pub(crate) struct AstIds {
|
||||
/// Maps expressions to their expression id.
|
||||
expressions_map: FxHashMap<ExpressionNodeKey, ScopedExpressionId>,
|
||||
@@ -74,6 +74,7 @@ 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 {
|
||||
@@ -181,7 +182,7 @@ pub(crate) mod node_key {
|
||||
|
||||
use crate::node_key::NodeKey;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)]
|
||||
pub(crate) struct ExpressionNodeKey(NodeKey);
|
||||
|
||||
impl From<ast::ExprRef<'_>> for ExpressionNodeKey {
|
||||
|
||||
@@ -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)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) enum AttributeAssignment<'db> {
|
||||
/// An attribute assignment with an explicit type annotation, either
|
||||
/// `self.x: <annotation>` or `self.x: <annotation> = …`.
|
||||
|
||||
@@ -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)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct Constraint<'db> {
|
||||
pub(crate) node: ConstraintNode<'db>,
|
||||
pub(crate) is_positive: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update)]
|
||||
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)]
|
||||
#[derive(Debug, Clone, Hash, PartialEq, salsa::Update)]
|
||||
pub(crate) enum PatternConstraintKind<'db> {
|
||||
Singleton(Singleton, Option<Expression<'db>>),
|
||||
Value(Expression<'db>, Option<Expression<'db>>),
|
||||
@@ -28,21 +28,15 @@ 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>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -25,22 +25,19 @@ 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>>,
|
||||
}
|
||||
|
||||
@@ -435,6 +432,13 @@ 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(AstNodeRef<ast::Alias>),
|
||||
@@ -540,7 +544,7 @@ impl DefinitionKind<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Hash)]
|
||||
pub(crate) enum TargetKind<'db> {
|
||||
Sequence(Unpack<'db>),
|
||||
Name,
|
||||
@@ -713,7 +717,7 @@ impl ExceptHandlerDefinitionKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, salsa::Update)]
|
||||
pub(crate) struct DefinitionNodeKey(NodeKey);
|
||||
|
||||
impl From<&ast::Alias> for DefinitionNodeKey {
|
||||
|
||||
@@ -33,23 +33,20 @@ 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>>,
|
||||
}
|
||||
|
||||
|
||||
@@ -96,18 +96,16 @@ 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>>,
|
||||
}
|
||||
|
||||
@@ -159,6 +157,7 @@ impl<'db> ScopeId<'db> {
|
||||
|
||||
/// ID that uniquely identifies a scope inside of a module.
|
||||
#[newtype_index]
|
||||
#[derive(salsa::Update)]
|
||||
pub struct FileScopeId;
|
||||
|
||||
impl FileScopeId {
|
||||
@@ -177,7 +176,7 @@ impl FileScopeId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, salsa::Update)]
|
||||
pub struct Scope {
|
||||
pub(super) parent: Option<FileScopeId>,
|
||||
pub(super) node: NodeWithScopeKind,
|
||||
@@ -216,7 +215,7 @@ impl ScopeKind {
|
||||
}
|
||||
|
||||
/// Symbol table for a specific [`Scope`].
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, salsa::Update)]
|
||||
pub struct SymbolTable {
|
||||
/// The symbols in this scope.
|
||||
symbols: IndexVec<ScopedSymbolId, Symbol>,
|
||||
@@ -424,7 +423,7 @@ impl NodeWithScopeRef<'_> {
|
||||
}
|
||||
|
||||
/// Node that introduces a new scope.
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, salsa::Update)]
|
||||
pub enum NodeWithScopeKind {
|
||||
Module,
|
||||
Class(AstNodeRef<ast::StmtClassDef>),
|
||||
|
||||
@@ -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)]
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
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)]
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
enum SymbolDefinitions {
|
||||
Bindings(SymbolBindings),
|
||||
Declarations(SymbolDeclarations),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{
|
||||
types::{Type, UnionType},
|
||||
types::{todo_type, Type, UnionType},
|
||||
Db,
|
||||
};
|
||||
|
||||
@@ -26,13 +26,25 @@ pub(crate) enum Boundness {
|
||||
/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
|
||||
/// non_existent: Symbol::Unbound,
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
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)]
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ use context::InferContext;
|
||||
use diagnostic::{report_not_iterable, report_not_iterable_possibly_unbound};
|
||||
use indexmap::IndexSet;
|
||||
use itertools::Itertools;
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
use type_ordering::union_elements_ordering;
|
||||
@@ -36,7 +35,7 @@ use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symb
|
||||
use crate::suppression::check_suppressions;
|
||||
use crate::symbol::{Boundness, Symbol};
|
||||
use crate::types::call::{
|
||||
bind_call, CallArguments, CallBinding, CallDunderResult, CallOutcome, StaticAssertionErrorKind,
|
||||
bind_call, CallArguments, CallBinding, CallDunderLenOutcome, CallDunderOutcome, CallOutcome,
|
||||
};
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::diagnostic::INVALID_TYPE_FORM;
|
||||
@@ -56,7 +55,6 @@ mod mro;
|
||||
mod narrow;
|
||||
mod signatures;
|
||||
mod slots;
|
||||
mod statistics;
|
||||
mod string_annotation;
|
||||
mod subclass_of;
|
||||
mod type_ordering;
|
||||
@@ -144,7 +142,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
}
|
||||
// Symbol is possibly undeclared and (possibly) bound
|
||||
Symbol::Type(inferred_ty, boundness) => Symbol::Type(
|
||||
UnionType::from_elements(db, [inferred_ty, declared_ty].iter().copied()),
|
||||
UnionType::from_elements(db, [inferred_ty, declared_ty]),
|
||||
boundness,
|
||||
),
|
||||
}
|
||||
@@ -160,7 +158,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
Err((declared_ty, _)) => {
|
||||
// Intentionally ignore conflicting declared types; that's not our problem,
|
||||
// it's the problem of the module we are importing from.
|
||||
declared_ty.inner_type().into()
|
||||
Symbol::bound(declared_ty.inner_type())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +186,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
&& file_to_module(db, scope.file(db))
|
||||
.is_some_and(|module| module.is_known(KnownModule::Typing))
|
||||
{
|
||||
return Symbol::Type(Type::BooleanLiteral(true), Boundness::Bound);
|
||||
return Symbol::bound(Type::BooleanLiteral(true));
|
||||
}
|
||||
if name == "platform"
|
||||
&& file_to_module(db, scope.file(db))
|
||||
@@ -196,10 +194,7 @@ fn symbol<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Symbol<'db>
|
||||
{
|
||||
match Program::get(db).python_platform(db) {
|
||||
crate::PythonPlatform::Identifier(platform) => {
|
||||
return Symbol::Type(
|
||||
Type::StringLiteral(StringLiteralType::new(db, platform.as_str())),
|
||||
Boundness::Bound,
|
||||
);
|
||||
return Symbol::bound(Type::string_literal(db, platform.as_str()));
|
||||
}
|
||||
crate::PythonPlatform::All => {
|
||||
// Fall through to the looked up type
|
||||
@@ -402,9 +397,16 @@ fn symbol_from_bindings<'db>(
|
||||
/// If we look up the declared type of `variable` in the scope of class `C`, we will get
|
||||
/// the type `int`, a "declaredness" of [`Boundness::PossiblyUnbound`], and the information
|
||||
/// that this comes with a [`TypeQualifiers::CLASS_VAR`] type qualifier.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SymbolAndQualifiers<'db>(Symbol<'db>, TypeQualifiers);
|
||||
|
||||
impl SymbolAndQualifiers<'_> {
|
||||
/// Constructor that creates a [`SymbolAndQualifiers`] instance with a [`TodoType`] type
|
||||
/// and no qualifiers.
|
||||
fn todo(message: &'static str) -> Self {
|
||||
Self(Symbol::todo(message), TypeQualifiers::empty())
|
||||
}
|
||||
|
||||
fn is_class_var(&self) -> bool {
|
||||
self.1.contains(TypeQualifiers::CLASS_VAR)
|
||||
}
|
||||
@@ -420,12 +422,6 @@ impl<'db> From<Symbol<'db>> for SymbolAndQualifiers<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Type<'db>> for SymbolAndQualifiers<'db> {
|
||||
fn from(ty: Type<'db>) -> Self {
|
||||
SymbolAndQualifiers(ty.into(), TypeQualifiers::empty())
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of looking up a declared type from declarations; see [`symbol_from_declarations`].
|
||||
type SymbolFromDeclarationsResult<'db> =
|
||||
Result<SymbolAndQualifiers<'db>, (TypeAndQualifiers<'db>, Box<[Type<'db>]>)>;
|
||||
@@ -561,6 +557,11 @@ macro_rules! todo_type {
|
||||
$crate::types::TodoType::Message($message),
|
||||
))
|
||||
};
|
||||
($message:ident) => {
|
||||
$crate::types::Type::Dynamic($crate::types::DynamicType::Todo(
|
||||
$crate::types::TodoType::Message($message),
|
||||
))
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
@@ -571,6 +572,9 @@ macro_rules! todo_type {
|
||||
($message:literal) => {
|
||||
$crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType))
|
||||
};
|
||||
($message:ident) => {
|
||||
$crate::types::Type::Dynamic($crate::types::DynamicType::Todo(crate::types::TodoType))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use todo_type;
|
||||
@@ -631,6 +635,10 @@ impl<'db> Type<'db> {
|
||||
Self::Dynamic(DynamicType::Unknown)
|
||||
}
|
||||
|
||||
pub fn object(db: &'db dyn Db) -> Self {
|
||||
KnownClass::Object.to_instance(db)
|
||||
}
|
||||
|
||||
pub const fn is_unknown(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(DynamicType::Unknown))
|
||||
}
|
||||
@@ -639,6 +647,11 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::Never)
|
||||
}
|
||||
|
||||
pub fn is_object(&self, db: &'db dyn Db) -> bool {
|
||||
self.into_instance()
|
||||
.is_some_and(|instance| instance.class.is_object(db))
|
||||
}
|
||||
|
||||
pub const fn is_todo(&self) -> bool {
|
||||
matches!(self, Type::Dynamic(DynamicType::Todo(_)))
|
||||
}
|
||||
@@ -895,7 +908,7 @@ impl<'db> Type<'db> {
|
||||
// `object` is the only type that can be known to be a supertype of any intersection,
|
||||
// even an intersection with no positive elements
|
||||
(Type::Intersection(_), Type::Instance(InstanceType { class }))
|
||||
if class.is_known(db, KnownClass::Object) =>
|
||||
if class.is_object(db) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
@@ -949,7 +962,7 @@ impl<'db> Type<'db> {
|
||||
(left, Type::AlwaysTruthy) => left.bool(db).is_always_true(),
|
||||
// Currently, the only supertype of `AlwaysFalsy` and `AlwaysTruthy` is the universal set (object instance).
|
||||
(Type::AlwaysFalsy | Type::AlwaysTruthy, _) => {
|
||||
target.is_equivalent_to(db, KnownClass::Object.to_instance(db))
|
||||
target.is_equivalent_to(db, Type::object(db))
|
||||
}
|
||||
|
||||
// All `StringLiteral` types are a subtype of `LiteralString`.
|
||||
@@ -1088,11 +1101,7 @@ impl<'db> Type<'db> {
|
||||
|
||||
// All types are assignable to `object`.
|
||||
// TODO this special case might be removable once the below cases are comprehensive
|
||||
(_, Type::Instance(InstanceType { class }))
|
||||
if class.is_known(db, KnownClass::Object) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(_, Type::Instance(InstanceType { class })) if class.is_object(db) => true,
|
||||
|
||||
// A union is assignable to a type T iff every element of the union is assignable to T.
|
||||
(Type::Union(union), ty) => union
|
||||
@@ -1684,17 +1693,17 @@ impl<'db> Type<'db> {
|
||||
#[must_use]
|
||||
pub(crate) fn member(&self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
if name == "__class__" {
|
||||
return self.to_meta_type(db).into();
|
||||
return Symbol::bound(self.to_meta_type(db));
|
||||
}
|
||||
|
||||
match self {
|
||||
Type::Dynamic(_) => self.into(),
|
||||
Type::Dynamic(_) => Symbol::bound(self),
|
||||
|
||||
Type::Never => todo_type!("attribute lookup on Never").into(),
|
||||
Type::Never => Symbol::todo("attribute lookup on Never"),
|
||||
|
||||
Type::FunctionLiteral(_) => match name {
|
||||
"__get__" => todo_type!("`__get__` method on functions").into(),
|
||||
"__call__" => todo_type!("`__call__` method on functions").into(),
|
||||
"__get__" => Symbol::todo("`__get__` method on functions"),
|
||||
"__call__" => Symbol::todo("`__call__` method on functions"),
|
||||
_ => KnownClass::FunctionType.to_instance(db).member(db, name),
|
||||
},
|
||||
|
||||
@@ -1707,12 +1716,12 @@ impl<'db> Type<'db> {
|
||||
Type::KnownInstance(known_instance) => known_instance.member(db, name),
|
||||
|
||||
Type::Instance(InstanceType { class }) => match (class.known(db), name) {
|
||||
(Some(KnownClass::VersionInfo), "major") => {
|
||||
Type::IntLiteral(Program::get(db).python_version(db).major.into()).into()
|
||||
}
|
||||
(Some(KnownClass::VersionInfo), "minor") => {
|
||||
Type::IntLiteral(Program::get(db).python_version(db).minor.into()).into()
|
||||
}
|
||||
(Some(KnownClass::VersionInfo), "major") => Symbol::bound(Type::IntLiteral(
|
||||
Program::get(db).python_version(db).major.into(),
|
||||
)),
|
||||
(Some(KnownClass::VersionInfo), "minor") => Symbol::bound(Type::IntLiteral(
|
||||
Program::get(db).python_version(db).minor.into(),
|
||||
)),
|
||||
_ => {
|
||||
let SymbolAndQualifiers(symbol, _) = class.instance_member(db, name);
|
||||
symbol
|
||||
@@ -1758,30 +1767,30 @@ impl<'db> Type<'db> {
|
||||
Type::Intersection(_) => {
|
||||
// TODO perform the get_member on each type in the intersection
|
||||
// TODO return the intersection of those results
|
||||
todo_type!("Attribute access on `Intersection` types").into()
|
||||
Symbol::todo("Attribute access on `Intersection` types")
|
||||
}
|
||||
|
||||
Type::IntLiteral(_) => match name {
|
||||
"real" | "numerator" => self.into(),
|
||||
"real" | "numerator" => Symbol::bound(self),
|
||||
// TODO more attributes could probably be usefully special-cased
|
||||
_ => KnownClass::Int.to_instance(db).member(db, name),
|
||||
},
|
||||
|
||||
Type::BooleanLiteral(bool_value) => match name {
|
||||
"real" | "numerator" => Type::IntLiteral(i64::from(*bool_value)).into(),
|
||||
"real" | "numerator" => Symbol::bound(Type::IntLiteral(i64::from(*bool_value))),
|
||||
_ => KnownClass::Bool.to_instance(db).member(db, name),
|
||||
},
|
||||
|
||||
Type::StringLiteral(_) => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
todo_type!("Attribute access on `StringLiteral` types").into()
|
||||
Symbol::todo("Attribute access on `StringLiteral` types")
|
||||
}
|
||||
|
||||
Type::LiteralString => {
|
||||
// TODO defer to `typing.LiteralString`/`builtins.str` methods
|
||||
// from typeshed's stubs
|
||||
todo_type!("Attribute access on `LiteralString` types").into()
|
||||
Symbol::todo("Attribute access on `LiteralString` types")
|
||||
}
|
||||
|
||||
Type::BytesLiteral(_) => KnownClass::Bytes.to_instance(db).member(db, name),
|
||||
@@ -1793,15 +1802,15 @@ impl<'db> Type<'db> {
|
||||
|
||||
Type::Tuple(_) => {
|
||||
// TODO: implement tuple methods
|
||||
todo_type!("Attribute access on heterogeneous tuple types").into()
|
||||
Symbol::todo("Attribute access on heterogeneous tuple types")
|
||||
}
|
||||
|
||||
Type::AlwaysTruthy | Type::AlwaysFalsy => match name {
|
||||
"__bool__" => {
|
||||
// TODO should be `Callable[[], Literal[True/False]]`
|
||||
todo_type!("`__bool__` for `AlwaysTruthy`/`AlwaysFalsy` Type variants").into()
|
||||
Symbol::todo("`__bool__` for `AlwaysTruthy`/`AlwaysFalsy` Type variants")
|
||||
}
|
||||
_ => KnownClass::Object.to_instance(db).member(db, name),
|
||||
_ => Type::object(db).member(db, name),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1882,21 +1891,40 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the type of `len()` on a type if it is known more precisely than `int`,
|
||||
/// Return the type of calling the `__len__` method on a type.
|
||||
fn __len__(&self, db: &'db dyn Db) -> CallDunderLenOutcome<'db> {
|
||||
let statically_known_len = match self {
|
||||
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
|
||||
Type::StringLiteral(string) => Some(string.python_len(db)),
|
||||
Type::Tuple(tuple) => Some(tuple.len(db)),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(len) = statically_known_len {
|
||||
return CallDunderLenOutcome::StaticallyKnown(len);
|
||||
}
|
||||
|
||||
match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) {
|
||||
CallDunderOutcome::MethodNotAvailable => CallDunderLenOutcome::MethodNotAvailable,
|
||||
CallDunderOutcome::Call(outcome) => CallDunderLenOutcome::Call(outcome),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the type of `len(type)` on a type if it is known more precisely than `int`,
|
||||
/// or `None` otherwise.
|
||||
///
|
||||
/// In the second case, the return type of `len()` in `typeshed` (`int`)
|
||||
/// is used as a fallback.
|
||||
#[must_use]
|
||||
fn len(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
fn non_negative_int_literal<'db>(db: &'db dyn Db, ty: Type<'db>) -> Option<Type<'db>> {
|
||||
match ty {
|
||||
// TODO: Emit diagnostic for non-integers and negative integers
|
||||
Type::IntLiteral(value) => (value >= 0).then_some(ty),
|
||||
Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())),
|
||||
Type::Union(union) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in union.elements(db) {
|
||||
builder = builder.add(non_negative_int_literal(db, *element)?);
|
||||
for &element in union.elements(db) {
|
||||
builder = builder.add(non_negative_int_literal(db, element)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
@@ -1904,70 +1932,17 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
let usize_len = match self {
|
||||
Type::BytesLiteral(bytes) => Some(bytes.python_len(db)),
|
||||
Type::StringLiteral(string) => Some(string.python_len(db)),
|
||||
Type::Tuple(tuple) => Some(tuple.len(db)),
|
||||
_ => None,
|
||||
};
|
||||
let len = self.__len__(db).return_type(db)?;
|
||||
|
||||
if let Some(usize_len) = usize_len {
|
||||
return usize_len.try_into().ok().map(Type::IntLiteral);
|
||||
}
|
||||
|
||||
let return_ty = match self.call_dunder(db, "__len__", &CallArguments::positional([*self])) {
|
||||
// TODO: emit a diagnostic
|
||||
CallDunderResult::MethodNotAvailable => return None,
|
||||
|
||||
CallDunderResult::CallOutcome(outcome) | CallDunderResult::PossiblyUnbound(outcome) => {
|
||||
outcome.return_type(db)?
|
||||
}
|
||||
};
|
||||
|
||||
non_negative_int_literal(db, return_ty)
|
||||
non_negative_int_literal(db, len)
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an object of this type.
|
||||
#[must_use]
|
||||
fn call(self, db: &'db dyn Db, arguments: &CallArguments<'_, 'db>) -> CallOutcome<'db> {
|
||||
match self {
|
||||
Type::FunctionLiteral(function_type) => {
|
||||
let mut binding = bind_call(db, arguments, function_type.signature(db), Some(self));
|
||||
match function_type.known(db) {
|
||||
Some(KnownFunction::RevealType) => {
|
||||
let revealed_ty = binding.one_parameter_type().unwrap_or(Type::unknown());
|
||||
CallOutcome::revealed(binding, revealed_ty)
|
||||
}
|
||||
Some(KnownFunction::StaticAssert) => {
|
||||
if let Some((parameter_ty, message)) = binding.two_parameter_types() {
|
||||
let truthiness = parameter_ty.bool(db);
|
||||
|
||||
if truthiness.is_always_true() {
|
||||
CallOutcome::callable(binding)
|
||||
} else {
|
||||
let error_kind = if let Some(message) =
|
||||
message.into_string_literal().map(|s| &**s.value(db))
|
||||
{
|
||||
StaticAssertionErrorKind::CustomError(message)
|
||||
} else if parameter_ty == Type::BooleanLiteral(false) {
|
||||
StaticAssertionErrorKind::ArgumentIsFalse
|
||||
} else if truthiness.is_always_false() {
|
||||
StaticAssertionErrorKind::ArgumentIsFalsy(parameter_ty)
|
||||
} else {
|
||||
StaticAssertionErrorKind::ArgumentTruthinessIsAmbiguous(
|
||||
parameter_ty,
|
||||
)
|
||||
};
|
||||
|
||||
CallOutcome::StaticAssertionError {
|
||||
binding,
|
||||
error_kind,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
}
|
||||
Some(KnownFunction::IsEquivalentTo) => {
|
||||
let (ty_a, ty_b) = binding
|
||||
.two_parameter_types()
|
||||
@@ -2042,14 +2017,6 @@ impl<'db> Type<'db> {
|
||||
CallOutcome::callable(binding)
|
||||
}
|
||||
|
||||
Some(KnownFunction::AssertType) => {
|
||||
let Some((_, asserted_ty)) = binding.two_parameter_types() else {
|
||||
return CallOutcome::callable(binding);
|
||||
};
|
||||
|
||||
CallOutcome::asserted(binding, asserted_ty)
|
||||
}
|
||||
|
||||
Some(KnownFunction::Cast) => {
|
||||
// TODO: Use `.two_parameter_tys()` exclusively
|
||||
// when overloads are supported.
|
||||
@@ -2091,23 +2058,26 @@ impl<'db> Type<'db> {
|
||||
|
||||
instance_ty @ Type::Instance(_) => {
|
||||
match instance_ty.call_dunder(db, "__call__", &arguments.with_self(instance_ty)) {
|
||||
CallDunderResult::CallOutcome(CallOutcome::NotCallable { .. }) => {
|
||||
CallDunderOutcome::Call(CallOutcome::NotCallable { .. }) => {
|
||||
// Turn "`<type of illegal '__call__'>` not callable" into
|
||||
// "`X` not callable"
|
||||
CallOutcome::NotCallable {
|
||||
not_callable_ty: self,
|
||||
}
|
||||
}
|
||||
CallDunderResult::CallOutcome(outcome) => outcome,
|
||||
CallDunderResult::PossiblyUnbound(call_outcome) => {
|
||||
// Turn "possibly unbound object of type `Literal['__call__']`"
|
||||
// into "`X` not callable (possibly unbound `__call__` method)"
|
||||
CallOutcome::PossiblyUnboundDunderCall {
|
||||
called_ty: self,
|
||||
call_outcome: Box::new(call_outcome),
|
||||
}
|
||||
}
|
||||
CallDunderResult::MethodNotAvailable => {
|
||||
// Turn "possibly unbound object of type `Literal['__call__']`"
|
||||
// into "`X` not callable (possibly unbound `__call__` method)"
|
||||
CallDunderOutcome::Call(CallOutcome::PossiblyUnboundDunderCall {
|
||||
called_ty: _,
|
||||
call_outcome,
|
||||
}) => CallOutcome::PossiblyUnboundDunderCall {
|
||||
called_ty: self,
|
||||
call_outcome,
|
||||
},
|
||||
|
||||
CallDunderOutcome::Call(outcome) => outcome,
|
||||
|
||||
CallDunderOutcome::MethodNotAvailable => {
|
||||
// Turn "`X.__call__` unbound" into "`X` not callable"
|
||||
CallOutcome::NotCallable {
|
||||
not_callable_ty: self,
|
||||
@@ -2141,7 +2111,6 @@ impl<'db> Type<'db> {
|
||||
/// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`.
|
||||
///
|
||||
/// TODO: handle `super()` objects properly
|
||||
#[must_use]
|
||||
fn call_bound(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
@@ -2187,15 +2156,18 @@ impl<'db> Type<'db> {
|
||||
db: &'db dyn Db,
|
||||
name: &str,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> CallDunderResult<'db> {
|
||||
) -> CallDunderOutcome<'db> {
|
||||
match self.to_meta_type(db).member(db, name) {
|
||||
Symbol::Type(callable_ty, Boundness::Bound) => {
|
||||
CallDunderResult::CallOutcome(callable_ty.call(db, arguments))
|
||||
CallDunderOutcome::Call(callable_ty.call(db, arguments))
|
||||
}
|
||||
Symbol::Type(callable_ty, Boundness::PossiblyUnbound) => {
|
||||
CallDunderResult::PossiblyUnbound(callable_ty.call(db, arguments))
|
||||
CallDunderOutcome::Call(CallOutcome::PossiblyUnboundDunderCall {
|
||||
call_outcome: Box::new(callable_ty.call(db, arguments)),
|
||||
called_ty: callable_ty,
|
||||
})
|
||||
}
|
||||
Symbol::Unbound => CallDunderResult::MethodNotAvailable,
|
||||
Symbol::Unbound => CallDunderOutcome::MethodNotAvailable,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2217,8 +2189,7 @@ impl<'db> Type<'db> {
|
||||
let dunder_iter_result =
|
||||
self.call_dunder(db, "__iter__", &CallArguments::positional([self]));
|
||||
match dunder_iter_result {
|
||||
CallDunderResult::CallOutcome(ref call_outcome)
|
||||
| CallDunderResult::PossiblyUnbound(ref call_outcome) => {
|
||||
CallDunderOutcome::Call(ref call_outcome) => {
|
||||
let Some(iterator_ty) = call_outcome.return_type(db) else {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: self,
|
||||
@@ -2229,7 +2200,7 @@ impl<'db> Type<'db> {
|
||||
.call_dunder(db, "__next__", &CallArguments::positional([iterator_ty]))
|
||||
.return_type(db)
|
||||
{
|
||||
if matches!(dunder_iter_result, CallDunderResult::PossiblyUnbound(..)) {
|
||||
if call_outcome.is_possibly_unbound() {
|
||||
IterationOutcome::PossiblyUnboundDunderIter {
|
||||
iterable_ty: self,
|
||||
element_ty,
|
||||
@@ -2243,7 +2214,7 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
};
|
||||
}
|
||||
CallDunderResult::MethodNotAvailable => {}
|
||||
CallDunderOutcome::MethodNotAvailable => {}
|
||||
}
|
||||
|
||||
// Although it's not considered great practice,
|
||||
@@ -2524,18 +2495,6 @@ impl<'db> From<&Type<'db>> for Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<Type<'db>> for Symbol<'db> {
|
||||
fn from(value: Type<'db>) -> Self {
|
||||
Symbol::Type(value, Boundness::Bound)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<&Type<'db>> for Symbol<'db> {
|
||||
fn from(value: &Type<'db>) -> Self {
|
||||
Self::from(*value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum DynamicType {
|
||||
// An explicitly annotated `typing.Any`
|
||||
@@ -2568,7 +2527,7 @@ impl std::fmt::Display for DynamicType {
|
||||
|
||||
bitflags! {
|
||||
/// Type qualifiers that appear in an annotation expression.
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
|
||||
pub(crate) struct TypeQualifiers: u8 {
|
||||
/// `typing.ClassVar`
|
||||
const CLASS_VAR = 1 << 0;
|
||||
@@ -2584,7 +2543,7 @@ bitflags! {
|
||||
///
|
||||
/// Example: `Annotated[ClassVar[tuple[int]], "metadata"]` would have type `tuple[int]` and the
|
||||
/// qualifier `ClassVar`.
|
||||
#[derive(Clone, Debug, Copy, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Copy, Eq, PartialEq, salsa::Update)]
|
||||
pub(crate) struct TypeAndQualifiers<'db> {
|
||||
inner: Type<'db>,
|
||||
qualifiers: TypeQualifiers,
|
||||
@@ -2595,6 +2554,14 @@ impl<'db> TypeAndQualifiers<'db> {
|
||||
Self { inner, qualifiers }
|
||||
}
|
||||
|
||||
/// Constructor that creates a [`TypeAndQualifiers`] instance with type `Unknown` and no qualifiers.
|
||||
pub(crate) fn unknown() -> Self {
|
||||
Self {
|
||||
inner: Type::unknown(),
|
||||
qualifiers: TypeQualifiers::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Forget about type qualifiers and only return the inner type.
|
||||
pub(crate) fn inner_type(&self) -> Type<'db> {
|
||||
self.inner
|
||||
@@ -3326,7 +3293,7 @@ impl<'db> KnownInstanceType<'db> {
|
||||
(Self::TypeAliasType(alias), "__name__") => Type::string_literal(db, alias.name(db)),
|
||||
_ => return self.instance_fallback(db).member(db, name),
|
||||
};
|
||||
ty.into()
|
||||
Symbol::bound(ty)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3784,8 +3751,7 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
full_submodule_name.extend(&submodule_name);
|
||||
if imported_submodules.contains(&full_submodule_name) {
|
||||
if let Some(submodule) = resolve_module(db, &full_submodule_name) {
|
||||
let submodule_ty = Type::module_literal(db, importing_file, submodule);
|
||||
return Symbol::Type(submodule_ty, Boundness::Bound);
|
||||
return Symbol::bound(Type::module_literal(db, importing_file, submodule));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3853,6 +3819,11 @@ impl<'db> Class<'db> {
|
||||
self.known(db) == Some(known_class)
|
||||
}
|
||||
|
||||
/// Return `true` if this class represents the builtin class `object`
|
||||
pub fn is_object(self, db: &'db dyn Db) -> bool {
|
||||
self.is_known(db, KnownClass::Object)
|
||||
}
|
||||
|
||||
/// Return an iterator over the inferred types of this class's *explicit* bases.
|
||||
///
|
||||
/// Note that any class (except for `object`) that has no explicit
|
||||
@@ -4064,10 +4035,7 @@ impl<'db> Class<'db> {
|
||||
|
||||
// TODO we should also check for binding errors that would indicate the metaclass
|
||||
// does not accept the right arguments
|
||||
CallOutcome::Callable { binding }
|
||||
| CallOutcome::RevealType { binding, .. }
|
||||
| CallOutcome::StaticAssertionError { binding, .. }
|
||||
| CallOutcome::AssertType { binding, .. } => Ok(binding.return_type()),
|
||||
CallOutcome::Callable { binding } => Ok(binding.return_type()),
|
||||
};
|
||||
|
||||
return return_ty_result.map(|ty| ty.to_meta_type(db));
|
||||
@@ -4114,7 +4082,7 @@ impl<'db> Class<'db> {
|
||||
pub(crate) fn class_member(self, db: &'db dyn Db, name: &str) -> Symbol<'db> {
|
||||
if name == "__mro__" {
|
||||
let tuple_elements = self.iter_mro(db).map(Type::from);
|
||||
return TupleType::from_elements(db, tuple_elements).into();
|
||||
return Symbol::bound(TupleType::from_elements(db, tuple_elements));
|
||||
}
|
||||
|
||||
for superclass in self.iter_mro(db) {
|
||||
@@ -4154,7 +4122,9 @@ impl<'db> Class<'db> {
|
||||
for superclass in self.iter_mro(db) {
|
||||
match superclass {
|
||||
ClassBase::Dynamic(_) => {
|
||||
return todo_type!("instance attribute on class with dynamic base").into();
|
||||
return SymbolAndQualifiers::todo(
|
||||
"instance attribute on class with dynamic base",
|
||||
);
|
||||
}
|
||||
ClassBase::Class(class) => {
|
||||
if let member @ SymbolAndQualifiers(Symbol::Type(_, _), _) =
|
||||
@@ -4204,7 +4174,7 @@ impl<'db> Class<'db> {
|
||||
.and_then(|assignments| assignments.get(name))
|
||||
else {
|
||||
if inferred_type_from_class_body.is_some() {
|
||||
return union_of_inferred_types.build().into();
|
||||
return Symbol::bound(union_of_inferred_types.build());
|
||||
}
|
||||
return Symbol::Unbound;
|
||||
};
|
||||
@@ -4221,7 +4191,7 @@ impl<'db> Class<'db> {
|
||||
let annotation_ty = infer_expression_type(db, *annotation);
|
||||
|
||||
// TODO: check if there are conflicting declarations
|
||||
return annotation_ty.into();
|
||||
return Symbol::bound(annotation_ty);
|
||||
}
|
||||
AttributeAssignment::Unannotated { value } => {
|
||||
// We found an un-annotated attribute assignment of the form:
|
||||
@@ -4261,7 +4231,7 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
union_of_inferred_types.build().into()
|
||||
Symbol::bound(union_of_inferred_types.build())
|
||||
}
|
||||
|
||||
/// A helper function for `instance_member` that looks up the `name` attribute only on
|
||||
@@ -4290,12 +4260,12 @@ impl<'db> Class<'db> {
|
||||
// just a temporary heuristic to provide a broad categorization into properties
|
||||
// and non-property methods.
|
||||
if function.has_decorator(db, KnownClass::Property.to_class_literal(db)) {
|
||||
todo_type!("@property").into()
|
||||
SymbolAndQualifiers::todo("@property")
|
||||
} else {
|
||||
todo_type!("bound method").into()
|
||||
SymbolAndQualifiers::todo("bound method")
|
||||
}
|
||||
} else {
|
||||
SymbolAndQualifiers(Symbol::Type(declared_ty, Boundness::Bound), qualifiers)
|
||||
SymbolAndQualifiers(Symbol::bound(declared_ty), qualifiers)
|
||||
}
|
||||
}
|
||||
Ok(SymbolAndQualifiers(Symbol::Unbound, _)) => {
|
||||
@@ -4310,7 +4280,10 @@ impl<'db> Class<'db> {
|
||||
}
|
||||
Err((declared_ty, _conflicting_declarations)) => {
|
||||
// There are conflicting declarations for this attribute in the class body.
|
||||
SymbolAndQualifiers(declared_ty.inner_type().into(), declared_ty.qualifiers())
|
||||
SymbolAndQualifiers(
|
||||
Symbol::bound(declared_ty.inner_type()),
|
||||
declared_ty.qualifiers(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -4390,7 +4363,7 @@ impl<'db> TypeAliasType<'db> {
|
||||
}
|
||||
|
||||
/// Either the explicit `metaclass=` keyword of the class, or the inferred metaclass of one of its base classes.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct MetaclassCandidate<'db> {
|
||||
metaclass: Class<'db>,
|
||||
explicit_metaclass_of: Class<'db>,
|
||||
@@ -4433,7 +4406,7 @@ impl<'db> From<InstanceType<'db>> for Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct MetaclassError<'db> {
|
||||
kind: MetaclassErrorKind<'db>,
|
||||
}
|
||||
@@ -4445,7 +4418,7 @@ impl<'db> MetaclassError<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) enum MetaclassErrorKind<'db> {
|
||||
/// The class has incompatible metaclasses in its inheritance hierarchy.
|
||||
///
|
||||
|
||||
@@ -43,6 +43,13 @@ 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 {
|
||||
@@ -53,7 +60,12 @@ 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))
|
||||
@@ -76,7 +88,10 @@ impl<'db> UnionBuilder<'db> {
|
||||
break;
|
||||
}
|
||||
|
||||
if ty.is_same_gradual_form(*element) || ty.is_subtype_of(self.db, *element) {
|
||||
if ty.is_same_gradual_form(*element)
|
||||
|| ty.is_subtype_of(self.db, *element)
|
||||
|| element.is_object(self.db)
|
||||
{
|
||||
return self;
|
||||
} else if element.is_subtype_of(self.db, ty) {
|
||||
to_remove.push(index);
|
||||
@@ -88,9 +103,7 @@ 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`.
|
||||
self.elements.clear();
|
||||
self.elements.push(KnownClass::Object.to_instance(self.db));
|
||||
return self;
|
||||
return self.collapse_to_object();
|
||||
}
|
||||
}
|
||||
match to_remove[..] {
|
||||
@@ -416,7 +429,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
Type::Never => {
|
||||
// Adding ~Never to an intersection is a no-op.
|
||||
}
|
||||
Type::Instance(instance) if instance.class.is_known(db, KnownClass::Object) => {
|
||||
Type::Instance(instance) if instance.class.is_object(db) => {
|
||||
// Adding ~object to an intersection results in Never.
|
||||
*self = Self::default();
|
||||
self.positive.insert(Type::Never);
|
||||
@@ -481,7 +494,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
|
||||
fn build(mut self, db: &'db dyn Db) -> Type<'db> {
|
||||
match (self.positive.len(), self.negative.len()) {
|
||||
(0, 0) => KnownClass::Object.to_instance(db),
|
||||
(0, 0) => Type::object(db),
|
||||
(1, 0) => self.positive[0],
|
||||
_ => {
|
||||
self.positive.shrink_to_fit();
|
||||
@@ -534,7 +547,7 @@ mod tests {
|
||||
let db = setup_db();
|
||||
|
||||
let intersection = IntersectionBuilder::new(&db).build();
|
||||
assert_eq!(intersection, KnownClass::Object.to_instance(&db));
|
||||
assert_eq!(intersection, Type::object(&db));
|
||||
}
|
||||
|
||||
#[test_case(Type::BooleanLiteral(true))]
|
||||
@@ -548,7 +561,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 = KnownClass::Object.to_instance(&db);
|
||||
let t_object = Type::object(&db);
|
||||
let t_bool = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use super::context::InferContext;
|
||||
use super::diagnostic::{CALL_NON_CALLABLE, TYPE_ASSERTION_FAILURE};
|
||||
use super::{Severity, Signature, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use crate::types::diagnostic::STATIC_ASSERT_ERROR;
|
||||
use super::diagnostic::CALL_NON_CALLABLE;
|
||||
use super::{Signature, Type, TypeArrayDisplay, UnionBuilder};
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::DiagnosticId;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
mod arguments;
|
||||
@@ -12,23 +10,12 @@ mod bind;
|
||||
pub(super) use arguments::{Argument, CallArguments};
|
||||
pub(super) use bind::{bind_call, CallBinding};
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
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> {
|
||||
pub(crate) enum CallOutcome<'db> {
|
||||
Callable {
|
||||
binding: CallBinding<'db>,
|
||||
},
|
||||
RevealType {
|
||||
binding: CallBinding<'db>,
|
||||
revealed_ty: Type<'db>,
|
||||
},
|
||||
NotCallable {
|
||||
not_callable_ty: Type<'db>,
|
||||
},
|
||||
@@ -40,14 +27,6 @@ pub(super) 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> {
|
||||
@@ -61,14 +40,6 @@ 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>,
|
||||
@@ -80,22 +51,14 @@ 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,
|
||||
}
|
||||
pub(super) fn is_possibly_unbound(&self) -> bool {
|
||||
matches!(self, Self::PossiblyUnboundDunderCall { .. })
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -114,11 +77,6 @@ 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()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,22 +162,12 @@ 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(),
|
||||
@@ -239,24 +187,12 @@ 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);
|
||||
@@ -280,88 +216,27 @@ 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) enum CallDunderResult<'db> {
|
||||
CallOutcome(CallOutcome<'db>),
|
||||
PossiblyUnbound(CallOutcome<'db>),
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub(super) enum CallDunderOutcome<'db> {
|
||||
Call(CallOutcome<'db>),
|
||||
MethodNotAvailable,
|
||||
}
|
||||
|
||||
impl<'db> CallDunderResult<'db> {
|
||||
impl<'db> CallDunderOutcome<'db> {
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Self::CallOutcome(outcome) => outcome.return_type(db),
|
||||
Self::PossiblyUnbound { .. } => None,
|
||||
Self::Call(outcome) => {
|
||||
if outcome.is_possibly_unbound() {
|
||||
None
|
||||
} else {
|
||||
outcome.return_type(db)
|
||||
}
|
||||
}
|
||||
Self::MethodNotAvailable => None,
|
||||
}
|
||||
}
|
||||
@@ -421,3 +296,29 @@ impl<'db> NotCallableError<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub(super) enum CallDunderLenOutcome<'db> {
|
||||
/// The length is statically known.
|
||||
StaticallyKnown(usize),
|
||||
|
||||
/// The length is determined by calling `__len__`.
|
||||
Call(CallOutcome<'db>),
|
||||
|
||||
/// The object doesn't have a `__len__` method and, thus, doesn't implement sized.
|
||||
MethodNotAvailable,
|
||||
}
|
||||
|
||||
impl<'db> CallDunderLenOutcome<'db> {
|
||||
pub(super) fn return_type(&self, db: &'db dyn Db) -> Option<Type<'db>> {
|
||||
match self {
|
||||
CallDunderLenOutcome::StaticallyKnown(len) => {
|
||||
// TODO: Fall back to `int` if value is too large?
|
||||
i64::try_from(*len).ok().map(Type::IntLiteral)
|
||||
}
|
||||
CallDunderLenOutcome::Call(call_outcome) => call_outcome.return_type(db),
|
||||
CallDunderLenOutcome::MethodNotAvailable => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
@@ -802,12 +802,8 @@ impl Diagnostic for TypeCheckDiagnostic {
|
||||
TypeCheckDiagnostic::message(self).into()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(TypeCheckDiagnostic::file(self))
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(Ranged::range(self))
|
||||
fn span(&self) -> Option<Span> {
|
||||
Some(Span::from(self.file).with_range(self.range))
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
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};
|
||||
@@ -49,7 +50,9 @@ 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::types::call::{Argument, CallArguments};
|
||||
use crate::types::call::{
|
||||
Argument, CallArguments, CallDunderLenOutcome, CallDunderOutcome, CallOutcome,
|
||||
};
|
||||
use crate::types::diagnostic::{
|
||||
report_invalid_arguments_to_annotated, report_invalid_assignment,
|
||||
report_invalid_attribute_assignment, report_unresolved_module, TypeCheckDiagnostics,
|
||||
@@ -58,19 +61,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,
|
||||
UNDEFINED_REVEAL, UNRESOLVED_ATTRIBUTE, UNRESOLVED_IMPORT, UNSUPPORTED_OPERATOR,
|
||||
STATIC_ASSERT_ERROR, TYPE_ASSERTION_FAILURE, 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, 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,
|
||||
todo_type, typing_extensions_symbol, Boundness, 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};
|
||||
@@ -117,7 +120,9 @@ 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, Type::unknown().into());
|
||||
inference
|
||||
.declarations
|
||||
.insert(input, TypeAndQualifiers::unknown());
|
||||
}
|
||||
if category.is_binding() {
|
||||
inference.bindings.insert(input, Type::unknown());
|
||||
@@ -237,7 +242,7 @@ impl<'db> InferenceRegion<'db> {
|
||||
}
|
||||
|
||||
/// The inferred types for a single region.
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq, salsa::Update)]
|
||||
pub(crate) struct TypeInference<'db> {
|
||||
/// The types of every expression in this region.
|
||||
expressions: FxHashMap<ScopedExpressionId, Type<'db>>,
|
||||
@@ -300,14 +305,6 @@ 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<'_> {
|
||||
@@ -928,7 +925,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
inferred_ty.display(self.db())
|
||||
),
|
||||
);
|
||||
Type::unknown().into()
|
||||
TypeAndQualifiers::unknown()
|
||||
};
|
||||
self.types.declarations.insert(declaration, ty);
|
||||
}
|
||||
@@ -3252,9 +3249,117 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.unwrap_or_default();
|
||||
|
||||
let call_arguments = self.infer_arguments(arguments, parameter_expectations);
|
||||
function_type
|
||||
.call(self.db(), &call_arguments)
|
||||
.unwrap_with_diagnostic(&self.context, call_expression.into())
|
||||
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())
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
KnownFunction::Len => {
|
||||
let Some(sized) = binding.one_parameter_type() else {
|
||||
return call;
|
||||
};
|
||||
|
||||
if let CallDunderLenOutcome::Call(_) = sized.__len__(self.db()) {
|
||||
// TODO: Assert that the argument implements the `Sized` protocol (correctly).
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
call
|
||||
}
|
||||
|
||||
fn infer_starred_expression(&mut self, starred: &ast::ExprStarred) -> Type<'db> {
|
||||
@@ -3297,38 +3402,76 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
todo_type!("generic `typing.Awaitable` type")
|
||||
}
|
||||
|
||||
/// Look up a name reference that isn't bound in the local scope.
|
||||
fn lookup_name(&mut self, name_node: &ast::ExprName) -> Symbol<'db> {
|
||||
/// 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;
|
||||
|
||||
let db = self.db();
|
||||
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()
|
||||
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))
|
||||
} else {
|
||||
assert!(
|
||||
self.deferred_state.in_string_annotation(),
|
||||
"Expected the symbol table to create a symbol for every Name node"
|
||||
);
|
||||
false
|
||||
Symbol::Unbound
|
||||
}
|
||||
} else {
|
||||
let use_id = name_node.scoped_use_id(db, scope);
|
||||
symbol_from_bindings(db, use_def.bindings_at_use(use_id))
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
// 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) {
|
||||
// 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();
|
||||
|
||||
// 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, self.file());
|
||||
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, current_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(name) else {
|
||||
let Some(enclosing_symbol) = enclosing_symbol_table.symbol_by_name(symbol_name)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if enclosing_symbol.is_bound() {
|
||||
@@ -3337,7 +3480,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, name);
|
||||
return symbol(db, enclosing_scope_id, symbol_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3348,7 +3491,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if file_scope_id.is_global() {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
global_symbol(db, self.file(), name)
|
||||
global_symbol(db, self.file(), symbol_name)
|
||||
}
|
||||
})
|
||||
// Not found in globals? Fallback to builtins
|
||||
@@ -3357,12 +3500,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
if Some(self.scope()) == builtins_module_scope(db) {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
builtins_symbol(db, name)
|
||||
builtins_symbol(db, symbol_name)
|
||||
}
|
||||
})
|
||||
// Still not found? It might be `reveal_type`...
|
||||
.or_fall_back_to(db, || {
|
||||
if name == "reveal_type" {
|
||||
if symbol_name == "reveal_type" {
|
||||
self.context.report_lint(
|
||||
&UNDEFINED_REVEAL,
|
||||
name_node.into(),
|
||||
@@ -3371,68 +3514,22 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
this is allowed for debugging convenience but will fail at runtime"
|
||||
),
|
||||
);
|
||||
typing_extensions_symbol(db, name)
|
||||
typing_extensions_symbol(db, symbol_name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/// 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
|
||||
match symbol {
|
||||
Symbol::Type(ty, Boundness::Bound) => ty,
|
||||
Symbol::Type(ty, Boundness::PossiblyUnbound) => {
|
||||
report_possibly_unresolved_reference(&self.context, name_node);
|
||||
ty
|
||||
}
|
||||
} 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"),
|
||||
},
|
||||
Symbol::Unbound => {
|
||||
report_unresolved_reference(&self.context, name_node);
|
||||
Type::unknown()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3456,19 +3553,17 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
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_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Symbol::Type(member_ty, Boundness::Bound) => member_ty,
|
||||
Symbol::Type(member_ty, Boundness::PossiblyUnbound) => {
|
||||
self.context.report_lint(
|
||||
&POSSIBLY_UNBOUND_ATTRIBUTE,
|
||||
attribute.into(),
|
||||
format_args!(
|
||||
"Attribute `{}` on type `{}` is possibly unbound",
|
||||
attr.id,
|
||||
value_ty.display(self.db()),
|
||||
),
|
||||
);
|
||||
member_ty
|
||||
}
|
||||
Symbol::Unbound => {
|
||||
@@ -3584,8 +3679,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
};
|
||||
|
||||
if let CallDunderResult::CallOutcome(call)
|
||||
| CallDunderResult::PossiblyUnbound(call) = operand_type.call_dunder(
|
||||
if let CallDunderOutcome::Call(call) = operand_type.call_dunder(
|
||||
self.db(),
|
||||
unary_dunder_method,
|
||||
&CallArguments::positional([operand_type]),
|
||||
@@ -4862,7 +4956,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
bytes.into(),
|
||||
format_args!("Type expressions cannot use bytes literal"),
|
||||
);
|
||||
Type::unknown().into()
|
||||
TypeAndQualifiers::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::FString(fstring) => {
|
||||
@@ -4872,7 +4966,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
format_args!("Type expressions cannot use f-strings"),
|
||||
);
|
||||
self.infer_fstring_expression(fstring);
|
||||
Type::unknown().into()
|
||||
TypeAndQualifiers::unknown()
|
||||
}
|
||||
|
||||
ast::Expr::Name(name) => match name.ctx {
|
||||
@@ -4893,7 +4987,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.into(),
|
||||
}
|
||||
}
|
||||
ast::ExprContext::Invalid => Type::unknown().into(),
|
||||
ast::ExprContext::Invalid => TypeAndQualifiers::unknown(),
|
||||
ast::ExprContext::Store | ast::ExprContext::Del => todo_type!().into(),
|
||||
},
|
||||
|
||||
@@ -4931,7 +5025,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
inner_annotation_ty
|
||||
} else {
|
||||
self.infer_type_expression(slice);
|
||||
Type::unknown().into()
|
||||
TypeAndQualifiers::unknown()
|
||||
}
|
||||
} else {
|
||||
report_invalid_arguments_to_annotated(
|
||||
@@ -5000,7 +5094,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
DeferredExpressionState::InStringAnnotation,
|
||||
)
|
||||
}
|
||||
None => Type::unknown().into(),
|
||||
None => TypeAndQualifiers::unknown(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6441,6 +6535,7 @@ 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,
|
||||
|
||||
@@ -4,13 +4,13 @@ use std::ops::Deref;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use crate::types::class_base::ClassBase;
|
||||
use crate::types::{Class, KnownClass, Type};
|
||||
use crate::types::{Class, 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)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, salsa::Update)]
|
||||
pub(super) struct Mro<'db>(Box<[ClassBase<'db>]>);
|
||||
|
||||
impl<'db> Mro<'db> {
|
||||
@@ -52,9 +52,7 @@ 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_known(db, KnownClass::Object) => {
|
||||
Ok(Self::from([ClassBase::Class(class)]))
|
||||
}
|
||||
[] if class.is_object(db) => 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,
|
||||
@@ -238,7 +236,7 @@ impl<'db> Iterator for MroIterator<'db> {
|
||||
|
||||
impl std::iter::FusedIterator for MroIterator<'_> {}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) struct MroError<'db> {
|
||||
kind: MroErrorKind<'db>,
|
||||
fallback_mro: Mro<'db>,
|
||||
@@ -258,7 +256,7 @@ impl<'db> MroError<'db> {
|
||||
}
|
||||
|
||||
/// Possible ways in which attempting to resolve the MRO of a class might fail.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(super) enum MroErrorKind<'db> {
|
||||
/// The class inherits from one or more invalid bases.
|
||||
///
|
||||
|
||||
@@ -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(KnownClass::Object.to_instance(db));
|
||||
entry.insert(Type::object(db));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (key, value) in into.iter_mut() {
|
||||
if !from.contains_key(key) {
|
||||
*value = KnownClass::Object.to_instance(db);
|
||||
*value = Type::object(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,
|
||||
|
||||
@@ -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::{KnownClass, Type};
|
||||
use crate::types::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, KnownClass::Object.to_instance(db))
|
||||
forall types t. t.is_assignable_to(db, Type::object(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, KnownClass::Object.to_instance(db))
|
||||
forall types t. t.is_fully_static(db) => t.is_subtype_of(db, Type::object(db))
|
||||
);
|
||||
|
||||
// Never should be assignable to every type
|
||||
|
||||
@@ -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)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
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)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
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)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct Parameter<'db> {
|
||||
/// Parameter name.
|
||||
///
|
||||
@@ -304,7 +304,7 @@ impl<'db> Parameter<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) enum ParameterKind<'db> {
|
||||
/// Positional-only parameter, e.g. `def f(x, /): ...`
|
||||
PositionalOnly { default_ty: Option<Type<'db>> },
|
||||
@@ -411,7 +411,7 @@ mod tests {
|
||||
},
|
||||
Parameter {
|
||||
name: Some(Name::new_static("args")),
|
||||
annotated_ty: Some(KnownClass::Object.to_instance(&db)),
|
||||
annotated_ty: Some(Type::object(&db)),
|
||||
kind: ParameterKind::Variadic,
|
||||
},
|
||||
Parameter {
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ impl<'db> SubclassOfType<'db> {
|
||||
ClassBase::Class(class) => {
|
||||
if class.is_final(db) {
|
||||
Type::ClassLiteral(ClassLiteralType { class })
|
||||
} else if class.is_known(db, KnownClass::Object) {
|
||||
} else if class.is_object(db) {
|
||||
KnownClass::Type.to_instance(db)
|
||||
} else {
|
||||
Type::SubclassOf(Self { subclass_of })
|
||||
|
||||
@@ -261,7 +261,7 @@ impl<'db> Unpacker<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, PartialEq, Eq, salsa::Update)]
|
||||
pub(crate) struct UnpackResult<'db> {
|
||||
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
|
||||
diagnostics: TypeCheckDiagnostics,
|
||||
|
||||
@@ -28,16 +28,15 @@ 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
|
||||
@@ -45,7 +44,6 @@ pub(crate) struct Unpack<'db> {
|
||||
#[no_eq]
|
||||
pub(crate) value: UnpackValue<'db>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<Unpack<'static>>,
|
||||
}
|
||||
|
||||
@@ -62,7 +60,7 @@ impl<'db> Unpack<'db> {
|
||||
}
|
||||
|
||||
/// The expression that is being unpacked.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, Hash)]
|
||||
pub(crate) enum UnpackValue<'db> {
|
||||
/// An iterable expression like the one in a `for` loop or a comprehension.
|
||||
Iterable(Expression<'db>),
|
||||
|
||||
@@ -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)]
|
||||
#[derive(Debug, PartialEq, Eq, salsa::Update)]
|
||||
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
|
||||
|
||||
@@ -12,7 +12,7 @@ license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
red_knot_project = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_db = { workspace = true, features = ["os"] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
|
||||
@@ -75,11 +75,13 @@ fn to_lsp_diagnostic(
|
||||
diagnostic: &dyn ruff_db::diagnostic::Diagnostic,
|
||||
encoding: crate::PositionEncoding,
|
||||
) -> Diagnostic {
|
||||
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);
|
||||
let range = if let Some(span) = diagnostic.span() {
|
||||
let index = line_index(db.upcast(), span.file());
|
||||
let source = source_text(db.upcast(), span.file());
|
||||
|
||||
range.to_range(&source, &index, encoding)
|
||||
span.range()
|
||||
.map(|range| range.to_range(&source, &index, encoding))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
Range::default()
|
||||
};
|
||||
|
||||
@@ -68,7 +68,9 @@ impl Session {
|
||||
let system = LSPSystem::new(index.clone());
|
||||
|
||||
// TODO(dhruvmanila): Get the values from the client settings
|
||||
let metadata = ProjectMetadata::discover(system_path, &system)?;
|
||||
let mut metadata = ProjectMetadata::discover(system_path, &system)?;
|
||||
metadata.apply_configuration_files(&system)?;
|
||||
|
||||
// TODO(micha): Handle the case where the program settings are incorrect more gracefully.
|
||||
workspaces.insert(path, ProjectDatabase::new(metadata, system)?);
|
||||
}
|
||||
|
||||
@@ -187,6 +187,10 @@ 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,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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,
|
||||
@@ -16,7 +18,7 @@ pub(crate) struct Db {
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
rule_selection: RuleSelection,
|
||||
rule_selection: Arc<RuleSelection>,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
@@ -29,7 +31,7 @@ impl Db {
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
rule_selection,
|
||||
rule_selection: Arc::new(rule_selection),
|
||||
};
|
||||
|
||||
db.memory_file_system()
|
||||
@@ -94,8 +96,8 @@ impl SemanticDb for Db {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -26,7 +26,8 @@ where
|
||||
.into_iter()
|
||||
.map(|diagnostic| DiagnosticWithLine {
|
||||
line_number: diagnostic
|
||||
.range()
|
||||
.span()
|
||||
.and_then(|span| span.range())
|
||||
.map_or(OneIndexed::from_zero_indexed(0), |range| {
|
||||
line_index.line_index(range.start())
|
||||
}),
|
||||
@@ -144,7 +145,7 @@ struct DiagnosticWithLine<T> {
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity};
|
||||
use ruff_db::diagnostic::{DiagnosticId, LintName, Severity, Span};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
@@ -198,12 +199,8 @@ mod tests {
|
||||
"dummy".into()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.range)
|
||||
fn span(&self) -> Option<Span> {
|
||||
Some(Span::from(self.file).with_range(self.range))
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
|
||||
@@ -257,7 +257,8 @@ impl Matcher {
|
||||
|
||||
fn column<T: Diagnostic>(&self, diagnostic: &T) -> OneIndexed {
|
||||
diagnostic
|
||||
.range()
|
||||
.span()
|
||||
.and_then(|span| span.range())
|
||||
.map(|range| {
|
||||
self.line_index
|
||||
.source_location(range.start(), &self.source)
|
||||
@@ -334,7 +335,7 @@ impl Matcher {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
|
||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity, Span};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
@@ -385,12 +386,8 @@ mod tests {
|
||||
self.message.into()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.range)
|
||||
fn span(&self) -> Option<Span> {
|
||||
Some(Span::from(self.file).with_range(self.range))
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
|
||||
@@ -22,7 +22,7 @@ default = ["console_error_panic_hook"]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
|
||||
|
||||
ruff_db = { workspace = true, features = [] }
|
||||
ruff_db = { workspace = true, default-features = false, features = [] }
|
||||
ruff_notebook = { workspace = true }
|
||||
|
||||
console_error_panic_hook = { workspace = true, optional = true }
|
||||
|
||||
@@ -262,6 +262,10 @@ impl System for WasmSystem {
|
||||
self.fs.current_directory()
|
||||
}
|
||||
|
||||
fn user_config_directory(&self) -> Option<SystemPathBuf> {
|
||||
None
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &SystemPath,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -2272,38 +2272,36 @@ class Foo[_T, __T]:
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a005_module_shadowing_strict() -> Result<()> {
|
||||
/// construct a directory tree with this structure:
|
||||
/// .
|
||||
/// ├── abc
|
||||
/// │ └── __init__.py
|
||||
/// ├── collections
|
||||
/// │ ├── __init__.py
|
||||
/// │ ├── abc
|
||||
/// │ │ └── __init__.py
|
||||
/// │ └── foobar
|
||||
/// │ └── __init__.py
|
||||
/// ├── foobar
|
||||
/// │ ├── __init__.py
|
||||
/// │ ├── abc
|
||||
/// │ │ └── __init__.py
|
||||
/// │ └── collections
|
||||
/// │ ├── __init__.py
|
||||
/// │ ├── abc
|
||||
/// │ │ └── __init__.py
|
||||
/// │ └── foobar
|
||||
/// │ └── __init__.py
|
||||
/// ├── ruff.toml
|
||||
/// └── urlparse
|
||||
/// └── __init__.py
|
||||
fn create_a005_module_structure(tempdir: &TempDir) -> Result<()> {
|
||||
fn create_module(path: &Path) -> Result<()> {
|
||||
fs::create_dir(path)?;
|
||||
fs::File::create(path.join("__init__.py"))?;
|
||||
Ok(())
|
||||
}
|
||||
// construct a directory tree with this structure:
|
||||
// .
|
||||
// ├── abc
|
||||
// │ └── __init__.py
|
||||
// ├── collections
|
||||
// │ ├── __init__.py
|
||||
// │ ├── abc
|
||||
// │ │ └── __init__.py
|
||||
// │ └── foobar
|
||||
// │ └── __init__.py
|
||||
// ├── foobar
|
||||
// │ ├── __init__.py
|
||||
// │ ├── abc
|
||||
// │ │ └── __init__.py
|
||||
// │ └── collections
|
||||
// │ ├── __init__.py
|
||||
// │ ├── abc
|
||||
// │ │ └── __init__.py
|
||||
// │ └── foobar
|
||||
// │ └── __init__.py
|
||||
// ├── ruff.toml
|
||||
// └── urlparse
|
||||
// └── __init__.py
|
||||
|
||||
let tempdir = TempDir::new()?;
|
||||
let foobar = tempdir.path().join("foobar");
|
||||
create_module(&foobar)?;
|
||||
for base in [&tempdir.path().into(), &foobar] {
|
||||
@@ -2317,6 +2315,82 @@ fn a005_module_shadowing_strict() -> Result<()> {
|
||||
// also create a ruff.toml to mark the project root
|
||||
fs::File::create(tempdir.path().join("ruff.toml"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test A005 with `builtins-strict-checking = true`
|
||||
#[test]
|
||||
fn a005_module_shadowing_strict() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
create_a005_module_structure(&tempdir)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(r"\\", "/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(r#"lint.flake8-builtins.builtins-strict-checking = true"#)
|
||||
.args(["--select", "A005"])
|
||||
.current_dir(tempdir.path()),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
|
||||
collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
|
||||
foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
Found 6 errors.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test A005 with `builtins-strict-checking = false`
|
||||
#[test]
|
||||
fn a005_module_shadowing_non_strict() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
create_a005_module_structure(&tempdir)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(r"\\", "/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(r#"lint.flake8-builtins.builtins-strict-checking = false"#)
|
||||
.args(["--select", "A005"])
|
||||
.current_dir(tempdir.path()),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test A005 with `builtins-strict-checking` unset
|
||||
/// TODO(brent) This should currently match the strict version, but after the next minor
|
||||
/// release it will match the non-strict version directly above
|
||||
#[test]
|
||||
fn a005_module_shadowing_strict_default() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
create_a005_module_structure(&tempdir)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(r"\\", "/")]
|
||||
}, {
|
||||
@@ -2338,46 +2412,6 @@ fn a005_module_shadowing_strict() -> Result<()> {
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--select", "A005"])
|
||||
.current_dir(tempdir.path()),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
|
||||
collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
|
||||
foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
Found 6 errors.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
|
||||
// TODO(brent) Default should currently match the strict version, but after the next minor
|
||||
// release it will match the non-strict version directly above
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--select", "A005"])
|
||||
.current_dir(tempdir.path()),
|
||||
@r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
|
||||
collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
foobar/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
foobar/collections/__init__.py:1:1: A005 Module `collections` shadows a Python standard-library module
|
||||
foobar/collections/abc/__init__.py:1:1: A005 Module `abc` shadows a Python standard-library module
|
||||
Found 6 errors.
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -228,6 +228,7 @@ linter.flake8_bandit.check_typed_exception = false
|
||||
linter.flake8_bugbear.extend_immutable_calls = []
|
||||
linter.flake8_builtins.builtins_allowed_modules = []
|
||||
linter.flake8_builtins.builtins_ignorelist = []
|
||||
linter.flake8_builtins.builtins_strict_checking = true
|
||||
linter.flake8_comprehensions.allow_dict_calls_with_keyword_arguments = false
|
||||
linter.flake8_copyright.notice_rgx = (?i)Copyright\s+((?:\(C\)|©)\s+)?\d{4}((-|,\s)\d{4})*
|
||||
linter.flake8_copyright.author = none
|
||||
|
||||
@@ -229,8 +229,14 @@ fn assert_diagnostics(db: &dyn Db, diagnostics: &[Box<dyn Diagnostic>]) {
|
||||
.map(|diagnostic| {
|
||||
(
|
||||
diagnostic.id(),
|
||||
diagnostic.file().map(|file| file.path(db).as_str()),
|
||||
diagnostic.range().map(Range::<usize>::from),
|
||||
diagnostic
|
||||
.span()
|
||||
.map(|span| span.file())
|
||||
.map(|file| file.path(db).as_str()),
|
||||
diagnostic
|
||||
.span()
|
||||
.and_then(|span| span.range())
|
||||
.map(Range::<usize>::from),
|
||||
diagnostic.message(),
|
||||
diagnostic.severity(),
|
||||
)
|
||||
|
||||
@@ -43,14 +43,16 @@ zip = { workspace = true }
|
||||
[target.'cfg(target_arch="wasm32")'.dependencies]
|
||||
web-time = { version = "1.1.0" }
|
||||
|
||||
[target.'cfg(not(target_arch="wasm32"))'.dependencies]
|
||||
etcetera = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["os"]
|
||||
cache = ["ruff_cache"]
|
||||
os = ["ignore"]
|
||||
os = ["ignore", "dep:etcetera"]
|
||||
serde = ["dep:serde", "camino/serde1"]
|
||||
# Exposes testing utilities.
|
||||
testing = ["tracing-subscriber", "tracing-tree"]
|
||||
|
||||
@@ -164,19 +164,11 @@ pub trait Diagnostic: Send + Sync + std::fmt::Debug {
|
||||
|
||||
fn message(&self) -> Cow<str>;
|
||||
|
||||
/// The file this diagnostic is associated with.
|
||||
///
|
||||
/// File can be `None` for diagnostics that don't originate from a file.
|
||||
/// For example:
|
||||
/// * A diagnostic indicating that a directory couldn't be read.
|
||||
/// * A diagnostic related to a CLI argument
|
||||
fn file(&self) -> Option<File>;
|
||||
|
||||
/// The primary range of the diagnostic in `file`.
|
||||
/// The primary span of the diagnostic.
|
||||
///
|
||||
/// The range can be `None` if the diagnostic doesn't have a file
|
||||
/// or it applies to the entire file (e.g. the file should be executable but isn't).
|
||||
fn range(&self) -> Option<TextRange>;
|
||||
fn span(&self) -> Option<Span>;
|
||||
|
||||
fn severity(&self) -> Severity;
|
||||
|
||||
@@ -191,6 +183,47 @@ pub trait Diagnostic: Send + Sync + std::fmt::Debug {
|
||||
}
|
||||
}
|
||||
|
||||
/// A span represents the source of a diagnostic.
|
||||
///
|
||||
/// It consists of a `File` and an optional range into that file. When the
|
||||
/// range isn't present, it semantically implies that the diagnostic refers to
|
||||
/// the entire file. For example, when the file should be executable but isn't.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Span {
|
||||
file: File,
|
||||
range: Option<TextRange>,
|
||||
}
|
||||
|
||||
impl Span {
|
||||
/// Returns the `File` attached to this `Span`.
|
||||
pub fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
/// Returns the range, if available, attached to this `Span`.
|
||||
///
|
||||
/// When there is no range, it is convention to assume that this `Span`
|
||||
/// refers to the corresponding `File` as a whole. In some cases, consumers
|
||||
/// of this API may use the range `0..0` to represent this case.
|
||||
pub fn range(&self) -> Option<TextRange> {
|
||||
self.range
|
||||
}
|
||||
|
||||
/// Returns a new `Span` with the given `range` attached to it.
|
||||
pub fn with_range(self, range: TextRange) -> Span {
|
||||
Span {
|
||||
range: Some(range),
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<File> for Span {
|
||||
fn from(file: File) -> Span {
|
||||
Span { file, range: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
@@ -236,8 +269,8 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
let rendered = renderer.render(message);
|
||||
writeln!(f, "{rendered}")
|
||||
};
|
||||
match (self.diagnostic.file(), self.diagnostic.range()) {
|
||||
(None, _) => {
|
||||
match self.diagnostic.span() {
|
||||
None => {
|
||||
// NOTE: This is pretty sub-optimal. It doesn't render well. We
|
||||
// really want a snippet, but without a `File`, we can't really
|
||||
// render anything. It looks like this case currently happens
|
||||
@@ -248,20 +281,20 @@ impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
let msg = format!("{}: {}", self.diagnostic.id(), self.diagnostic.message());
|
||||
render(f, level.title(&msg))
|
||||
}
|
||||
(Some(file), range) => {
|
||||
let path = file.path(self.db).to_string();
|
||||
let source = source_text(self.db, file);
|
||||
Some(span) => {
|
||||
let path = span.file.path(self.db).to_string();
|
||||
let source = source_text(self.db, span.file);
|
||||
let title = self.diagnostic.id().to_string();
|
||||
let message = self.diagnostic.message();
|
||||
|
||||
let Some(range) = range else {
|
||||
let Some(range) = span.range else {
|
||||
let snippet = Snippet::source(source.as_str()).origin(&path).line_start(1);
|
||||
return render(f, level.title(&title).snippet(snippet));
|
||||
};
|
||||
|
||||
// The bits below are a simplified copy from
|
||||
// `crates/ruff_linter/src/message/text.rs`.
|
||||
let index = line_index(self.db, file);
|
||||
let index = line_index(self.db, span.file);
|
||||
let source_code = SourceCode::new(source.as_str(), &index);
|
||||
|
||||
let content_start_index = source_code.line_index(range.start());
|
||||
@@ -315,12 +348,8 @@ where
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
fn span(&self) -> Option<Span> {
|
||||
(**self).span()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
@@ -340,12 +369,8 @@ where
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
fn span(&self) -> Option<Span> {
|
||||
(**self).span()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
@@ -362,12 +387,8 @@ impl Diagnostic for Box<dyn Diagnostic> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
fn span(&self) -> Option<Span> {
|
||||
(**self).span()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
@@ -384,12 +405,8 @@ impl Diagnostic for &'_ dyn Diagnostic {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
fn span(&self) -> Option<Span> {
|
||||
(**self).span()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
@@ -406,12 +423,8 @@ impl Diagnostic for std::sync::Arc<dyn Diagnostic> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
fn span(&self) -> Option<Span> {
|
||||
(**self).span()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
@@ -440,12 +453,8 @@ impl Diagnostic for ParseDiagnostic {
|
||||
self.error.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
Some(self.file)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.error.location)
|
||||
fn span(&self) -> Option<Span> {
|
||||
Some(Span::from(self.file).with_range(self.error.location))
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
|
||||
@@ -73,6 +73,14 @@ impl std::fmt::Debug for ParsedModule {
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ParsedModule {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
Arc::ptr_eq(&self.inner, &other.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ParsedModule {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::files::{system_path_to_file, vendored_path_to_file};
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
pub use glob::PatternError;
|
||||
pub use memory_fs::MemoryFileSystem;
|
||||
|
||||
#[cfg(all(feature = "testing", feature = "os"))]
|
||||
pub use os::testing::UserConfigDirectoryOverrideGuard;
|
||||
|
||||
#[cfg(feature = "os")]
|
||||
pub use os::OsSystem;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use std::error::Error;
|
||||
use std::fmt::Debug;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::{fmt, io};
|
||||
pub use test::{DbWithTestSystem, TestSystem};
|
||||
pub use test::{DbWithTestSystem, InMemorySystem, TestSystem};
|
||||
use walk_directory::WalkDirectoryBuilder;
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
@@ -99,6 +104,11 @@ pub trait System: Debug {
|
||||
/// Returns the current working directory
|
||||
fn current_directory(&self) -> &SystemPath;
|
||||
|
||||
/// Returns the directory path where user configurations are stored.
|
||||
///
|
||||
/// Returns `None` if no such convention exists for the system.
|
||||
fn user_config_directory(&self) -> Option<SystemPathBuf>;
|
||||
|
||||
/// Iterate over the contents of the directory at `path`.
|
||||
///
|
||||
/// The returned iterator must have the following properties:
|
||||
|
||||
@@ -6,8 +6,6 @@ use camino::{Utf8Path, Utf8PathBuf};
|
||||
use filetime::FileTime;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
|
||||
use crate::system::{
|
||||
walk_directory, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result,
|
||||
SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf,
|
||||
@@ -131,14 +129,6 @@ impl MemoryFileSystem {
|
||||
read_to_string(self, path.as_ref())
|
||||
}
|
||||
|
||||
pub(crate) fn read_to_notebook(
|
||||
&self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
) -> std::result::Result<ruff_notebook::Notebook, ruff_notebook::NotebookError> {
|
||||
let content = self.read_to_string(path)?;
|
||||
ruff_notebook::Notebook::from_source_code(&content)
|
||||
}
|
||||
|
||||
pub(crate) fn read_virtual_path_to_string(
|
||||
&self,
|
||||
path: impl AsRef<SystemVirtualPath>,
|
||||
@@ -151,14 +141,6 @@ impl MemoryFileSystem {
|
||||
Ok(file.content.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn read_virtual_path_to_notebook(
|
||||
&self,
|
||||
path: &SystemVirtualPath,
|
||||
) -> std::result::Result<Notebook, NotebookError> {
|
||||
let content = self.read_virtual_path_to_string(path)?;
|
||||
ruff_notebook::Notebook::from_source_code(&content)
|
||||
}
|
||||
|
||||
pub fn exists(&self, path: &SystemPath) -> bool {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = self.normalize_path(path);
|
||||
|
||||
@@ -24,6 +24,11 @@ pub struct OsSystem {
|
||||
#[derive(Default, Debug)]
|
||||
struct OsSystemInner {
|
||||
cwd: SystemPathBuf,
|
||||
|
||||
/// Overrides the user's configuration directory for testing.
|
||||
/// This is an `Option<Option<..>>` to allow setting an override of `None`.
|
||||
#[cfg(feature = "testing")]
|
||||
user_config_directory_override: std::sync::Mutex<Option<Option<SystemPathBuf>>>,
|
||||
}
|
||||
|
||||
impl OsSystem {
|
||||
@@ -32,8 +37,11 @@ impl OsSystem {
|
||||
assert!(cwd.as_utf8_path().is_absolute());
|
||||
|
||||
Self {
|
||||
// Spreading `..Default` because it isn't possible to feature gate the initializer of a single field.
|
||||
#[allow(clippy::needless_update)]
|
||||
inner: Arc::new(OsSystemInner {
|
||||
cwd: cwd.to_path_buf(),
|
||||
..Default::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -98,6 +106,35 @@ impl System for OsSystem {
|
||||
&self.inner.cwd
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn user_config_directory(&self) -> Option<SystemPathBuf> {
|
||||
// In testing, we allow overriding the user configuration directory by using a
|
||||
// thread local because overriding the environment variables breaks test isolation
|
||||
// (tests run concurrently) and mutating environment variable in a multithreaded
|
||||
// application is inherently unsafe.
|
||||
#[cfg(feature = "testing")]
|
||||
if let Ok(directory_override) = self.try_get_user_config_directory_override() {
|
||||
return directory_override;
|
||||
}
|
||||
|
||||
use etcetera::BaseStrategy as _;
|
||||
|
||||
let strategy = etcetera::base_strategy::choose_base_strategy().ok()?;
|
||||
SystemPathBuf::from_path_buf(strategy.config_dir()).ok()
|
||||
}
|
||||
|
||||
// TODO: Remove this feature gating once `ruff_wasm` no longer indirectly depends on `ruff_db` with the
|
||||
// `os` feature enabled (via `ruff_workspace` -> `ruff_graph` -> `ruff_db`).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn user_config_directory(&self) -> Option<SystemPathBuf> {
|
||||
#[cfg(feature = "testing")]
|
||||
if let Ok(directory_override) = self.try_get_user_config_directory_override() {
|
||||
return directory_override;
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Creates a builder to recursively walk `path`.
|
||||
///
|
||||
/// The walker ignores files according to [`ignore::WalkBuilder::standard_filters`]
|
||||
@@ -321,6 +358,64 @@ fn not_found() -> std::io::Error {
|
||||
std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory")
|
||||
}
|
||||
|
||||
#[cfg(feature = "testing")]
|
||||
pub(super) mod testing {
|
||||
|
||||
use crate::system::{OsSystem, SystemPathBuf};
|
||||
|
||||
impl OsSystem {
|
||||
/// Overrides the user configuration directory for the current scope
|
||||
/// (for as long as the returned override is not dropped).
|
||||
pub fn with_user_config_directory(
|
||||
&self,
|
||||
directory: Option<SystemPathBuf>,
|
||||
) -> UserConfigDirectoryOverrideGuard {
|
||||
let mut directory_override = self.inner.user_config_directory_override.lock().unwrap();
|
||||
let previous = directory_override.replace(directory);
|
||||
|
||||
UserConfigDirectoryOverrideGuard {
|
||||
previous,
|
||||
system: self.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns [`Ok`] if any override is set and [`Err`] otherwise.
|
||||
pub(super) fn try_get_user_config_directory_override(
|
||||
&self,
|
||||
) -> Result<Option<SystemPathBuf>, ()> {
|
||||
let directory_override = self.inner.user_config_directory_override.lock().unwrap();
|
||||
match directory_override.as_ref() {
|
||||
Some(directory_override) => Ok(directory_override.clone()),
|
||||
None => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A scoped override of the [user's configuration directory](crate::System::user_config_directory) for the [`OsSystem`].
|
||||
///
|
||||
/// Prefer overriding the user's configuration directory for tests that require
|
||||
/// spawning a new process (e.g. CLI tests) by setting the `APPDATA` (windows)
|
||||
/// or `XDG_CONFIG_HOME` (unix and other platforms) environment variables.
|
||||
/// For example, by setting the environment variables when invoking the CLI with insta.
|
||||
///
|
||||
/// Requires the `testing` feature.
|
||||
#[must_use]
|
||||
pub struct UserConfigDirectoryOverrideGuard {
|
||||
previous: Option<Option<SystemPathBuf>>,
|
||||
system: OsSystem,
|
||||
}
|
||||
|
||||
impl Drop for UserConfigDirectoryOverrideGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut directory_override) =
|
||||
self.system.inner.user_config_directory_override.try_lock()
|
||||
{
|
||||
*directory_override = self.previous.take();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use glob::PatternError;
|
||||
use ruff_notebook::{Notebook, NotebookError};
|
||||
use ruff_python_trivia::textwrap;
|
||||
use std::any::Any;
|
||||
use std::panic::RefUnwindSafe;
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::files::File;
|
||||
use crate::system::{
|
||||
@@ -21,104 +20,91 @@ use super::walk_directory::WalkDirectoryBuilder;
|
||||
///
|
||||
/// ## Warning
|
||||
/// Don't use this system for production code. It's intended for testing only.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TestSystem {
|
||||
inner: TestSystemInner,
|
||||
inner: Arc<dyn System + RefUnwindSafe + Send + Sync>,
|
||||
}
|
||||
|
||||
impl TestSystem {
|
||||
/// Returns the [`InMemorySystem`].
|
||||
///
|
||||
/// ## Panics
|
||||
/// If the underlying test system isn't the [`InMemorySystem`].
|
||||
pub fn in_memory(&self) -> &InMemorySystem {
|
||||
self.as_in_memory()
|
||||
.expect("The test db is not using a memory file system")
|
||||
}
|
||||
|
||||
/// Returns the `InMemorySystem` or `None` if the underlying test system isn't the [`InMemorySystem`].
|
||||
pub fn as_in_memory(&self) -> Option<&InMemorySystem> {
|
||||
self.system().as_any().downcast_ref::<InMemorySystem>()
|
||||
}
|
||||
|
||||
/// Returns the memory file system.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If this test db isn't using a memory file system.
|
||||
/// If the underlying test system isn't the [`InMemorySystem`].
|
||||
pub fn memory_file_system(&self) -> &MemoryFileSystem {
|
||||
if let TestSystemInner::Stub(fs) = &self.inner {
|
||||
fs
|
||||
} else {
|
||||
panic!("The test db is not using a memory file system");
|
||||
}
|
||||
self.in_memory().fs()
|
||||
}
|
||||
|
||||
fn use_system<S>(&mut self, system: S)
|
||||
where
|
||||
S: System + Send + Sync + RefUnwindSafe + 'static,
|
||||
{
|
||||
self.inner = TestSystemInner::System(Arc::new(system));
|
||||
self.inner = Arc::new(system);
|
||||
}
|
||||
|
||||
pub fn system(&self) -> &dyn System {
|
||||
&*self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl System for TestSystem {
|
||||
fn path_metadata(&self, path: &SystemPath) -> crate::system::Result<Metadata> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.metadata(path),
|
||||
TestSystemInner::System(system) => system.path_metadata(path),
|
||||
}
|
||||
fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> {
|
||||
self.system().path_metadata(path)
|
||||
}
|
||||
|
||||
fn read_to_string(&self, path: &SystemPath) -> crate::system::Result<String> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.read_to_string(path),
|
||||
TestSystemInner::System(system) => system.read_to_string(path),
|
||||
}
|
||||
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf> {
|
||||
self.system().canonicalize_path(path)
|
||||
}
|
||||
|
||||
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
|
||||
self.system().read_to_string(path)
|
||||
}
|
||||
|
||||
fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.read_to_notebook(path),
|
||||
TestSystemInner::System(system) => system.read_to_notebook(path),
|
||||
}
|
||||
self.system().read_to_notebook(path)
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.read_virtual_path_to_string(path),
|
||||
TestSystemInner::System(system) => system.read_virtual_path_to_string(path),
|
||||
}
|
||||
self.system().read_virtual_path_to_string(path)
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_notebook(
|
||||
&self,
|
||||
path: &SystemVirtualPath,
|
||||
) -> std::result::Result<Notebook, NotebookError> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.read_virtual_path_to_notebook(path),
|
||||
TestSystemInner::System(system) => system.read_virtual_path_to_notebook(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn path_exists(&self, path: &SystemPath) -> bool {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.exists(path),
|
||||
TestSystemInner::System(system) => system.path_exists(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_directory(&self, path: &SystemPath) -> bool {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.is_directory(path),
|
||||
TestSystemInner::System(system) => system.is_directory(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_file(&self, path: &SystemPath) -> bool {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.is_file(path),
|
||||
TestSystemInner::System(system) => system.is_file(path),
|
||||
}
|
||||
self.system().read_virtual_path_to_notebook(path)
|
||||
}
|
||||
|
||||
fn current_directory(&self) -> &SystemPath {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.current_directory(),
|
||||
TestSystemInner::System(system) => system.current_directory(),
|
||||
}
|
||||
self.system().current_directory()
|
||||
}
|
||||
|
||||
fn user_config_directory(&self) -> Option<SystemPathBuf> {
|
||||
self.system().user_config_directory()
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &SystemPath,
|
||||
) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>> {
|
||||
self.system().read_directory(path)
|
||||
}
|
||||
|
||||
fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => fs.walk_directory(path),
|
||||
TestSystemInner::System(system) => system.walk_directory(path),
|
||||
}
|
||||
self.system().walk_directory(path)
|
||||
}
|
||||
|
||||
fn glob(
|
||||
@@ -128,37 +114,22 @@ impl System for TestSystem {
|
||||
Box<dyn Iterator<Item = std::result::Result<SystemPathBuf, GlobError>>>,
|
||||
PatternError,
|
||||
> {
|
||||
match &self.inner {
|
||||
TestSystemInner::Stub(fs) => {
|
||||
let iterator = fs.glob(pattern)?;
|
||||
Ok(Box::new(iterator))
|
||||
}
|
||||
TestSystemInner::System(system) => system.glob(pattern),
|
||||
}
|
||||
self.system().glob(pattern)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &SystemPath,
|
||||
) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>> {
|
||||
match &self.inner {
|
||||
TestSystemInner::System(fs) => fs.read_directory(path),
|
||||
TestSystemInner::Stub(fs) => Ok(Box::new(fs.read_directory(path)?)),
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf> {
|
||||
match &self.inner {
|
||||
TestSystemInner::System(fs) => fs.canonicalize_path(path),
|
||||
TestSystemInner::Stub(fs) => fs.canonicalize(path),
|
||||
impl Default for TestSystem {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(InMemorySystem::default()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,8 +144,8 @@ pub trait DbWithTestSystem: Db + Sized {
|
||||
|
||||
/// Writes the content of the given file and notifies the Db about the change.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the system isn't using the memory file system.
|
||||
/// ## Panics
|
||||
/// If the db isn't using the [`InMemorySystem`].
|
||||
fn write_file(&mut self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
@@ -201,6 +172,9 @@ pub trait DbWithTestSystem: Db + Sized {
|
||||
}
|
||||
|
||||
/// Writes the content of the given virtual file.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If the db isn't using the [`InMemorySystem`].
|
||||
fn write_virtual_file(&mut self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
|
||||
let path = path.as_ref();
|
||||
self.test_system()
|
||||
@@ -209,6 +183,9 @@ pub trait DbWithTestSystem: Db + Sized {
|
||||
}
|
||||
|
||||
/// Writes auto-dedented text to a file.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If the db isn't using the [`InMemorySystem`].
|
||||
fn write_dedented(&mut self, path: &str, content: &str) -> crate::system::Result<()> {
|
||||
self.write_file(path, textwrap::dedent(content))?;
|
||||
Ok(())
|
||||
@@ -216,8 +193,8 @@ pub trait DbWithTestSystem: Db + Sized {
|
||||
|
||||
/// Writes the content of the given files and notifies the Db about the change.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the system isn't using the memory file system for testing.
|
||||
/// ## Panics
|
||||
/// If the db isn't using the [`InMemorySystem`].
|
||||
fn write_files<P, C, I>(&mut self, files: I) -> crate::system::Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (P, C)>,
|
||||
@@ -246,20 +223,94 @@ pub trait DbWithTestSystem: Db + Sized {
|
||||
/// Returns the memory file system.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If this system isn't using a memory file system.
|
||||
/// If the underlying test system isn't the [`InMemorySystem`].
|
||||
fn memory_file_system(&self) -> &MemoryFileSystem {
|
||||
self.test_system().memory_file_system()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum TestSystemInner {
|
||||
Stub(MemoryFileSystem),
|
||||
System(Arc<dyn System + RefUnwindSafe + Send + Sync>),
|
||||
#[derive(Default, Debug)]
|
||||
pub struct InMemorySystem {
|
||||
user_config_directory: Mutex<Option<SystemPathBuf>>,
|
||||
memory_fs: MemoryFileSystem,
|
||||
}
|
||||
|
||||
impl Default for TestSystemInner {
|
||||
fn default() -> Self {
|
||||
Self::Stub(MemoryFileSystem::default())
|
||||
impl InMemorySystem {
|
||||
pub fn fs(&self) -> &MemoryFileSystem {
|
||||
&self.memory_fs
|
||||
}
|
||||
|
||||
pub fn set_user_configuration_directory(&self, directory: Option<SystemPathBuf>) {
|
||||
let mut user_directory = self.user_config_directory.lock().unwrap();
|
||||
*user_directory = directory;
|
||||
}
|
||||
}
|
||||
|
||||
impl System for InMemorySystem {
|
||||
fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> {
|
||||
self.memory_fs.metadata(path)
|
||||
}
|
||||
|
||||
fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf> {
|
||||
self.memory_fs.canonicalize(path)
|
||||
}
|
||||
|
||||
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
|
||||
self.memory_fs.read_to_string(path)
|
||||
}
|
||||
|
||||
fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError> {
|
||||
let content = self.read_to_string(path)?;
|
||||
Notebook::from_source_code(&content)
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String> {
|
||||
self.memory_fs.read_virtual_path_to_string(path)
|
||||
}
|
||||
|
||||
fn read_virtual_path_to_notebook(
|
||||
&self,
|
||||
path: &SystemVirtualPath,
|
||||
) -> std::result::Result<Notebook, NotebookError> {
|
||||
let content = self.read_virtual_path_to_string(path)?;
|
||||
Notebook::from_source_code(&content)
|
||||
}
|
||||
|
||||
fn current_directory(&self) -> &SystemPath {
|
||||
self.memory_fs.current_directory()
|
||||
}
|
||||
|
||||
fn user_config_directory(&self) -> Option<SystemPathBuf> {
|
||||
self.user_config_directory.lock().unwrap().clone()
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &SystemPath,
|
||||
) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>> {
|
||||
Ok(Box::new(self.memory_fs.read_directory(path)?))
|
||||
}
|
||||
|
||||
fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder {
|
||||
self.memory_fs.walk_directory(path)
|
||||
}
|
||||
|
||||
fn glob(
|
||||
&self,
|
||||
pattern: &str,
|
||||
) -> std::result::Result<
|
||||
Box<dyn Iterator<Item = std::result::Result<SystemPathBuf, GlobError>>>,
|
||||
PatternError,
|
||||
> {
|
||||
let iterator = self.memory_fs.glob(pattern)?;
|
||||
Ok(Box::new(iterator))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub fn assert_function_query_was_not_run<Db, Q, QDb, I, R>(
|
||||
|
||||
db.attach(|_| {
|
||||
if let Some(will_execute_event) = will_execute_event {
|
||||
panic!("Expected query {query_name}({id}) not to have run but it did: {will_execute_event:?}");
|
||||
panic!("Expected query {query_name}({id}) not to have run but it did: {will_execute_event:?}\n\n{events:#?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -46,7 +46,7 @@ pub fn assert_const_function_query_was_not_run<Db, Q, QDb, R>(
|
||||
db.attach(|_| {
|
||||
if let Some(will_execute_event) = event {
|
||||
panic!(
|
||||
"Expected query {query_name}() not to have run but it did: {will_execute_event:?}"
|
||||
"Expected query {query_name}() not to have run but it did: {will_execute_event:?}\n\n{events:#?}"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -79,8 +79,8 @@ impl Db for ModuleDb {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
|
||||
fn rule_selection(&self) -> &RuleSelection {
|
||||
&self.rule_selection
|
||||
fn rule_selection(&self) -> Arc<RuleSelection> {
|
||||
self.rule_selection.clone()
|
||||
}
|
||||
|
||||
fn lint_registry(&self) -> &LintRegistry {
|
||||
|
||||
@@ -15,6 +15,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
ruff_macros = { workspace = true }
|
||||
salsa = { workspace = true, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
static_assertions = { workspace = true }
|
||||
|
||||
@@ -181,3 +181,16 @@ impl<I: Idx, T, const N: usize> From<[T; N]> for IndexVec<I, T> {
|
||||
// not the phantom data.
|
||||
#[allow(unsafe_code)]
|
||||
unsafe impl<I: Idx, T> Send for IndexVec<I, T> where T: Send {}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
#[cfg(feature = "salsa")]
|
||||
unsafe impl<I, T> salsa::Update for IndexVec<I, T>
|
||||
where
|
||||
T: salsa::Update,
|
||||
{
|
||||
#[allow(unsafe_code)]
|
||||
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
|
||||
let old_vec: &mut IndexVec<I, T> = unsafe { &mut *old_pointer };
|
||||
salsa::Update::maybe_update(&mut old_vec.raw, new_value.raw)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.9.5"
|
||||
version = "0.9.6"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -45,8 +45,6 @@ from airflow.lineage.hook import DatasetLineageInfo
|
||||
from airflow.listeners.spec.dataset import on_dataset_changed, on_dataset_created
|
||||
from airflow.metrics.validators import AllowListValidator, BlockListValidator
|
||||
from airflow.operators import dummy_operator
|
||||
from airflow.operators.bash import BashOperator
|
||||
from airflow.operators.bash_operator import BashOperator as LegacyBashOperator
|
||||
from airflow.operators.branch_operator import BaseBranchOperator
|
||||
from airflow.operators.dagrun_operator import TriggerDagRunLink, TriggerDagRunOperator
|
||||
from airflow.operators.dummy import DummyOperator, EmptyOperator
|
||||
@@ -166,10 +164,6 @@ AllowListValidator(), BlockListValidator()
|
||||
dummy_operator.EmptyOperator()
|
||||
dummy_operator.DummyOperator()
|
||||
|
||||
# airflow.operators.bash / airflow.operators.bash_operator
|
||||
BashOperator()
|
||||
LegacyBashOperator()
|
||||
|
||||
# airflow.operators.branch_operator
|
||||
BaseBranchOperator()
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from airflow.api.auth.backend import basic_auth, kerberos_auth
|
||||
from airflow.api.auth.backend.basic_auth import auth_current_user
|
||||
from airflow.auth.managers.fab.api.auth.backend import (
|
||||
@@ -41,7 +43,11 @@ from airflow.hooks.subprocess import SubprocessHook
|
||||
from airflow.hooks.webhdfs_hook import WebHDFSHook
|
||||
from airflow.hooks.zendesk_hook import ZendeskHook
|
||||
from airflow.kubernetes.k8s_model import K8SModel, append_to_pod
|
||||
from airflow.kubernetes.kube_client import _disable_verify_ssl, _enable_tcp_keepalive, get_kube_client
|
||||
from airflow.kubernetes.kube_client import (
|
||||
_disable_verify_ssl,
|
||||
_enable_tcp_keepalive,
|
||||
get_kube_client,
|
||||
)
|
||||
from airflow.kubernetes.kubernetes_helper_functions import (
|
||||
add_pod_suffix,
|
||||
annotations_for_logging_task_metadata,
|
||||
@@ -55,24 +61,38 @@ from airflow.kubernetes.pod_generator import (
|
||||
PodDefaults,
|
||||
PodGenerator,
|
||||
PodGeneratorDeprecated,
|
||||
add_pod_suffix as add_pod_suffix2,
|
||||
datetime_to_label_safe_datestring,
|
||||
extend_object_field,
|
||||
label_safe_datestring_to_datetime,
|
||||
make_safe_label_value,
|
||||
merge_objects,
|
||||
)
|
||||
from airflow.kubernetes.pod_generator import (
|
||||
add_pod_suffix as add_pod_suffix2,
|
||||
)
|
||||
from airflow.kubernetes.pod_generator import (
|
||||
rand_str as rand_str2,
|
||||
)
|
||||
from airflow.kubernetes.pod_generator_deprecated import (
|
||||
PodDefaults as PodDefaults3,
|
||||
)
|
||||
from airflow.kubernetes.pod_generator_deprecated import (
|
||||
PodGenerator as PodGenerator2,
|
||||
)
|
||||
from airflow.kubernetes.pod_generator_deprecated import (
|
||||
make_safe_label_value as make_safe_label_value2,
|
||||
)
|
||||
from airflow.kubernetes.pod_launcher import PodLauncher, PodStatus
|
||||
from airflow.kubernetes.pod_launcher_deprecated import (
|
||||
PodDefaults as PodDefaults2,
|
||||
)
|
||||
from airflow.kubernetes.pod_launcher_deprecated import (
|
||||
PodLauncher as PodLauncher2,
|
||||
)
|
||||
from airflow.kubernetes.pod_launcher_deprecated import (
|
||||
PodStatus as PodStatus2,
|
||||
)
|
||||
from airflow.kubernetes.pod_launcher_deprecated import (
|
||||
get_kube_client as get_kube_client2,
|
||||
)
|
||||
from airflow.kubernetes.pod_runtime_info_env import PodRuntimeInfoEnv
|
||||
@@ -80,6 +100,8 @@ from airflow.kubernetes.secret import K8SModel2, Secret
|
||||
from airflow.kubernetes.volume import Volume
|
||||
from airflow.kubernetes.volume_mount import VolumeMount
|
||||
from airflow.macros.hive import closest_ds_partition, max_partition
|
||||
from airflow.operators.bash import BashOperator
|
||||
from airflow.operators.bash_operator import BashOperator as LegacyBashOperator
|
||||
from airflow.operators.check_operator import (
|
||||
CheckOperator,
|
||||
IntervalCheckOperator,
|
||||
@@ -117,8 +139,14 @@ from airflow.operators.presto_check_operator import (
|
||||
PrestoCheckOperator,
|
||||
PrestoIntervalCheckOperator,
|
||||
PrestoValueCheckOperator,
|
||||
)
|
||||
from airflow.operators.presto_check_operator import (
|
||||
SQLCheckOperator as SQLCheckOperator2,
|
||||
)
|
||||
from airflow.operators.presto_check_operator import (
|
||||
SQLIntervalCheckOperator as SQLIntervalCheckOperator2,
|
||||
)
|
||||
from airflow.operators.presto_check_operator import (
|
||||
SQLValueCheckOperator as SQLValueCheckOperator2,
|
||||
)
|
||||
from airflow.operators.presto_to_mysql import (
|
||||
@@ -139,15 +167,25 @@ from airflow.operators.slack_operator import SlackAPIOperator, SlackAPIPostOpera
|
||||
from airflow.operators.sql import (
|
||||
BaseSQLOperator,
|
||||
BranchSQLOperator,
|
||||
SQLCheckOperator as SQLCheckOperator3,
|
||||
SQLColumnCheckOperator as SQLColumnCheckOperator2,
|
||||
SQLIntervalCheckOperator as SQLIntervalCheckOperator3,
|
||||
SQLTableCheckOperator,
|
||||
SQLThresholdCheckOperator as SQLThresholdCheckOperator2,
|
||||
SQLValueCheckOperator as SQLValueCheckOperator3,
|
||||
_convert_to_float_if_possible,
|
||||
parse_boolean,
|
||||
)
|
||||
from airflow.operators.sql import (
|
||||
SQLCheckOperator as SQLCheckOperator3,
|
||||
)
|
||||
from airflow.operators.sql import (
|
||||
SQLColumnCheckOperator as SQLColumnCheckOperator2,
|
||||
)
|
||||
from airflow.operators.sql import (
|
||||
SQLIntervalCheckOperator as SQLIntervalCheckOperator3,
|
||||
)
|
||||
from airflow.operators.sql import (
|
||||
SQLThresholdCheckOperator as SQLThresholdCheckOperator2,
|
||||
)
|
||||
from airflow.operators.sql import (
|
||||
SQLValueCheckOperator as SQLValueCheckOperator3,
|
||||
)
|
||||
from airflow.operators.sqlite_operator import SqliteOperator
|
||||
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
|
||||
from airflow.operators.weekday import BranchDayOfWeekOperator
|
||||
@@ -193,6 +231,8 @@ CeleryKubernetesExecutor()
|
||||
_convert_to_float_if_possible()
|
||||
parse_boolean()
|
||||
BaseSQLOperator()
|
||||
BashOperator()
|
||||
LegacyBashOperator()
|
||||
BranchSQLOperator()
|
||||
CheckOperator()
|
||||
ConnectorProtocol()
|
||||
|
||||
@@ -19,3 +19,21 @@ datetime.now()
|
||||
|
||||
# uses `astimezone` method
|
||||
datetime.now().astimezone()
|
||||
datetime.now().astimezone
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/15998
|
||||
|
||||
## Errors
|
||||
datetime.now().replace.astimezone()
|
||||
datetime.now().replace[0].astimezone()
|
||||
datetime.now()().astimezone()
|
||||
datetime.now().replace(datetime.now()).astimezone()
|
||||
|
||||
foo.replace(datetime.now().replace).astimezone()
|
||||
|
||||
## No errors
|
||||
datetime.now().replace(microsecond=0).astimezone()
|
||||
datetime.now().replace(0).astimezone()
|
||||
datetime.now().replace(0).astimezone
|
||||
datetime.now().replace(0).replace(1).astimezone
|
||||
|
||||
@@ -176,4 +176,45 @@ def f(x, *args, **kwargs):
|
||||
x: the value
|
||||
*args: var-arguments
|
||||
"""
|
||||
return x
|
||||
return x
|
||||
|
||||
|
||||
# regression test for https://github.com/astral-sh/ruff/issues/16007.
|
||||
# attributes is a section name without subsections, so it was failing the
|
||||
# previous workaround for Args: args: sections
|
||||
def send(payload: str, attributes: dict[str, Any]) -> None:
|
||||
"""
|
||||
Send a message.
|
||||
|
||||
Args:
|
||||
payload:
|
||||
The message payload.
|
||||
|
||||
attributes:
|
||||
Additional attributes to be sent alongside the message.
|
||||
"""
|
||||
|
||||
|
||||
# undocumented argument with the same name as a section
|
||||
def should_fail(payload, Args):
|
||||
"""
|
||||
Send a message.
|
||||
|
||||
Args:
|
||||
payload:
|
||||
The message payload.
|
||||
"""
|
||||
|
||||
|
||||
# documented argument with the same name as a section
|
||||
def should_not_fail(payload, Args):
|
||||
"""
|
||||
Send a message.
|
||||
|
||||
Args:
|
||||
payload:
|
||||
The message payload.
|
||||
|
||||
Args:
|
||||
The other arguments.
|
||||
"""
|
||||
|
||||
@@ -80,6 +80,16 @@ b''.rstrip(b'http://')
|
||||
''.strip(r'\b\x09')
|
||||
''.strip('\\\x5C')
|
||||
|
||||
# Errors: Type inference
|
||||
b = b''
|
||||
b.strip(b'//')
|
||||
|
||||
# Errors: Type inference (preview)
|
||||
foo: str = ""; bar: bytes = b""
|
||||
foo.rstrip("//")
|
||||
bar.lstrip(b"//")
|
||||
|
||||
|
||||
# OK: Different types
|
||||
b"".strip("//")
|
||||
"".strip(b"//")
|
||||
@@ -93,13 +103,8 @@ b"".lstrip(b"//", foo = "bar")
|
||||
"".rstrip()
|
||||
|
||||
# OK: Not literals
|
||||
foo: str = ""; bar: bytes = b""
|
||||
"".strip(foo)
|
||||
b"".strip(bar)
|
||||
|
||||
# False negative
|
||||
foo.rstrip("//")
|
||||
bar.lstrip(b"//")
|
||||
|
||||
# OK: Not `.[lr]?strip`
|
||||
"".mobius_strip("")
|
||||
|
||||
@@ -1,39 +1,98 @@
|
||||
# pylint: disable=missing-docstring, invalid-name, too-few-public-methods, redefined-outer-name
|
||||
|
||||
|
||||
# the rule take care of the following cases:
|
||||
#
|
||||
# | Case | Expression | Fix |
|
||||
# |-------|------------------|---------------|
|
||||
# | 1 | if a >= b: a = b | a = min(b, a) |
|
||||
# | 2 | if a <= b: a = b | a = max(b, a) |
|
||||
# | 3 | if a <= b: b = a | b = min(a, b) |
|
||||
# | 4 | if a >= b: b = a | b = max(a, b) |
|
||||
# | 5 | if a > b: a = b | a = min(a, b) |
|
||||
# | 6 | if a < b: a = b | a = max(a, b) |
|
||||
# | 7 | if a < b: b = a | b = min(b, a) |
|
||||
# | 8 | if a > b: b = a | b = max(b, a) |
|
||||
|
||||
# the 8 base cases
|
||||
a, b = [], []
|
||||
|
||||
# case 1: a = min(b, a)
|
||||
if a >= b:
|
||||
a = b
|
||||
|
||||
# case 2: a = max(b, a)
|
||||
if a <= b:
|
||||
a = b
|
||||
|
||||
# case 3: b = min(a, b)
|
||||
if a <= b:
|
||||
b = a
|
||||
|
||||
# case 4: b = max(a, b)
|
||||
if a >= b:
|
||||
b = a
|
||||
|
||||
# case 5: a = min(a, b)
|
||||
if a > b:
|
||||
a = b
|
||||
|
||||
# case 6: a = max(a, b)
|
||||
if a < b:
|
||||
a = b
|
||||
|
||||
# case 7: b = min(b, a)
|
||||
if a < b:
|
||||
b = a
|
||||
|
||||
# case 8: b = max(b, a)
|
||||
if a > b:
|
||||
b = a
|
||||
|
||||
|
||||
# test cases with assigned variables and primitives
|
||||
value = 10
|
||||
value2 = 0
|
||||
value3 = 3
|
||||
|
||||
# Positive
|
||||
if value < 10: # [max-instead-of-if]
|
||||
# base case 6: value = max(value, 10)
|
||||
if value < 10:
|
||||
value = 10
|
||||
|
||||
if value <= 10: # [max-instead-of-if]
|
||||
# base case 2: value = max(10, value)
|
||||
if value <= 10:
|
||||
value = 10
|
||||
|
||||
if value < value2: # [max-instead-of-if]
|
||||
# base case 6: value = max(value, value2)
|
||||
if value < value2:
|
||||
value = value2
|
||||
|
||||
if value > 10: # [min-instead-of-if]
|
||||
# base case 5: value = min(value, 10)
|
||||
if value > 10:
|
||||
value = 10
|
||||
|
||||
if value >= 10: # [min-instead-of-if]
|
||||
# base case 1: value = min(10, value)
|
||||
if value >= 10:
|
||||
value = 10
|
||||
|
||||
if value > value2: # [min-instead-of-if]
|
||||
# base case 5: value = min(value, value2)
|
||||
if value > value2:
|
||||
value = value2
|
||||
|
||||
|
||||
# cases with calls
|
||||
class A:
|
||||
def __init__(self):
|
||||
self.value = 13
|
||||
|
||||
|
||||
A1 = A()
|
||||
if A1.value < 10: # [max-instead-of-if]
|
||||
|
||||
|
||||
if A1.value < 10:
|
||||
A1.value = 10
|
||||
|
||||
if A1.value > 10: # [min-instead-of-if]
|
||||
if A1.value > 10:
|
||||
A1.value = 10
|
||||
|
||||
|
||||
@@ -159,3 +218,22 @@ class Foo:
|
||||
self._min = value
|
||||
if self._max >= value:
|
||||
self._max = value
|
||||
|
||||
|
||||
counter = {"a": 0, "b": 0}
|
||||
|
||||
# base case 2: counter["a"] = max(counter["b"], counter["a"])
|
||||
if counter["a"] <= counter["b"]:
|
||||
counter["a"] = counter["b"]
|
||||
|
||||
# case 3: counter["b"] = min(counter["a"], counter["b"])
|
||||
if counter["a"] <= counter["b"]:
|
||||
counter["b"] = counter["a"]
|
||||
|
||||
# case 5: counter["a"] = min(counter["a"], counter["b"])
|
||||
if counter["a"] > counter["b"]:
|
||||
counter["b"] = counter["a"]
|
||||
|
||||
# case 8: counter["a"] = max(counter["b"], counter["a"])
|
||||
if counter["a"] > counter["b"]:
|
||||
counter["b"] = counter["a"]
|
||||
|
||||
@@ -28,3 +28,46 @@ else:
|
||||
else:
|
||||
print(3)
|
||||
return None
|
||||
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/16082
|
||||
|
||||
## Errors
|
||||
if sys.version_info < (3, 12, 0):
|
||||
print()
|
||||
|
||||
if sys.version_info <= (3, 12, 0):
|
||||
print()
|
||||
|
||||
if sys.version_info < (3, 12, 11):
|
||||
print()
|
||||
|
||||
if sys.version_info < (3, 13, 0):
|
||||
print()
|
||||
|
||||
if sys.version_info <= (3, 13, 100000):
|
||||
print()
|
||||
|
||||
|
||||
## No errors
|
||||
|
||||
if sys.version_info <= (3, 13, foo):
|
||||
print()
|
||||
|
||||
if sys.version_info <= (3, 13, 'final'):
|
||||
print()
|
||||
|
||||
if sys.version_info <= (3, 13, 0):
|
||||
print()
|
||||
|
||||
if sys.version_info < (3, 13, 37):
|
||||
print()
|
||||
|
||||
if sys.version_info <= (3, 13, 37):
|
||||
print()
|
||||
|
||||
if sys.version_info <= (3, 14, 0):
|
||||
print()
|
||||
|
||||
if sys.version_info <= (3, 14, 15):
|
||||
print()
|
||||
|
||||
@@ -97,3 +97,16 @@ class DataclassWithNewTypeFields:
|
||||
# No errors
|
||||
e: SpecialString = SpecialString("Lorem ipsum")
|
||||
f: NegativeInteger = NegativeInteger(-110)
|
||||
|
||||
|
||||
# Test for:
|
||||
# https://github.com/astral-sh/ruff/issues/15772
|
||||
def f() -> int:
|
||||
return 0
|
||||
|
||||
@dataclass
|
||||
class ShouldMatchB008RuleOfImmutableTypeAnnotationIgnored:
|
||||
this_is_not_fine: list[int] = default_function()
|
||||
# ignored
|
||||
this_is_fine: int = f()
|
||||
|
||||
|
||||
@@ -72,3 +72,10 @@ def method_calls():
|
||||
def format_specifiers():
|
||||
a = 4
|
||||
b = "{a:b} {a:^5}"
|
||||
|
||||
# fstrings are never correct as type definitions
|
||||
# so we should always skip those
|
||||
def in_type_def():
|
||||
from typing import cast
|
||||
a = 'int'
|
||||
cast('f"{a}"','11')
|
||||
|
||||
@@ -42,3 +42,7 @@ import typing
|
||||
type Y = typing.Literal[1, 2]
|
||||
Z: typing.TypeAlias = dict[int, int]
|
||||
class Foo(dict[str, int]): pass
|
||||
|
||||
# Skip tuples of length one that are single-starred expressions
|
||||
# https://github.com/astral-sh/ruff/issues/16077
|
||||
d[*x]
|
||||
|
||||
@@ -42,3 +42,7 @@ import typing
|
||||
type Y = typing.Literal[1, 2]
|
||||
Z: typing.TypeAlias = dict[int, int]
|
||||
class Foo(dict[str, int]): pass
|
||||
|
||||
# Skip tuples of length one that are single-starred expressions
|
||||
# https://github.com/astral-sh/ruff/issues/16077
|
||||
d[*x]
|
||||
|
||||
32
crates/ruff_linter/resources/test/fixtures/ruff/RUF054.py
vendored
Normal file
32
crates/ruff_linter/resources/test/fixtures/ruff/RUF054.py
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
############# Warning ############
|
||||
# This file contains form feeds. #
|
||||
############# Warning ############
|
||||
|
||||
|
||||
# Errors
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _():
|
||||
pass
|
||||
|
||||
if False:
|
||||
print('F')
|
||||
print('T')
|
||||
|
||||
|
||||
# No errors
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def _():
|
||||
pass
|
||||
|
||||
def f():
|
||||
pass
|
||||
@@ -13,6 +13,7 @@ use crate::rules::pycodestyle::rules::{
|
||||
trailing_whitespace,
|
||||
};
|
||||
use crate::rules::pylint;
|
||||
use crate::rules::ruff::rules::indented_form_feed;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::Locator;
|
||||
|
||||
@@ -71,6 +72,12 @@ pub(crate) fn check_physical_lines(
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::IndentedFormFeed) {
|
||||
if let Some(diagnostic) = indented_form_feed(&line) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_no_newline_at_end_of_file {
|
||||
|
||||
@@ -1006,6 +1006,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "051") => (RuleGroup::Preview, rules::ruff::rules::IfKeyInDictDel),
|
||||
(Ruff, "052") => (RuleGroup::Preview, rules::ruff::rules::UsedDummyVariable),
|
||||
(Ruff, "053") => (RuleGroup::Preview, rules::ruff::rules::ClassWithMixedTypeVars),
|
||||
(Ruff, "054") => (RuleGroup::Preview, rules::ruff::rules::IndentedFormFeed),
|
||||
(Ruff, "055") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRegularExpression),
|
||||
(Ruff, "056") => (RuleGroup::Preview, rules::ruff::rules::FalsyDictGetFallback),
|
||||
(Ruff, "057") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryRound),
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::fmt::{Debug, Formatter};
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use ruff_python_ast::docstrings::{leading_space, leading_words};
|
||||
use ruff_python_semantic::Definition;
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
use strum_macros::EnumIter;
|
||||
|
||||
@@ -130,34 +131,6 @@ impl SectionKind {
|
||||
Self::Yields => "Yields",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if a section can contain subsections, as in:
|
||||
/// ```python
|
||||
/// Yields
|
||||
/// ------
|
||||
/// int
|
||||
/// Description of the anonymous integer return value.
|
||||
/// ```
|
||||
///
|
||||
/// For NumPy, see: <https://numpydoc.readthedocs.io/en/latest/format.html>
|
||||
///
|
||||
/// For Google, see: <https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings>
|
||||
pub(crate) fn has_subsections(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Args
|
||||
| Self::Arguments
|
||||
| Self::OtherArgs
|
||||
| Self::OtherParameters
|
||||
| Self::OtherParams
|
||||
| Self::Parameters
|
||||
| Self::Raises
|
||||
| Self::Returns
|
||||
| Self::SeeAlso
|
||||
| Self::Warns
|
||||
| Self::Yields
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SectionContexts<'a> {
|
||||
@@ -195,6 +168,7 @@ impl<'a> SectionContexts<'a> {
|
||||
last.as_ref(),
|
||||
previous_line.as_ref(),
|
||||
lines.peek(),
|
||||
docstring.definition,
|
||||
) {
|
||||
if let Some(mut last) = last.take() {
|
||||
last.range = TextRange::new(last.start(), line.start());
|
||||
@@ -444,6 +418,7 @@ fn suspected_as_section(line: &str, style: SectionStyle) -> Option<SectionKind>
|
||||
}
|
||||
|
||||
/// Check if the suspected context is really a section header.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn is_docstring_section(
|
||||
line: &Line,
|
||||
indent_size: TextSize,
|
||||
@@ -452,7 +427,9 @@ fn is_docstring_section(
|
||||
previous_section: Option<&SectionContextData>,
|
||||
previous_line: Option<&Line>,
|
||||
next_line: Option<&Line>,
|
||||
definition: &Definition<'_>,
|
||||
) -> bool {
|
||||
// for function definitions, track the known argument names for more accurate section detection.
|
||||
// Determine whether the current line looks like a section header, e.g., "Args:".
|
||||
let section_name_suffix = line[usize::from(indent_size + section_name_size)..].trim();
|
||||
let this_looks_like_a_section_name =
|
||||
@@ -509,60 +486,80 @@ fn is_docstring_section(
|
||||
// ```
|
||||
// However, if the header is an _exact_ match (like `Returns:`, as opposed to `returns:`), then
|
||||
// continue to treat it as a section header.
|
||||
if section_kind.has_subsections() {
|
||||
if let Some(previous_section) = previous_section {
|
||||
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
|
||||
if let Some(previous_section) = previous_section {
|
||||
let verbatim = &line[TextRange::at(indent_size, section_name_size)];
|
||||
|
||||
// If the section is more deeply indented, assume it's a subsection, as in:
|
||||
// ```python
|
||||
// def func(args: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Args:
|
||||
// args: The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
if previous_section.indent_size < indent_size {
|
||||
if section_kind.as_str() != verbatim {
|
||||
return false;
|
||||
}
|
||||
// If the section is more deeply indented, assume it's a subsection, as in:
|
||||
// ```python
|
||||
// def func(args: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Args:
|
||||
// args: The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
// As noted above, an exact match for a section name (like the inner `Args:` below) is
|
||||
// treated as a section header, unless the enclosing `Definition` is a function and contains
|
||||
// a parameter with the same name, as in:
|
||||
// ```python
|
||||
// def func(Args: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Args:
|
||||
// Args: The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
if previous_section.indent_size < indent_size {
|
||||
let section_name = section_kind.as_str();
|
||||
if section_name != verbatim || has_parameter(definition, section_name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If the section has a preceding empty line, assume it's _not_ a subsection, as in:
|
||||
// ```python
|
||||
// def func(args: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Args:
|
||||
// args: The arguments to the function.
|
||||
//
|
||||
// returns:
|
||||
// The return value of the function.
|
||||
// """
|
||||
// ```
|
||||
if previous_line.is_some_and(|line| line.trim().is_empty()) {
|
||||
return true;
|
||||
}
|
||||
// If the section has a preceding empty line, assume it's _not_ a subsection, as in:
|
||||
// ```python
|
||||
// def func(args: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Args:
|
||||
// args: The arguments to the function.
|
||||
//
|
||||
// returns:
|
||||
// The return value of the function.
|
||||
// """
|
||||
// ```
|
||||
if previous_line.is_some_and(|line| line.trim().is_empty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the section isn't underlined, and isn't title-cased, assume it's a subsection,
|
||||
// as in:
|
||||
// ```python
|
||||
// def func(parameters: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Parameters:
|
||||
// -----
|
||||
// parameters:
|
||||
// The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
if !next_line_is_underline && verbatim.chars().next().is_some_and(char::is_lowercase) {
|
||||
if section_kind.as_str() != verbatim {
|
||||
return false;
|
||||
}
|
||||
// If the section isn't underlined, and isn't title-cased, assume it's a subsection,
|
||||
// as in:
|
||||
// ```python
|
||||
// def func(parameters: tuple[int]):
|
||||
// """Toggle the gizmo.
|
||||
//
|
||||
// Parameters:
|
||||
// -----
|
||||
// parameters:
|
||||
// The arguments to the function.
|
||||
// """
|
||||
// ```
|
||||
if !next_line_is_underline && verbatim.chars().next().is_some_and(char::is_lowercase) {
|
||||
if section_kind.as_str() != verbatim {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns whether or not `definition` is a function definition and contains a parameter with the
|
||||
/// same name as `section_name`.
|
||||
fn has_parameter(definition: &Definition, section_name: &str) -> bool {
|
||||
definition.as_function_def().is_some_and(|func| {
|
||||
func.parameters
|
||||
.iter()
|
||||
.any(|param| param.name() == section_name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,6 +253,7 @@ impl Rule {
|
||||
Rule::BidirectionalUnicode
|
||||
| Rule::BlankLineWithWhitespace
|
||||
| Rule::DocLineTooLong
|
||||
| Rule::IndentedFormFeed
|
||||
| Rule::LineTooLong
|
||||
| Rule::MissingCopyrightNotice
|
||||
| Rule::MissingNewlineAtEndOfFile
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user