Compare commits

..

65 Commits

Author SHA1 Message Date
David Peter
b0986debd3 Interned visibility constraints 2024-12-20 11:36:21 +01:00
David Peter
fafecc97e4 Revert possibly-unbound changes 2024-12-20 10:18:36 +01:00
David Peter
f0e9cce52a More minor changes 2024-12-20 09:44:21 +01:00
David Peter
b85a56db5b Minor iterator changes 2024-12-20 09:40:43 +01:00
David Peter
0d6425ae4e Use KnownModule 2024-12-20 09:38:24 +01:00
David Peter
03818856eb Minor review comments 2024-12-20 09:36:53 +01:00
David Peter
df3536969f ::default() instead of ::new() 2024-12-20 09:34:42 +01:00
David Peter
3dde382236 Use struct instead of three-tuple 2024-12-20 09:32:09 +01:00
David Peter
2dfe6820f0 Use actual return values in dummy functions 2024-12-20 09:25:43 +01:00
David Peter
025804e548 Simplify code in simplify_visibility_constraints 2024-12-20 09:18:08 +01:00
David Peter
2c104c6955 Add section for yet-unsupported features 2024-12-20 09:18:08 +01:00
David Peter
0b70c1c159 Raise diagnostic for possibly-undeclared symbols 2024-12-20 09:18:08 +01:00
David Peter
89fefe0164 Always build a union of inferred_ty and declared_ty 2024-12-20 09:18:08 +01:00
David Peter
c0e7c08705 Short circuit if declared type is available 2024-12-20 09:18:08 +01:00
David Peter
402f2792ea Use iterators instead of vec 2024-12-20 09:18:08 +01:00
David Peter
4353730913 Store Expression<'db> inside VisibilityConstraint 2024-12-20 09:18:08 +01:00
David Peter
f9f461540c Remove _iter suffix 2024-12-20 09:18:07 +01:00
David Peter
a4d805f86c Fix tests after rebase 2024-12-20 09:18:07 +01:00
David Peter
22e345abc9 Clippy 2024-12-20 09:18:07 +01:00
David Peter
c9eb782d57 Handle ambiguous visibility, fix while-loop inference 2024-12-20 09:18:07 +01:00
David Peter
47bb571b4d Update comment 2024-12-20 09:18:07 +01:00
David Peter
090563e4a6 Fix comment 2024-12-20 09:18:07 +01:00
David Peter
b37f095f6d Rename to simplify_visibility_constraints 2024-12-20 09:18:07 +01:00
David Peter
91fa462fba scope_start_visibility 2024-12-20 09:18:07 +01:00
David Peter
167337b647 Simplify iterators 2024-12-20 09:18:07 +01:00
David Peter
c3d3437846 Rename 2024-12-20 09:18:07 +01:00
David Peter
8dd5cc5b3e Add cross-module test to show that result is based on type inference 2024-12-20 09:18:07 +01:00
David Peter
32390f3710 Fix doc comments 2024-12-20 09:18:07 +01:00
David Peter
0c6c0ee529 Clippy 2024-12-20 09:18:07 +01:00
David Peter
2beabc61c2 Minor renamings 2024-12-20 09:18:07 +01:00
David Peter
c5e32d937e Finally!! 2024-12-20 09:18:07 +01:00
David Peter
2bbd586725 Rename constraints 2024-12-20 09:18:07 +01:00
David Peter
6c64ae0c05 Rename constraints 2024-12-20 09:18:07 +01:00
David Peter
45d544b601 Document current limitations 2024-12-20 09:18:07 +01:00
David Peter
55330c4ed7 Revert "Fix boolean expressions"
This reverts commit 79a1b6bb52230705c10fe668093316c16444f953.
2024-12-20 09:18:07 +01:00
David Peter
8b60946875 Fix boolean expressions 2024-12-20 09:18:07 +01:00
David Peter
d249801bf2 Implement if-expressions 2024-12-20 09:18:07 +01:00
David Peter
c1748f07d6 Extend sys.platform tests 2024-12-20 09:18:07 +01:00
David Peter
189f2ef3cc Fix fuzz build 2024-12-20 09:18:07 +01:00
David Peter
81bfc5227b Clippy 2024-12-20 09:18:07 +01:00
David Peter
9a1d1ea33b sys.platform documentation 2024-12-20 09:18:07 +01:00
David Peter
12f139df87 Reactivate symbol_state tests 2024-12-20 09:18:07 +01:00
David Peter
8a93a9a55a Refactor 2024-12-20 09:18:07 +01:00
David Peter
2e6f757456 Rename, comment 2024-12-20 09:18:07 +01:00
David Peter
853e171ed1 Further cleanup 2024-12-20 09:18:07 +01:00
David Peter
afe1572d7f Rename 2024-12-20 09:18:07 +01:00
David Peter
79b582c584 Refactoring 2024-12-20 09:18:07 +01:00
David Peter
5df51f26cc Minor cleanup 2024-12-20 09:18:07 +01:00
David Peter
4f6fba2cab Introduce VisibilityConstraints struct 2024-12-20 09:18:07 +01:00
David Peter
3a1dbc182f Renamings 2024-12-20 09:18:07 +01:00
David Peter
90e639bd20 Short circuit, increase threshold 2024-12-20 09:18:07 +01:00
David Peter
e044fde784 Add recursion limit hack 2024-12-20 09:18:07 +01:00
David Peter
a37dac1b41 Fix serde feature compilation problem 2024-12-20 09:18:07 +01:00
David Peter
27b64a0c66 Update snapshots 2024-12-20 09:18:07 +01:00
David Peter
00e698b346 Clippy suggestions 2024-12-20 09:18:07 +01:00
David Peter
e871ea19e7 Fix boolean expression tests 2024-12-20 09:18:07 +01:00
David Peter
2b12c496ff Fix match control flow 2024-12-20 09:18:07 +01:00
David Peter
c28bcdbc20 Fix match control flow 2024-12-20 09:18:07 +01:00
David Peter
dc9fbaaef1 Add tests for common use cases 2024-12-20 09:18:07 +01:00
David Peter
3d85c7d09c Add support for sys.platform 2024-12-20 09:18:07 +01:00
David Peter
76e277b02a Reset symbol states after if-elif-else chains 2024-12-20 09:18:07 +01:00
David Peter
2042c687b1 Fix string annotation tests 2024-12-20 09:18:07 +01:00
David Peter
7b79a56ea6 Another patch to fix the sys.version_info tests 2024-12-20 09:18:07 +01:00
David Peter
34be5b6b00 Temporarily patch typeshed to avoid cycles 2024-12-20 09:18:07 +01:00
David Peter
e8cfb341f2 [red-knot] Statically known branches 2024-12-20 09:18:07 +01:00
156 changed files with 3638 additions and 9910 deletions

View File

@@ -349,7 +349,7 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@v4
- uses: actions/download-artifact@v4
name: Download Ruff binary to test
id: download-cached-binary
@@ -613,7 +613,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: Install uv
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v4
- uses: Swatinem/rust-cache@v2
- name: "Install Insiders dependencies"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}

View File

@@ -34,7 +34,7 @@ jobs:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v5
- uses: astral-sh/setup-uv@v4
- name: "Install Rust toolchain"
run: rustup show
- name: "Install mold"

View File

@@ -22,7 +22,7 @@ jobs:
id-token: write
steps:
- name: "Install uv"
uses: astral-sh/setup-uv@v5
uses: astral-sh/setup-uv@v4
- uses: actions/download-artifact@v4
with:
pattern: wheels-*

View File

@@ -23,7 +23,7 @@ repos:
- id: validate-pyproject
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.21
rev: 0.7.19
hooks:
- id: mdformat
additional_dependencies:
@@ -59,7 +59,7 @@ repos:
- black==24.10.0
- repo: https://github.com/crate-ci/typos
rev: v1.28.4
rev: v1.28.3
hooks:
- id: typos
@@ -73,7 +73,7 @@ repos:
pass_filenames: false # This makes it a lot faster
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.8.4
rev: v0.8.3
hooks:
- id: ruff-format
- id: ruff
@@ -91,7 +91,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: v0.10.0
rev: v0.9.2
hooks:
- id: zizmor
@@ -103,7 +103,7 @@ repos:
# `actionlint` hook, for verifying correct syntax in GitHub Actions workflows.
# Some additional configuration for `actionlint` can be found in `.github/actionlint.yaml`.
- repo: https://github.com/rhysd/actionlint
rev: v1.7.5
rev: v1.7.4
hooks:
- id: actionlint
stages:

View File

@@ -467,7 +467,7 @@ cargo build --release && hyperfine --warmup 10 \
"./target/release/ruff check ./crates/ruff_linter/resources/test/cpython/ --no-cache -e --select W505,E501"
```
You can run `uv venv --project ./scripts/benchmarks`, activate the venv and then run `uv sync --project ./scripts/benchmarks` to create a working environment for the
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
above. All reported benchmarks were computed using the versions specified by
`./scripts/benchmarks/pyproject.toml` on Python 3.11.

146
Cargo.lock generated
View File

@@ -117,9 +117,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.95"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "append-only-vec"
@@ -413,7 +413,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -470,14 +470,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
name = "compact_str"
version = "0.8.1"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32"
checksum = "6050c3a16ddab2e412160b31f2c871015704239bca62f72f6e5f0be631d3f644"
dependencies = [
"castaway",
"cfg-if",
@@ -693,7 +693,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -704,7 +704,7 @@ checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
dependencies = [
"darling_core",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -774,7 +774,7 @@ dependencies = [
"glob",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -826,7 +826,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -877,9 +877,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.11.6"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0"
checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d"
dependencies = [
"anstream",
"anstyle",
@@ -1019,9 +1019,9 @@ dependencies = [
[[package]]
name = "glob"
version = "0.3.2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "globset"
@@ -1246,7 +1246,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -1420,7 +1420,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -1521,9 +1521,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.169"
version = "0.2.168"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d"
[[package]]
name = "libcst"
@@ -1547,7 +1547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2ae40017ac09cd2c6a53504cb3c871c7f2b41466eac5bc66ba63f39073b467b"
dependencies = [
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -1607,14 +1607,13 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lsp-server"
version = "0.7.8"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9"
checksum = "550446e84739dcaf6d48a4a093973850669e13e8a34d8f8d64851041be267cd9"
dependencies = [
"crossbeam-channel",
"log",
"serde",
"serde_derive",
"serde_json",
]
@@ -2015,7 +2014,7 @@ dependencies = [
"pest_meta",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -2162,7 +2161,7 @@ dependencies = [
"newtype-uuid",
"quick-xml",
"strip-ansi-escapes",
"thiserror 2.0.9",
"thiserror 2.0.7",
"uuid",
]
@@ -2197,9 +2196,9 @@ dependencies = [
[[package]]
name = "quote"
version = "1.0.38"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -2307,7 +2306,6 @@ dependencies = [
"ruff_python_literal",
"ruff_python_parser",
"ruff_python_stdlib",
"ruff_python_trivia",
"ruff_source_file",
"ruff_text_size",
"rustc-hash 2.1.0",
@@ -2317,7 +2315,7 @@ dependencies = [
"static_assertions",
"tempfile",
"test-case",
"thiserror 2.0.9",
"thiserror 2.0.7",
"tracing",
]
@@ -2414,7 +2412,7 @@ dependencies = [
"rustc-hash 2.1.0",
"salsa",
"serde",
"thiserror 2.0.9",
"thiserror 2.0.7",
"toml",
"tracing",
]
@@ -2567,7 +2565,7 @@ dependencies = [
"strum",
"tempfile",
"test-case",
"thiserror 2.0.9",
"thiserror 2.0.7",
"tikv-jemallocator",
"toml",
"tracing",
@@ -2637,7 +2635,7 @@ dependencies = [
"salsa",
"serde",
"tempfile",
"thiserror 2.0.9",
"thiserror 2.0.7",
"tracing",
"tracing-subscriber",
"tracing-tree",
@@ -2789,7 +2787,7 @@ dependencies = [
"strum",
"strum_macros",
"test-case",
"thiserror 2.0.9",
"thiserror 2.0.7",
"toml",
"typed-arena",
"unicode-normalization",
@@ -2806,7 +2804,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -2823,7 +2821,7 @@ dependencies = [
"serde_json",
"serde_with",
"test-case",
"thiserror 2.0.9",
"thiserror 2.0.7",
"uuid",
]
@@ -2895,7 +2893,7 @@ dependencies = [
"similar",
"smallvec",
"static_assertions",
"thiserror 2.0.9",
"thiserror 2.0.7",
"tracing",
]
@@ -3028,7 +3026,7 @@ dependencies = [
"serde",
"serde_json",
"shellexpand",
"thiserror 2.0.9",
"thiserror 2.0.7",
"tracing",
"tracing-subscriber",
]
@@ -3227,7 +3225,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
"synstructure",
]
@@ -3261,7 +3259,7 @@ dependencies = [
"proc-macro2",
"quote",
"serde_derive_internals",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3284,9 +3282,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "serde"
version = "1.0.217"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
dependencies = [
"serde_derive",
]
@@ -3304,13 +3302,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.217"
version = "1.0.216"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3321,14 +3319,14 @@ checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
name = "serde_json"
version = "1.0.134"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
@@ -3344,7 +3342,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3367,9 +3365,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.12.0"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa"
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
dependencies = [
"serde",
"serde_derive",
@@ -3378,14 +3376,14 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.12.0"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e"
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3499,7 +3497,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3521,9 +3519,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.93"
version = "2.0.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058"
checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31"
dependencies = [
"proc-macro2",
"quote",
@@ -3538,7 +3536,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3601,7 +3599,7 @@ dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3612,7 +3610,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
"test-case-core",
]
@@ -3627,11 +3625,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.9"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc"
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767"
dependencies = [
"thiserror-impl 2.0.9",
"thiserror-impl 2.0.7",
]
[[package]]
@@ -3642,18 +3640,18 @@ checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
name = "thiserror-impl"
version = "2.0.9"
version = "2.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4"
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -3775,7 +3773,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -4045,7 +4043,7 @@ checksum = "6b91f57fe13a38d0ce9e28a03463d8d3c2468ed03d75375110ec71d93b449a08"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -4140,7 +4138,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
"wasm-bindgen-shared",
]
@@ -4175,7 +4173,7 @@ checksum = "98c9ae5a76e46f4deecd0f0255cc223cfa18dc9b261213b8aa0c7b36f61b3f1d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -4209,7 +4207,7 @@ checksum = "222ebde6ea87fbfa6bdd2e9f1fd8a91d60aee5db68792632176c4e16a74fc7d8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -4512,7 +4510,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
"synstructure",
]
@@ -4533,7 +4531,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]
@@ -4553,7 +4551,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
"synstructure",
]
@@ -4582,7 +4580,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.93",
"syn 2.0.90",
]
[[package]]

View File

@@ -20,7 +20,6 @@ ruff_python_stdlib = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
ruff_python_literal = { workspace = true }
ruff_python_trivia = { workspace = true }
anyhow = { workspace = true }
bitflags = { workspace = true }

View File

@@ -31,10 +31,10 @@ class C:
def __lt__(self, other) -> C: ...
x = A() < B() < C()
reveal_type(x) # revealed: A & ~AlwaysTruthy | B
reveal_type(x) # revealed: A | B
y = 0 < 1 < A() < 3
reveal_type(y) # revealed: Literal[False] | A
reveal_type(y) # revealed: bool | A
z = 10 < 0 < A() < B() < C()
reveal_type(z) # revealed: Literal[False]

View File

@@ -10,8 +10,8 @@ def _(foo: str):
reveal_type(False or "z") # revealed: Literal["z"]
reveal_type(False or True) # revealed: Literal[True]
reveal_type(False or False) # revealed: Literal[False]
reveal_type(foo or False) # revealed: str & ~AlwaysFalsy | Literal[False]
reveal_type(foo or True) # revealed: str & ~AlwaysFalsy | Literal[True]
reveal_type(foo or False) # revealed: str | Literal[False]
reveal_type(foo or True) # revealed: str | Literal[True]
```
## AND
@@ -20,8 +20,8 @@ def _(foo: str):
def _(foo: str):
reveal_type(True and False) # revealed: Literal[False]
reveal_type(False and True) # revealed: Literal[False]
reveal_type(foo and False) # revealed: str & ~AlwaysTruthy | Literal[False]
reveal_type(foo and True) # revealed: str & ~AlwaysTruthy | Literal[True]
reveal_type(foo and False) # revealed: str | Literal[False]
reveal_type(foo and True) # revealed: str | Literal[True]
reveal_type("x" and "y" and "z") # revealed: Literal["z"]
reveal_type("x" and "y" and "") # revealed: Literal[""]
reveal_type("" and "y") # revealed: Literal[""]

View File

@@ -219,97 +219,3 @@ else:
# TODO: It should be A. We should improve UnionBuilder or IntersectionBuilder. (issue #15023)
reveal_type(y) # revealed: A & ~AlwaysTruthy | A & ~AlwaysFalsy
```
## Truthiness of classes
```py
class MetaAmbiguous(type):
def __bool__(self) -> bool: ...
class MetaFalsy(type):
def __bool__(self) -> Literal[False]: ...
class MetaTruthy(type):
def __bool__(self) -> Literal[True]: ...
class MetaDeferred(type):
def __bool__(self) -> MetaAmbiguous: ...
class AmbiguousClass(metaclass=MetaAmbiguous): ...
class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ...
class DeferredClass(metaclass=MetaDeferred): ...
def _(
a: type[AmbiguousClass],
t: type[TruthyClass],
f: type[FalsyClass],
d: type[DeferredClass],
ta: type[TruthyClass | AmbiguousClass],
af: type[AmbiguousClass] | type[FalsyClass],
flag: bool,
):
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass]
if ta:
reveal_type(ta) # revealed: type[TruthyClass] | type[AmbiguousClass] & ~AlwaysFalsy
reveal_type(af) # revealed: type[AmbiguousClass] | type[FalsyClass]
if af:
reveal_type(af) # revealed: type[AmbiguousClass] & ~AlwaysFalsy
# TODO: Emit a diagnostic (`d` is not valid in boolean context)
if d:
# TODO: Should be `Unknown`
reveal_type(d) # revealed: type[DeferredClass] & ~AlwaysFalsy
tf = TruthyClass if flag else FalsyClass
reveal_type(tf) # revealed: Literal[TruthyClass, FalsyClass]
if tf:
reveal_type(tf) # revealed: Literal[TruthyClass]
else:
reveal_type(tf) # revealed: Literal[FalsyClass]
```
## Narrowing in chained boolean expressions
```py
from typing import Literal
class A: ...
def _(x: Literal[0, 1]):
reveal_type(x or A()) # revealed: Literal[1] | A
reveal_type(x and A()) # revealed: Literal[0] | A
def _(x: str):
reveal_type(x or A()) # revealed: str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: str & ~AlwaysTruthy | A
def _(x: bool | str):
reveal_type(x or A()) # revealed: Literal[True] | str & ~AlwaysFalsy | A
reveal_type(x and A()) # revealed: Literal[False] | str & ~AlwaysTruthy | A
class Falsy:
def __bool__(self) -> Literal[False]: ...
class Truthy:
def __bool__(self) -> Literal[True]: ...
def _(x: Falsy | Truthy):
reveal_type(x or A()) # revealed: Truthy | A
reveal_type(x and A()) # revealed: Falsy | A
class MetaFalsy(type):
def __bool__(self) -> Literal[False]: ...
class MetaTruthy(type):
def __bool__(self) -> Literal[True]: ...
class FalsyClass(metaclass=MetaFalsy): ...
class TruthyClass(metaclass=MetaTruthy): ...
def _(x: type[FalsyClass] | type[TruthyClass]):
reveal_type(x or A()) # revealed: type[TruthyClass] | A
reveal_type(x and A()) # revealed: type[FalsyClass] | A
```

View File

@@ -25,29 +25,3 @@ def f(): ...
f: int = 1
```
## Explicit shadowing involving `def` statements
Since a `def` statement is a declaration, one `def` can shadow another `def`, or shadow a previous
non-`def` declaration, without error.
```py
f = 1
reveal_type(f) # revealed: Literal[1]
def f(): ...
reveal_type(f) # revealed: Literal[f]
def f(x: int) -> int:
raise NotImplementedError
reveal_type(f) # revealed: Literal[f]
f: int = 1
reveal_type(f) # revealed: Literal[1]
def f(): ...
reveal_type(f) # revealed: Literal[f]
```

View File

@@ -1,184 +0,0 @@
# `__slots__`
## Not specified and empty
```py
class A: ...
class B:
__slots__ = ()
class C:
__slots__ = ("lorem", "ipsum")
class AB(A, B): ... # fine
class AC(A, C): ... # fine
class BC(B, C): ... # fine
class ABC(A, B, C): ... # fine
```
## Incompatible tuples
```py
class A:
__slots__ = ("a", "b")
class B:
__slots__ = ("c", "d")
class C(
A, # error: [incompatible-slots]
B, # error: [incompatible-slots]
): ...
```
## Same value
```py
class A:
__slots__ = ("a", "b")
class B:
__slots__ = ("a", "b")
class C(
A, # error: [incompatible-slots]
B, # error: [incompatible-slots]
): ...
```
## Strings
```py
class A:
__slots__ = "abc"
class B:
__slots__ = ("abc",)
class AB(
A, # error: [incompatible-slots]
B, # error: [incompatible-slots]
): ...
```
## Invalid
TODO: Emit diagnostics
```py
class NonString1:
__slots__ = 42
class NonString2:
__slots__ = b"ar"
class NonIdentifier1:
__slots__ = "42"
class NonIdentifier2:
__slots__ = ("lorem", "42")
class NonIdentifier3:
__slots__ = (e for e in ("lorem", "42"))
```
## Inheritance
```py
class A:
__slots__ = ("a", "b")
class B(A): ...
class C:
__slots__ = ("c", "d")
class D(C): ...
class E(
B, # error: [incompatible-slots]
D, # error: [incompatible-slots]
): ...
```
## Single solid base
```py
class A:
__slots__ = ("a", "b")
class B(A): ...
class C(A): ...
class D(B, A): ... # fine
class E(B, C, A): ... # fine
```
## False negatives
### Possibly unbound
```py
def _(flag: bool):
class A:
if flag:
__slots__ = ("a", "b")
class B:
__slots__ = ("c", "d")
# Might or might not be fine at runtime
class C(A, B): ...
```
### Bound but with different types
```py
def _(flag: bool):
class A:
if flag:
__slots__ = ("a", "b")
else:
__slots__ = ()
class B:
__slots__ = ("c", "d")
# Might or might not be fine at runtime
class C(A, B): ...
```
### Non-tuples
```py
class A:
__slots__ = ["a", "b"] # This is treated as "dynamic"
class B:
__slots__ = ("c", "d")
# False negative: [incompatible-slots]
class C(A, B): ...
```
### Post-hoc modifications
```py
class A:
__slots__ = ()
__slots__ += ("a", "b")
reveal_type(A.__slots__) # revealed: @Todo(Support for more binary expressions)
class B:
__slots__ = ("c", "d")
# False negative: [incompatible-slots]
class C(A, B): ...
```
### Built-ins with implicit layouts
```py
# False negative: [incompatible-slots]
class A(int, str): ...
```

View File

@@ -1487,37 +1487,6 @@ if True:
from module import symbol
```
## Known limitations
We currently have a limitation in the complexity (depth) of the visibility constraints that are
supported. This is to avoid pathological cases that would require us to recurse deeply.
```py
x = 1
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or (x := 2) # fmt: skip
# This still works fine:
reveal_type(x) # revealed: Literal[2]
y = 1
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or False or \
False or False or False or (y := 2) # fmt: skip
# TODO: This should ideally be `Literal[2]` as well:
reveal_type(y) # revealed: Literal[1, 2]
```
## Unsupported features
We do not support full unreachable code analysis yet. We also raise diagnostics from

View File

@@ -1,182 +0,0 @@
# Suppressing errors with `knot: ignore`
Type check errors can be suppressed by a `knot: ignore` comment on the same line as the violation.
## Simple `knot: ignore`
```py
a = 4 + test # knot: ignore
```
## Suppressing a specific code
```py
a = 4 + test # knot: ignore[unresolved-reference]
```
## Unused suppression
```py
test = 10
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'"
a = test + 3 # knot: ignore[possibly-unresolved-reference]
```
## Unused suppression if the error codes don't match
```py
# error: [unresolved-reference]
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'possibly-unresolved-reference'"
a = test + 3 # knot: ignore[possibly-unresolved-reference]
```
## Suppressed unused comment
```py
# error: [unused-ignore-comment]
a = 10 / 2 # knot: ignore[division-by-zero]
a = 10 / 2 # knot: ignore[division-by-zero, unused-ignore-comment]
a = 10 / 2 # knot: ignore[unused-ignore-comment, division-by-zero]
a = 10 / 2 # knot: ignore[unused-ignore-comment] # type: ignore
a = 10 / 2 # type: ignore # knot: ignore[unused-ignore-comment]
```
## Unused ignore comment
```py
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unused-ignore-comment'"
a = 10 / 0 # knot: ignore[division-by-zero, unused-ignore-comment]
```
## Multiple unused comments
Today, Red Knot emits a diagnostic for every unused code. We might want to group the codes by
comment at some point in the future.
```py
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'division-by-zero'"
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
a = 10 / 2 # knot: ignore[division-by-zero, unresolved-reference]
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'invalid-assignment'"
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
a = 10 / 0 # knot: ignore[invalid-assignment, division-by-zero, unresolved-reference]
```
## Multiple suppressions
```py
# fmt: off
def test(a: f"f-string type annotation", b: b"byte-string-type-annotation"): ... # knot: ignore[fstring-type-annotation, byte-string-type-annotation]
```
## Can't suppress syntax errors
<!-- blacken-docs:off -->
```py
# error: [invalid-syntax]
# error: [unused-ignore-comment]
def test( # knot: ignore
```
<!-- blacken-docs:on -->
## Can't suppress `revealed-type` diagnostics
```py
a = 10
# revealed: Literal[10]
# error: [unknown-rule] "Unknown rule `revealed-type`"
reveal_type(a) # knot: ignore[revealed-type]
```
## Extra whitespace in type ignore comments is allowed
```py
a = 10 / 0 # knot : ignore
a = 10 / 0 # knot: ignore [ division-by-zero ]
```
## Whitespace is optional
```py
# fmt: off
a = 10 / 0 #knot:ignore[division-by-zero]
```
## Trailing codes comma
Trailing commas in the codes section are allowed:
```py
a = 10 / 0 # knot: ignore[division-by-zero,]
```
## Invalid characters in codes
```py
# error: [division-by-zero]
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a alphanumeric character or `-` or `_` as code"
a = 10 / 0 # knot: ignore[*-*]
```
## Trailing whitespace
<!-- blacken-docs:off -->
```py
a = 10 / 0 # knot: ignore[division-by-zero]
# ^^^^^^ trailing whitespace
```
<!-- blacken-docs:on -->
## Missing comma
A missing comma results in an invalid suppression comment. We may want to recover from this in the
future.
```py
# error: [unresolved-reference]
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes"
a = x / 0 # knot: ignore[division-by-zero unresolved-reference]
```
## Missing closing bracket
```py
# error: [unresolved-reference] "Name `x` used when not defined"
# error: [invalid-ignore-comment] "Invalid `knot: ignore` comment: expected a comma separating the rule codes"
a = x / 2 # knot: ignore[unresolved-reference
```
## Empty codes
An empty codes array suppresses no-diagnostics and is always useless
```py
# error: [division-by-zero]
# error: [unused-ignore-comment] "Unused `knot: ignore` without a code"
a = 4 / 0 # knot: ignore[]
```
## File-level suppression comments
File level suppression comments are currently intentionally unsupported because we've yet to decide
if they should use a different syntax that also supports enabling rules or changing the rule's
severity: `knot: possibly-undefined-reference=error`
```py
# error: [unused-ignore-comment]
# knot: ignore[division-by-zero]
a = 4 / 0 # error: [division-by-zero]
```
## Unknown rule
```py
# error: [unknown-rule] "Unknown rule `is-equal-14`"
a = 10 + 4 # knot: ignore[is-equal-14]
```

View File

@@ -1,118 +0,0 @@
# `@no_type_check`
> If a type checker supports the `no_type_check` decorator for functions, it should suppress all
> type errors for the def statement and its body including any nested functions or classes. It
> should also ignore all parameter and return type annotations and treat the function as if it were
> unannotated. [source](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
## Error in the function body
```py
from typing import no_type_check
@no_type_check
def test() -> int:
return a + 5
```
## Error in nested function
```py
from typing import no_type_check
@no_type_check
def test() -> int:
def nested():
return a + 5
```
## Error in nested class
```py
from typing import no_type_check
@no_type_check
def test() -> int:
class Nested:
def inner(self):
return a + 5
```
## Error in preceding decorator
Don't suppress diagnostics for decorators appearing before the `no_type_check` decorator.
```py
from typing import no_type_check
@unknown_decorator # error: [unresolved-reference]
@no_type_check
def test() -> int:
return a + 5
```
## Error in following decorator
Unlike Pyright and mypy, suppress diagnostics appearing after the `no_type_check` decorator. We do
this because it more closely matches Python's runtime semantics of decorators. For more details, see
the discussion on the
[PR adding `@no_type_check` support](https://github.com/astral-sh/ruff/pull/15122#discussion_r1896869411).
```py
from typing import no_type_check
@no_type_check
@unknown_decorator
def test() -> int:
return a + 5
```
## Error in default value
```py
from typing import no_type_check
@no_type_check
def test(a: int = "test"):
return x + 5
```
## Error in return value position
```py
from typing import no_type_check
@no_type_check
def test() -> Undefined:
return x + 5
```
## `no_type_check` on classes isn't supported
Red Knot does not support decorating classes with `no_type_check`. The behaviour of `no_type_check`
when applied to classes is
[not specified currently](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check),
and is not supported by Pyright or mypy.
A future improvement might be to emit a diagnostic if a `no_type_check` annotation is applied to a
class.
```py
from typing import no_type_check
@no_type_check
class Test:
def test(self):
return a + 5 # error: [unresolved-reference]
```
## `type: ignore` comments in `@no_type_check` blocks
```py
from typing import no_type_check
@no_type_check
def test():
# error: [unused-ignore-comment] "Unused `knot: ignore` directive: 'unresolved-reference'"
return x + 5 # knot: ignore[unresolved-reference]
```

View File

@@ -1,162 +0,0 @@
# Suppressing errors with `type: ignore`
Type check errors can be suppressed by a `type: ignore` comment on the same line as the violation.
## Simple `type: ignore`
```py
a = 4 + test # type: ignore
```
## Multiline ranges
A diagnostic with a multiline range can be suppressed by a comment on the same line as the
diagnostic's start or end. This is the same behavior as Mypy's.
```py
# fmt: off
y = (
4 / 0 # type: ignore
)
y = (
4 / # type: ignore
0
)
y = (
4 /
0 # type: ignore
)
```
Pyright diverges from this behavior and instead applies a suppression if its range intersects with
the diagnostic range. This can be problematic for nested expressions because a suppression in a
child expression now suppresses errors in the outer expression.
For example, the `type: ignore` comment in this example suppresses the error of adding `2` to
`"test"` and adding `"other"` to the result of the cast.
```py path=nested.py
# fmt: off
from typing import cast
y = (
cast(int, "test" +
# TODO: Remove the expected error after implementing `invalid-operator` for binary expressions
# error: [unused-ignore-comment]
2 # type: ignore
)
+ "other" # TODO: expected-error[invalid-operator]
)
```
Mypy flags the second usage.
## Before opening parenthesis
A suppression that applies to all errors before the opening parenthesis.
```py
a: Test = ( # type: ignore
Test() # error: [unresolved-reference]
) # fmt: skip
```
## Multiline string
```py
a: int = 4
a = """
This is a multiline string and the suppression is at its end
""" # type: ignore
```
## Line continuations
Suppressions after a line continuation apply to all previous lines.
```py
# fmt: off
a = test \
+ 2 # type: ignore
a = test \
+ a \
+ 2 # type: ignore
```
## Codes
Mypy supports `type: ignore[code]`. Red Knot doesn't understand mypy's rule names. Therefore, ignore
the codes and suppress all errors.
```py
a = test # type: ignore[name-defined]
```
## Nested comments
```py
# fmt: off
a = test \
+ 2 # fmt: skip # type: ignore
a = test \
+ 2 # type: ignore # fmt: skip
```
## Misspelled `type: ignore`
```py
# error: [unresolved-reference]
# error: [invalid-ignore-comment]
a = test + 2 # type: ignoree
```
## Invalid - ignore on opening parentheses
`type: ignore` comments after an opening parentheses suppress any type errors inside the parentheses
in Pyright. Neither Ruff, nor mypy support this and neither does Red Knot.
```py
# fmt: off
# error: [unused-ignore-comment]
a = ( # type: ignore
test + 4 # error: [unresolved-reference]
)
```
## File level suppression
```py
# type: ignore
a = 10 / 0
b = a / 0
```
## File level suppression with leading shebang
```py
#!/usr/bin/env/python
# type: ignore
a = 10 / 0
b = a / 0
```
## Invalid own-line suppression
```py
"""
File level suppressions must come before any non-trivia token,
including module docstrings.
"""
# error: [unused-ignore-comment] "Unused blanket `type: ignore` directive"
# type: ignore
a = 10 / 0 # error: [division-by-zero]
b = a / 0 # error: [division-by-zero]
```

View File

@@ -61,7 +61,7 @@ reveal_type(c) # revealed: Literal[4]
### Uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# TODO: Add diagnostic (there aren't enough values to unpack)
(a, b, c) = (1, 2)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
@@ -71,7 +71,7 @@ reveal_type(c) # revealed: Unknown
### Uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# TODO: Add diagnostic (too many values to unpack)
(a, b) = (1, 2, 3)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Literal[2]
@@ -80,7 +80,7 @@ reveal_type(b) # revealed: Literal[2]
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
# TODO: Add diagnostic (need more values to unpack)
[a, *b, c, d] = (1, 2)
reveal_type(a) # revealed: Literal[1]
# TODO: Should be list[Any] once support for assigning to starred expression is added
@@ -133,7 +133,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
### Starred expression (6)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 5 or more, got 1)"
# TODO: Add diagnostic (need more values to unpack)
(a, b, c, *d, e, f) = (1,)
reveal_type(a) # revealed: Literal[1]
reveal_type(b) # revealed: Unknown
@@ -199,7 +199,7 @@ reveal_type(b) # revealed: LiteralString
### Uneven unpacking (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# TODO: Add diagnostic (there aren't enough values to unpack)
a, b, c = "ab"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
@@ -209,7 +209,7 @@ reveal_type(c) # revealed: Unknown
### Uneven unpacking (2)
```py
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# TODO: Add diagnostic (too many values to unpack)
a, b = "abc"
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
@@ -218,7 +218,7 @@ reveal_type(b) # revealed: LiteralString
### Starred expression (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 3 or more, got 2)"
# TODO: Add diagnostic (need more values to unpack)
(a, *b, c, d) = "ab"
reveal_type(a) # revealed: LiteralString
# TODO: Should be list[LiteralString] once support for assigning to starred expression is added
@@ -271,7 +271,7 @@ reveal_type(c) # revealed: @Todo(starred unpacking)
### Unicode
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# TODO: Add diagnostic (need more values to unpack)
(a, b) = "é"
reveal_type(a) # revealed: LiteralString
@@ -281,7 +281,7 @@ reveal_type(b) # revealed: Unknown
### Unicode escape (1)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# TODO: Add diagnostic (need more values to unpack)
(a, b) = "\u9E6C"
reveal_type(a) # revealed: LiteralString
@@ -291,7 +291,7 @@ reveal_type(b) # revealed: Unknown
### Unicode escape (2)
```py
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
# TODO: Add diagnostic (need more values to unpack)
(a, b) = "\U0010FFFF"
reveal_type(a) # revealed: LiteralString
@@ -306,273 +306,3 @@ reveal_type(b) # revealed: Unknown
reveal_type(a) # revealed: LiteralString
reveal_type(b) # revealed: LiteralString
```
## Union
### Same types
Union of two tuples of equal length and each element is of the same type.
```py
def _(arg: tuple[int, int] | tuple[int, int]):
(a, b) = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
### Mixed types (1)
Union of two tuples of equal length and one element differs in its type.
```py
def _(arg: tuple[int, int] | tuple[int, str]):
a, b = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int | str
```
### Mixed types (2)
Union of two tuples of equal length and both the element types are different.
```py
def _(arg: tuple[int, str] | tuple[str, int]):
a, b = arg
reveal_type(a) # revealed: int | str
reveal_type(b) # revealed: str | int
```
### Mixed types (3)
Union of three tuples of equal length and various combination of element types:
1. All same types
1. One different type
1. All different types
```py
def _(arg: tuple[int, int, int] | tuple[int, str, bytes] | tuple[int, int, str]):
a, b, c = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int | str
reveal_type(c) # revealed: int | bytes | str
```
### Nested
```py
def _(arg: tuple[int, tuple[str, bytes]] | tuple[tuple[int, bytes], Literal["ab"]]):
a, (b, c) = arg
reveal_type(a) # revealed: int | tuple[int, bytes]
reveal_type(b) # revealed: str
reveal_type(c) # revealed: bytes | LiteralString
```
### Starred expression
```py
def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
a, *b, c = arg
reveal_type(a) # revealed: int
# TODO: Should be `list[bytes | int | str]`
reveal_type(b) # revealed: @Todo(starred unpacking)
reveal_type(c) # revealed: int | bytes
```
### Size mismatch (1)
```py
def _(arg: tuple[int, bytes, int] | tuple[int, int, str, int, bytes]):
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 3)"
# error: [invalid-assignment] "Too many values to unpack (expected 2, got 5)"
a, b = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: bytes | int
```
### Size mismatch (2)
```py
def _(arg: tuple[int, bytes] | tuple[int, str]):
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
# error: [invalid-assignment] "Not enough values to unpack (expected 3, got 2)"
a, b, c = arg
reveal_type(a) # revealed: int
reveal_type(b) # revealed: bytes | str
reveal_type(c) # revealed: Unknown
```
### Same literal types
```py
def _(flag: bool):
if flag:
value = (1, 2)
else:
value = (3, 4)
a, b = value
reveal_type(a) # revealed: Literal[1, 3]
reveal_type(b) # revealed: Literal[2, 4]
```
### Mixed literal types
```py
def _(flag: bool):
if flag:
value = (1, 2)
else:
value = ("a", "b")
a, b = value
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
```
### Typing literal
```py
from typing import Literal
def _(arg: tuple[int, int] | Literal["ab"]):
a, b = arg
reveal_type(a) # revealed: int | LiteralString
reveal_type(b) # revealed: int | LiteralString
```
### Custom iterator (1)
```py
class Iterator:
def __next__(self) -> tuple[int, int] | tuple[int, str]:
return (1, 2)
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
((a, b), c) = Iterable()
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int | str
reveal_type(c) # revealed: tuple[int, int] | tuple[int, str]
```
### Custom iterator (2)
```py
class Iterator:
def __next__(self) -> bytes:
return b""
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
def _(arg: tuple[int, str] | Iterable):
a, b = arg
reveal_type(a) # revealed: int | bytes
reveal_type(b) # revealed: str | bytes
```
## For statement
Unpacking in a `for` statement.
### Same types
```py
def _(arg: tuple[tuple[int, int], tuple[int, int]]):
for a, b in arg:
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
### Mixed types (1)
```py
def _(arg: tuple[tuple[int, int], tuple[int, str]]):
for a, b in arg:
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int | str
```
### Mixed types (2)
```py
def _(arg: tuple[tuple[int, str], tuple[str, int]]):
for a, b in arg:
reveal_type(a) # revealed: int | str
reveal_type(b) # revealed: str | int
```
### Mixed types (3)
```py
def _(arg: tuple[tuple[int, int, int], tuple[int, str, bytes], tuple[int, int, str]]):
for a, b, c in arg:
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int | str
reveal_type(c) # revealed: int | bytes | str
```
### Same literal values
```py
for a, b in ((1, 2), (3, 4)):
reveal_type(a) # revealed: Literal[1, 3]
reveal_type(b) # revealed: Literal[2, 4]
```
### Mixed literal values (1)
```py
for a, b in ((1, 2), ("a", "b")):
reveal_type(a) # revealed: Literal[1] | Literal["a"]
reveal_type(b) # revealed: Literal[2] | Literal["b"]
```
### Mixed literals values (2)
```py
# error: "Object of type `Literal[1]` is not iterable"
# error: "Object of type `Literal[2]` is not iterable"
# error: "Object of type `Literal[4]` is not iterable"
# error: [invalid-assignment] "Not enough values to unpack (expected 2, got 1)"
for a, b in (1, 2, (3, "a"), 4, (5, "b"), "c"):
reveal_type(a) # revealed: Unknown | Literal[3, 5] | LiteralString
reveal_type(b) # revealed: Unknown | Literal["a", "b"]
```
### Custom iterator (1)
```py
class Iterator:
def __next__(self) -> tuple[int, int]:
return (1, 2)
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
for a, b in Iterable():
reveal_type(a) # revealed: int
reveal_type(b) # revealed: int
```
### Custom iterator (2)
```py
class Iterator:
def __next__(self) -> bytes:
return b""
class Iterable:
def __iter__(self) -> Iterator:
return Iterator()
def _(arg: tuple[tuple[int, str], Iterable]):
for a, b in arg:
reveal_type(a) # revealed: int | bytes
reveal_type(b) # revealed: str | bytes
```

View File

@@ -43,7 +43,7 @@ impl<T> AstNodeRef<T> {
}
/// Returns a reference to the wrapped node.
pub const fn node(&self) -> &T {
pub fn node(&self) -> &T {
// SAFETY: Holding on to `parsed` ensures that the AST to which `node` belongs is still
// alive and not moved.
unsafe { self.node.as_ref() }

View File

@@ -1,4 +1,4 @@
use crate::lint::{LintRegistry, RuleSelection};
use crate::lint::RuleSelection;
use ruff_db::files::File;
use ruff_db::{Db as SourceDb, Upcast};
@@ -8,8 +8,6 @@ pub trait Db: SourceDb + Upcast<dyn SourceDb> {
fn is_file_open(&self, file: File) -> bool;
fn rule_selection(&self) -> &RuleSelection;
fn lint_registry(&self) -> &LintRegistry;
}
#[cfg(test)]
@@ -21,7 +19,7 @@ pub(crate) mod tests {
use crate::{default_lint_registry, ProgramSettings, PythonPlatform};
use super::Db;
use crate::lint::{LintRegistry, RuleSelection};
use crate::lint::RuleSelection;
use anyhow::Context;
use ruff_db::files::{File, Files};
use ruff_db::system::{DbWithTestSystem, System, SystemPathBuf, TestSystem};
@@ -47,7 +45,7 @@ pub(crate) mod tests {
vendored: red_knot_vendored::file_system().clone(),
events: Arc::default(),
files: Files::default(),
rule_selection: Arc::new(RuleSelection::from_registry(default_lint_registry())),
rule_selection: Arc::new(RuleSelection::from_registry(&default_lint_registry())),
}
}
@@ -114,10 +112,6 @@ pub(crate) mod tests {
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {
default_lint_registry()
}
}
#[salsa::db]

View File

@@ -3,7 +3,6 @@ use std::hash::BuildHasherDefault;
use rustc_hash::FxHasher;
use crate::lint::{LintRegistry, LintRegistryBuilder};
use crate::suppression::{INVALID_IGNORE_COMMENT, UNKNOWN_RULE, UNUSED_IGNORE_COMMENT};
pub use db::Db;
pub use module_name::ModuleName;
pub use module_resolver::{resolve_module, system_module_search_paths, KnownModule, Module};
@@ -25,7 +24,6 @@ pub mod semantic_index;
mod semantic_model;
pub(crate) mod site_packages;
mod stdlib;
mod suppression;
pub(crate) mod symbol;
pub mod types;
mod unpack;
@@ -34,21 +32,14 @@ mod visibility_constraints;
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
/// Returns the default registry with all known semantic lints.
pub fn default_lint_registry() -> &'static LintRegistry {
static REGISTRY: std::sync::LazyLock<LintRegistry> = std::sync::LazyLock::new(|| {
let mut registry = LintRegistryBuilder::default();
register_lints(&mut registry);
registry.build()
});
&REGISTRY
/// Creates a new registry with all known semantic lints.
pub fn default_lint_registry() -> LintRegistry {
let mut registry = LintRegistryBuilder::default();
register_lints(&mut registry);
registry.build()
}
/// Register all known semantic lints.
pub fn register_lints(registry: &mut LintRegistryBuilder) {
types::register_lints(registry);
registry.register_lint(&UNUSED_IGNORE_COMMENT);
registry.register_lint(&UNKNOWN_RULE);
registry.register_lint(&INVALID_IGNORE_COMMENT);
}

View File

@@ -321,7 +321,7 @@ impl LintRegistryBuilder {
}
}
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug)]
pub struct LintRegistry {
lints: Vec<LintId>,
by_name: FxHashMap<&'static str, LintEntry>,
@@ -374,7 +374,7 @@ impl LintRegistry {
}
}
#[derive(Error, Debug, Clone, PartialEq, Eq)]
#[derive(Error, Debug, Clone)]
pub enum GetLintError {
/// The name maps to this removed lint.
#[error("lint {0} has been removed")]
@@ -385,7 +385,7 @@ pub enum GetLintError {
Unknown(String),
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[derive(Debug, PartialEq, Eq)]
pub enum LintEntry {
/// An existing lint rule. Can be in preview, stable or deprecated.
Lint(LintId),
@@ -444,11 +444,6 @@ impl RuleSelection {
self.lints.get(&lint).copied()
}
/// Returns `true` if the `lint` is enabled.
pub fn is_enabled(&self, lint: LintId) -> bool {
self.severity(lint).is_some()
}
/// Enables `lint` and configures with the given `severity`.
///
/// Overrides any previous configuration for the lint.

View File

@@ -30,7 +30,7 @@ mod use_def;
pub(crate) use self::use_def::{
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationWithConstraint,
DeclarationsIterator, ScopedVisibilityConstraintId,
DeclarationsIterator,
};
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
@@ -156,7 +156,7 @@ impl<'db> SemanticIndex<'db> {
/// Use the Salsa cached [`use_def_map()`] query if you only need the
/// use-def map for a single scope.
#[track_caller]
pub(super) fn use_def_map(&self, scope_id: FileScopeId) -> Arc<UseDefMap> {
pub(super) fn use_def_map(&'db self, scope_id: FileScopeId) -> Arc<UseDefMap<'db>> {
self.use_def_maps[scope_id].clone()
}

View File

@@ -6,9 +6,10 @@ use rustc_hash::{FxHashMap, FxHashSet};
use ruff_db::files::File;
use ruff_db::parsed::ParsedModule;
use ruff_index::IndexVec;
use ruff_python_ast as ast;
use ruff_python_ast::name::Name;
use ruff_python_ast::visitor::{walk_expr, walk_pattern, walk_stmt, Visitor};
use ruff_python_ast::{self as ast, Pattern};
use ruff_python_ast::{BoolOp, Expr};
use crate::ast_node_ref::AstNodeRef;
use crate::module_name::ModuleName;
@@ -24,11 +25,9 @@ use crate::semantic_index::symbol::{
FileScopeId, NodeWithScopeKey, NodeWithScopeRef, Scope, ScopeId, ScopedSymbolId,
SymbolTableBuilder,
};
use crate::semantic_index::use_def::{
FlowSnapshot, ScopedConstraintId, ScopedVisibilityConstraintId, UseDefMapBuilder,
};
use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
use crate::semantic_index::SemanticIndex;
use crate::unpack::{Unpack, UnpackValue};
use crate::unpack::Unpack;
use crate::visibility_constraints::VisibilityConstraint;
use crate::Db;
@@ -66,9 +65,9 @@ pub(super) struct SemanticIndexBuilder<'db> {
current_match_case: Option<CurrentMatchCase<'db>>,
/// Flow states at each `break` in the current loop.
loop_break_states: Vec<FlowSnapshot>,
loop_break_states: Vec<FlowSnapshot<'db>>,
/// Per-scope contexts regarding nested `try`/`except` statements
try_node_context_stack_manager: TryNodeContextStackManager,
try_node_context_stack_manager: TryNodeContextStackManager<'db>,
/// Flags about the file's global scope
has_future_annotations: bool,
@@ -160,7 +159,7 @@ impl<'db> SemanticIndexBuilder<'db> {
let file_scope_id = self.scopes.push(scope);
self.symbol_tables.push(SymbolTableBuilder::default());
self.use_def_maps.push(UseDefMapBuilder::default());
self.use_def_maps.push(UseDefMapBuilder::new(self.db));
let ast_id_scope = self.ast_ids.push(AstIdsBuilder::default());
let scope_id = ScopeId::new(self.db, self.file, file_scope_id, countme::Count::default());
@@ -203,16 +202,17 @@ impl<'db> SemanticIndexBuilder<'db> {
&mut self.ast_ids[scope_id]
}
fn flow_snapshot(&self) -> FlowSnapshot {
fn flow_snapshot(&'db self) -> FlowSnapshot<'db> {
self.current_use_def_map().snapshot()
}
fn flow_restore(&mut self, state: FlowSnapshot) {
fn flow_restore(&'db mut self, state: FlowSnapshot<'db>) {
self.current_use_def_map_mut().restore(state);
}
fn flow_merge(&mut self, state: FlowSnapshot) {
self.current_use_def_map_mut().merge(state);
fn flow_merge(&mut self, state: FlowSnapshot<'db>) {
let db = self.db;
self.current_use_def_map_mut().merge(db, state);
}
fn add_symbol(&mut self, name: Name) -> ScopedSymbolId {
@@ -236,7 +236,8 @@ impl<'db> SemanticIndexBuilder<'db> {
}
fn add_definition(
&mut self,
&'db mut self,
db: &'db dyn Db,
symbol: ScopedSymbolId,
definition_node: impl Into<DefinitionNodeRef<'db>>,
) -> Definition<'db> {
@@ -269,10 +270,10 @@ impl<'db> SemanticIndexBuilder<'db> {
let use_def = self.current_use_def_map_mut();
match category {
DefinitionCategory::DeclarationAndBinding => {
use_def.record_declaration_and_binding(symbol, definition);
use_def.record_declaration_and_binding(db, symbol, definition);
}
DefinitionCategory::Declaration => use_def.record_declaration(symbol, definition),
DefinitionCategory::Binding => use_def.record_binding(symbol, definition),
DefinitionCategory::Declaration => use_def.record_declaration(db, symbol, definition),
DefinitionCategory::Binding => use_def.record_binding(db, symbol, definition),
}
let mut try_node_stack_manager = std::mem::take(&mut self.try_node_context_stack_manager);
@@ -288,7 +289,7 @@ impl<'db> SemanticIndexBuilder<'db> {
constraint
}
fn build_constraint(&mut self, constraint_node: &ast::Expr) -> Constraint<'db> {
fn build_constraint(&mut self, constraint_node: &Expr) -> Constraint<'db> {
let expression = self.add_standalone_expression(constraint_node);
Constraint {
node: ConstraintNode::Expression(expression),
@@ -296,91 +297,71 @@ impl<'db> SemanticIndexBuilder<'db> {
}
}
/// Adds a new constraint to the list of all constraints, but does not record it. Returns the
/// constraint ID for later recording using [`SemanticIndexBuilder::record_constraint_id`].
fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
self.current_use_def_map_mut().add_constraint(constraint)
fn add_constraint(&mut self, constraint: Constraint<'db>) -> Constraint<'db> {
self.current_use_def_map_mut().add_constraint(constraint);
constraint
}
/// Negates a constraint and adds it to the list of all constraints, does not record it.
fn add_negated_constraint(
&mut self,
constraint: Constraint<'db>,
) -> (Constraint<'db>, ScopedConstraintId) {
fn add_negated_constraint(&mut self, constraint: Constraint<'db>) -> Constraint<'db> {
let negated = Constraint {
node: constraint.node,
is_positive: false,
};
let id = self.current_use_def_map_mut().add_constraint(negated);
(negated, id)
self.current_use_def_map_mut().add_constraint(negated);
negated
}
/// Records a previously added constraint by adding it to all live bindings.
fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
self.current_use_def_map_mut()
.record_constraint_id(constraint);
}
/// Adds and records a constraint, i.e. adds it to all live bindings.
fn record_constraint(&mut self, constraint: Constraint<'db>) {
self.current_use_def_map_mut().record_constraint(constraint);
}
/// Negates the given constraint and then adds it to all live bindings.
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
let (_, id) = self.add_negated_constraint(constraint);
self.record_constraint_id(id);
id
fn record_negated_constraint(&mut self, constraint: Constraint<'db>) -> Constraint<'db> {
let negated = self.add_negated_constraint(constraint);
self.record_constraint(negated);
negated
}
/// Adds a new visibility constraint, but does not record it. Returns the constraint ID
/// for later recording using [`SemanticIndexBuilder::record_visibility_constraint_id`].
fn add_visibility_constraint(
&mut self,
constraint: VisibilityConstraint<'db>,
) -> ScopedVisibilityConstraintId {
self.current_use_def_map_mut()
.add_visibility_constraint(constraint)
}
/// Records a previously added visibility constraint by applying it to all live bindings
/// and declarations.
fn record_visibility_constraint_id(&mut self, constraint: ScopedVisibilityConstraintId) {
self.current_use_def_map_mut()
.record_visibility_constraint_id(constraint);
}
/// Negates the given visibility constraint and then adds it to all live bindings and declarations.
fn record_negated_visibility_constraint(
&mut self,
constraint: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
self.current_use_def_map_mut()
.record_visibility_constraint(VisibilityConstraint::VisibleIfNot(constraint))
}
/// Records a visibility constraint by applying it to all live bindings and declarations.
fn record_visibility_constraint(
&mut self,
db: &'db dyn Db,
constraint: VisibilityConstraint<'db>,
) {
self.current_use_def_map_mut()
.record_visibility_constraint(db, constraint);
}
fn create_and_record_visibility_constraint(
&mut self,
db: &'db dyn Db,
constraint: Constraint<'db>,
) -> ScopedVisibilityConstraintId {
) -> VisibilityConstraint<'db> {
let constraint = VisibilityConstraint::visible_if(db, constraint);
self.current_use_def_map_mut()
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
.record_visibility_constraint(db, constraint);
constraint
}
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
fn record_ambiguous_visibility(&mut self, db: &'db dyn Db) {
self.current_use_def_map_mut()
.record_visibility_constraint(VisibilityConstraint::Ambiguous)
.record_visibility_constraint(db, VisibilityConstraint::ambiguous(db));
}
/// Simplifies (resets) visibility constraints on all live bindings and declarations that did
/// not see any new definitions since the given snapshot.
fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot<'db>) {
self.current_use_def_map_mut()
.simplify_visibility_constraints(snapshot);
}
fn record_negated_visibility_constraint(
&mut self,
db: &'db dyn Db,
constraint: VisibilityConstraint<'db>,
) -> VisibilityConstraint<'db> {
let constraint = VisibilityConstraint::visible_if_not(db, constraint);
self.current_use_def_map_mut()
.record_visibility_constraint(db, constraint);
constraint
}
fn push_assignment(&mut self, assignment: CurrentAssignment<'db>) {
self.current_assignments.push(assignment);
}
@@ -407,11 +388,11 @@ impl<'db> SemanticIndexBuilder<'db> {
let guard = guard.map(|guard| self.add_standalone_expression(guard));
let kind = match pattern {
ast::Pattern::MatchValue(pattern) => {
Pattern::MatchValue(pattern) => {
let value = self.add_standalone_expression(&pattern.value);
PatternConstraintKind::Value(value, guard)
}
ast::Pattern::MatchSingleton(singleton) => {
Pattern::MatchSingleton(singleton) => {
PatternConstraintKind::Singleton(singleton.value, guard)
}
_ => PatternConstraintKind::Unsupported,
@@ -490,9 +471,11 @@ impl<'db> SemanticIndexBuilder<'db> {
self.visit_expr(default);
}
match type_param {
ast::TypeParam::TypeVar(node) => self.add_definition(symbol, node),
ast::TypeParam::ParamSpec(node) => self.add_definition(symbol, node),
ast::TypeParam::TypeVarTuple(node) => self.add_definition(symbol, node),
ast::TypeParam::TypeVar(node) => self.add_definition(self.db, symbol, node),
ast::TypeParam::ParamSpec(node) => self.add_definition(self.db, symbol, node),
ast::TypeParam::TypeVarTuple(node) => {
self.add_definition(self.db, symbol, node)
}
};
}
}
@@ -573,20 +556,25 @@ impl<'db> SemanticIndexBuilder<'db> {
if let Some(vararg) = parameters.vararg.as_ref() {
let symbol = self.add_symbol(vararg.name.id().clone());
self.add_definition(
self.db,
symbol,
DefinitionNodeRef::VariadicPositionalParameter(vararg),
);
}
if let Some(kwarg) = parameters.kwarg.as_ref() {
let symbol = self.add_symbol(kwarg.name.id().clone());
self.add_definition(symbol, DefinitionNodeRef::VariadicKeywordParameter(kwarg));
self.add_definition(
self.db,
symbol,
DefinitionNodeRef::VariadicKeywordParameter(kwarg),
);
}
}
fn declare_parameter(&mut self, parameter: &'db ast::ParameterWithDefault) {
let symbol = self.add_symbol(parameter.parameter.name.id().clone());
let definition = self.add_definition(symbol, parameter);
let definition = self.add_definition(self.db, symbol, parameter);
// Insert a mapping from the inner Parameter node to the same definition.
// This ensures that calling `HasTy::ty` on the inner parameter returns
@@ -701,7 +689,7 @@ where
// at the end to match the runtime evaluation of parameter defaults
// and return-type annotations.
let symbol = self.add_symbol(name.id.clone());
self.add_definition(symbol, function_def);
self.add_definition(self.db, symbol, function_def);
}
ast::Stmt::ClassDef(class) => {
for decorator in &class.decorator_list {
@@ -709,7 +697,7 @@ where
}
let symbol = self.add_symbol(class.name.id.clone());
self.add_definition(symbol, class);
self.add_definition(self.db, symbol, class);
self.with_type_params(
NodeWithScopeRef::ClassTypeParameters(class),
@@ -734,7 +722,7 @@ where
.map(|name| name.id.clone())
.unwrap_or("<unknown>".into()),
);
self.add_definition(symbol, type_alias);
self.add_definition(self.db, symbol, type_alias);
self.visit_expr(&type_alias.name);
self.with_type_params(
@@ -762,7 +750,7 @@ where
};
let symbol = self.add_symbol(symbol_name);
self.add_definition(symbol, alias);
self.add_definition(self.db, symbol, alias);
}
}
ast::Stmt::ImportFrom(node) => {
@@ -783,7 +771,11 @@ where
let symbol = self.add_symbol(symbol_name.clone());
self.add_definition(symbol, ImportFromDefinitionNodeRef { node, alias_index });
self.add_definition(
self.db,
symbol,
ImportFromDefinitionNodeRef { node, alias_index },
);
}
}
ast::Stmt::Assign(node) => {
@@ -809,7 +801,7 @@ where
unsafe {
AstNodeRef::new(self.module.clone(), target)
},
UnpackValue::Assign(value),
value,
countme::Count::default(),
)),
})
@@ -883,7 +875,8 @@ where
let mut constraints = vec![constraint];
self.visit_body(&node.body);
let visibility_constraint_id = self.record_visibility_constraint(constraint);
let visibility_constraint_id =
self.create_and_record_visibility_constraint(self.db, constraint);
let mut vis_constraints = vec![visibility_constraint_id];
let mut post_clauses: Vec<FlowSnapshot> = vec![];
@@ -924,10 +917,11 @@ where
self.visit_body(clause_body);
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
self.record_negated_visibility_constraint(self.db, *id);
}
if let Some(elif_constraint) = elif_constraint {
let id = self.record_visibility_constraint(elif_constraint);
let id =
self.create_and_record_visibility_constraint(self.db, elif_constraint);
vis_constraints.push(id);
}
}
@@ -959,7 +953,8 @@ where
self.visit_body(body);
self.set_inside_loop(outer_loop_state);
let vis_constraint_id = self.record_visibility_constraint(constraint);
let vis_constraint_id =
self.create_and_record_visibility_constraint(self.db, constraint);
// Get the break states from the body of this loop, and restore the saved outer
// ones.
@@ -971,14 +966,14 @@ where
self.flow_merge(pre_loop.clone());
self.record_negated_constraint(constraint);
self.visit_body(orelse);
self.record_negated_visibility_constraint(vis_constraint_id);
self.record_negated_visibility_constraint(self.db, vis_constraint_id);
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
// states after visiting `else`.
for break_state in break_states {
let snapshot = self.flow_snapshot();
self.flow_restore(break_state);
self.record_visibility_constraint(constraint);
self.create_and_record_visibility_constraint(self.db, constraint);
self.flow_merge(snapshot);
}
@@ -1020,47 +1015,18 @@ where
orelse,
},
) => {
debug_assert_eq!(&self.current_assignments, &[]);
let iter_expr = self.add_standalone_expression(iter);
self.add_standalone_expression(iter);
self.visit_expr(iter);
self.record_ambiguous_visibility();
self.record_ambiguous_visibility(self.db);
let pre_loop = self.flow_snapshot();
let saved_break_states = std::mem::take(&mut self.loop_break_states);
let current_assignment = match &**target {
ast::Expr::List(_) | ast::Expr::Tuple(_) => Some(CurrentAssignment::For {
node: for_stmt,
first: true,
unpack: Some(Unpack::new(
self.db,
self.file,
self.current_scope(),
#[allow(unsafe_code)]
unsafe {
AstNodeRef::new(self.module.clone(), target)
},
UnpackValue::Iterable(iter_expr),
countme::Count::default(),
)),
}),
ast::Expr::Name(_) => Some(CurrentAssignment::For {
node: for_stmt,
unpack: None,
first: false,
}),
_ => None,
};
if let Some(current_assignment) = current_assignment {
self.push_assignment(current_assignment);
}
debug_assert_eq!(&self.current_assignments, &[]);
self.push_assignment(for_stmt.into());
self.visit_expr(target);
if current_assignment.is_some() {
self.pop_assignment();
}
self.pop_assignment();
// TODO: Definitions created by loop variables
// (and definitions created inside the body)
@@ -1106,7 +1072,7 @@ where
self.visit_match_case(first);
let first_vis_constraint_id =
self.record_visibility_constraint(first_constraint_id);
self.create_and_record_visibility_constraint(self.db, first_constraint_id);
let mut vis_constraints = vec![first_vis_constraint_id];
let mut post_case_snapshots = vec![];
@@ -1121,9 +1087,10 @@ where
self.visit_match_case(case);
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
self.record_negated_visibility_constraint(self.db, *id);
}
let vis_constraint_id = self.record_visibility_constraint(constraint_id);
let vis_constraint_id =
self.create_and_record_visibility_constraint(self.db, constraint_id);
vis_constraints.push(vis_constraint_id);
}
@@ -1137,7 +1104,7 @@ where
self.flow_restore(after_subject.clone());
for id in &vis_constraints {
self.record_negated_visibility_constraint(*id);
self.record_negated_visibility_constraint(self.db, *id);
}
}
@@ -1155,7 +1122,7 @@ where
is_star,
range: _,
}) => {
self.record_ambiguous_visibility();
self.record_ambiguous_visibility(self.db);
// Save the state prior to visiting any of the `try` block.
//
@@ -1214,6 +1181,7 @@ where
let symbol = self.add_symbol(symbol_name.id.clone());
self.add_definition(
self.db,
symbol,
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
handler: except_handler,
@@ -1296,6 +1264,7 @@ where
unpack,
}) => {
self.add_definition(
self.db,
symbol,
AssignmentDefinitionNodeRef {
unpack,
@@ -1306,23 +1275,18 @@ where
);
}
Some(CurrentAssignment::AnnAssign(ann_assign)) => {
self.add_definition(symbol, ann_assign);
self.add_definition(self.db, symbol, ann_assign);
}
Some(CurrentAssignment::AugAssign(aug_assign)) => {
self.add_definition(symbol, aug_assign);
self.add_definition(self.db, symbol, aug_assign);
}
Some(CurrentAssignment::For {
node,
first,
unpack,
}) => {
Some(CurrentAssignment::For(node)) => {
self.add_definition(
self.db,
symbol,
ForStmtDefinitionNodeRef {
unpack,
first,
iterable: &node.iter,
name: name_node,
target: name_node,
is_async: node.is_async,
},
);
@@ -1331,10 +1295,11 @@ where
// TODO(dhruvmanila): If the current scope is a comprehension, then the
// named expression is implicitly nonlocal. This is yet to be
// implemented.
self.add_definition(symbol, named);
self.add_definition(self.db, symbol, named);
}
Some(CurrentAssignment::Comprehension { node, first }) => {
self.add_definition(
self.db,
symbol,
ComprehensionDefinitionNodeRef {
iterable: &node.iter,
@@ -1346,6 +1311,7 @@ where
}
Some(CurrentAssignment::WithItem { item, is_async }) => {
self.add_definition(
self.db,
symbol,
WithItemDefinitionNodeRef {
node: item,
@@ -1358,9 +1324,7 @@ where
}
}
if let Some(
CurrentAssignment::Assign { first, .. } | CurrentAssignment::For { first, .. },
) = self.current_assignment_mut()
if let Some(CurrentAssignment::Assign { first, .. }) = self.current_assignment_mut()
{
*first = false;
}
@@ -1409,13 +1373,14 @@ where
let pre_if = self.flow_snapshot();
let constraint = self.record_expression_constraint(test);
self.visit_expr(body);
let visibility_constraint = self.record_visibility_constraint(constraint);
let visibility_constraint =
self.create_and_record_visibility_constraint(self.db, constraint);
let post_body = self.flow_snapshot();
self.flow_restore(pre_if.clone());
self.record_negated_constraint(constraint);
self.visit_expr(orelse);
self.record_negated_visibility_constraint(visibility_constraint);
self.record_negated_visibility_constraint(self.db, visibility_constraint);
self.flow_merge(post_body);
self.simplify_visibility_constraints(pre_if);
}
@@ -1483,19 +1448,19 @@ where
self.visit_expr(value);
for vid in &visibility_constraints {
self.record_visibility_constraint_id(*vid);
self.record_visibility_constraint(self.db, *vid);
}
// For the last value, we don't need to model control flow. There is short-circuiting
// anymore.
if index < values.len() - 1 {
let constraint = self.build_constraint(value);
let (constraint, constraint_id) = match op {
ast::BoolOp::And => (constraint, self.add_constraint(constraint)),
ast::BoolOp::Or => self.add_negated_constraint(constraint),
let constraint = match op {
BoolOp::And => self.add_constraint(constraint),
BoolOp::Or => self.add_negated_constraint(constraint),
};
let visibility_constraint = self
.add_visibility_constraint(VisibilityConstraint::VisibleIf(constraint));
let visibility_constraint =
VisibilityConstraint::visible_if(self.db, constraint);
let after_expr = self.flow_snapshot();
@@ -1504,16 +1469,16 @@ where
// we record all previously existing visibility constraints, and negate the
// one for the current expression.
for vid in &visibility_constraints {
self.record_visibility_constraint_id(*vid);
self.record_visibility_constraint(*vid);
}
self.record_negated_visibility_constraint(visibility_constraint);
self.record_negated_visibility_constraint(self.db, visibility_constraint);
snapshots.push(self.flow_snapshot());
// Then we model the non-short-circuiting behavior. Here, we need to delay
// the application of the visibility constraint until after the expression
// has been evaluated, so we only push it onto the stack here.
self.flow_restore(after_expr);
self.record_constraint_id(constraint_id);
self.record_constraint(constraint);
visibility_constraints.push(visibility_constraint);
}
}
@@ -1559,6 +1524,7 @@ where
let symbol = self.add_symbol(name.id().clone());
let state = self.current_match_case.as_ref().unwrap();
self.add_definition(
self.db,
symbol,
MatchPatternDefinitionNodeRef {
pattern: state.pattern,
@@ -1580,6 +1546,7 @@ where
let symbol = self.add_symbol(name.id().clone());
let state = self.current_match_case.as_ref().unwrap();
self.add_definition(
self.db,
symbol,
MatchPatternDefinitionNodeRef {
pattern: state.pattern,
@@ -1602,11 +1569,7 @@ enum CurrentAssignment<'a> {
},
AnnAssign(&'a ast::StmtAnnAssign),
AugAssign(&'a ast::StmtAugAssign),
For {
node: &'a ast::StmtFor,
first: bool,
unpack: Option<Unpack<'a>>,
},
For(&'a ast::StmtFor),
Named(&'a ast::ExprNamed),
Comprehension {
node: &'a ast::Comprehension,
@@ -1630,6 +1593,12 @@ impl<'a> From<&'a ast::StmtAugAssign> for CurrentAssignment<'a> {
}
}
impl<'a> From<&'a ast::StmtFor> for CurrentAssignment<'a> {
fn from(value: &'a ast::StmtFor) -> Self {
Self::For(value)
}
}
impl<'a> From<&'a ast::ExprNamed> for CurrentAssignment<'a> {
fn from(value: &'a ast::ExprNamed) -> Self {
Self::Named(value)

View File

@@ -4,9 +4,9 @@ use super::SemanticIndexBuilder;
/// An abstraction over the fact that each scope should have its own [`TryNodeContextStack`]
#[derive(Debug, Default)]
pub(super) struct TryNodeContextStackManager(Vec<TryNodeContextStack>);
pub(super) struct TryNodeContextStackManager<'db>(Vec<TryNodeContextStack<'db>>);
impl TryNodeContextStackManager {
impl<'db> TryNodeContextStackManager<'db> {
/// Push a new [`TryNodeContextStack`] onto the stack of stacks.
///
/// Each [`TryNodeContextStack`] is only valid for a single scope
@@ -46,7 +46,7 @@ impl TryNodeContextStackManager {
}
/// Retrieve the [`TryNodeContextStack`] that is relevant for the current scope.
fn current_try_context_stack(&mut self) -> &mut TryNodeContextStack {
fn current_try_context_stack(&'db mut self) -> &'db mut TryNodeContextStack<'db> {
self.0
.last_mut()
.expect("There should always be at least one `TryBlockContexts` on the stack")
@@ -55,9 +55,9 @@ impl TryNodeContextStackManager {
/// The contexts of nested `try`/`except` blocks for a single scope
#[derive(Debug, Default)]
struct TryNodeContextStack(Vec<TryNodeContext>);
struct TryNodeContextStack<'db>(Vec<TryNodeContext<'db>>);
impl TryNodeContextStack {
impl<'db> TryNodeContextStack<'db> {
/// Push a new [`TryNodeContext`] for recording intermediate states
/// while visiting a [`ruff_python_ast::StmtTry`] node that has a `finally` branch.
fn push_context(&mut self) {
@@ -90,11 +90,11 @@ impl TryNodeContextStack {
/// It will likely be necessary to add more fields to this struct in the future
/// when we add more advanced handling of `finally` branches.
#[derive(Debug, Default)]
struct TryNodeContext {
try_suite_snapshots: Vec<FlowSnapshot>,
struct TryNodeContext<'db> {
try_suite_snapshots: Vec<FlowSnapshot<'db>>,
}
impl TryNodeContext {
impl<'db> TryNodeContext<'db> {
/// Take a record of what the internal state looked like after a definition
fn record_definition(&mut self, snapshot: FlowSnapshot) {
self.try_suite_snapshots.push(snapshot);

View File

@@ -5,13 +5,13 @@ use crate::db::Db;
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) struct Constraint<'db> {
pub(crate) node: ConstraintNode<'db>,
pub(crate) is_positive: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum ConstraintNode<'db> {
Expression(Expression<'db>),
Pattern(PatternConstraint<'db>),

View File

@@ -225,10 +225,8 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
#[derive(Copy, Clone, Debug)]
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
pub(crate) unpack: Option<Unpack<'a>>,
pub(crate) iterable: &'a ast::Expr,
pub(crate) name: &'a ast::ExprName,
pub(crate) first: bool,
pub(crate) target: &'a ast::ExprName,
pub(crate) is_async: bool,
}
@@ -300,16 +298,12 @@ impl<'db> DefinitionNodeRef<'db> {
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
}
DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
unpack,
iterable,
name,
first,
target,
is_async,
}) => DefinitionKind::For(ForStmtDefinitionKind {
target: TargetKind::from(unpack),
iterable: AstNodeRef::new(parsed.clone(), iterable),
name: AstNodeRef::new(parsed, name),
first,
target: AstNodeRef::new(parsed, target),
is_async,
}),
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
@@ -388,12 +382,10 @@ impl<'db> DefinitionNodeRef<'db> {
Self::AnnotatedAssignment(node) => node.into(),
Self::AugmentedAssignment(node) => node.into(),
Self::For(ForStmtDefinitionNodeRef {
unpack: _,
iterable: _,
name,
first: _,
target,
is_async: _,
}) => name.into(),
}) => target.into(),
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
Self::VariadicPositionalParameter(node) => node.into(),
Self::VariadicKeywordParameter(node) => node.into(),
@@ -460,7 +452,7 @@ pub enum DefinitionKind<'db> {
Assignment(AssignmentDefinitionKind<'db>),
AnnotatedAssignment(AstNodeRef<ast::StmtAnnAssign>),
AugmentedAssignment(AstNodeRef<ast::StmtAugAssign>),
For(ForStmtDefinitionKind<'db>),
For(ForStmtDefinitionKind),
Comprehension(ComprehensionDefinitionKind),
VariadicPositionalParameter(AstNodeRef<ast::Parameter>),
VariadicKeywordParameter(AstNodeRef<ast::Parameter>),
@@ -473,14 +465,8 @@ pub enum DefinitionKind<'db> {
TypeVarTuple(AstNodeRef<ast::TypeParamTypeVarTuple>),
}
impl DefinitionKind<'_> {
/// Returns the [`TextRange`] of the definition target.
///
/// A definition target would mainly be the node representing the symbol being defined i.e.,
/// [`ast::ExprName`] or [`ast::Identifier`] but could also be other nodes.
///
/// This is mainly used for logging and debugging purposes.
pub(crate) fn target_range(&self) -> TextRange {
impl Ranged for DefinitionKind<'_> {
fn range(&self) -> TextRange {
match self {
DefinitionKind::Import(alias) => alias.range(),
DefinitionKind::ImportFrom(import) => import.alias().range(),
@@ -491,7 +477,7 @@ impl DefinitionKind<'_> {
DefinitionKind::Assignment(assignment) => assignment.name().range(),
DefinitionKind::AnnotatedAssignment(assign) => assign.target.range(),
DefinitionKind::AugmentedAssignment(aug_assign) => aug_assign.target.range(),
DefinitionKind::For(for_stmt) => for_stmt.name().range(),
DefinitionKind::For(for_stmt) => for_stmt.target().range(),
DefinitionKind::Comprehension(comp) => comp.target().range(),
DefinitionKind::VariadicPositionalParameter(parameter) => parameter.name.range(),
DefinitionKind::VariadicKeywordParameter(parameter) => parameter.name.range(),
@@ -504,7 +490,9 @@ impl DefinitionKind<'_> {
DefinitionKind::TypeVarTuple(type_var_tuple) => type_var_tuple.name.range(),
}
}
}
impl DefinitionKind<'_> {
pub(crate) fn category(&self) -> DefinitionCategory {
match self {
// functions, classes, and imports always bind, and we consider them declarations
@@ -677,32 +665,22 @@ impl WithItemDefinitionKind {
}
#[derive(Clone, Debug)]
pub struct ForStmtDefinitionKind<'db> {
target: TargetKind<'db>,
pub struct ForStmtDefinitionKind {
iterable: AstNodeRef<ast::Expr>,
name: AstNodeRef<ast::ExprName>,
first: bool,
target: AstNodeRef<ast::ExprName>,
is_async: bool,
}
impl<'db> ForStmtDefinitionKind<'db> {
impl ForStmtDefinitionKind {
pub(crate) fn iterable(&self) -> &ast::Expr {
self.iterable.node()
}
pub(crate) fn target(&self) -> TargetKind<'db> {
self.target
pub(crate) fn target(&self) -> &ast::ExprName {
self.target.node()
}
pub(crate) fn name(&self) -> &ast::ExprName {
self.name.node()
}
pub(crate) const fn is_first(&self) -> bool {
self.first
}
pub(crate) const fn is_async(&self) -> bool {
pub(crate) fn is_async(&self) -> bool {
self.is_async
}
}
@@ -778,6 +756,12 @@ impl From<&ast::StmtAugAssign> for DefinitionNodeKey {
}
}
impl From<&ast::StmtFor> for DefinitionNodeKey {
fn from(value: &ast::StmtFor) -> Self {
Self(NodeKey::from_node(value))
}
}
impl From<&ast::Parameter> for DefinitionNodeKey {
fn from(node: &ast::Parameter) -> Self {
Self(NodeKey::from_node(node))

View File

@@ -463,7 +463,10 @@ impl NodeWithScopeKind {
}
pub fn expect_function(&self) -> &ast::StmtFunctionDef {
self.as_function().expect("expected function")
match self {
Self::Function(function) => function.node(),
_ => panic!("expected function"),
}
}
pub fn expect_type_alias(&self) -> &ast::StmtTypeAlias {
@@ -472,13 +475,6 @@ impl NodeWithScopeKind {
_ => panic!("expected type alias"),
}
}
pub const fn as_function(&self) -> Option<&ast::StmtFunctionDef> {
match self {
Self::Function(function) => Some(function.node()),
_ => None,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]

View File

@@ -169,11 +169,17 @@
//! indexvecs in the [`UseDefMap`].
//!
//! There is another special kind of possible "definition" for a symbol: there might be a path from
//! the scope entry to a given use in which the symbol is never bound. We model this with a special
//! "unbound" definition (a `None` entry at the start of the `all_definitions` vector). If that
//! sentinel definition is present in the live bindings at a given use, it means that there is a
//! possible path through control flow in which that symbol is unbound. Similarly, if that sentinel
//! is present in the live declarations, it means that the symbol is (possibly) undeclared.
//! the scope entry to a given use in which the symbol is never bound.
//!
//! The simplest way to model "unbound" would be as a "binding" itself: the initial "binding" for
//! each symbol in a scope. But actually modeling it this way would unnecessarily increase the
//! number of [`Definition`]s that Salsa must track. Since "unbound" is special in that all symbols
//! share it, and it doesn't have any additional per-symbol state, and constraints are irrelevant
//! to it, we can represent it more efficiently: we use the `may_be_unbound` boolean on the
//! [`SymbolBindings`] struct. If this flag is `true` for a use of a symbol, it means the symbol
//! has a path to the use in which it is never bound. If this flag is `false`, it means we've
//! eliminated the possibility of unbound: every control flow path to the use includes a binding
//! for this symbol.
//!
//! To build a [`UseDefMap`], the [`UseDefMapBuilder`] is notified of each new use, definition, and
//! constraint as they are encountered by the
@@ -184,13 +190,11 @@
//! end of the scope, it records the state for each symbol as the public definitions of that
//! symbol.
//!
//! Let's walk through the above example. Initially we do not have any record of `x`. When we add
//! the new symbol (before we process the first binding), we create a new undefined `SymbolState`
//! which has a single live binding (the "unbound" definition) and a single live declaration (the
//! "undeclared" definition). When we see `x = 1`, we record that as the sole live binding of `x`.
//! The "unbound" binding is no longer visible. Then we see `x = 2`, and we replace `x = 1` as the
//! sole live binding of `x`. When we get to `y = x`, we record that the live bindings for that use
//! of `x` are just the `x = 2` definition.
//! Let's walk through the above example. Initially we record for `x` that it has no bindings, and
//! may be unbound. When we see `x = 1`, we record that as the sole live binding of `x`, and flip
//! `may_be_unbound` to `false`. Then we see `x = 2`, and we replace `x = 1` as the sole live
//! binding of `x`. When we get to `y = x`, we record that the live bindings for that use of `x`
//! are just the `x = 2` definition.
//!
//! Then we hit the `if` branch. We visit the `test` node (`flag` in this case), since that will
//! happen regardless. Then we take a pre-branch snapshot of the current state for all symbols,
@@ -203,8 +207,8 @@
//! be the pre-if conditions; if we are entering the `else` clause, we know that the `if` test
//! failed and we didn't execute the `if` body. So we first reset the builder to the pre-if state,
//! using the snapshot we took previously (meaning we now have `x = 2` as the sole binding for `x`
//! again), and record a *negative* `flag` constraint for all live bindings (`x = 2`). We then
//! visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding of `x`.
//! again), then visit the `else` clause, where `x = 4` replaces `x = 2` as the sole live binding
//! of `x`.
//!
//! Now we reach the end of the if/else, and want to visit the following code. The state here needs
//! to reflect that we might have gone through the `if` branch, or we might have gone through the
@@ -213,58 +217,20 @@
//! snapshot (which has `x = 3` as the only live binding). The result of this merge is that we now
//! have two live bindings of `x`: `x = 3` and `x = 4`.
//!
//! Another piece of information that the `UseDefMap` needs to provide are visibility constraints.
//! These are similar to the narrowing constraints, but apply to bindings and declarations within a
//! control flow path. Consider the following example:
//! ```py
//! x = 1
//! if test:
//! x = 2
//! y = "y"
//! ```
//! In principle, there are two possible control flow paths here. However, if we can statically
//! infer `test` to be always truthy or always falsy (that is, `__bool__` of `test` is of type
//! `Literal[True]` or `Literal[False]`), we can rule out one of the possible paths. To support
//! this feature, we record a visibility constraint of `test` to all live bindings and declarations
//! *after* visiting the body of the `if` statement. And we record a negative visibility constraint
//! `~test` to all live bindings/declarations in the (implicit) `else` branch. For the example
//! above, we would record the following visibility constraints (adding the implicit "unbound"
//! definitions for clarity):
//! ```py
//! x = <unbound> # not live, shadowed by `x = 1`
//! y = <unbound> # visibility constraint: ~test
//!
//! x = 1 # visibility constraint: ~test
//! if test:
//! x = 2 # visibility constraint: test
//! y = "y" # visibility constraint: test
//! ```
//! When we encounter a use of `x` after this `if` statement, we would record two live bindings: `x
//! = 1` with a constraint of `~test`, and `x = 2` with a constraint of `test`. In type inference,
//! when we iterate over all live bindings, we can evaluate these constraints to determine if a
//! particular binding is actually visible. For example, if `test` is always truthy, we only see
//! the `x = 2` binding. If `test` is always falsy, we only see the `x = 1` binding. And if the
//! `__bool__` method of `test` returns type `bool`, we can see both bindings.
//!
//! Note that we also record visibility constraints for the start of the scope. This is important
//! to determine if a symbol is definitely bound, possibly unbound, or definitely unbound. In the
//! example above, The `y = <unbound>` binding is constrained by `~test`, so `y` would only be
//! definitely-bound if `test` is always truthy.
//!
//! The [`UseDefMapBuilder`] itself just exposes methods for taking a snapshot, resetting to a
//! snapshot, and merging a snapshot into the current state. The logic using these methods lives in
//! [`SemanticIndexBuilder`](crate::semantic_index::builder::SemanticIndexBuilder), e.g. where it
//! visits a `StmtIf` node.
pub(crate) use self::symbol_state::ScopedConstraintId;
use self::symbol_state::{
BindingIdWithConstraintsIterator, ConstraintIdIterator, DeclarationIdIterator,
ScopedDefinitionId, SymbolBindings, SymbolDeclarations, SymbolState,
};
pub(crate) use self::symbol_state::{ScopedConstraintId, ScopedVisibilityConstraintId};
use crate::semantic_index::ast_ids::ScopedUseId;
use crate::semantic_index::definition::Definition;
use crate::semantic_index::symbol::ScopedSymbolId;
use crate::semantic_index::use_def::symbol_state::DeclarationIdWithConstraint;
use crate::visibility_constraints::{VisibilityConstraint, VisibilityConstraints};
use crate::visibility_constraints::VisibilityConstraint;
use crate::Db;
use ruff_index::IndexVec;
use rustc_hash::FxHashMap;
@@ -273,23 +239,19 @@ use super::constraint::Constraint;
mod bitset;
mod symbol_state;
type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
pub(crate) type AllConstraints<'db> = IndexVec<ScopedConstraintId, Constraint<'db>>;
/// Applicable definitions and constraints for every use of a name.
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct UseDefMap<'db> {
/// Array of [`Definition`] in this scope. Only the first entry should be `None`;
/// this represents the implicit "unbound"/"undeclared" definition of every symbol.
/// Array of [`Definition`] in this scope.
all_definitions: IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
/// Array of [`Constraint`] in this scope.
all_constraints: AllConstraints<'db>,
/// Array of [`VisibilityConstraint`]s in this scope.
visibility_constraints: VisibilityConstraints<'db>,
/// [`SymbolBindings`] reaching a [`ScopedUseId`].
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings<'db>>,
/// [`SymbolBindings`] or [`SymbolDeclarations`] reaching a given [`Definition`].
///
@@ -303,10 +265,10 @@ pub(crate) struct UseDefMap<'db> {
/// If the definition is both a declaration and a binding -- `x: int = 1` for example -- then
/// we don't actually need anything here, all we'll need to validate is that our own RHS is a
/// valid assignment to our own annotation.
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions<'db>>,
/// [`SymbolState`] visible at end of scope for each symbol.
public_symbols: IndexVec<ScopedSymbolId, SymbolState>,
public_symbols: IndexVec<ScopedSymbolId, SymbolState<'db>>,
}
impl<'db> UseDefMap<'db> {
@@ -318,9 +280,9 @@ impl<'db> UseDefMap<'db> {
}
pub(crate) fn public_bindings(
&self,
&'db self,
symbol: ScopedSymbolId,
) -> BindingWithConstraintsIterator<'_, 'db> {
) -> BindingWithConstraintsIterator<'map, 'db> {
self.bindings_iterator(self.public_symbols[symbol].bindings())
}
@@ -359,12 +321,11 @@ impl<'db> UseDefMap<'db> {
fn bindings_iterator<'map>(
&'map self,
bindings: &'map SymbolBindings,
bindings: &'map SymbolBindings<'db>,
) -> BindingWithConstraintsIterator<'map, 'db> {
BindingWithConstraintsIterator {
all_definitions: &self.all_definitions,
all_constraints: &self.all_constraints,
visibility_constraints: &self.visibility_constraints,
inner: bindings.iter(),
}
}
@@ -375,7 +336,6 @@ impl<'db> UseDefMap<'db> {
) -> DeclarationsIterator<'map, 'db> {
DeclarationsIterator {
all_definitions: &self.all_definitions,
visibility_constraints: &self.visibility_constraints,
inner: declarations.iter(),
}
}
@@ -383,17 +343,16 @@ impl<'db> UseDefMap<'db> {
/// Either live bindings or live declarations for a symbol.
#[derive(Debug, PartialEq, Eq)]
enum SymbolDefinitions {
Bindings(SymbolBindings),
Declarations(SymbolDeclarations),
enum SymbolDefinitions<'db> {
Bindings(SymbolBindings<'db>),
Declarations(SymbolDeclarations<'db>),
}
#[derive(Debug)]
pub(crate) struct BindingWithConstraintsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
all_constraints: &'map AllConstraints<'db>,
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
inner: BindingIdWithConstraintsIterator<'map>,
inner: BindingIdWithConstraintsIterator<'map, 'db>,
}
impl<'map, 'db> Iterator for BindingWithConstraintsIterator<'map, 'db> {
@@ -420,7 +379,7 @@ impl std::iter::FusedIterator for BindingWithConstraintsIterator<'_, '_> {}
pub(crate) struct BindingWithConstraints<'map, 'db> {
pub(crate) binding: Option<Definition<'db>>,
pub(crate) constraints: ConstraintsIterator<'map, 'db>,
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
pub(crate) visibility_constraint: VisibilityConstraint<'db>,
}
pub(crate) struct ConstraintsIterator<'map, 'db> {
@@ -442,28 +401,22 @@ impl std::iter::FusedIterator for ConstraintsIterator<'_, '_> {}
pub(crate) struct DeclarationsIterator<'map, 'db> {
all_definitions: &'map IndexVec<ScopedDefinitionId, Option<Definition<'db>>>,
pub(crate) visibility_constraints: &'map VisibilityConstraints<'db>,
inner: DeclarationIdIterator<'map>,
}
pub(crate) struct DeclarationWithConstraint<'db> {
pub(crate) declaration: Option<Definition<'db>>,
pub(crate) visibility_constraint: ScopedVisibilityConstraintId,
pub(crate) visibility_constraint: VisibilityConstraint<'db>,
}
impl<'db> Iterator for DeclarationsIterator<'_, 'db> {
impl<'map, 'db> Iterator for DeclarationsIterator<'map, 'db> {
type Item = DeclarationWithConstraint<'db>;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(
|DeclarationIdWithConstraint {
definition,
visibility_constraint,
}| {
DeclarationWithConstraint {
declaration: self.all_definitions[definition],
visibility_constraint,
}
|(def_id, visibility_constraint)| DeclarationWithConstraint {
declaration: self.all_definitions[def_id],
visibility_constraint,
},
)
}
@@ -473,9 +426,9 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
/// A snapshot of the definitions and constraints state at a particular point in control flow.
#[derive(Clone, Debug)]
pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
scope_start_visibility: ScopedVisibilityConstraintId,
pub(super) struct FlowSnapshot<'db> {
symbol_states: IndexVec<ScopedSymbolId, SymbolState<'db>>,
scope_start_visibility: VisibilityConstraint<'db>,
}
#[derive(Debug)]
@@ -486,40 +439,34 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Append-only array of [`Constraint`].
all_constraints: AllConstraints<'db>,
/// Append-only array of [`VisibilityConstraint`].
visibility_constraints: VisibilityConstraints<'db>,
/// A constraint which describes the visibility of the unbound/undeclared state, i.e.
/// whether or not the start of the scope is visible. This is important for cases like
/// `if True: x = 1; use(x)` where we need to hide the implicit "x = unbound" binding
/// in the "else" branch.
scope_start_visibility: ScopedVisibilityConstraintId,
scope_start_visibility: VisibilityConstraint<'db>,
/// Live bindings at each so-far-recorded use.
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings>,
bindings_by_use: IndexVec<ScopedUseId, SymbolBindings<'db>>,
/// Live bindings or declarations for each so-far-recorded definition.
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions>,
definitions_by_definition: FxHashMap<Definition<'db>, SymbolDefinitions<'db>>,
/// Currently live bindings and declarations for each symbol.
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
symbol_states: IndexVec<ScopedSymbolId, SymbolState<'db>>,
}
impl Default for UseDefMapBuilder<'_> {
fn default() -> Self {
impl<'db> UseDefMapBuilder<'db> {
pub(crate) fn new(db: &'db dyn Db) -> Self {
Self {
all_definitions: IndexVec::from_iter([None]),
all_constraints: IndexVec::new(),
visibility_constraints: VisibilityConstraints::default(),
scope_start_visibility: ScopedVisibilityConstraintId::ALWAYS_TRUE,
scope_start_visibility: VisibilityConstraint::always_true(db),
bindings_by_use: IndexVec::new(),
definitions_by_definition: FxHashMap::default(),
symbol_states: IndexVec::new(),
}
}
}
impl<'db> UseDefMapBuilder<'db> {
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
let new_symbol = self
.symbol_states
@@ -527,59 +474,43 @@ impl<'db> UseDefMapBuilder<'db> {
debug_assert_eq!(symbol, new_symbol);
}
pub(super) fn record_binding(&mut self, symbol: ScopedSymbolId, binding: Definition<'db>) {
pub(super) fn record_binding(
&'db mut self,
db: &'db dyn Db,
symbol: ScopedSymbolId,
binding: Definition<'db>,
) {
let def_id = self.all_definitions.push(Some(binding));
let symbol_state = &mut self.symbol_states[symbol];
self.definitions_by_definition.insert(
binding,
SymbolDefinitions::Declarations(symbol_state.declarations().clone()),
);
symbol_state.record_binding(def_id);
symbol_state.record_binding(db, def_id);
}
pub(super) fn add_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
self.all_constraints.push(constraint)
}
pub(super) fn record_constraint_id(&mut self, constraint: ScopedConstraintId) {
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) {
let constraint_id = self.add_constraint(constraint);
for state in &mut self.symbol_states {
state.record_constraint(constraint);
state.record_constraint(constraint_id);
}
}
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) -> ScopedConstraintId {
let new_constraint_id = self.add_constraint(constraint);
self.record_constraint_id(new_constraint_id);
new_constraint_id
}
pub(super) fn add_visibility_constraint(
&mut self,
constraint: VisibilityConstraint<'db>,
) -> ScopedVisibilityConstraintId {
self.visibility_constraints.add(constraint)
}
pub(super) fn record_visibility_constraint_id(
&mut self,
constraint: ScopedVisibilityConstraintId,
) {
for state in &mut self.symbol_states {
state.record_visibility_constraint(&mut self.visibility_constraints, constraint);
}
self.scope_start_visibility = self
.visibility_constraints
.add_and_constraint(self.scope_start_visibility, constraint);
}
pub(super) fn record_visibility_constraint(
&mut self,
db: &'db dyn Db,
constraint: VisibilityConstraint<'db>,
) -> ScopedVisibilityConstraintId {
let new_constraint_id = self.add_visibility_constraint(constraint);
self.record_visibility_constraint_id(new_constraint_id);
new_constraint_id
) {
for state in &mut self.symbol_states {
state.record_visibility_constraint(db, constraint);
}
self.scope_start_visibility =
VisibilityConstraint::kleene_and(db, self.scope_start_visibility, constraint);
}
/// This method resets the visibility constraints for all symbols to a previous state
@@ -600,7 +531,7 @@ impl<'db> UseDefMapBuilder<'db> {
/// We build a complex visibility constraint for the `y = 0` binding. We build the same
/// constraint for the `x = 0` binding as well, but at the `RESET` point, we can get rid
/// of it, as the `if`-`elif`-`elif` chain doesn't include any new bindings of `x`.
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot) {
pub(super) fn simplify_visibility_constraints(&mut self, snapshot: FlowSnapshot<'db>) {
debug_assert!(self.symbol_states.len() >= snapshot.symbol_states.len());
self.scope_start_visibility = snapshot.scope_start_visibility;
@@ -617,7 +548,8 @@ impl<'db> UseDefMapBuilder<'db> {
}
pub(super) fn record_declaration(
&mut self,
&'db mut self,
db: &'db dyn Db,
symbol: ScopedSymbolId,
declaration: Definition<'db>,
) {
@@ -627,22 +559,23 @@ impl<'db> UseDefMapBuilder<'db> {
declaration,
SymbolDefinitions::Bindings(symbol_state.bindings().clone()),
);
symbol_state.record_declaration(def_id);
symbol_state.record_declaration(db, def_id);
}
pub(super) fn record_declaration_and_binding(
&mut self,
&'db mut self,
db: &'db dyn Db,
symbol: ScopedSymbolId,
definition: Definition<'db>,
) {
// We don't need to store anything in self.definitions_by_definition.
let def_id = self.all_definitions.push(Some(definition));
let symbol_state = &mut self.symbol_states[symbol];
symbol_state.record_declaration(def_id);
symbol_state.record_binding(def_id);
symbol_state.record_declaration(db, def_id);
symbol_state.record_binding(db, def_id);
}
pub(super) fn record_use(&mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
pub(super) fn record_use(&'db mut self, symbol: ScopedSymbolId, use_id: ScopedUseId) {
// We have a use of a symbol; clone the current bindings for that symbol, and record them
// as the live bindings for this use.
let new_use = self
@@ -652,7 +585,7 @@ impl<'db> UseDefMapBuilder<'db> {
}
/// Take a snapshot of the current visible-symbols state.
pub(super) fn snapshot(&self) -> FlowSnapshot {
pub(super) fn snapshot(&'db self) -> FlowSnapshot<'db> {
FlowSnapshot {
symbol_states: self.symbol_states.clone(),
scope_start_visibility: self.scope_start_visibility,
@@ -660,7 +593,7 @@ impl<'db> UseDefMapBuilder<'db> {
}
/// Restore the current builder symbols state to the given snapshot.
pub(super) fn restore(&mut self, snapshot: FlowSnapshot) {
pub(super) fn restore(&mut self, snapshot: FlowSnapshot<'db>) {
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
// IDs must line up), so the current number of known symbols must always be equal to or
// greater than the number of known symbols in a previously-taken snapshot.
@@ -683,7 +616,7 @@ impl<'db> UseDefMapBuilder<'db> {
/// Merge the given snapshot into the current state, reflecting that we might have taken either
/// path to get here. The new state for each symbol should include definitions from both the
/// prior state and the snapshot.
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
pub(super) fn merge(&mut self, db: &'db dyn Db, snapshot: FlowSnapshot<'db>) {
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
// IDs must line up), so the current number of known symbols must always be equal to or
// greater than the number of known symbols in a previously-taken snapshot.
@@ -692,19 +625,18 @@ impl<'db> UseDefMapBuilder<'db> {
let mut snapshot_definitions_iter = snapshot.symbol_states.into_iter();
for current in &mut self.symbol_states {
if let Some(snapshot) = snapshot_definitions_iter.next() {
current.merge(snapshot, &mut self.visibility_constraints);
current.merge(db, snapshot);
} else {
current.merge(
SymbolState::undefined(snapshot.scope_start_visibility),
&mut self.visibility_constraints,
);
current.merge(db, SymbolState::undefined(snapshot.scope_start_visibility));
// Symbol not present in snapshot, so it's unbound/undeclared from that path.
}
}
self.scope_start_visibility = self
.visibility_constraints
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
self.scope_start_visibility = VisibilityConstraint::kleene_or(
db,
self.scope_start_visibility,
snapshot.scope_start_visibility,
);
}
pub(super) fn finish(mut self) -> UseDefMap<'db> {
@@ -717,7 +649,6 @@ impl<'db> UseDefMapBuilder<'db> {
UseDefMap {
all_definitions: self.all_definitions,
all_constraints: self.all_constraints,
visibility_constraints: self.visibility_constraints,
bindings_by_use: self.bindings_by_use,
public_symbols: self.symbol_states,
definitions_by_definition: self.definitions_by_definition,

View File

@@ -43,7 +43,7 @@
//!
//! Tracking live declarations is simpler, since constraints are not involved, but otherwise very
//! similar to tracking live bindings.
use crate::semantic_index::use_def::VisibilityConstraints;
use crate::{visibility_constraints::VisibilityConstraint, Db};
use super::bitset::{BitSet, BitSetIterator};
use ruff_index::newtype_index;
@@ -54,11 +54,6 @@ use smallvec::SmallVec;
pub(super) struct ScopedDefinitionId;
impl ScopedDefinitionId {
/// A special ID that is used to describe an implicit start-of-scope state. When
/// we see that this definition is live, we know that the symbol is (possibly)
/// unbound or undeclared at a given usage site.
/// When creating a use-def-map builder, we always add an empty `None` definition
/// at index 0, so this ID is always present.
pub(super) const UNBOUND: ScopedDefinitionId = ScopedDefinitionId::from_u32(0);
}
@@ -98,46 +93,29 @@ type ConstraintsPerBinding = SmallVec<InlineConstraintArray>;
type ConstraintsIterator<'a> = std::slice::Iter<'a, Constraints>;
type ConstraintsIntoIterator = smallvec::IntoIter<InlineConstraintArray>;
/// A newtype-index for a visibility constraint in a particular scope.
#[newtype_index]
pub(crate) struct ScopedVisibilityConstraintId;
impl ScopedVisibilityConstraintId {
/// A special ID that is used for an "always true" / "always visible" constraint.
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
/// present at index 0.
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
ScopedVisibilityConstraintId::from_u32(0);
}
/// Similar to what we have above, but for visibility constraints.
const INLINE_VISIBILITY_CONSTRAINTS: usize = 4;
type InlineVisibilityConstraintsArray =
[ScopedVisibilityConstraintId; INLINE_VISIBILITY_CONSTRAINTS];
type InlineVisibilityConstraintsArray<'db> =
[VisibilityConstraint<'db>; INLINE_VISIBILITY_CONSTRAINTS];
type VisibilityConstraintPerDeclaration<'db> = SmallVec<InlineVisibilityConstraintsArray<'db>>;
type VisibilityConstraintPerBinding<'db> = SmallVec<InlineVisibilityConstraintsArray<'db>>;
type VisibilityConstraintsIterator<'db> = std::slice::Iter<'db, VisibilityConstraint<'db>>;
type VisibilityConstraintsIntoIterator<'db> =
smallvec::IntoIter<InlineVisibilityConstraintsArray<'db>>;
/// One [`ScopedVisibilityConstraintId`] per live declaration.
type VisibilityConstraintPerDeclaration = SmallVec<InlineVisibilityConstraintsArray>;
/// One [`ScopedVisibilityConstraintId`] per live binding.
type VisibilityConstraintPerBinding = SmallVec<InlineVisibilityConstraintsArray>;
/// Iterator over the visibility constraints for all live bindings/declarations.
type VisibilityConstraintsIterator<'a> = std::slice::Iter<'a, ScopedVisibilityConstraintId>;
type VisibilityConstraintsIntoIterator = smallvec::IntoIter<InlineVisibilityConstraintsArray>;
/// Live declarations for a single symbol at some point in control flow, with their
/// corresponding visibility constraints.
/// Live declarations for a single symbol at some point in control flow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SymbolDeclarations {
pub(super) struct SymbolDeclarations<'db> {
/// [`BitSet`]: which declarations (as [`ScopedDefinitionId`]) can reach the current location?
pub(crate) live_declarations: Declarations,
/// For each live declaration, which visibility constraint applies to it?
pub(crate) visibility_constraints: VisibilityConstraintPerDeclaration,
/// For each live declaration, which visibility constraints apply to it?
pub(crate) visibility_constraints: VisibilityConstraintPerDeclaration<'db>,
}
impl SymbolDeclarations {
fn undeclared(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
impl<'db> SymbolDeclarations<'db> {
fn undeclared(scope_start_visibility: VisibilityConstraint<'db>) -> Self {
Self {
live_declarations: Declarations::with(0),
visibility_constraints: VisibilityConstraintPerDeclaration::from_iter([
@@ -147,22 +125,22 @@ impl SymbolDeclarations {
}
/// Record a newly-encountered declaration for this symbol.
fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
fn record_declaration(&mut self, db: &'db dyn Db, declaration_id: ScopedDefinitionId) {
self.live_declarations = Declarations::with(declaration_id.into());
self.visibility_constraints = VisibilityConstraintPerDeclaration::with_capacity(1);
self.visibility_constraints
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
.push(VisibilityConstraint::always_true(db));
}
/// Add given visibility constraint to all live declarations.
/// Add given visibility constraint to all live bindings.
pub(super) fn record_visibility_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraints,
constraint: ScopedVisibilityConstraintId,
db: &'db dyn Db,
constraint: VisibilityConstraint<'db>,
) {
for existing in &mut self.visibility_constraints {
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
*existing = VisibilityConstraint::kleene_and(db, *existing, constraint);
}
}
@@ -175,10 +153,9 @@ impl SymbolDeclarations {
}
}
/// Live bindings for a single symbol at some point in control flow. Each live binding comes
/// with a set of narrowing constraints and a visibility constraint.
/// Live bindings and narrowing constraints for a single symbol at some point in control flow.
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SymbolBindings {
pub(super) struct SymbolBindings<'db> {
/// [`BitSet`]: which bindings (as [`ScopedDefinitionId`]) can reach the current location?
live_bindings: Bindings,
@@ -188,14 +165,14 @@ pub(super) struct SymbolBindings {
/// binding in `live_bindings`.
constraints: ConstraintsPerBinding,
/// For each live binding, which visibility constraint applies to it?
visibility_constraints: VisibilityConstraintPerBinding,
/// For each live binding, which visibility constraints apply to it?
visibility_constraints: VisibilityConstraintPerBinding<'db>,
}
impl SymbolBindings {
fn unbound(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
impl<'db> SymbolBindings<'db> {
fn unbound(scope_start_visibility: VisibilityConstraint<'db>) -> Self {
Self {
live_bindings: Bindings::with(ScopedDefinitionId::UNBOUND.as_u32()),
live_bindings: Bindings::with(0),
constraints: ConstraintsPerBinding::from_iter([Constraints::default()]),
visibility_constraints: VisibilityConstraintPerBinding::from_iter([
scope_start_visibility,
@@ -204,7 +181,7 @@ impl SymbolBindings {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
pub(super) fn record_binding(&mut self, db: &'db dyn Db, binding_id: ScopedDefinitionId) {
// The new binding replaces all previous live bindings in this path, and has no
// constraints.
self.live_bindings = Bindings::with(binding_id.into());
@@ -213,7 +190,7 @@ impl SymbolBindings {
self.visibility_constraints = VisibilityConstraintPerBinding::with_capacity(1);
self.visibility_constraints
.push(ScopedVisibilityConstraintId::ALWAYS_TRUE);
.push(VisibilityConstraint::always_true(db));
}
/// Add given constraint to all live bindings.
@@ -226,11 +203,11 @@ impl SymbolBindings {
/// Add given visibility constraint to all live bindings.
pub(super) fn record_visibility_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraints,
constraint: ScopedVisibilityConstraintId,
db: &'db dyn Db,
constraint: VisibilityConstraint<'db>,
) {
for existing in &mut self.visibility_constraints {
*existing = visibility_constraints.add_and_constraint(*existing, constraint);
*existing = VisibilityConstraint::kleene_and(db, *existing, constraint);
}
}
@@ -245,14 +222,14 @@ impl SymbolBindings {
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(super) struct SymbolState {
declarations: SymbolDeclarations,
bindings: SymbolBindings,
pub(super) struct SymbolState<'db> {
declarations: SymbolDeclarations<'db>,
bindings: SymbolBindings<'db>,
}
impl SymbolState {
impl<'db> SymbolState<'db> {
/// Return a new [`SymbolState`] representing an unbound, undeclared symbol.
pub(super) fn undefined(scope_start_visibility: ScopedVisibilityConstraintId) -> Self {
pub(super) fn undefined(scope_start_visibility: VisibilityConstraint<'db>) -> Self {
Self {
declarations: SymbolDeclarations::undeclared(scope_start_visibility),
bindings: SymbolBindings::unbound(scope_start_visibility),
@@ -260,9 +237,9 @@ impl SymbolState {
}
/// Record a newly-encountered binding for this symbol.
pub(super) fn record_binding(&mut self, binding_id: ScopedDefinitionId) {
pub(super) fn record_binding(&mut self, db: &'db dyn Db, binding_id: ScopedDefinitionId) {
debug_assert_ne!(binding_id, ScopedDefinitionId::UNBOUND);
self.bindings.record_binding(binding_id);
self.bindings.record_binding(db, binding_id);
}
/// Add given constraint to all live bindings.
@@ -273,16 +250,15 @@ impl SymbolState {
/// Add given visibility constraint to all live bindings.
pub(super) fn record_visibility_constraint(
&mut self,
visibility_constraints: &mut VisibilityConstraints,
constraint: ScopedVisibilityConstraintId,
db: &'db dyn Db,
constraint: VisibilityConstraint<'db>,
) {
self.bindings
.record_visibility_constraint(visibility_constraints, constraint);
self.bindings.record_visibility_constraint(db, constraint);
self.declarations
.record_visibility_constraint(visibility_constraints, constraint);
.record_visibility_constraint(db, constraint);
}
pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: SymbolState) {
pub(super) fn simplify_visibility_constraints(&mut self, snapshot_state: SymbolState<'db>) {
if self.bindings.live_bindings == snapshot_state.bindings.live_bindings {
self.bindings.visibility_constraints = snapshot_state.bindings.visibility_constraints;
}
@@ -293,16 +269,16 @@ impl SymbolState {
}
/// Record a newly-encountered declaration of this symbol.
pub(super) fn record_declaration(&mut self, declaration_id: ScopedDefinitionId) {
self.declarations.record_declaration(declaration_id);
pub(super) fn record_declaration(
&mut self,
db: &'db dyn Db,
declaration_id: ScopedDefinitionId,
) {
self.declarations.record_declaration(db, declaration_id);
}
/// Merge another [`SymbolState`] into this one.
pub(super) fn merge(
&mut self,
b: SymbolState,
visibility_constraints: &mut VisibilityConstraints,
) {
pub(super) fn merge(&mut self, db: &'db dyn Db, b: SymbolState<'db>) {
let mut a = Self {
bindings: SymbolBindings {
live_bindings: Bindings::default(),
@@ -339,13 +315,10 @@ impl SymbolState {
visibility_constraints_iter: &mut VisibilityConstraintsIntoIterator,
merged: &mut Self| {
merged.bindings.live_bindings.insert(def);
// SAFETY: we only ever create SymbolState using [`SymbolState::undefined`], which adds
// one "unbound" definition with corresponding narrowing and visibility constraints, or
// using [`SymbolState::record_binding`] or [`SymbolState::record_declaration`], which
// similarly add one definition with corresponding constraints. [`SymbolState::merge`]
// always pushes one definition and one constraint bitset and one visibility constraint
// together (just below), so the number of definitions and the number of constraints can
// never get out of sync.
// SAFETY: we only ever create SymbolState with either no definitions and no constraint
// bitsets (`::unbound`) or one definition and one constraint bitset (`::with`), and
// `::merge` always pushes one definition and one constraint bitset together (just
// below), so the number of definitions and the number of constraint bitsets can never
// get out of sync.
let constraints = constraints_iter
.next()
@@ -391,26 +364,31 @@ impl SymbolState {
&mut b_vis_constraints_iter,
self,
);
// SAFETY: see comment in `push` above.
// SAFETY: we only ever create SymbolState with either no definitions and
// no constraint bitsets (`::unbound`) or one definition and one constraint
// bitset (`::with`), and `::merge` always pushes one definition and one
// constraint bitset together (just below), so the number of definitions
// and the number of constraint bitsets can never get out of sync.
let a_constraints = a_constraints_iter
.next()
.expect("definitions and constraints length mismatch");
let current_constraints = self.bindings.constraints.last_mut().unwrap();
// If the same definition is visible through both paths, any constraint
// that applies on only one path is irrelevant to the resulting type from
// unioning the two paths, so we intersect the constraints.
current_constraints.intersect(&a_constraints);
self.bindings
.constraints
.last_mut()
.unwrap()
.intersect(&a_constraints);
// For visibility constraints, we merge them using a ternary OR operation:
// TODO: documentation
// SAFETY: See above
let a_vis_constraint = a_vis_constraints_iter
.next()
.expect("visibility_constraints length mismatch");
let current_vis_constraint =
self.bindings.visibility_constraints.last_mut().unwrap();
*current_vis_constraint = visibility_constraints
.add_or_constraint(*current_vis_constraint, a_vis_constraint);
let current = self.bindings.visibility_constraints.last_mut().unwrap();
*current = VisibilityConstraint::kleene_or(db, *current, a_vis_constraint);
opt_a_def = a_defs_iter.next();
opt_b_def = b_defs_iter.next();
@@ -480,8 +458,7 @@ impl SymbolState {
.next()
.expect("declarations and visibility_constraints length mismatch");
let current = self.declarations.visibility_constraints.last_mut().unwrap();
*current =
visibility_constraints.add_or_constraint(*current, a_vis_constraint);
*current = VisibilityConstraint::kleene_or(db, *current, a_vis_constraint);
opt_a_decl = a_decls_iter.next();
opt_b_decl = b_decls_iter.next();
@@ -500,34 +477,33 @@ impl SymbolState {
}
}
pub(super) fn bindings(&self) -> &SymbolBindings {
pub(super) fn bindings(&'db self) -> &'db SymbolBindings<'db> {
&self.bindings
}
pub(super) fn declarations(&self) -> &SymbolDeclarations {
pub(super) fn declarations(&'db self) -> &'db SymbolDeclarations<'db> {
&self.declarations
}
}
/// A single binding (as [`ScopedDefinitionId`]) with an iterator of its applicable
/// narrowing constraints ([`ScopedConstraintId`]) and a corresponding visibility
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
/// [`ScopedConstraintId`].
#[derive(Debug)]
pub(super) struct BindingIdWithConstraints<'map> {
pub(super) struct BindingIdWithConstraints<'map, 'db> {
pub(super) definition: ScopedDefinitionId,
pub(super) constraint_ids: ConstraintIdIterator<'map>,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
pub(super) visibility_constraint: VisibilityConstraint<'db>,
}
#[derive(Debug)]
pub(super) struct BindingIdWithConstraintsIterator<'map> {
pub(super) struct BindingIdWithConstraintsIterator<'map, 'db> {
definitions: BindingsIterator<'map>,
constraints: ConstraintsIterator<'map>,
visibility_constraints: VisibilityConstraintsIterator<'map>,
visibility_constraints: VisibilityConstraintsIterator<'db>,
}
impl<'map> Iterator for BindingIdWithConstraintsIterator<'map> {
type Item = BindingIdWithConstraints<'map>;
impl<'map, 'db> Iterator for BindingIdWithConstraintsIterator<'map, 'db> {
type Item = BindingIdWithConstraints<'map, 'db>;
fn next(&mut self) -> Option<Self::Item> {
match (
@@ -551,7 +527,7 @@ impl<'map> Iterator for BindingIdWithConstraintsIterator<'map> {
}
}
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_> {}
impl std::iter::FusedIterator for BindingIdWithConstraintsIterator<'_, '_> {}
#[derive(Debug)]
pub(super) struct ConstraintIdIterator<'a> {
@@ -568,31 +544,21 @@ impl Iterator for ConstraintIdIterator<'_> {
impl std::iter::FusedIterator for ConstraintIdIterator<'_> {}
/// A single declaration (as [`ScopedDefinitionId`]) with a corresponding visibility
/// visibility constraint ([`ScopedVisibilityConstraintId`]).
#[derive(Debug)]
pub(super) struct DeclarationIdWithConstraint {
pub(super) definition: ScopedDefinitionId,
pub(super) visibility_constraint: ScopedVisibilityConstraintId,
}
pub(super) struct DeclarationIdIterator<'map> {
pub(crate) declarations: DeclarationsIterator<'map>,
pub(crate) visibility_constraints: VisibilityConstraintsIterator<'map>,
}
impl Iterator for DeclarationIdIterator<'_> {
type Item = DeclarationIdWithConstraint;
impl<'db> Iterator for DeclarationIdIterator<'db> {
type Item = (ScopedDefinitionId, VisibilityConstraint<'db>);
fn next(&mut self) -> Option<Self::Item> {
match (self.declarations.next(), self.visibility_constraints.next()) {
(None, None) => None,
(Some(declaration), Some(&visibility_constraint)) => {
Some(DeclarationIdWithConstraint {
definition: ScopedDefinitionId::from_u32(declaration),
visibility_constraint,
})
}
(Some(declaration), Some(visibility_constraints_id)) => Some((
ScopedDefinitionId::from_u32(declaration),
*visibility_constraints_id,
)),
// SAFETY: see above.
_ => unreachable!("declarations and visibility_constraints length mismatch"),
}
@@ -601,174 +567,169 @@ impl Iterator for DeclarationIdIterator<'_> {
impl std::iter::FusedIterator for DeclarationIdIterator<'_> {}
#[cfg(test)]
mod tests {
use super::*;
// #[cfg(test)]
// mod tests {
// use super::*;
#[track_caller]
fn assert_bindings(symbol: &SymbolState, expected: &[&str]) {
let actual = symbol
.bindings()
.iter()
.map(|def_id_with_constraints| {
let def_id = def_id_with_constraints.definition;
let def = if def_id == ScopedDefinitionId::UNBOUND {
"unbound".into()
} else {
def_id.as_u32().to_string()
};
let constraints = def_id_with_constraints
.constraint_ids
.map(ScopedConstraintId::as_u32)
.map(|idx| idx.to_string())
.collect::<Vec<_>>()
.join(", ");
format!("{def}<{constraints}>")
})
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
// #[track_caller]
// fn assert_bindings(symbol: &SymbolState, expected: &[&str]) {
// let actual = symbol
// .bindings()
// .iter()
// .map(|def_id_with_constraints| {
// let def_id = def_id_with_constraints.definition;
// let def = if def_id == ScopedDefinitionId::UNBOUND {
// "unbound".into()
// } else {
// def_id.as_u32().to_string()
// };
// let constraints = def_id_with_constraints
// .constraint_ids
// .map(ScopedConstraintId::as_u32)
// .map(|idx| idx.to_string())
// .collect::<Vec<_>>()
// .join(", ");
// format!("{def}<{constraints}>")
// })
// .collect::<Vec<_>>();
// assert_eq!(actual, expected);
// }
#[track_caller]
pub(crate) fn assert_declarations(symbol: &SymbolState, expected: &[&str]) {
let actual = symbol
.declarations()
.iter()
.map(
|DeclarationIdWithConstraint {
definition,
visibility_constraint: _,
}| {
if definition == ScopedDefinitionId::UNBOUND {
"undeclared".into()
} else {
definition.as_u32().to_string()
}
},
)
.collect::<Vec<_>>();
assert_eq!(actual, expected);
}
// #[track_caller]
// pub(crate) fn assert_declarations(symbol: &SymbolState, expected: &[&str]) {
// let actual = symbol
// .declarations()
// .iter()
// .map(|(def_id, _)| {
// if def_id == ScopedDefinitionId::UNBOUND {
// "undeclared".into()
// } else {
// def_id.as_u32().to_string()
// }
// })
// .collect::<Vec<_>>();
// assert_eq!(actual, expected);
// }
#[test]
fn unbound() {
let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// #[test]
// fn unbound() {
// let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
assert_bindings(&sym, &["unbound<>"]);
}
// assert_bindings(&sym, &["unbound<>"]);
// }
#[test]
fn with() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(ScopedDefinitionId::from_u32(1));
// #[test]
// fn with() {
// let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym.record_binding(ScopedDefinitionId::from_u32(1));
assert_bindings(&sym, &["1<>"]);
}
// assert_bindings(&sym, &["1<>"]);
// }
#[test]
fn record_constraint() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_binding(ScopedDefinitionId::from_u32(1));
sym.record_constraint(ScopedConstraintId::from_u32(0));
// #[test]
// fn record_constraint() {
// let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym.record_binding(ScopedDefinitionId::from_u32(1));
// sym.record_constraint(ScopedConstraintId::from_u32(0));
assert_bindings(&sym, &["1<0>"]);
}
// assert_bindings(&sym, &["1<0>"]);
// }
#[test]
fn merge() {
let mut visibility_constraints = VisibilityConstraints::default();
// #[test]
// fn merge() {
// let mut visibility_constraints = VisibilityConstraints::default();
// merging the same definition with the same constraint keeps the constraint
let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1a.record_binding(ScopedDefinitionId::from_u32(1));
sym1a.record_constraint(ScopedConstraintId::from_u32(0));
// // merging the same definition with the same constraint keeps the constraint
// let mut sym1a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym1a.record_binding(ScopedDefinitionId::from_u32(1));
// sym1a.record_constraint(ScopedConstraintId::from_u32(0));
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(ScopedDefinitionId::from_u32(1));
sym1b.record_constraint(ScopedConstraintId::from_u32(0));
// let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym1b.record_binding(ScopedDefinitionId::from_u32(1));
// sym1b.record_constraint(ScopedConstraintId::from_u32(0));
sym1a.merge(sym1b, &mut visibility_constraints);
let mut sym1 = sym1a;
assert_bindings(&sym1, &["1<0>"]);
// sym1a.merge(sym1b, &mut visibility_constraints);
// let mut sym1 = sym1a;
// assert_bindings(&sym1, &["1<0>"]);
// merging the same definition with differing constraints drops all constraints
let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym2a.record_binding(ScopedDefinitionId::from_u32(2));
sym2a.record_constraint(ScopedConstraintId::from_u32(1));
// // merging the same definition with differing constraints drops all constraints
// let mut sym2a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym2a.record_binding(ScopedDefinitionId::from_u32(2));
// sym2a.record_constraint(ScopedConstraintId::from_u32(1));
let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym1b.record_binding(ScopedDefinitionId::from_u32(2));
sym1b.record_constraint(ScopedConstraintId::from_u32(2));
// let mut sym1b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym1b.record_binding(ScopedDefinitionId::from_u32(2));
// sym1b.record_constraint(ScopedConstraintId::from_u32(2));
sym2a.merge(sym1b, &mut visibility_constraints);
let sym2 = sym2a;
assert_bindings(&sym2, &["2<>"]);
// sym2a.merge(sym1b, &mut visibility_constraints);
// let sym2 = sym2a;
// assert_bindings(&sym2, &["2<>"]);
// merging a constrained definition with unbound keeps both
let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym3a.record_binding(ScopedDefinitionId::from_u32(3));
sym3a.record_constraint(ScopedConstraintId::from_u32(3));
// // merging a constrained definition with unbound keeps both
// let mut sym3a = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym3a.record_binding(ScopedDefinitionId::from_u32(3));
// sym3a.record_constraint(ScopedConstraintId::from_u32(3));
let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// let sym2b = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym3a.merge(sym2b, &mut visibility_constraints);
let sym3 = sym3a;
assert_bindings(&sym3, &["unbound<>", "3<3>"]);
// sym3a.merge(sym2b, &mut visibility_constraints);
// let sym3 = sym3a;
// assert_bindings(&sym3, &["unbound<>", "3<3>"]);
// merging different definitions keeps them each with their existing constraints
sym1.merge(sym3, &mut visibility_constraints);
let sym = sym1;
assert_bindings(&sym, &["unbound<>", "1<0>", "3<3>"]);
}
// // merging different definitions keeps them each with their existing constraints
// sym1.merge(sym3, &mut visibility_constraints);
// let sym = sym1;
// assert_bindings(&sym, &["unbound<>", "1<0>", "3<3>"]);
// }
#[test]
fn no_declaration() {
let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// #[test]
// fn no_declaration() {
// let sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
assert_declarations(&sym, &["undeclared"]);
}
// assert_declarations(&sym, &["undeclared"]);
// }
#[test]
fn record_declaration() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration() {
// let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
assert_declarations(&sym, &["1"]);
}
// assert_declarations(&sym, &["1"]);
// }
#[test]
fn record_declaration_override() {
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
sym.record_declaration(ScopedDefinitionId::from_u32(2));
// #[test]
// fn record_declaration_override() {
// let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
// sym.record_declaration(ScopedDefinitionId::from_u32(2));
assert_declarations(&sym, &["2"]);
}
// assert_declarations(&sym, &["2"]);
// }
#[test]
fn record_declaration_merge() {
let mut visibility_constraints = VisibilityConstraints::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration_merge() {
// let mut visibility_constraints = VisibilityConstraints::default();
// let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
let mut sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym2.record_declaration(ScopedDefinitionId::from_u32(2));
// let mut sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym2.record_declaration(ScopedDefinitionId::from_u32(2));
sym.merge(sym2, &mut visibility_constraints);
// sym.merge(sym2, &mut visibility_constraints);
assert_declarations(&sym, &["1", "2"]);
}
// assert_declarations(&sym, &["1", "2"]);
// }
#[test]
fn record_declaration_merge_partial_undeclared() {
let mut visibility_constraints = VisibilityConstraints::default();
let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.record_declaration(ScopedDefinitionId::from_u32(1));
// #[test]
// fn record_declaration_merge_partial_undeclared() {
// let mut visibility_constraints = VisibilityConstraints::default();
// let mut sym = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// sym.record_declaration(ScopedDefinitionId::from_u32(1));
let sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
// let sym2 = SymbolState::undefined(ScopedVisibilityConstraintId::ALWAYS_TRUE);
sym.merge(sym2, &mut visibility_constraints);
// sym.merge(sym2, &mut visibility_constraints);
assert_declarations(&sym, &["undeclared", "1"]);
}
}
// assert_declarations(&sym, &["undeclared", "1"]);
// }
// }

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ impl Boundness {
/// possibly_unbound: Symbol::Type(Type::IntLiteral(2), Boundness::PossiblyUnbound),
/// non_existent: Symbol::Unbound,
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Symbol<'db> {
Type(Type<'db>, Boundness),
Unbound,

View File

@@ -27,7 +27,6 @@ use crate::semantic_index::{
DeclarationsIterator,
};
use crate::stdlib::{builtins_symbol, known_module_symbol, typing_extensions_symbol};
use crate::suppression::check_suppressions;
use crate::symbol::{Boundness, Symbol};
use crate::types::call::{CallDunderResult, CallOutcome};
use crate::types::class_base::ClassBase;
@@ -46,7 +45,6 @@ mod infer;
mod mro;
mod narrow;
mod signatures;
mod slots;
mod string_annotation;
mod unpacker;
@@ -67,13 +65,10 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
diagnostics.extend(result.diagnostics());
}
check_suppressions(db, file, &mut diagnostics);
diagnostics
}
/// Infer the public type of a symbol (its type as seen from outside its scope).
#[salsa::tracked]
fn symbol_by_id<'db>(db: &'db dyn Db, scope: ScopeId<'db>, symbol: ScopedSymbolId) -> Symbol<'db> {
let _span = tracing::trace_span!("symbol_by_id", ?symbol).entered();
@@ -279,7 +274,6 @@ fn bindings_ty<'db>(
db: &'db dyn Db,
bindings_with_constraints: BindingWithConstraintsIterator<'_, 'db>,
) -> Symbol<'db> {
let visibility_constraints = bindings_with_constraints.visibility_constraints;
let mut bindings_with_constraints = bindings_with_constraints.peekable();
let unbound_visibility = if let Some(BindingWithConstraints {
@@ -288,7 +282,7 @@ fn bindings_ty<'db>(
visibility_constraint,
}) = bindings_with_constraints.peek()
{
visibility_constraints.evaluate(db, *visibility_constraint)
crate::visibility_constraints::evaluate(db, *visibility_constraint)
} else {
Truthiness::AlwaysFalse
};
@@ -300,7 +294,8 @@ fn bindings_ty<'db>(
visibility_constraint,
}| {
let binding = binding?;
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
let static_visibility =
crate::visibility_constraints::evaluate(db, visibility_constraint);
if static_visibility.is_always_false() {
return None;
@@ -367,7 +362,6 @@ fn declarations_ty<'db>(
db: &'db dyn Db,
declarations: DeclarationsIterator<'_, 'db>,
) -> DeclaredTypeResult<'db> {
let visibility_constraints = declarations.visibility_constraints;
let mut declarations = declarations.peekable();
let undeclared_visibility = if let Some(DeclarationWithConstraint {
@@ -375,7 +369,7 @@ fn declarations_ty<'db>(
visibility_constraint,
}) = declarations.peek()
{
visibility_constraints.evaluate(db, *visibility_constraint)
crate::visibility_constraints::evaluate(db, *visibility_constraint)
} else {
Truthiness::AlwaysFalse
};
@@ -386,7 +380,8 @@ fn declarations_ty<'db>(
visibility_constraint,
}| {
let declaration = declaration?;
let static_visibility = visibility_constraints.evaluate(db, visibility_constraint);
let static_visibility =
crate::visibility_constraints::evaluate(db, visibility_constraint);
if static_visibility.is_always_false() {
None
@@ -670,13 +665,6 @@ impl<'db> Type<'db> {
.expect("Expected a Type::KnownInstance variant")
}
pub const fn into_tuple(self) -> Option<TupleType<'db>> {
match self {
Type::Tuple(tuple_type) => Some(tuple_type),
_ => None,
}
}
pub const fn is_boolean_literal(&self) -> bool {
matches!(self, Type::BooleanLiteral(..))
}
@@ -1210,6 +1198,14 @@ impl<'db> Type<'db> {
Type::SubclassOf(_),
) => true,
(Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => {
// TODO: Once we have support for final classes, we can determine disjointness in some cases
// here. However, note that it might be better to turn `Type::SubclassOf('FinalClass')` into
// `Type::ClassLiteral('FinalClass')` during construction, instead of adding special cases for
// final classes inside `Type::SubclassOf` everywhere.
false
}
(Type::AlwaysTruthy, ty) | (ty, Type::AlwaysTruthy) => {
// `Truthiness::Ambiguous` may include `AlwaysTrue` as a subset, so it's not guaranteed to be disjoint.
// Thus, they are only disjoint if `ty.bool() == AlwaysFalse`.
@@ -1220,14 +1216,6 @@ impl<'db> Type<'db> {
matches!(ty.bool(db), Truthiness::AlwaysTrue)
}
(Type::SubclassOf(_), _) | (_, Type::SubclassOf(_)) => {
// TODO: Once we have support for final classes, we can determine disjointness in some cases
// here. However, note that it might be better to turn `Type::SubclassOf('FinalClass')` into
// `Type::ClassLiteral('FinalClass')` during construction, instead of adding special cases for
// final classes inside `Type::SubclassOf` everywhere.
false
}
(Type::KnownInstance(left), right) => {
left.instance_fallback(db).is_disjoint_from(db, right)
}
@@ -1682,13 +1670,15 @@ impl<'db> Type<'db> {
Type::Any | Type::Todo(_) | Type::Never | Type::Unknown => Truthiness::Ambiguous,
Type::FunctionLiteral(_) => Truthiness::AlwaysTrue,
Type::ModuleLiteral(_) => Truthiness::AlwaysTrue,
Type::ClassLiteral(ClassLiteralType { class }) => {
class.metaclass(db).to_instance(db).bool(db)
Type::ClassLiteral(_) => {
// TODO: lookup `__bool__` and `__len__` methods on the class's metaclass
// More info in https://docs.python.org/3/library/stdtypes.html#truth-value-testing
Truthiness::Ambiguous
}
Type::SubclassOf(_) => {
// TODO: see above
Truthiness::Ambiguous
}
Type::SubclassOf(SubclassOfType { base }) => base
.into_class()
.map(|class| Type::class_literal(class).bool(db))
.unwrap_or(Truthiness::Ambiguous),
Type::AlwaysTruthy => Truthiness::AlwaysTrue,
Type::AlwaysFalsy => Truthiness::AlwaysFalse,
instance_ty @ Type::Instance(InstanceType { class }) => {
@@ -3086,16 +3076,13 @@ pub enum KnownFunction {
Len,
/// `typing(_extensions).final`
Final,
/// [`typing(_extensions).no_type_check`](https://typing.readthedocs.io/en/latest/spec/directives.html#no-type-check)
NoTypeCheck,
}
impl KnownFunction {
pub fn constraint_function(self) -> Option<KnownConstraintFunction> {
match self {
Self::ConstraintFunction(f) => Some(f),
Self::RevealType | Self::Len | Self::Final | Self::NoTypeCheck => None,
Self::RevealType | Self::Len | Self::Final => None,
}
}
@@ -3114,9 +3101,6 @@ impl KnownFunction {
),
"len" if definition.is_builtin_definition(db) => Some(KnownFunction::Len),
"final" if definition.is_typing_definition(db) => Some(KnownFunction::Final),
"no_type_check" if definition.is_typing_definition(db) => {
Some(KnownFunction::NoTypeCheck)
}
_ => None,
}
}

View File

@@ -8,16 +8,13 @@ use ruff_db::{
use ruff_python_ast::AnyNodeRef;
use ruff_text_size::Ranged;
use super::{binding_ty, KnownFunction, TypeCheckDiagnostic, TypeCheckDiagnostics};
use crate::semantic_index::semantic_index;
use crate::semantic_index::symbol::ScopeId;
use crate::{
lint::{LintId, LintMetadata},
suppression::suppressions,
Db,
};
use super::{TypeCheckDiagnostic, TypeCheckDiagnostics};
/// Context for inferring the types of a single file.
///
/// One context exists for at least for every inferred region but it's
@@ -32,21 +29,17 @@ use crate::{
/// on the current [`TypeInference`](super::infer::TypeInference) result.
pub(crate) struct InferContext<'db> {
db: &'db dyn Db,
scope: ScopeId<'db>,
file: File,
diagnostics: std::cell::RefCell<TypeCheckDiagnostics>,
no_type_check: InNoTypeCheck,
bomb: DebugDropBomb,
}
impl<'db> InferContext<'db> {
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
Self {
db,
scope,
file: scope.file(db),
file,
diagnostics: std::cell::RefCell::new(TypeCheckDiagnostics::default()),
no_type_check: InNoTypeCheck::default(),
bomb: DebugDropBomb::new("`InferContext` needs to be explicitly consumed by calling `::finish` to prevent accidental loss of diagnostics."),
}
}
@@ -64,7 +57,9 @@ impl<'db> InferContext<'db> {
where
T: WithDiagnostics,
{
self.diagnostics.get_mut().extend(other.diagnostics());
self.diagnostics
.get_mut()
.extend(other.diagnostics().iter().cloned());
}
/// Reports a lint located at `node`.
@@ -72,28 +67,13 @@ impl<'db> InferContext<'db> {
&self,
lint: &'static LintMetadata,
node: AnyNodeRef,
message: fmt::Arguments,
message: std::fmt::Arguments,
) {
if !self.db.is_file_open(self.file) {
return;
}
// Skip over diagnostics if the rule is disabled.
let Some(severity) = self.db.rule_selection().severity(LintId::of(lint)) else {
return;
};
if self.is_in_no_type_check() {
return;
}
let suppressions = suppressions(self.db, self.file);
if let Some(suppression) = suppressions.find_suppression(node.range(), LintId::of(lint)) {
self.diagnostics.borrow_mut().mark_used(suppression.id());
return;
}
self.report_diagnostic(node, DiagnosticId::Lint(lint.name()), severity, message);
}
@@ -105,7 +85,7 @@ impl<'db> InferContext<'db> {
node: AnyNodeRef,
id: DiagnosticId,
severity: Severity,
message: fmt::Arguments,
message: std::fmt::Arguments,
) {
if !self.db.is_file_open(self.file) {
return;
@@ -116,6 +96,7 @@ impl<'db> InferContext<'db> {
// * The rule is disabled for this file. We probably want to introduce a new query that
// returns a rule selector for a given file that respects the package's settings,
// any global pragma comments in the file, and any per-file-ignores.
// * Check for suppression comments, bump a counter if the diagnostic is suppressed.
self.diagnostics.borrow_mut().push(TypeCheckDiagnostic {
file: self.file,
@@ -126,42 +107,6 @@ impl<'db> InferContext<'db> {
});
}
pub(super) fn set_in_no_type_check(&mut self, no_type_check: InNoTypeCheck) {
self.no_type_check = no_type_check;
}
fn is_in_no_type_check(&self) -> bool {
match self.no_type_check {
InNoTypeCheck::Possibly => {
// Accessing the semantic index here is fine because
// the index belongs to the same file as for which we emit the diagnostic.
let index = semantic_index(self.db, self.file);
let scope_id = self.scope.file_scope_id(self.db);
// Inspect all ancestor function scopes by walking bottom up and infer the function's type.
let mut function_scope_tys = index
.ancestor_scopes(scope_id)
.filter_map(|(_, scope)| scope.node().as_function())
.filter_map(|function| {
binding_ty(self.db, index.definition(function)).into_function_literal()
});
// Iterate over all functions and test if any is decorated with `@no_type_check`.
function_scope_tys.any(|function_ty| {
function_ty
.decorators(self.db)
.iter()
.filter_map(|decorator| decorator.into_function_literal())
.any(|decorator_ty| {
decorator_ty.is_known(self.db, KnownFunction::NoTypeCheck)
})
})
}
InNoTypeCheck::Yes => true,
}
}
#[must_use]
pub(crate) fn finish(mut self) -> TypeCheckDiagnostics {
self.bomb.defuse();
@@ -181,17 +126,6 @@ impl fmt::Debug for InferContext<'_> {
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub(crate) enum InNoTypeCheck {
/// The inference might be in a `no_type_check` block but only if any
/// ancestor function is decorated with `@no_type_check`.
#[default]
Possibly,
/// The inference is known to be in an `@no_type_check` decorated function.
Yes,
}
pub(crate) trait WithDiagnostics {
fn diagnostics(&self) -> &TypeCheckDiagnostics;
}

View File

@@ -1,7 +1,5 @@
use super::context::InferContext;
use crate::declare_lint;
use crate::lint::{Level, LintRegistryBuilder, LintStatus};
use crate::suppression::FileSuppressionId;
use crate::types::string_annotation::{
BYTE_STRING_TYPE_ANNOTATION, ESCAPE_CHARACTER_IN_FORWARD_ANNOTATION, FSTRING_TYPE_ANNOTATION,
IMPLICIT_CONCATENATED_STRING_TYPE_ANNOTATION, INVALID_SYNTAX_IN_FORWARD_ANNOTATION,
@@ -12,12 +10,13 @@ use ruff_db::diagnostic::{Diagnostic, DiagnosticId, Severity};
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
use rustc_hash::FxHashSet;
use std::borrow::Cow;
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;
use super::context::InferContext;
/// Registers all known type check lints.
pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&CALL_NON_CALLABLE);
@@ -27,7 +26,6 @@ pub(crate) fn register_lints(registry: &mut LintRegistryBuilder) {
registry.register_lint(&CYCLIC_CLASS_DEFINITION);
registry.register_lint(&DIVISION_BY_ZERO);
registry.register_lint(&DUPLICATE_BASE);
registry.register_lint(&INCOMPATIBLE_SLOTS);
registry.register_lint(&INCONSISTENT_MRO);
registry.register_lint(&INDEX_OUT_OF_BOUNDS);
registry.register_lint(&INVALID_ASSIGNMENT);
@@ -149,64 +147,6 @@ declare_lint! {
}
}
declare_lint! {
/// ## What it does
/// Checks for classes whose bases define incompatible `__slots__`.
///
/// ## Why is this bad?
/// Inheriting from bases with incompatible `__slots__`s
/// will lead to a `TypeError` at runtime.
///
/// Classes with no or empty `__slots__` are always compatible:
///
/// ```python
/// class A: ...
/// class B:
/// __slots__ = ()
/// class C:
/// __slots__ = ("a", "b")
///
/// # fine
/// class D(A, B, C): ...
/// ```
///
/// Multiple inheritance from more than one different class
/// defining non-empty `__slots__` is not allowed:
///
/// ```python
/// class A:
/// __slots__ = ("a", "b")
///
/// class B:
/// __slots__ = ("a", "b") # Even if the values are the same
///
/// # TypeError: multiple bases have instance lay-out conflict
/// class C(A, B): ...
/// ```
///
/// ## Known problems
/// Dynamic (not tuple or string literal) `__slots__` are not checked.
/// Additionally, classes inheriting from built-in classes with implicit layouts
/// like `str` or `int` are also not checked.
///
/// ```pycon
/// >>> hasattr(int, "__slots__")
/// False
/// >>> hasattr(str, "__slots__")
/// False
/// >>> class A(int, str): ...
/// Traceback (most recent call last):
/// File "<python-input-0>", line 1, in <module>
/// class A(int, str): ...
/// TypeError: multiple bases have instance lay-out conflict
/// ```
pub(crate) static INCOMPATIBLE_SLOTS = {
summary: "detects class definitions whose MRO has conflicting `__slots__`",
status: LintStatus::preview("1.0.0"),
default_level: Level::Error,
}
}
declare_lint! {
/// TODO #14889
pub(crate) static INCONSISTENT_MRO = {
@@ -572,11 +512,11 @@ declare_lint! {
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
pub(crate) id: DiagnosticId,
pub(crate) message: String,
pub(crate) range: TextRange,
pub(crate) severity: Severity,
pub(crate) file: File,
pub(super) id: DiagnosticId,
pub(super) message: String,
pub(super) range: TextRange,
pub(super) severity: Severity,
pub(super) file: File,
}
impl TypeCheckDiagnostic {
@@ -630,41 +570,41 @@ impl Ranged for TypeCheckDiagnostic {
/// each Salsa-struct comes with an overhead.
#[derive(Default, Eq, PartialEq)]
pub struct TypeCheckDiagnostics {
diagnostics: Vec<Arc<TypeCheckDiagnostic>>,
used_suppressions: FxHashSet<FileSuppressionId>,
inner: Vec<std::sync::Arc<TypeCheckDiagnostic>>,
}
impl TypeCheckDiagnostics {
pub(crate) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
self.diagnostics.push(Arc::new(diagnostic));
}
pub(super) fn extend(&mut self, other: &TypeCheckDiagnostics) {
self.diagnostics.extend_from_slice(&other.diagnostics);
self.used_suppressions.extend(&other.used_suppressions);
}
pub(crate) fn mark_used(&mut self, suppression_id: FileSuppressionId) {
self.used_suppressions.insert(suppression_id);
}
pub(crate) fn is_used(&self, suppression_id: FileSuppressionId) -> bool {
self.used_suppressions.contains(&suppression_id)
}
pub(crate) fn used_len(&self) -> usize {
self.used_suppressions.len()
pub(super) fn push(&mut self, diagnostic: TypeCheckDiagnostic) {
self.inner.push(Arc::new(diagnostic));
}
pub(crate) fn shrink_to_fit(&mut self) {
self.used_suppressions.shrink_to_fit();
self.diagnostics.shrink_to_fit();
self.inner.shrink_to_fit();
}
}
impl Extend<TypeCheckDiagnostic> for TypeCheckDiagnostics {
fn extend<T: IntoIterator<Item = TypeCheckDiagnostic>>(&mut self, iter: T) {
self.inner.extend(iter.into_iter().map(std::sync::Arc::new));
}
}
impl Extend<std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
fn extend<T: IntoIterator<Item = Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
self.inner.extend(iter);
}
}
impl<'a> Extend<&'a std::sync::Arc<TypeCheckDiagnostic>> for TypeCheckDiagnostics {
fn extend<T: IntoIterator<Item = &'a Arc<TypeCheckDiagnostic>>>(&mut self, iter: T) {
self.inner
.extend(iter.into_iter().map(std::sync::Arc::clone));
}
}
impl std::fmt::Debug for TypeCheckDiagnostics {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.diagnostics.fmt(f)
self.inner.fmt(f)
}
}
@@ -672,7 +612,7 @@ impl Deref for TypeCheckDiagnostics {
type Target = [std::sync::Arc<TypeCheckDiagnostic>];
fn deref(&self) -> &Self::Target {
&self.diagnostics
&self.inner
}
}
@@ -681,7 +621,7 @@ impl IntoIterator for TypeCheckDiagnostics {
type IntoIter = std::vec::IntoIter<std::sync::Arc<TypeCheckDiagnostic>>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.into_iter()
self.inner.into_iter()
}
}
@@ -690,7 +630,7 @@ impl<'a> IntoIterator for &'a TypeCheckDiagnostics {
type IntoIter = std::slice::Iter<'a, std::sync::Arc<TypeCheckDiagnostic>>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.iter()
self.inner.iter()
}
}
@@ -872,11 +812,3 @@ pub(crate) fn report_invalid_exception_cause(context: &InferContext, node: &ast:
),
);
}
pub(crate) fn report_base_with_incompatible_slots(context: &InferContext, node: &ast::Expr) {
context.report_lint(
&INCOMPATIBLE_SLOTS,
node.into(),
format_args!("Class base has incompatible `__slots__`"),
);
}

View File

@@ -324,12 +324,6 @@ impl<'db> TypeArrayDisplay<'db> for Vec<Type<'db>> {
}
}
impl<'db> TypeArrayDisplay<'db> for [Type<'db>] {
fn display(&self, db: &'db dyn Db) -> DisplayTypeArray {
DisplayTypeArray { types: self, db }
}
}
pub(crate) struct DisplayTypeArray<'b, 'db> {
types: &'b [Type<'db>],
db: &'db dyn Db,

View File

@@ -31,7 +31,7 @@ use std::num::NonZeroU32;
use itertools::Itertools;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext};
use ruff_python_ast::{self as ast, AnyNodeRef, ExprContext, UnaryOp};
use ruff_text_size::Ranged;
use rustc_hash::{FxHashMap, FxHashSet};
use salsa;
@@ -42,7 +42,7 @@ use crate::module_resolver::{file_to_module, resolve_module};
use crate::semantic_index::ast_ids::{HasScopedExpressionId, HasScopedUseId, ScopedExpressionId};
use crate::semantic_index::definition::{
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
ExceptHandlerDefinitionKind, ForStmtDefinitionKind, TargetKind,
ExceptHandlerDefinitionKind, TargetKind,
};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::semantic_index;
@@ -72,14 +72,13 @@ use crate::unpack::Unpack;
use crate::util::subscript::{PyIndex, PySlice};
use crate::Db;
use super::context::{InNoTypeCheck, InferContext, WithDiagnostics};
use super::context::{InferContext, WithDiagnostics};
use super::diagnostic::{
report_index_out_of_bounds, report_invalid_exception_caught, report_invalid_exception_cause,
report_invalid_exception_raised, report_non_subscriptable,
report_possibly_unresolved_reference, report_slice_step_size_zero, report_unresolved_reference,
SUBCLASS_OF_FINAL_CLASS,
};
use super::slots::check_class_slots;
use super::string_annotation::{
parse_string_annotation, BYTE_STRING_TYPE_ANNOTATION, FSTRING_TYPE_ANNOTATION,
};
@@ -132,7 +131,7 @@ pub(crate) fn infer_definition_types<'db>(
let file = definition.file(db);
let _span = tracing::trace_span!(
"infer_definition_types",
range = ?definition.kind(db).target_range(),
range = ?definition.kind(db).range(),
file = %file.path(db)
)
.entered();
@@ -155,7 +154,7 @@ pub(crate) fn infer_deferred_types<'db>(
let _span = tracing::trace_span!(
"infer_deferred_types",
definition = ?definition.as_id(),
range = ?definition.kind(db).target_range(),
range = ?definition.kind(db).range(),
file = %file.path(db)
)
.entered();
@@ -169,6 +168,7 @@ pub(crate) fn infer_deferred_types<'db>(
/// Use rarely; only for cases where we'd otherwise risk double-inferring an expression: RHS of an
/// assignment, which might be unpacking/multi-target and thus part of multiple definitions, or a
/// type narrowing guard expression (e.g. if statement test node).
#[allow(unused)]
#[salsa::tracked(return_ref)]
pub(crate) fn infer_expression_types<'db>(
db: &'db dyn Db,
@@ -198,16 +198,21 @@ pub(crate) fn infer_expression_types<'db>(
fn infer_unpack_types<'db>(db: &'db dyn Db, unpack: Unpack<'db>) -> UnpackResult<'db> {
let file = unpack.file(db);
let _span =
tracing::trace_span!("infer_unpack_types", range=?unpack.range(db), file=%file.path(db))
tracing::trace_span!("infer_unpack_types", unpack=?unpack.as_id(), file=%file.path(db))
.entered();
let mut unpacker = Unpacker::new(db, unpack.scope(db));
unpacker.unpack(unpack.target(db), unpack.value(db));
let value = unpack.value(db);
let scope = unpack.scope(db);
let result = infer_expression_types(db, value);
let value_ty = result.expression_ty(value.node_ref(db).scoped_expression_id(db, scope));
let mut unpacker = Unpacker::new(db, file);
unpacker.unpack(unpack.target(db), value_ty, scope);
unpacker.finish()
}
/// A region within which we can infer types.
#[derive(Copy, Clone, Debug)]
pub(crate) enum InferenceRegion<'db> {
/// infer types for a standalone [`Expression`]
Expression(Expression<'db>),
@@ -219,18 +224,6 @@ pub(crate) enum InferenceRegion<'db> {
Scope(ScopeId<'db>),
}
impl<'db> InferenceRegion<'db> {
fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
match self {
InferenceRegion::Expression(expression) => expression.scope(db),
InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => {
definition.scope(db)
}
InferenceRegion::Scope(scope) => scope,
}
}
}
/// The inferred types for a single region.
#[derive(Debug, Eq, PartialEq)]
pub(crate) struct TypeInference<'db> {
@@ -389,10 +382,16 @@ impl<'db> TypeInferenceBuilder<'db> {
region: InferenceRegion<'db>,
index: &'db SemanticIndex<'db>,
) -> Self {
let scope = region.scope(db);
let (file, scope) = match region {
InferenceRegion::Expression(expression) => (expression.file(db), expression.scope(db)),
InferenceRegion::Definition(definition) | InferenceRegion::Deferred(definition) => {
(definition.file(db), definition.scope(db))
}
InferenceRegion::Scope(scope) => (scope.file(db), scope),
};
Self {
context: InferContext::new(db, scope),
context: InferContext::new(db, file),
index,
region,
deferred_state: DeferredExpressionState::None,
@@ -592,44 +591,41 @@ impl<'db> TypeInferenceBuilder<'db> {
}
// (3) Check that the class's MRO is resolvable
match class.try_mro(self.db()).as_ref() {
Err(mro_error) => {
match mro_error.reason() {
MroErrorKind::DuplicateBases(duplicates) => {
let base_nodes = class_node.bases();
for (index, duplicate) in duplicates {
self.context.report_lint(
&DUPLICATE_BASE,
(&base_nodes[*index]).into(),
format_args!("Duplicate base class `{}`", duplicate.name(self.db())),
);
}
if let Err(mro_error) = class.try_mro(self.db()).as_ref() {
match mro_error.reason() {
MroErrorKind::DuplicateBases(duplicates) => {
let base_nodes = class_node.bases();
for (index, duplicate) in duplicates {
self.context.report_lint(
&DUPLICATE_BASE,
(&base_nodes[*index]).into(),
format_args!("Duplicate base class `{}`", duplicate.name(self.db())),
);
}
MroErrorKind::InvalidBases(bases) => {
let base_nodes = class_node.bases();
for (index, base_ty) in bases {
self.context.report_lint(
&INVALID_BASE,
(&base_nodes[*index]).into(),
format_args!(
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
base_ty.display(self.db())
),
);
}
}
MroErrorKind::UnresolvableMro { bases_list } => self.context.report_lint(
&INCONSISTENT_MRO,
class_node.into(),
format_args!(
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
class.name(self.db()),
bases_list.iter().map(|base| base.display(self.db())).join(", ")
),
)
}
MroErrorKind::InvalidBases(bases) => {
let base_nodes = class_node.bases();
for (index, base_ty) in bases {
self.context.report_lint(
&INVALID_BASE,
(&base_nodes[*index]).into(),
format_args!(
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
base_ty.display(self.db())
),
);
}
}
MroErrorKind::UnresolvableMro { bases_list } => self.context.report_lint(
&INCONSISTENT_MRO,
class_node.into(),
format_args!(
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
class.name(self.db()),
bases_list.iter().map(|base| base.display(self.db())).join(", ")
),
)
}
Ok(_) => check_class_slots(&self.context, class, class_node)
}
// (4) Check that the class's metaclass can be determined without error.
@@ -714,7 +710,12 @@ impl<'db> TypeInferenceBuilder<'db> {
self.infer_augment_assignment_definition(augmented_assignment.node(), definition);
}
DefinitionKind::For(for_statement_definition) => {
self.infer_for_statement_definition(for_statement_definition, definition);
self.infer_for_statement_definition(
for_statement_definition.target(),
for_statement_definition.iterable(),
for_statement_definition.is_async(),
definition,
);
}
DefinitionKind::NamedExpression(named_expression) => {
self.infer_named_expression_definition(named_expression.node(), definition);
@@ -1028,20 +1029,10 @@ impl<'db> TypeInferenceBuilder<'db> {
decorator_list,
} = function;
// Check if the function is decorated with the `no_type_check` decorator
// and, if so, suppress any errors that come after the decorators.
let mut decorator_tys = Vec::with_capacity(decorator_list.len());
for decorator in decorator_list {
let ty = self.infer_decorator(decorator);
decorator_tys.push(ty);
if let Type::FunctionLiteral(function) = ty {
if function.is_known(self.db(), KnownFunction::NoTypeCheck) {
self.context.set_in_no_type_check(InNoTypeCheck::Yes);
}
}
}
let decorator_tys: Box<[Type]> = decorator_list
.iter()
.map(|decorator| self.infer_decorator(decorator))
.collect();
for default in parameters
.iter_non_variadic_params()
@@ -1077,7 +1068,7 @@ impl<'db> TypeInferenceBuilder<'db> {
&name.id,
function_kind,
body_scope,
decorator_tys.into_boxed_slice(),
decorator_tys,
));
self.add_declaration_with_binding(function.into(), definition, function_ty, function_ty);
@@ -1842,28 +1833,18 @@ impl<'db> TypeInferenceBuilder<'db> {
} = assignment;
for target in targets {
self.infer_target(target, value);
self.infer_assignment_target(target, value);
}
}
/// Infer the definition types involved in a `target` expression.
///
/// This is used for assignment statements, for statements, etc. with a single or multiple
/// targets (unpacking).
///
/// # Panics
///
/// If the `value` is not a standalone expression.
fn infer_target(&mut self, target: &ast::Expr, value: &ast::Expr) {
// TODO: Remove the `value` argument once we handle all possible assignment targets.
fn infer_assignment_target(&mut self, target: &ast::Expr, value: &ast::Expr) {
match target {
ast::Expr::Name(name) => self.infer_definition(name),
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
for element in elts {
self.infer_target(element, value);
}
if elts.is_empty() {
self.infer_standalone_expression(value);
self.infer_assignment_target(element, value);
}
}
_ => {
@@ -1882,7 +1863,10 @@ impl<'db> TypeInferenceBuilder<'db> {
let value = assignment.value();
let name = assignment.name();
let value_ty = self.infer_standalone_expression(value);
self.infer_standalone_expression(value);
let value_ty = self.expression_ty(value);
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
let mut target_ty = match assignment.target() {
TargetKind::Sequence(unpack) => {
@@ -1893,7 +1877,6 @@ impl<'db> TypeInferenceBuilder<'db> {
self.context.extend(unpacked);
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
}
TargetKind::Name => value_ty,
@@ -2121,41 +2104,36 @@ impl<'db> TypeInferenceBuilder<'db> {
is_async: _,
} = for_statement;
self.infer_target(target, iter);
// TODO more complex assignment targets
if let ast::Expr::Name(name) = &**target {
self.infer_definition(name);
} else {
self.infer_standalone_expression(iter);
self.infer_expression(target);
}
self.infer_body(body);
self.infer_body(orelse);
}
fn infer_for_statement_definition(
&mut self,
for_stmt: &ForStmtDefinitionKind<'db>,
target: &ast::ExprName,
iterable: &ast::Expr,
is_async: bool,
definition: Definition<'db>,
) {
let iterable = for_stmt.iterable();
let name = for_stmt.name();
let iterable_ty = self.infer_standalone_expression(iterable);
let loop_var_value_ty = if for_stmt.is_async() {
let loop_var_value_ty = if is_async {
todo_type!("async iterables/iterators")
} else {
match for_stmt.target() {
TargetKind::Sequence(unpack) => {
let unpacked = infer_unpack_types(self.db(), unpack);
if for_stmt.is_first() {
self.context.extend(unpacked);
}
let name_ast_id = name.scoped_expression_id(self.db(), self.scope());
unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
}
TargetKind::Name => iterable_ty
.iterate(self.db())
.unwrap_with_diagnostic(&self.context, iterable.into()),
}
iterable_ty
.iterate(self.db())
.unwrap_with_diagnostic(&self.context, iterable.into())
};
self.store_expression_type(name, loop_var_value_ty);
self.add_binding(name.into(), definition, loop_var_value_ty);
self.store_expression_type(target, loop_var_value_ty);
self.add_binding(target.into(), definition, loop_var_value_ty);
}
fn infer_while_statement(&mut self, while_statement: &ast::StmtWhile) {
@@ -3255,19 +3233,17 @@ impl<'db> TypeInferenceBuilder<'db> {
(_, Type::Never) => Type::Never,
(_, Type::Unknown) => Type::Unknown,
(ast::UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value),
(ast::UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value),
(ast::UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value),
(UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value),
(UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value),
(UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value),
(ast::UnaryOp::UAdd, Type::BooleanLiteral(bool)) => Type::IntLiteral(i64::from(bool)),
(ast::UnaryOp::USub, Type::BooleanLiteral(bool)) => Type::IntLiteral(-i64::from(bool)),
(ast::UnaryOp::Invert, Type::BooleanLiteral(bool)) => {
Type::IntLiteral(!i64::from(bool))
}
(UnaryOp::UAdd, Type::BooleanLiteral(bool)) => Type::IntLiteral(i64::from(bool)),
(UnaryOp::USub, Type::BooleanLiteral(bool)) => Type::IntLiteral(-i64::from(bool)),
(UnaryOp::Invert, Type::BooleanLiteral(bool)) => Type::IntLiteral(!i64::from(bool)),
(ast::UnaryOp::Not, ty) => ty.bool(self.db()).negate().into_type(self.db()),
(UnaryOp::Not, ty) => ty.bool(self.db()).negate().into_type(self.db()),
(
op @ (ast::UnaryOp::UAdd | ast::UnaryOp::USub | ast::UnaryOp::Invert),
op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert),
Type::FunctionLiteral(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
@@ -3285,10 +3261,10 @@ impl<'db> TypeInferenceBuilder<'db> {
| Type::Tuple(_),
) => {
let unary_dunder_method = match op {
ast::UnaryOp::Invert => "__invert__",
ast::UnaryOp::UAdd => "__pos__",
ast::UnaryOp::USub => "__neg__",
ast::UnaryOp::Not => {
UnaryOp::Invert => "__invert__",
UnaryOp::UAdd => "__pos__",
UnaryOp::USub => "__neg__",
UnaryOp::Not => {
unreachable!("Not operator is handled in its own case");
}
};
@@ -3606,37 +3582,27 @@ impl<'db> TypeInferenceBuilder<'db> {
n_values: usize,
) -> Type<'db> {
let mut done = false;
let elements = values.into_iter().enumerate().map(|(i, ty)| {
if done {
return Type::Never;
}
let is_last = i == n_values - 1;
match (ty.bool(db), is_last, op) {
(Truthiness::AlwaysTrue, false, ast::BoolOp::And) => Type::Never,
(Truthiness::AlwaysFalse, false, ast::BoolOp::Or) => Type::Never,
(Truthiness::AlwaysFalse, _, ast::BoolOp::And)
| (Truthiness::AlwaysTrue, _, ast::BoolOp::Or) => {
done = true;
ty
UnionType::from_elements(
db,
values.into_iter().enumerate().map(|(i, ty)| {
if done {
Type::Never
} else {
let is_last = i == n_values - 1;
match (ty.bool(db), is_last, op) {
(Truthiness::Ambiguous, _, _) => ty,
(Truthiness::AlwaysTrue, false, ast::BoolOp::And) => Type::Never,
(Truthiness::AlwaysFalse, false, ast::BoolOp::Or) => Type::Never,
(Truthiness::AlwaysFalse, _, ast::BoolOp::And)
| (Truthiness::AlwaysTrue, _, ast::BoolOp::Or) => {
done = true;
ty
}
(_, true, _) => ty,
}
}
(Truthiness::Ambiguous, false, _) => IntersectionBuilder::new(db)
.add_positive(ty)
.add_negative(match op {
ast::BoolOp::And => Type::AlwaysTruthy,
ast::BoolOp::Or => Type::AlwaysFalsy,
})
.build(),
(_, true, _) => ty,
}
});
UnionType::from_elements(db, elements)
}),
)
}
fn infer_compare_expression(&mut self, compare: &ast::ExprCompare) -> Type<'db> {
@@ -5233,7 +5199,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
// for negative and positive numbers
ast::Expr::UnaryOp(ref u)
if matches!(u.op, ast::UnaryOp::USub | ast::UnaryOp::UAdd)
if matches!(u.op, UnaryOp::USub | UnaryOp::UAdd)
&& u.operand.is_number_literal_expr() =>
{
self.infer_unary_expression(u)

View File

@@ -1,103 +0,0 @@
use ruff_python_ast as ast;
use crate::db::Db;
use crate::symbol::{Boundness, Symbol};
use crate::types::class_base::ClassBase;
use crate::types::diagnostic::report_base_with_incompatible_slots;
use crate::types::{Class, ClassLiteralType, Type};
use super::InferContext;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum SlotsKind {
/// `__slots__` is not found in the class.
NotSpecified,
/// `__slots__` is defined but empty: `__slots__ = ()`.
Empty,
/// `__slots__` is defined and is not empty: `__slots__ = ("a", "b")`.
NotEmpty,
/// `__slots__` is defined but its value is dynamic:
/// * `__slots__ = tuple(a for a in b)`
/// * `__slots__ = ["a", "b"]`
Dynamic,
}
impl SlotsKind {
fn from(db: &dyn Db, base: Class) -> Self {
let Symbol::Type(slots_ty, bound) = base.own_class_member(db, "__slots__") else {
return Self::NotSpecified;
};
if matches!(bound, Boundness::PossiblyUnbound) {
return Self::Dynamic;
};
match slots_ty {
// __slots__ = ("a", "b")
Type::Tuple(tuple) => {
if tuple.elements(db).is_empty() {
Self::Empty
} else {
Self::NotEmpty
}
}
// __slots__ = "abc" # Same as `("abc",)`
Type::StringLiteral(_) => Self::NotEmpty,
_ => Self::Dynamic,
}
}
}
pub(super) fn check_class_slots(context: &InferContext, class: Class, node: &ast::StmtClassDef) {
let db = context.db();
let mut first_with_solid_base = None;
let mut common_solid_base = None;
let mut found_second = false;
for (index, base) in class.explicit_bases(db).iter().enumerate() {
let Type::ClassLiteral(ClassLiteralType { class: base }) = base else {
continue;
};
let solid_base = base.iter_mro(db).find_map(|current| {
let ClassBase::Class(current) = current else {
return None;
};
match SlotsKind::from(db, current) {
SlotsKind::NotEmpty => Some(current),
SlotsKind::NotSpecified | SlotsKind::Empty => None,
SlotsKind::Dynamic => None,
}
});
if solid_base.is_none() {
continue;
}
let base_node = &node.bases()[index];
if first_with_solid_base.is_none() {
first_with_solid_base = Some(index);
common_solid_base = solid_base;
continue;
}
if solid_base == common_solid_base {
continue;
}
found_second = true;
report_base_with_incompatible_slots(context, base_node);
}
if found_second {
if let Some(index) = first_with_solid_base {
let base_node = &node.bases()[index];
report_base_with_incompatible_slots(context, base_node);
};
}
}

View File

@@ -1,33 +1,27 @@
use std::borrow::Cow;
use std::cmp::Ordering;
use rustc_hash::FxHashMap;
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use rustc_hash::FxHashMap;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::symbol::ScopeId;
use crate::types::{infer_expression_types, todo_type, Type, TypeCheckDiagnostics};
use crate::unpack::UnpackValue;
use crate::types::{todo_type, Type, TypeCheckDiagnostics};
use crate::Db;
use super::context::{InferContext, WithDiagnostics};
use super::diagnostic::INVALID_ASSIGNMENT;
use super::{TupleType, UnionType};
/// Unpacks the value expression type to their respective targets.
pub(crate) struct Unpacker<'db> {
context: InferContext<'db>,
scope: ScopeId<'db>,
targets: FxHashMap<ScopedExpressionId, Type<'db>>,
}
impl<'db> Unpacker<'db> {
pub(crate) fn new(db: &'db dyn Db, scope: ScopeId<'db>) -> Self {
pub(crate) fn new(db: &'db dyn Db, file: File) -> Self {
Self {
context: InferContext::new(db, scope),
context: InferContext::new(db, file),
targets: FxHashMap::default(),
scope,
}
}
@@ -35,211 +29,98 @@ impl<'db> Unpacker<'db> {
self.context.db()
}
/// Unpack the value to the target expression.
pub(crate) fn unpack(&mut self, target: &ast::Expr, value: UnpackValue<'db>) {
debug_assert!(
matches!(target, ast::Expr::List(_) | ast::Expr::Tuple(_)),
"Unpacking target must be a list or tuple expression"
);
let mut value_ty = infer_expression_types(self.db(), value.expression())
.expression_ty(value.scoped_expression_id(self.db(), self.scope));
if value.is_iterable() {
// If the value is an iterable, then the type that needs to be unpacked is the iterator
// type.
value_ty = value_ty
.iterate(self.db())
.unwrap_with_diagnostic(&self.context, value.as_any_node_ref(self.db()));
}
self.unpack_inner(target, value_ty);
}
fn unpack_inner(&mut self, target: &ast::Expr, value_ty: Type<'db>) {
pub(crate) fn unpack(&mut self, target: &ast::Expr, value_ty: Type<'db>, scope: ScopeId<'db>) {
match target {
ast::Expr::Name(target_name) => {
self.targets.insert(
target_name.scoped_expression_id(self.db(), self.scope),
value_ty,
);
self.targets
.insert(target_name.scoped_expression_id(self.db(), scope), value_ty);
}
ast::Expr::Starred(ast::ExprStarred { value, .. }) => {
self.unpack_inner(value, value_ty);
self.unpack(value, value_ty, scope);
}
ast::Expr::List(ast::ExprList { elts, .. })
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => {
// Initialize the vector of target types, one for each target.
//
// This is mainly useful for the union type where the target type at index `n` is
// going to be a union of types from every union type element at index `n`.
//
// For example, if the type is `tuple[int, int] | tuple[int, str]` and the target
// has two elements `(a, b)`, then
// * The type of `a` will be a union of `int` and `int` which are at index 0 in the
// first and second tuple respectively which resolves to an `int`.
// * Similarly, the type of `b` will be a union of `int` and `str` which are at
// index 1 in the first and second tuple respectively which will be `int | str`.
let mut target_types = vec![vec![]; elts.len()];
| ast::Expr::Tuple(ast::ExprTuple { elts, .. }) => match value_ty {
Type::Tuple(tuple_ty) => {
let starred_index = elts.iter().position(ast::Expr::is_starred_expr);
let unpack_types = match value_ty {
Type::Union(union_ty) => union_ty.elements(self.db()),
_ => std::slice::from_ref(&value_ty),
};
let element_types = if let Some(starred_index) = starred_index {
if tuple_ty.len(self.db()) >= elts.len() - 1 {
let mut element_types = Vec::with_capacity(elts.len());
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[..starred_index],
);
for ty in unpack_types.iter().copied() {
// Deconstruct certain types to delegate the inference back to the tuple type
// for correct handling of starred expressions.
let ty = match ty {
Type::StringLiteral(string_literal_ty) => {
// We could go further and deconstruct to an array of `StringLiteral`
// with each individual character, instead of just an array of
// `LiteralString`, but there would be a cost and it's not clear that
// it's worth it.
Type::tuple(
self.db(),
std::iter::repeat(Type::LiteralString)
.take(string_literal_ty.python_len(self.db())),
)
}
_ => ty,
};
// E.g., in `(a, *b, c, d) = ...`, the index of starred element `b`
// is 1 and the remaining elements after that are 2.
let remaining = elts.len() - (starred_index + 1);
// This index represents the type of the last element that belongs
// to the starred expression, in an exclusive manner.
let starred_end_index = tuple_ty.len(self.db()) - remaining;
// SAFETY: Safe because of the length check above.
let _starred_element_types =
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
// TODO: Combine the types into a list type. If the
// starred_element_types is empty, then it should be `List[Any]`.
// combine_types(starred_element_types);
element_types.push(todo_type!("starred unpacking"));
if let Some(tuple_ty) = ty.into_tuple() {
let tuple_ty_elements = self.tuple_ty_elements(target, elts, tuple_ty);
match elts.len().cmp(&tuple_ty_elements.len()) {
Ordering::Less => {
self.context.report_lint(
&INVALID_ASSIGNMENT,
target.into(),
format_args!(
"Too many values to unpack (expected {}, got {})",
elts.len(),
tuple_ty_elements.len()
),
);
}
Ordering::Greater => {
self.context.report_lint(
&INVALID_ASSIGNMENT,
target.into(),
format_args!(
"Not enough values to unpack (expected {}, got {})",
elts.len(),
tuple_ty_elements.len()
),
);
}
Ordering::Equal => {}
}
for (index, ty) in tuple_ty_elements.iter().enumerate() {
if let Some(element_types) = target_types.get_mut(index) {
element_types.push(*ty);
}
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[starred_end_index..],
);
Cow::Owned(element_types)
} else {
let mut element_types = tuple_ty.elements(self.db()).to_vec();
// Subtract 1 to insert the starred expression type at the correct
// index.
element_types.resize(elts.len() - 1, Type::Unknown);
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, todo_type!("starred unpacking"));
Cow::Owned(element_types)
}
} else {
let ty = if ty.is_literal_string() {
Type::LiteralString
} else {
ty.iterate(self.db())
.unwrap_with_diagnostic(&self.context, AnyNodeRef::from(target))
};
for target_type in &mut target_types {
target_type.push(ty);
}
Cow::Borrowed(tuple_ty.elements(self.db()).as_ref())
};
for (index, element) in elts.iter().enumerate() {
self.unpack(
element,
element_types.get(index).copied().unwrap_or(Type::Unknown),
scope,
);
}
}
for (index, element) in elts.iter().enumerate() {
// SAFETY: `target_types` is initialized with the same length as `elts`.
let element_ty = match target_types[index].as_slice() {
[] => Type::Unknown,
types => UnionType::from_elements(self.db(), types),
};
self.unpack_inner(element, element_ty);
Type::StringLiteral(string_literal_ty) => {
// Deconstruct the string literal to delegate the inference back to the
// tuple type for correct handling of starred expressions. We could go
// further and deconstruct to an array of `StringLiteral` with each
// individual character, instead of just an array of `LiteralString`, but
// there would be a cost and it's not clear that it's worth it.
let value_ty = Type::tuple(
self.db(),
std::iter::repeat(Type::LiteralString)
.take(string_literal_ty.python_len(self.db())),
);
self.unpack(target, value_ty, scope);
}
}
_ => {
let value_ty = if value_ty.is_literal_string() {
Type::LiteralString
} else {
value_ty
.iterate(self.db())
.unwrap_with_diagnostic(&self.context, AnyNodeRef::from(target))
};
for element in elts {
self.unpack(element, value_ty, scope);
}
}
},
_ => {}
}
}
/// Returns the [`Type`] elements inside the given [`TupleType`] taking into account that there
/// can be a starred expression in the `elements`.
fn tuple_ty_elements(
&self,
expr: &ast::Expr,
targets: &[ast::Expr],
tuple_ty: TupleType<'db>,
) -> Cow<'_, [Type<'db>]> {
// If there is a starred expression, it will consume all of the types at that location.
let Some(starred_index) = targets.iter().position(ast::Expr::is_starred_expr) else {
// Otherwise, the types will be unpacked 1-1 to the targets.
return Cow::Borrowed(tuple_ty.elements(self.db()).as_ref());
};
if tuple_ty.len(self.db()) >= targets.len() - 1 {
// This branch is only taken when there are enough elements in the tuple type to
// combine for the starred expression. So, the arithmetic and indexing operations are
// safe to perform.
let mut element_types = Vec::with_capacity(targets.len());
// Insert all the elements before the starred expression.
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[..starred_index],
);
// The number of target expressions that are remaining after the starred expression.
// For example, in `(a, *b, c, d) = ...`, the index of starred element `b` is 1 and the
// remaining elements after that are 2.
let remaining = targets.len() - (starred_index + 1);
// This index represents the position of the last element that belongs to the starred
// expression, in an exclusive manner. For example, in `(a, *b, c) = (1, 2, 3, 4)`, the
// starred expression `b` will consume the elements `Literal[2]` and `Literal[3]` and
// the index value would be 3.
let starred_end_index = tuple_ty.len(self.db()) - remaining;
// SAFETY: Safe because of the length check above.
let _starred_element_types =
&tuple_ty.elements(self.db())[starred_index..starred_end_index];
// TODO: Combine the types into a list type. If the
// starred_element_types is empty, then it should be `List[Any]`.
// combine_types(starred_element_types);
element_types.push(todo_type!("starred unpacking"));
// Insert the types remaining that aren't consumed by the starred expression.
element_types.extend_from_slice(
// SAFETY: Safe because of the length check above.
&tuple_ty.elements(self.db())[starred_end_index..],
);
Cow::Owned(element_types)
} else {
self.context.report_lint(
&INVALID_ASSIGNMENT,
expr.into(),
format_args!(
"Not enough values to unpack (expected {} or more, got {})",
targets.len() - 1,
tuple_ty.len(self.db())
),
);
let mut element_types = tuple_ty.elements(self.db()).to_vec();
// Subtract 1 to insert the starred expression type at the correct
// index.
element_types.resize(targets.len() - 1, Type::Unknown);
// TODO: This should be `list[Unknown]`
element_types.insert(starred_index, todo_type!("starred unpacking"));
Cow::Owned(element_types)
}
}
pub(crate) fn finish(mut self) -> UnpackResult<'db> {
self.targets.shrink_to_fit();
UnpackResult {

View File

@@ -1,9 +1,7 @@
use ruff_db::files::File;
use ruff_python_ast::{self as ast, AnyNodeRef};
use ruff_text_size::{Ranged, TextRange};
use ruff_python_ast::{self as ast};
use crate::ast_node_ref::AstNodeRef;
use crate::semantic_index::ast_ids::{HasScopedExpressionId, ScopedExpressionId};
use crate::semantic_index::expression::Expression;
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
use crate::Db;
@@ -43,7 +41,7 @@ pub(crate) struct Unpack<'db> {
/// The ingredient representing the value expression of the unpacking. For example, in
/// `(a, b) = (1, 2)`, the value expression is `(1, 2)`.
#[no_eq]
pub(crate) value: UnpackValue<'db>,
pub(crate) value: Expression<'db>,
#[no_eq]
count: countme::Count<Unpack<'static>>,
@@ -54,48 +52,4 @@ impl<'db> Unpack<'db> {
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
self.file_scope(db).to_scope_id(db, self.file(db))
}
/// Returns the range of the unpack target expression.
pub(crate) fn range(self, db: &'db dyn Db) -> TextRange {
self.target(db).range()
}
}
/// The expression that is being unpacked.
#[derive(Clone, Copy, Debug)]
pub(crate) enum UnpackValue<'db> {
/// An iterable expression like the one in a `for` loop or a comprehension.
Iterable(Expression<'db>),
/// An expression that is being assigned to a target.
Assign(Expression<'db>),
}
impl<'db> UnpackValue<'db> {
/// Returns `true` if the value is an iterable expression.
pub(crate) const fn is_iterable(self) -> bool {
matches!(self, UnpackValue::Iterable(_))
}
/// Returns the underlying [`Expression`] that is being unpacked.
pub(crate) const fn expression(self) -> Expression<'db> {
match self {
UnpackValue::Assign(expr) | UnpackValue::Iterable(expr) => expr,
}
}
/// Returns the [`ScopedExpressionId`] of the underlying expression.
pub(crate) fn scoped_expression_id(
self,
db: &'db dyn Db,
scope: ScopeId<'db>,
) -> ScopedExpressionId {
self.expression()
.node_ref(db)
.scoped_expression_id(db, scope)
}
/// Returns the expression as an [`AnyNodeRef`].
pub(crate) fn as_any_node_ref(self, db: &'db dyn Db) -> AnyNodeRef<'db> {
self.expression().node_ref(db).node().into()
}
}

View File

@@ -1,158 +1,3 @@
//! # Visibility constraints
//!
//! During semantic index building, we collect visibility constraints for each binding and
//! declaration. These constraints are then used during type-checking to determine the static
//! visibility of a certain definition. This allows us to re-analyze control flow during type
//! checking, potentially "hiding" some branches that we can statically determine to never be
//! taken. Consider the following example first. We added implicit "unbound" definitions at the
//! start of the scope. Note how visibility constraints can apply to bindings outside of the
//! if-statement:
//! ```py
//! x = <unbound> # not a live binding for the use of x below, shadowed by `x = 1`
//! y = <unbound> # visibility constraint: ~test
//!
//! x = 1 # visibility constraint: ~test
//! if test:
//! x = 2 # visibility constraint: test
//!
//! y = 2 # visibility constraint: test
//!
//! use(x)
//! use(y)
//! ```
//! The static truthiness of the `test` condition can either be always-false, ambiguous, or
//! always-true. Similarly, we have the same three options when evaluating a visibility constraint.
//! This outcome determines the visibility of a definition: always-true means that the definition
//! is definitely visible for a given use, always-false means that the definition is definitely
//! not visible, and ambiguous means that we might see this definition or not. In the latter case,
//! we need to consider both options during type inference and boundness analysis. For the example
//! above, these are the possible type inference / boundness results for the uses of `x` and `y`:
//!
//! ```text
//! | `test` truthiness | `~test` truthiness | type of `x` | boundness of `y` |
//! |-------------------|--------------------|-----------------|------------------|
//! | always false | always true | `Literal[1]` | unbound |
//! | ambiguous | ambiguous | `Literal[1, 2]` | possibly unbound |
//! | always true | always false | `Literal[2]` | bound |
//! ```
//!
//! ### Sequential constraints (ternary AND)
//!
//! As we have seen above, visibility constraints can apply outside of a control flow element.
//! So we need to consider the possibility that multiple constraints apply to the same binding.
//! Here, we consider what happens if multiple `if`-statements lead to a sequence of constraints.
//! Consider the following example:
//! ```py
//! x = 0
//!
//! if test1:
//! x = 1
//!
//! if test2:
//! x = 2
//! ```
//! The binding `x = 2` is easy to analyze. Its visibility corresponds to the truthiness of `test2`.
//! For the `x = 1` binding, things are a bit more interesting. It is always visible if `test1` is
//! always-true *and* `test2` is always-false. It is never visible if `test1` is always-false *or*
//! `test2` is always-true. And it is ambiguous otherwise. This corresponds to a ternary *test1 AND
//! ~test2* operation in three-valued Kleene logic [Kleene]:
//!
//! ```text
//! | AND | always-false | ambiguous | always-true |
//! |--------------|--------------|--------------|--------------|
//! | always false | always-false | always-false | always-false |
//! | ambiguous | always-false | ambiguous | ambiguous |
//! | always true | always-false | ambiguous | always-true |
//! ```
//!
//! The `x = 0` binding can be handled similarly, with the difference that both `test1` and `test2`
//! are negated:
//! ```py
//! x = 0 # ~test1 AND ~test2
//!
//! if test1:
//! x = 1 # test1 AND ~test2
//!
//! if test2:
//! x = 2 # test2
//! ```
//!
//! ### Merged constraints (ternary OR)
//!
//! Finally, we consider what happens in "parallel" control flow. Consider the following example
//! where we have omitted the test condition for the outer `if` for clarity:
//! ```py
//! x = 0
//!
//! if <…>:
//! if test1:
//! x = 1
//! else:
//! if test2:
//! x = 2
//!
//! use(x)
//! ```
//! At the usage of `x`, i.e. after control flow has been merged again, the visibility of the `x =
//! 0` binding behaves as follows: the binding is always visible if `test1` is always-false *or*
//! `test2` is always-false; and it is never visible if `test1` is always-true *and* `test2` is
//! always-true. This corresponds to a ternary *OR* operation in Kleene logic:
//!
//! ```text
//! | OR | always-false | ambiguous | always-true |
//! |--------------|--------------|--------------|--------------|
//! | always false | always-false | ambiguous | always-true |
//! | ambiguous | ambiguous | ambiguous | always-true |
//! | always true | always-true | always-true | always-true |
//! ```
//!
//! Using this, we can annotate the visibility constraints for the example above:
//! ```py
//! x = 0 # ~test1 OR ~test2
//!
//! if <…>:
//! if test1:
//! x = 1 # test1
//! else:
//! if test2:
//! x = 2 # test2
//!
//! use(x)
//! ```
//!
//! ### Explicit ambiguity
//!
//! In some cases, we explicitly add a `VisibilityConstraint::Ambiguous` constraint to all bindings
//! in a certain control flow path. We do this when branching on something that we can not (or
//! intentionally do not want to) analyze statically. `for` loops are one example:
//! ```py
//! x = <unbound>
//!
//! for _ in range(2):
//! x = 1
//! ```
//! Here, we report an ambiguous visibility constraint before branching off. If we don't do this,
//! the `x = <unbound>` binding would be considered unconditionally visible in the no-loop case.
//! And since the other branch does not have the live `x = <unbound>` binding, we would incorrectly
//! create a state where the `x = <unbound>` binding is always visible.
//!
//!
//! ### Properties
//!
//! The ternary `AND` and `OR` operations have the property that `~a OR ~b = ~(a AND b)`. This
//! means we could, in principle, get rid of either of these two to simplify the representation.
//!
//! However, we already apply negative constraints `~test1` and `~test2` to the "branches not
//! taken" in the example above. This means that the tree-representation `~test1 OR ~test2` is much
//! cheaper/shallower than basically creating `~(~(~test1) AND ~(~test2))`. Similarly, if we wanted
//! to get rid of `AND`, we would also have to create additional nodes. So for performance reasons,
//! there is a small "duplication" in the code between those two constraint types.
//!
//! [Kleene]: <https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics>
use ruff_index::IndexVec;
use crate::semantic_index::ScopedVisibilityConstraintId;
use crate::semantic_index::{
ast_ids::HasScopedExpressionId,
constraint::{Constraint, ConstraintNode, PatternConstraintKind},
@@ -160,179 +5,157 @@ use crate::semantic_index::{
use crate::types::{infer_expression_types, Truthiness};
use crate::Db;
/// The maximum depth of recursion when evaluating visibility constraints.
///
/// This is a performance optimization that prevents us from descending deeply in case of
/// pathological cases. The actual limit here has been derived from performance testing on
/// the `black` codebase. When increasing the limit beyond 32, we see a 5x runtime increase
/// resulting from a few files with a lot of boolean expressions and `if`-statements.
const MAX_RECURSION_DEPTH: usize = 24;
const MAX_RECURSION_DEPTH: usize = 10;
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum VisibilityConstraint<'db> {
#[salsa::interned]
pub(crate) struct VisibilityConstraint<'db> {
kind: VisibilityConstraintKind<'db>,
}
impl<'db> VisibilityConstraint<'db> {
pub(crate) fn always_true(db: &'db dyn Db) -> Self {
Self::new(db, VisibilityConstraintKind::AlwaysTrue)
}
pub(crate) fn ambiguous(db: &'db dyn Db) -> Self {
Self::new(db, VisibilityConstraintKind::Ambiguous)
}
pub(crate) fn visible_if(db: &'db dyn Db, constraint: Constraint<'db>) -> Self {
Self::new(db, VisibilityConstraintKind::VisibleIf(constraint))
}
pub(crate) fn visible_if_not(db: &'db dyn Db, constraint: VisibilityConstraint<'db>) -> Self {
Self::new(db, VisibilityConstraintKind::VisibleIfNot(constraint))
}
pub(crate) fn kleene_and(
db: &'db dyn Db,
lhs: VisibilityConstraint<'db>,
rhs: VisibilityConstraint<'db>,
) -> Self {
Self::new(db, VisibilityConstraintKind::KleeneAnd(lhs, rhs))
}
pub(crate) fn kleene_or(
db: &'db dyn Db,
lhs: VisibilityConstraint<'db>,
rhs: VisibilityConstraint<'db>,
) -> Self {
Self::new(db, VisibilityConstraintKind::KleeneOr(lhs, rhs))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) enum VisibilityConstraintKind<'db> {
AlwaysTrue,
Ambiguous,
VisibleIf(Constraint<'db>),
VisibleIfNot(ScopedVisibilityConstraintId),
KleeneAnd(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
KleeneOr(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
VisibleIfNot(VisibilityConstraint<'db>),
KleeneAnd(VisibilityConstraint<'db>, VisibilityConstraint<'db>),
KleeneOr(VisibilityConstraint<'db>, VisibilityConstraint<'db>),
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) struct VisibilityConstraints<'db> {
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
/// Analyze the statically known visibility for a given visibility constraint.
pub(crate) fn evaluate<'db>(db: &'db dyn Db, constraint: VisibilityConstraint<'db>) -> Truthiness {
evaluate_impl(db, constraint, MAX_RECURSION_DEPTH)
}
impl Default for VisibilityConstraints<'_> {
fn default() -> Self {
Self {
constraints: IndexVec::from_iter([VisibilityConstraint::AlwaysTrue]),
fn evaluate_impl<'db>(
db: &'db dyn Db,
constraint: VisibilityConstraint<'db>,
max_depth: usize,
) -> Truthiness {
if max_depth == 0 {
return Truthiness::Ambiguous;
}
match constraint.kind(db) {
VisibilityConstraintKind::AlwaysTrue => Truthiness::AlwaysTrue,
VisibilityConstraintKind::Ambiguous => Truthiness::Ambiguous,
VisibilityConstraintKind::VisibleIf(constraint) => analyze_single(db, &constraint),
VisibilityConstraintKind::VisibleIfNot(negated) => {
evaluate_impl(db, negated, max_depth - 1).negate()
}
VisibilityConstraintKind::KleeneAnd(lhs, rhs) => {
let lhs = evaluate_impl(db, lhs, max_depth - 1);
if lhs == Truthiness::AlwaysFalse {
return Truthiness::AlwaysFalse;
}
let rhs = evaluate_impl(db, rhs, max_depth - 1);
if rhs == Truthiness::AlwaysFalse {
Truthiness::AlwaysFalse
} else if lhs == Truthiness::AlwaysTrue && rhs == Truthiness::AlwaysTrue {
Truthiness::AlwaysTrue
} else {
Truthiness::Ambiguous
}
}
VisibilityConstraintKind::KleeneOr(lhs, rhs) => {
let lhs = evaluate_impl(db, lhs, max_depth - 1);
if lhs == Truthiness::AlwaysTrue {
return Truthiness::AlwaysTrue;
}
let rhs = evaluate_impl(db, rhs, max_depth - 1);
if rhs == Truthiness::AlwaysTrue {
Truthiness::AlwaysTrue
} else if lhs == Truthiness::AlwaysFalse && rhs == Truthiness::AlwaysFalse {
Truthiness::AlwaysFalse
} else {
Truthiness::Ambiguous
}
}
}
}
impl<'db> VisibilityConstraints<'db> {
pub(crate) fn add(
&mut self,
constraint: VisibilityConstraint<'db>,
) -> ScopedVisibilityConstraintId {
self.constraints.push(constraint)
}
fn analyze_single(db: &dyn Db, constraint: &Constraint) -> Truthiness {
match constraint.node {
ConstraintNode::Expression(test_expr) => {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
let ty =
inference.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope));
pub(crate) fn add_or_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
match (&self.constraints[a], &self.constraints[b]) {
(_, VisibilityConstraint::VisibleIfNot(id)) if a == *id => {
ScopedVisibilityConstraintId::ALWAYS_TRUE
}
(VisibilityConstraint::VisibleIfNot(id), _) if *id == b => {
ScopedVisibilityConstraintId::ALWAYS_TRUE
}
_ => self.add(VisibilityConstraint::KleeneOr(a, b)),
ty.bool(db).negate_if(!constraint.is_positive)
}
}
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 scope = subject_expression.scope(db);
let subject_ty = inference.expression_ty(
subject_expression
.node_ref(db)
.scoped_expression_id(db, scope),
);
pub(crate) fn add_and_constraint(
&mut self,
a: ScopedVisibilityConstraintId,
b: ScopedVisibilityConstraintId,
) -> ScopedVisibilityConstraintId {
if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
b
} else if b == ScopedVisibilityConstraintId::ALWAYS_TRUE {
a
} else {
self.add(VisibilityConstraint::KleeneAnd(a, b))
}
}
let inference = infer_expression_types(db, *value);
let scope = value.scope(db);
let value_ty =
inference.expression_ty(value.node_ref(db).scoped_expression_id(db, scope));
/// Analyze the statically known visibility for a given visibility constraint.
pub(crate) fn evaluate(&self, db: &'db dyn Db, id: ScopedVisibilityConstraintId) -> Truthiness {
self.evaluate_impl(db, id, MAX_RECURSION_DEPTH)
}
if subject_ty.is_single_valued(db) {
let truthiness = Truthiness::from(subject_ty.is_equivalent_to(db, value_ty));
fn evaluate_impl(
&self,
db: &'db dyn Db,
id: ScopedVisibilityConstraintId,
max_depth: usize,
) -> Truthiness {
if max_depth == 0 {
return Truthiness::Ambiguous;
}
let visibility_constraint = &self.constraints[id];
match visibility_constraint {
VisibilityConstraint::AlwaysTrue => Truthiness::AlwaysTrue,
VisibilityConstraint::Ambiguous => Truthiness::Ambiguous,
VisibilityConstraint::VisibleIf(constraint) => Self::analyze_single(db, constraint),
VisibilityConstraint::VisibleIfNot(negated) => {
self.evaluate_impl(db, *negated, max_depth - 1).negate()
}
VisibilityConstraint::KleeneAnd(lhs, rhs) => {
let lhs = self.evaluate_impl(db, *lhs, max_depth - 1);
if lhs == Truthiness::AlwaysFalse {
return Truthiness::AlwaysFalse;
}
let rhs = self.evaluate_impl(db, *rhs, max_depth - 1);
if rhs == Truthiness::AlwaysFalse {
Truthiness::AlwaysFalse
} else if lhs == Truthiness::AlwaysTrue && rhs == Truthiness::AlwaysTrue {
Truthiness::AlwaysTrue
} else {
Truthiness::Ambiguous
}
}
VisibilityConstraint::KleeneOr(lhs_id, rhs_id) => {
let lhs = self.evaluate_impl(db, *lhs_id, max_depth - 1);
if lhs == Truthiness::AlwaysTrue {
return Truthiness::AlwaysTrue;
}
let rhs = self.evaluate_impl(db, *rhs_id, max_depth - 1);
if rhs == Truthiness::AlwaysTrue {
Truthiness::AlwaysTrue
} else if lhs == Truthiness::AlwaysFalse && rhs == Truthiness::AlwaysFalse {
Truthiness::AlwaysFalse
} else {
Truthiness::Ambiguous
}
}
}
}
fn analyze_single(db: &dyn Db, constraint: &Constraint) -> Truthiness {
match constraint.node {
ConstraintNode::Expression(test_expr) => {
let inference = infer_expression_types(db, test_expr);
let scope = test_expr.scope(db);
let ty =
inference.expression_ty(test_expr.node_ref(db).scoped_expression_id(db, scope));
ty.bool(db).negate_if(!constraint.is_positive)
}
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 scope = subject_expression.scope(db);
let subject_ty = inference.expression_ty(
subject_expression
.node_ref(db)
.scoped_expression_id(db, scope),
);
let inference = infer_expression_types(db, *value);
let scope = value.scope(db);
let value_ty =
inference.expression_ty(value.node_ref(db).scoped_expression_id(db, scope));
if subject_ty.is_single_valued(db) {
let truthiness =
Truthiness::from(subject_ty.is_equivalent_to(db, value_ty));
if truthiness.is_always_true() && guard.is_some() {
// Fall back to ambiguous, the guard might change the result.
Truthiness::Ambiguous
} else {
truthiness
}
} else {
if truthiness.is_always_true() && guard.is_some() {
// Fall back to ambiguous, the guard might change the result.
Truthiness::Ambiguous
} else {
truthiness
}
}
PatternConstraintKind::Singleton(..) | PatternConstraintKind::Unsupported => {
} else {
Truthiness::Ambiguous
}
},
}
}
PatternConstraintKind::Singleton(..) | PatternConstraintKind::Unsupported => {
Truthiness::Ambiguous
}
},
}
}

View File

@@ -1,4 +1,4 @@
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{
default_lint_registry, Db as SemanticDb, Program, ProgramSettings, PythonPlatform,
PythonVersion, SearchPathSettings,
@@ -21,7 +21,7 @@ pub(crate) struct Db {
impl Db {
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
let rule_selection = RuleSelection::from_registry(default_lint_registry());
let rule_selection = RuleSelection::from_registry(&default_lint_registry());
let db = Self {
workspace_root,
@@ -97,10 +97,6 @@ impl SemanticDb for Db {
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {
default_lint_registry()
}
}
#[salsa::db]

View File

@@ -100,11 +100,7 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
let test_files: Vec<_> = test
.files()
.filter_map(|embedded| {
if embedded.lang == "ignore" {
return None;
}
.map(|embedded| {
assert!(
matches!(embedded.lang, "py" | "pyi"),
"Non-Python files not supported yet."
@@ -113,10 +109,10 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
db.write_file(&full_path, embedded.code).unwrap();
let file = system_path_to_file(db, full_path).unwrap();
Some(TestFile {
TestFile {
file,
backtick_offset: embedded.md_offset,
})
}
})
.collect();

View File

@@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
use crate::DEFAULT_LINT_REGISTRY;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{Db as SemanticDb, Program};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::files::{File, Files};
@@ -116,10 +116,6 @@ impl SemanticDb for RootDatabase {
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {
&DEFAULT_LINT_REGISTRY
}
}
#[salsa::db]
@@ -166,7 +162,7 @@ pub(crate) mod tests {
use salsa::Event;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::Db as SemanticDb;
use ruff_db::files::Files;
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
@@ -272,10 +268,6 @@ pub(crate) mod tests {
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {
&DEFAULT_LINT_REGISTRY
}
}
#[salsa::db]

View File

@@ -182,11 +182,11 @@ impl<'db> PullTypesVisitor<'db> {
}
}
fn visit_target(&mut self, target: &Expr) {
fn visit_assign_target(&mut self, target: &Expr) {
match target {
Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => {
for element in elts {
self.visit_target(element);
self.visit_assign_target(element);
}
}
_ => self.visit_expr(target),
@@ -205,16 +205,8 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
}
Stmt::Assign(assign) => {
for target in &assign.targets {
self.visit_target(target);
self.visit_assign_target(target);
}
self.visit_expr(&assign.value);
return;
}
Stmt::For(for_stmt) => {
self.visit_target(&for_stmt.target);
self.visit_expr(&for_stmt.iter);
self.visit_body(&for_stmt.body);
self.visit_body(&for_stmt.orelse);
return;
}
Stmt::AnnAssign(_)
@@ -222,6 +214,7 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> {
| Stmt::Delete(_)
| Stmt::AugAssign(_)
| Stmt::TypeAlias(_)
| Stmt::For(_)
| Stmt::While(_)
| Stmt::If(_)
| Stmt::With(_)

View File

@@ -799,7 +799,7 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
// ```
Rule::MissingTrailingComma,
// The formatter always removes blank lines before the docstring.
Rule::IncorrectBlankLineBeforeClass,
Rule::OneBlankLineBeforeClass,
] {
if setting.linter.rules.enabled(rule) {
incompatible_rules.insert(rule);
@@ -830,7 +830,7 @@ pub(super) fn warn_incompatible_formatter_settings(resolver: &Resolver) {
}
// Validate all rules that rely on tab styles.
if setting.linter.rules.enabled(Rule::DocstringTabIndentation)
if setting.linter.rules.enabled(Rule::IndentWithSpaces)
&& setting.formatter.indent_style.is_tab()
{
warn_user_once!("The `format.indent-style=\"tab\"` option is incompatible with `D206`, with requires space-based indentation. We recommend disabling these rules when using the formatter, which enforces a consistent indentation style. Alternatively, set the `format.indent-style` option to `\"space\"`.");

View File

@@ -1112,7 +1112,7 @@ def say_hy(name: str):
1 file reformatted
----- stderr -----
warning: `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`.
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
warning: The following rules may cause conflicts when used with the formatter: `COM812`, `ISC001`. To avoid unexpected behavior, we recommend disabling these rules, either by removing them from the `select` or `extend-select` configuration, or adding them to the `ignore` configuration.
");

View File

@@ -1037,7 +1037,7 @@ fn preview_enabled_all() {
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
----- stderr -----
warning: `incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `incorrect-blank-line-before-class`.
warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. Ignoring `one-blank-line-before-class`.
warning: `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible. Ignoring `multi-line-summary-second-line`.
");
}

View File

@@ -375,7 +375,6 @@ linter.pylint.max_locals = 15
linter.pyupgrade.keep_runtime_typing = false
linter.ruff.parenthesize_tuple_in_subscript = false
linter.ruff.extend_markup_names = []
linter.ruff.allowed_markup_calls = []
# Formatter Settings
formatter.exclude = []

View File

@@ -43,7 +43,6 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
"warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive"
];
fn get_test_file(name: &str) -> TestFile {

View File

@@ -2,10 +2,9 @@ use anyhow::Result;
use std::sync::Arc;
use zip::CompressionMethod;
use red_knot_python_semantic::lint::{LintRegistry, RuleSelection};
use red_knot_python_semantic::lint::RuleSelection;
use red_knot_python_semantic::{
default_lint_registry, Db, Program, ProgramSettings, PythonPlatform, PythonVersion,
SearchPathSettings,
Db, Program, ProgramSettings, PythonPlatform, PythonVersion, SearchPathSettings,
};
use ruff_db::files::{File, Files};
use ruff_db::system::{OsSystem, System, SystemPathBuf};
@@ -94,10 +93,6 @@ impl Db for ModuleDb {
fn rule_selection(&self) -> &RuleSelection {
&self.rule_selection
}
fn lint_registry(&self) -> &LintRegistry {
default_lint_registry()
}
}
#[salsa::db]

View File

@@ -1,29 +0,0 @@
from airflow.plugins_manager import AirflowPlugin
class AirflowTestPlugin(AirflowPlugin):
name = "test_plugin"
# --- Invalid extensions start
operators = [PluginOperator]
sensors = [PluginSensorOperator]
hooks = [PluginHook]
executors = [PluginExecutor]
# --- Invalid extensions end
macros = [plugin_macro]
flask_blueprints = [bp]
appbuilder_views = [v_appbuilder_package]
appbuilder_menu_items = [appbuilder_mitem, appbuilder_mitem_toplevel]
global_operator_extra_links = [
AirflowLink(),
GithubLink(),
]
operator_extra_links = [
GoogleLink(),
AirflowLink2(),
CustomOpLink(),
CustomBaseIndexOpLink(1),
]
timetables = [CustomCronDataIntervalTimetable]
listeners = [empty_listener, ClassBasedListener()]
ti_deps = [CustomTestTriggerRule()]
priority_weight_strategies = [CustomPriorityWeightStrategy]

View File

@@ -1,18 +1,15 @@
from datetime import timedelta
from airflow import DAG, dag
from airflow.operators.datetime import BranchDateTimeOperator
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
from airflow.providers.amazon.aws.log.s3_task_handler import S3TaskHandler
from airflow.providers.apache.hdfs.log.hdfs_task_handler import HdfsTaskHandler
from airflow.providers.elasticsearch.log.es_task_handler import ElasticsearchTaskHandler
from airflow.providers.fab.auth_manager.fab_auth_manager import FabAuthManager
from airflow.providers.google.cloud.log.gcs_task_handler import GCSTaskHandler
from airflow.providers.standard.operators import datetime, trigger_dagrun
from airflow.providers.standard.sensors import weekday
from airflow.sensors.weekday import BranchDayOfWeekOperator, DayOfWeekSensor
from airflow.timetables.simple import NullTimetable
from airflow.operators.trigger_dagrun import TriggerDagRunOperator
from airflow.providers.standard.operators import trigger_dagrun
from airflow.operators.datetime import BranchDateTimeOperator
from airflow.providers.standard.operators import datetime
from airflow.sensors.weekday import DayOfWeekSensor, BranchDayOfWeekOperator
from airflow.providers.standard.sensors import weekday
DAG(dag_id="class_schedule", schedule="@hourly")
DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
@@ -57,12 +54,10 @@ def decorator_deprecated_operator_args():
)
branch_dt_op = datetime.BranchDateTimeOperator(
task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
task_id="branch_dt_op", use_task_execution_day=True
)
branch_dt_op2 = BranchDateTimeOperator(
task_id="branch_dt_op2",
use_task_execution_day=True,
sla=timedelta(seconds=10),
task_id="branch_dt_op2", use_task_execution_day=True
)
dof_task_sensor = weekday.DayOfWeekSensor(
@@ -81,12 +76,3 @@ def decorator_deprecated_operator_args():
branch_dt_op >> branch_dt_op2
dof_task_sensor >> dof_task_sensor2
bdow_op >> bdow_op2
# deprecated filename_template arugment in FileTaskHandler
S3TaskHandler(filename_template="/tmp/test")
HdfsTaskHandler(filename_template="/tmp/test")
ElasticsearchTaskHandler(filename_template="/tmp/test")
GCSTaskHandler(filename_template="/tmp/test")
FabAuthManager(None)

View File

@@ -1,59 +0,0 @@
from airflow.datasets.manager import DatasetManager
from airflow.lineage.hook import DatasetLineageInfo, HookLineageCollector
from airflow.providers.amazon.auth_manager.aws_auth_manager import AwsAuthManager
from airflow.providers.apache.beam.hooks import BeamHook, NotAir302HookError
from airflow.providers.google.cloud.secrets.secret_manager import (
CloudSecretManagerBackend,
)
from airflow.providers.hashicorp.secrets.vault import NotAir302SecretError, VaultBackend
from airflow.providers_manager import ProvidersManager
from airflow.secrets.base_secrets import BaseSecretsBackend
dm = DatasetManager()
dm.register_dataset_change()
dm.create_datasets()
dm.notify_dataset_created()
dm.notify_dataset_changed()
dm.notify_dataset_alias_created()
hlc = HookLineageCollector()
hlc.create_dataset()
hlc.add_input_dataset()
hlc.add_output_dataset()
hlc.collected_datasets()
aam = AwsAuthManager()
aam.is_authorized_dataset()
pm = ProvidersManager()
pm.initialize_providers_asset_uri_resources()
pm.dataset_factories
base_secret_backend = BaseSecretsBackend()
base_secret_backend.get_conn_uri()
base_secret_backend.get_connections()
csm_backend = CloudSecretManagerBackend()
csm_backend.get_conn_uri()
csm_backend.get_connections()
vault_backend = VaultBackend()
vault_backend.get_conn_uri()
vault_backend.get_connections()
not_an_error = NotAir302SecretError()
not_an_error.get_conn_uri()
beam_hook = BeamHook()
beam_hook.get_conn_uri()
not_an_error = NotAir302HookError()
not_an_error.get_conn_uri()
provider_manager = ProvidersManager()
provider_manager.dataset_factories
provider_manager.dataset_uri_handlers
provider_manager.dataset_to_openlineage_converters
dl_info = DatasetLineageInfo()
dl_info.dataset

View File

@@ -36,7 +36,6 @@ from airflow.datasets.manager import (
dataset_manager,
resolve_dataset_manager,
)
from airflow.hooks.base_hook import BaseHook
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
@@ -97,10 +96,6 @@ from airflow.www.utils import get_sensitive_variables_fields, should_hide_value_
PY36, PY37, PY38, PY39, PY310, PY311, PY312
DatasetFromRoot
dataset_from_root = DatasetFromRoot()
dataset_from_root.iter_datasets()
dataset_from_root.iter_dataset_aliases()
# airflow.api_connexion.security
requires_access, requires_access_dataset
@@ -124,24 +119,9 @@ DatasetAny
expand_alias_to_datasets
Metadata
dataset_to_test_method_call = Dataset()
dataset_to_test_method_call.iter_datasets()
dataset_to_test_method_call.iter_dataset_aliases()
alias_to_test_method_call = DatasetAlias()
alias_to_test_method_call.iter_datasets()
alias_to_test_method_call.iter_dataset_aliases()
any_to_test_method_call = DatasetAny()
any_to_test_method_call.iter_datasets()
any_to_test_method_call.iter_dataset_aliases()
# airflow.datasets.manager
DatasetManager, dataset_manager, resolve_dataset_manager
# airflow.hooks
BaseHook()
# airflow.lineage.hook
DatasetLineageInfo
@@ -273,3 +253,34 @@ has_access_dataset
# airflow.www.utils
get_sensitive_variables_fields, should_hide_value_for_key
from airflow.datasets.manager import DatasetManager
dm = DatasetManager()
dm.register_dataset_change()
dm.create_datasets()
dm.notify_dataset_created()
dm.notify_dataset_changed()
dm.notify_dataset_alias_created()
from airflow.lineage.hook import HookLineageCollector
hlc = HookLineageCollector()
hlc.create_dataset()
hlc.add_input_dataset()
hlc.add_output_dataset()
hlc.collected_datasets()
from airflow.providers.amazon.auth_manager.aws_auth_manager import AwsAuthManager
aam = AwsAuthManager()
aam.is_authorized_dataset()
from airflow.providers_manager import ProvidersManager
pm = ProvidersManager()
pm.initialize_providers_asset_uri_resources()
pm.dataset_factories

View File

@@ -5,39 +5,12 @@ from airflow.auth.managers.fab.api.auth.backend import (
)
from airflow.auth.managers.fab.fab_auth_manager import FabAuthManager
from airflow.auth.managers.fab.security_manager import override as fab_override
from airflow.config_templates.default_celery import DEFAULT_CELERY_CONFIG
from airflow.executors.celery_executor import app
from airflow.executors.kubernetes_executor_types import (
ALL_NAMESPACES,
POD_EXECUTOR_DONE_KEY,
)
from airflow.hooks.dbapi import ConnectorProtocol, DbApiHook
from airflow.hooks.hive_hooks import HIVE_QUEUE_PRIORITIES
from airflow.macros.hive import closest_ds_partition, max_partition
from airflow.www.security import FabAirflowSecurityManagerOverride
# apache-airflow-providers-fab
basic_auth, kerberos_auth
auth_current_user
backend_kerberos_auth
fab_override
FabAuthManager()
FabAirflowSecurityManagerOverride()
# apache-airflow-providers-celery
DEFAULT_CELERY_CONFIG
app
# apache-airflow-providers-common-sql
ConnectorProtocol()
DbApiHook()
# apache-airflow-providers-cncf-kubernetes
ALL_NAMESPACES
POD_EXECUTOR_DONE_KEY
# apache-airflow-providers-apache-hive
HIVE_QUEUE_PRIORITIES
closest_ds_partition()
max_partition()
FabAuthManager
FabAirflowSecurityManagerOverride

View File

@@ -108,18 +108,3 @@ app = None
@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Item:
return item
# Routes might be defined inside functions
def setup_app(app_arg: FastAPI, non_app: str) -> None:
# Error
@app_arg.get("/", response_model=str)
async def get_root() -> str:
return "Hello World!"
# Ok
@non_app.get("/", response_model=str)
async def get_root() -> str:
return "Hello World!"

View File

@@ -57,9 +57,6 @@ def get_users(
pass
@app.get("/items/{item_id}")
async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
pass
# Non fixable errors

View File

@@ -8,7 +8,6 @@ d = {"a": 1, "b": 2, "c": 3}
{k: v for k, v in y}
{k: v for k, v in d.items()}
[(k, v) for k, v in d.items()]
[(k, v) for [k, v] in d.items()]
{k: (a, b) for k, (a, b) in d.items()}
[i for i, in z]
@@ -23,7 +22,3 @@ d = {"a": 1, "b": 2, "c": 3}
# Regression test for: https://github.com/astral-sh/ruff/issues/7196
any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
zz = [[1], [2], [3]]
[(i,) for (i,) in zz] # != list(zz)
{(i,) for (i,) in zz} # != set(zz)

View File

@@ -14,7 +14,6 @@ JediOrder.YODA == age # SIM300
0 < (number - 100) # SIM300
B<A[0][0]or B
B or(B)<A[0][0]
{"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
# Errors in preview
['upper'] == UPPER_LIST
@@ -40,7 +39,4 @@ age == JediOrder.YODA
(number - 100) > 0
SECONDS_IN_DAY == 60 * 60 * 24 # Error in 0.1.8
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # Error in 0.1.8
# https://github.com/astral-sh/ruff/issues/14761
{"": print(1)} == print(2)
{0: 1, **print(2)} == print(4)
{"non-empty-dict": "is-ok"} == DummyHandler.CONFIG

View File

@@ -1,70 +1,15 @@
from pathlib import Path, PurePath
from pathlib import Path as pth
# match
_ = Path(".")
_ = pth(".")
_ = PurePath(".")
_ = Path("")
Path('', )
Path(
'',
)
Path( # Comment before argument
'',
)
Path(
'', # EOL comment
)
Path(
'' # Comment in the middle of implicitly concatenated string
".",
)
Path(
'' # Comment before comma
,
)
Path(
'',
) / "bare"
Path( # Comment before argument
'',
) / ("parenthesized")
Path(
'', # EOL comment
) / ( ("double parenthesized" ) )
( Path(
'' # Comment in the middle of implicitly concatenated string
".",
) )/ (("parenthesized path call")
# Comment between closing parentheses
)
Path(
'' # Comment before comma
,
) / "multiple" / (
"frag" # Comment
'ment'
)
# no match
_ = Path()
print(".")
Path("file.txt")
Path(".", "folder")
PurePath(".", "folder")
Path()

View File

@@ -156,24 +156,3 @@ from typing import Callable
class FilterDataclass:
# OK
filter: Callable[[str], bool] = lambda _: True
# Regression tests for:
# * https://github.com/astral-sh/ruff/issues/7720
x = lambda: """
a
b
"""
# * https://github.com/astral-sh/ruff/issues/10277
at_least_one_million = lambda _: _ >= 1_000_000
x = lambda: (
# comment
5 + 10
)
x = lambda: (
# comment
y := 10
)

View File

@@ -1,4 +1,4 @@
"""Test: blank-line-after-function special-cases around comment handling."""
"""Test: no-blank-line-after-function special-cases around comment handling."""
# OK
def outer():

View File

@@ -41,49 +41,3 @@ def first_word_lots_of_whitespace():
What do you think?
"""
def single_word_newline():
"""singleword
"""
def single_word_dot_newline():
"""singleword.
"""
def single_word_second_line():
"""
singleword
"""
def single_word_dot_second_line():
"""
singleword.
"""
def single_word_then_more_text():
"""singleword
This is more text.
"""
def single_word_dot_then_more_text():
"""singleword.
This is more text.
"""
def single_word_second_line_then_more_text():
"""
singleword
This is more text.
"""
def single_word_dot_second_line_then_more_text():
"""
singleword.
This is more text.
"""

View File

@@ -1,28 +0,0 @@
# Regression tests for:
# https://github.com/astral-sh/ruff/issues/10812
from typing import Annotated, Literal, TypedDict
# No errors
single: TypedDict[{"foo": int}]
# Error at `qux`
multiple: TypedDict[{
"bar": str,
"baz": list["qux"],
}]
# Error at `dolor`
nested: TypedDict[
"lorem": TypedDict[{
"ipsum": "dolor"
}],
"sit": Literal["amet"]
]
# Error at `adipiscing`, `eiusmod`, `tempor`
unpack: TypedDict[{
"consectetur": Annotated["adipiscing", "elit"]
**{"sed do": str, int: "eiusmod", **tempor}
}]

View File

@@ -64,22 +64,3 @@ def f(x: typing.Deque[str]) -> None:
def f(x: typing.DefaultDict[str, str]) -> None:
...
def f(x: typing.AbstractSet[str]) -> None:
...
def f(x: typing.Pattern[str]) -> None:
...
def f(x: typing.Sequence[str]) -> None:
...
from typing import Collection
def f(x: typing.Collection[str]) -> None:
...

View File

@@ -8,19 +8,3 @@ if typing.TYPE_CHECKING:
def f(x: typing.DefaultDict[str, str]) -> None:
...
from collections.abc import Set
from typing_extensions import Awaitable
def f(x: typing.AbstractSet[str]) -> None:
...
def f(x: Set) -> None:
...
def f(x: Awaitable) -> None:
...

View File

@@ -80,20 +80,11 @@ async def test() -> str:
return ",".join(vals)
# FastApi routes can be async without actually using await
from fastapi import FastAPI
app = FastAPI()
@app.post("/count")
async def fastapi_route():
async def fastapi_route(): # Ok: FastApi routes can be async without actually using await
return 1
def setup_app(app_arg: FastAPI, non_app: str) -> None:
@app_arg.get("/")
async def get_root() -> str:
return "Hello World!"

View File

@@ -1,9 +0,0 @@
from bleach import clean
from markupsafe import Markup
content = "<script>alert('Hello, world!')</script>"
Markup(clean(content))
# indirect assignments are currently not supported
cleaned = clean(content)
Markup(cleaned) # RUF035

View File

@@ -29,33 +29,6 @@ int(round(1, None))
int(round(1.))
int(round(1., None))
int(1)
int(v := 1)
int(~1)
int(-1)
int(+1)
int(1 + 1)
int(1 - 1)
int(1 * 1)
int(1 % 1)
int(1 ** 1)
int(1 << 1)
int(1 >> 1)
int(1 | 1)
int(1 ^ 1)
int(1 & 1)
int(1 // 1)
int(1 if ... else 2)
int(1 and 0)
int(0 or -1)
if int(1 + 2) * 3:
...
### Unsafe
@@ -95,62 +68,3 @@ int(round(inferred_int, 3.14))
int(round(0, 0), base)
int(round(0, 0, extra=keyword))
int(foo if ... else 4)
int(3.14)
int(2.8j)
async def f():
int(await f())
int(foo.bar)
int(bar([1][False]))
int(1 == 1)
int(1 != 1)
int(1 < 1)
int(1 <= 1)
int(1 > 1)
int(1 >= 1)
int(1 in 1)
int(1 not in 1)
int(1 is 1)
int(2 is not 3)
int(foo in 1)
int(foo not in 1)
int(foo is 1)
int(foo is not 1)
int(1 == 2 == 3)
int(foo == 1)
int(foo != 1)
int(foo < 1)
int(foo <= 1)
int(foo > 1)
int(foo >= 1)
int(v := {}[{}['']])
int(foo + 1)
int(foo - 1)
int(foo * 1)
int(foo @ 1)
int(foo / 1)
int(foo % 1)
int(foo ** 1)
int(foo << 1)
int(foo >> 1)
int(foo | 1)
int(foo ^ 1)
int(foo & 1)
int(foo // 1)
int(v := 3.7)
int(not 109)
int(1 / 1)
int(1 @ 1)
int(1. if ... else .2)

View File

@@ -77,7 +77,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.source_type.is_stub() && checker.enabled(Rule::UnquotedTypeAlias) {
if checker.enabled(Rule::UnquotedTypeAlias) {
if let Some(diagnostics) =
flake8_type_checking::rules::unquoted_type_alias(checker, binding)
{

View File

@@ -34,40 +34,40 @@ pub(crate) fn definitions(checker: &mut Checker) {
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
let enforce_dunder_method = checker.enabled(Rule::BadDunderMethodName);
let enforce_docstrings = checker.any_enabled(&[
Rule::MissingBlankLineAfterLastSection,
Rule::MissingBlankLineAfterSummary,
Rule::BlankLineAfterLastSection,
Rule::BlankLineAfterSummary,
Rule::BlankLineBeforeClass,
Rule::BlankLinesBetweenHeaderAndContent,
Rule::NonCapitalizedSectionName,
Rule::MissingDashedUnderlineAfterSection,
Rule::CapitalizeSectionName,
Rule::DashedUnderlineAfterSection,
Rule::DocstringStartsWithThis,
Rule::EmptyDocstring,
Rule::EmptyDocstringSection,
Rule::MissingTrailingPeriod,
Rule::MissingTerminalPunctuation,
Rule::EndsInPeriod,
Rule::EndsInPunctuation,
Rule::EscapeSequenceInDocstring,
Rule::FirstWordUncapitalized,
Rule::UnnecessaryMultilineDocstring,
Rule::DocstringTabIndentation,
Rule::FitsOnOneLine,
Rule::IndentWithSpaces,
Rule::MultiLineSummaryFirstLine,
Rule::MultiLineSummarySecondLine,
Rule::NewLineAfterLastParagraph,
Rule::MissingNewLineAfterSectionName,
Rule::BlankLineAfterFunction,
Rule::NewLineAfterSectionName,
Rule::NoBlankLineAfterFunction,
Rule::NoBlankLineAfterSection,
Rule::BlankLineBeforeFunction,
Rule::NoBlankLineBeforeFunction,
Rule::NoBlankLineBeforeSection,
Rule::SignatureInDocstring,
Rule::NoSignature,
Rule::NonImperativeMood,
Rule::IncorrectBlankLineAfterClass,
Rule::IncorrectBlankLineBeforeClass,
Rule::OneBlankLineAfterClass,
Rule::OneBlankLineBeforeClass,
Rule::OverIndentation,
Rule::OverloadWithDocstring,
Rule::MissingSectionNameColon,
Rule::OverindentedSection,
Rule::MissingSectionUnderlineAfterName,
Rule::MismatchedSectionUnderlineLength,
Rule::OverindentedSectionUnderline,
Rule::SectionNameEndsInColon,
Rule::SectionNotOverIndented,
Rule::SectionUnderlineAfterName,
Rule::SectionUnderlineMatchesSectionLength,
Rule::SectionUnderlineNotOverIndented,
Rule::SurroundingWhitespace,
Rule::TripleSingleQuotes,
Rule::UnderIndentation,
@@ -220,24 +220,27 @@ pub(crate) fn definitions(checker: &mut Checker) {
if !pydocstyle::rules::not_empty(checker, &docstring) {
continue;
}
if checker.enabled(Rule::UnnecessaryMultilineDocstring) {
if checker.enabled(Rule::FitsOnOneLine) {
pydocstyle::rules::one_liner(checker, &docstring);
}
if checker.any_enabled(&[Rule::BlankLineAfterFunction, Rule::BlankLineBeforeFunction]) {
if checker.any_enabled(&[
Rule::NoBlankLineAfterFunction,
Rule::NoBlankLineBeforeFunction,
]) {
pydocstyle::rules::blank_before_after_function(checker, &docstring);
}
if checker.any_enabled(&[
Rule::BlankLineBeforeClass,
Rule::IncorrectBlankLineAfterClass,
Rule::IncorrectBlankLineBeforeClass,
Rule::OneBlankLineAfterClass,
Rule::OneBlankLineBeforeClass,
]) {
pydocstyle::rules::blank_before_after_class(checker, &docstring);
}
if checker.enabled(Rule::MissingBlankLineAfterSummary) {
if checker.enabled(Rule::BlankLineAfterSummary) {
pydocstyle::rules::blank_after_summary(checker, &docstring);
}
if checker.any_enabled(&[
Rule::DocstringTabIndentation,
Rule::IndentWithSpaces,
Rule::OverIndentation,
Rule::UnderIndentation,
]) {
@@ -261,7 +264,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
if checker.enabled(Rule::EscapeSequenceInDocstring) {
pydocstyle::rules::backslashes(checker, &docstring);
}
if checker.enabled(Rule::MissingTrailingPeriod) {
if checker.enabled(Rule::EndsInPeriod) {
pydocstyle::rules::ends_with_period(checker, &docstring);
}
if checker.enabled(Rule::NonImperativeMood) {
@@ -271,7 +274,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
&checker.settings.pydocstyle,
);
}
if checker.enabled(Rule::SignatureInDocstring) {
if checker.enabled(Rule::NoSignature) {
pydocstyle::rules::no_signature(checker, &docstring);
}
if checker.enabled(Rule::FirstWordUncapitalized) {
@@ -280,7 +283,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
if checker.enabled(Rule::DocstringStartsWithThis) {
pydocstyle::rules::starts_with_this(checker, &docstring);
}
if checker.enabled(Rule::MissingTerminalPunctuation) {
if checker.enabled(Rule::EndsInPunctuation) {
pydocstyle::rules::ends_with_punctuation(checker, &docstring);
}
if checker.enabled(Rule::OverloadWithDocstring) {
@@ -288,20 +291,20 @@ pub(crate) fn definitions(checker: &mut Checker) {
}
let enforce_sections = checker.any_enabled(&[
Rule::MissingBlankLineAfterLastSection,
Rule::BlankLineAfterLastSection,
Rule::BlankLinesBetweenHeaderAndContent,
Rule::NonCapitalizedSectionName,
Rule::MissingDashedUnderlineAfterSection,
Rule::CapitalizeSectionName,
Rule::DashedUnderlineAfterSection,
Rule::EmptyDocstringSection,
Rule::MultiLineSummaryFirstLine,
Rule::MissingNewLineAfterSectionName,
Rule::NewLineAfterSectionName,
Rule::NoBlankLineAfterSection,
Rule::NoBlankLineBeforeSection,
Rule::MissingSectionNameColon,
Rule::OverindentedSection,
Rule::MissingSectionUnderlineAfterName,
Rule::MismatchedSectionUnderlineLength,
Rule::OverindentedSectionUnderline,
Rule::SectionNameEndsInColon,
Rule::SectionNotOverIndented,
Rule::SectionUnderlineAfterName,
Rule::SectionUnderlineMatchesSectionLength,
Rule::SectionUnderlineNotOverIndented,
Rule::UndocumentedParam,
]);
if enforce_sections || enforce_pydoclint {

View File

@@ -279,9 +279,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
}
if checker.enabled(Rule::Airflow3Removal) {
airflow::rules::removed_in_3(checker, expr);
}
if checker.enabled(Rule::MixedCaseVariableInGlobalScope) {
if matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
pep8_naming::rules::mixed_case_variable_in_global_scope(
@@ -986,7 +983,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_use_pathlib::rules::replaceable_by_pathlib(checker, call);
}
if checker.enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
flake8_use_pathlib::rules::path_constructor_current_directory(checker, expr, func);
}
if checker.enabled(Rule::OsSepSplit) {
flake8_use_pathlib::rules::os_sep_split(checker, call);

View File

@@ -1282,7 +1282,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
if let Some(arg) = args.next() {
self.visit_type_definition(arg);
if !self.source_type.is_stub() && self.enabled(Rule::RuntimeCastValue) {
if self.enabled(Rule::RuntimeCastValue) {
flake8_type_checking::rules::runtime_cast_value(self, arg);
}
}
@@ -1505,20 +1505,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
debug!("Found non-Expr::Tuple argument to PEP 593 Annotation.");
}
}
Some(typing::SubscriptKind::TypedDict) => {
if let Expr::Dict(ast::ExprDict { items, range: _ }) = slice.as_ref() {
for item in items {
if let Some(key) = &item.key {
self.visit_non_type_definition(key);
self.visit_type_definition(&item.value);
} else {
self.visit_non_type_definition(&item.value);
}
}
} else {
self.visit_non_type_definition(slice);
}
}
None => {
self.visit_expr(slice);
self.visit_expr_context(ctx);

View File

@@ -545,13 +545,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pydocstyle, "105") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedMagicMethod),
(Pydocstyle, "106") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicNestedClass),
(Pydocstyle, "107") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicInit),
(Pydocstyle, "200") => (RuleGroup::Stable, rules::pydocstyle::rules::UnnecessaryMultilineDocstring),
(Pydocstyle, "201") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineBeforeFunction),
(Pydocstyle, "202") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineAfterFunction),
(Pydocstyle, "203") => (RuleGroup::Stable, rules::pydocstyle::rules::IncorrectBlankLineBeforeClass),
(Pydocstyle, "204") => (RuleGroup::Stable, rules::pydocstyle::rules::IncorrectBlankLineAfterClass),
(Pydocstyle, "205") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingBlankLineAfterSummary),
(Pydocstyle, "206") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringTabIndentation),
(Pydocstyle, "200") => (RuleGroup::Stable, rules::pydocstyle::rules::FitsOnOneLine),
(Pydocstyle, "201") => (RuleGroup::Stable, rules::pydocstyle::rules::NoBlankLineBeforeFunction),
(Pydocstyle, "202") => (RuleGroup::Stable, rules::pydocstyle::rules::NoBlankLineAfterFunction),
(Pydocstyle, "203") => (RuleGroup::Stable, rules::pydocstyle::rules::OneBlankLineBeforeClass),
(Pydocstyle, "204") => (RuleGroup::Stable, rules::pydocstyle::rules::OneBlankLineAfterClass),
(Pydocstyle, "205") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineAfterSummary),
(Pydocstyle, "206") => (RuleGroup::Stable, rules::pydocstyle::rules::IndentWithSpaces),
(Pydocstyle, "207") => (RuleGroup::Stable, rules::pydocstyle::rules::UnderIndentation),
(Pydocstyle, "208") => (RuleGroup::Stable, rules::pydocstyle::rules::OverIndentation),
(Pydocstyle, "209") => (RuleGroup::Stable, rules::pydocstyle::rules::NewLineAfterLastParagraph),
@@ -559,27 +559,27 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pydocstyle, "211") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineBeforeClass),
(Pydocstyle, "212") => (RuleGroup::Stable, rules::pydocstyle::rules::MultiLineSummaryFirstLine),
(Pydocstyle, "213") => (RuleGroup::Stable, rules::pydocstyle::rules::MultiLineSummarySecondLine),
(Pydocstyle, "214") => (RuleGroup::Stable, rules::pydocstyle::rules::OverindentedSection),
(Pydocstyle, "215") => (RuleGroup::Stable, rules::pydocstyle::rules::OverindentedSectionUnderline),
(Pydocstyle, "214") => (RuleGroup::Stable, rules::pydocstyle::rules::SectionNotOverIndented),
(Pydocstyle, "215") => (RuleGroup::Stable, rules::pydocstyle::rules::SectionUnderlineNotOverIndented),
(Pydocstyle, "300") => (RuleGroup::Stable, rules::pydocstyle::rules::TripleSingleQuotes),
(Pydocstyle, "301") => (RuleGroup::Stable, rules::pydocstyle::rules::EscapeSequenceInDocstring),
(Pydocstyle, "400") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingTrailingPeriod),
(Pydocstyle, "400") => (RuleGroup::Stable, rules::pydocstyle::rules::EndsInPeriod),
(Pydocstyle, "401") => (RuleGroup::Stable, rules::pydocstyle::rules::NonImperativeMood),
(Pydocstyle, "402") => (RuleGroup::Stable, rules::pydocstyle::rules::SignatureInDocstring),
(Pydocstyle, "402") => (RuleGroup::Stable, rules::pydocstyle::rules::NoSignature),
(Pydocstyle, "403") => (RuleGroup::Stable, rules::pydocstyle::rules::FirstWordUncapitalized),
(Pydocstyle, "404") => (RuleGroup::Stable, rules::pydocstyle::rules::DocstringStartsWithThis),
(Pydocstyle, "405") => (RuleGroup::Stable, rules::pydocstyle::rules::NonCapitalizedSectionName),
(Pydocstyle, "406") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingNewLineAfterSectionName),
(Pydocstyle, "407") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingDashedUnderlineAfterSection),
(Pydocstyle, "408") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingSectionUnderlineAfterName),
(Pydocstyle, "409") => (RuleGroup::Stable, rules::pydocstyle::rules::MismatchedSectionUnderlineLength),
(Pydocstyle, "405") => (RuleGroup::Stable, rules::pydocstyle::rules::CapitalizeSectionName),
(Pydocstyle, "406") => (RuleGroup::Stable, rules::pydocstyle::rules::NewLineAfterSectionName),
(Pydocstyle, "407") => (RuleGroup::Stable, rules::pydocstyle::rules::DashedUnderlineAfterSection),
(Pydocstyle, "408") => (RuleGroup::Stable, rules::pydocstyle::rules::SectionUnderlineAfterName),
(Pydocstyle, "409") => (RuleGroup::Stable, rules::pydocstyle::rules::SectionUnderlineMatchesSectionLength),
(Pydocstyle, "410") => (RuleGroup::Stable, rules::pydocstyle::rules::NoBlankLineAfterSection),
(Pydocstyle, "411") => (RuleGroup::Stable, rules::pydocstyle::rules::NoBlankLineBeforeSection),
(Pydocstyle, "412") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLinesBetweenHeaderAndContent),
(Pydocstyle, "413") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingBlankLineAfterLastSection),
(Pydocstyle, "413") => (RuleGroup::Stable, rules::pydocstyle::rules::BlankLineAfterLastSection),
(Pydocstyle, "414") => (RuleGroup::Stable, rules::pydocstyle::rules::EmptyDocstringSection),
(Pydocstyle, "415") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingTerminalPunctuation),
(Pydocstyle, "416") => (RuleGroup::Stable, rules::pydocstyle::rules::MissingSectionNameColon),
(Pydocstyle, "415") => (RuleGroup::Stable, rules::pydocstyle::rules::EndsInPunctuation),
(Pydocstyle, "416") => (RuleGroup::Stable, rules::pydocstyle::rules::SectionNameEndsInColon),
(Pydocstyle, "417") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedParam),
(Pydocstyle, "418") => (RuleGroup::Stable, rules::pydocstyle::rules::OverloadWithDocstring),
(Pydocstyle, "419") => (RuleGroup::Stable, rules::pydocstyle::rules::EmptyDocstring),

View File

@@ -142,11 +142,9 @@ fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Orderi
.then_with(|| fix1.min_start().cmp(&fix2.min_start()))
// Break ties in the event of overlapping rules, for some specific combinations.
.then_with(|| match (&rule1, &rule2) {
// Apply `MissingTrailingPeriod` fixes before `NewLineAfterLastParagraph` fixes.
(Rule::MissingTrailingPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less,
(Rule::NewLineAfterLastParagraph, Rule::MissingTrailingPeriod) => {
std::cmp::Ordering::Greater
}
// Apply `EndsInPeriod` fixes before `NewLineAfterLastParagraph` fixes.
(Rule::EndsInPeriod, Rule::NewLineAfterLastParagraph) => std::cmp::Ordering::Less,
(Rule::NewLineAfterLastParagraph, Rule::EndsInPeriod) => std::cmp::Ordering::Greater,
// Apply `IfElseBlockInsteadOfDictGet` fixes before `IfElseBlockInsteadOfIfExp` fixes.
(Rule::IfElseBlockInsteadOfDictGet, Rule::IfElseBlockInsteadOfIfExp) => {
std::cmp::Ordering::Less

View File

@@ -357,9 +357,9 @@ impl Rule {
pub const INCOMPATIBLE_CODES: &[(Rule, Rule, &str); 2] = &[
(
Rule::BlankLineBeforeClass,
Rule::IncorrectBlankLineBeforeClass,
"`incorrect-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are \
incompatible. Ignoring `incorrect-blank-line-before-class`.",
Rule::OneBlankLineBeforeClass,
"`one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are \
incompatible. Ignoring `one-blank-line-before-class`.",
),
(
Rule::MultiLineSummaryFirstLine,

View File

@@ -16,8 +16,6 @@ mod tests {
#[test_case(Rule::AirflowDagNoScheduleArgument, Path::new("AIR301.py"))]
#[test_case(Rule::Airflow3Removal, Path::new("AIR302_args.py"))]
#[test_case(Rule::Airflow3Removal, Path::new("AIR302_names.py"))]
#[test_case(Rule::Airflow3Removal, Path::new("AIR302_class_attribute.py"))]
#[test_case(Rule::Airflow3Removal, Path::new("AIR302_airflow_plugin.py"))]
#[test_case(Rule::Airflow3MovedToProvider, Path::new("AIR303.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View File

@@ -111,7 +111,7 @@ fn moved_to_provider(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
version: "1.0.0"
},
)),
["airflow", "auth", "managers", "fab", "fab_auth_manager", "FabAuthManager"] => Some((
["airflow","auth","managers","fab","fab_auth_manager", "FabAuthManager"] => Some((
qualname.to_string(),
Replacement::ProviderName{
name: "airflow.providers.fab.auth_manager.security_manager.FabAuthManager",
@@ -128,7 +128,7 @@ fn moved_to_provider(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
version: "1.0.0"
},
)),
["airflow", "api", "auth", "backend", "kerberos_auth", ..] => Some((
["airflow", "api","auth","backend","kerberos_auth", ..] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path:"airflow.api.auth.backend.kerberos_auth",
@@ -146,100 +146,16 @@ fn moved_to_provider(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
version: "1.0.0"
},
)),
["airflow", "auth", "managers", "fab", "security_manager", "override", ..] => Some((
["airflow","auth","managers","fab","security_manager","override", ..] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.auth.managers.fab.security_managr.override",
original_path: "airflow.auth.managers.fab.security_manager.override",
new_path: "airflow.providers.fab.auth_manager.security_manager.override",
provider: "fab",
version: "1.0.0"
},
)),
// apache-airflow-providers-celery
["airflow", "config_templates", "default_celery", "DEFAULT_CELERY_CONFIG"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG",
new_path: "airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG",
provider: "celery",
version: "3.3.0"
},
)),
["airflow", "executors", "celery_executor", "app"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.executors.celery_executor.app",
new_path: "airflow.providers.celery.executors.celery_executor_utils.app",
provider: "celery",
version: "3.3.0"
},
)),
// apache-airflow-providers-common-sql
["airflow", "hooks", "dbapi", "ConnectorProtocol"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.hooks.dbapi.ConnectorProtocol",
new_path: "airflow.providers.common.sql.hooks.sql.ConnectorProtocol",
provider: "Common SQL",
version: "1.0.0"
},
)),
["airflow", "hooks", "dbapi", "DbApiHook"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.hooks.dbapi.DbApiHook",
new_path: "airflow.providers.common.sql.hooks.sql.DbApiHook",
provider: "Common SQL",
version: "1.0.0"
},
)),
// apache-airflow-providers-cncf-kubernetes
["airflow", "executors", "kubernetes_executor_types", "ALL_NAMESPACES"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.executors.kubernetes_executor_types.ALL_NAMESPACES",
new_path: "airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.ALL_NAMESPACES",
provider: "Kubernetes",
version: "7.4.0"
},
)),
["airflow", "executors", "kubernetes_executor_types", "POD_EXECUTOR_DONE_KEY"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY",
new_path: "airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY",
provider: "Kubernetes",
version: "7.4.0"
},
)),
// apache-airflow-providers-apache-hive
["airflow", "hooks", "hive_hooks", "HIVE_QUEUE_PRIORITIES"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES",
new_path: "airflow.providers.apache.hive.hooks.hive.HIVE_QUEUE_PRIORITIES",
provider: "Apache Hive",
version: "1.0.0"
},
)),
["airflow", "macros", "hive", "closest_ds_partition"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.macros.hive.closest_ds_partition",
new_path: "airflow.providers.apache.hive.macros.hive.closest_ds_partition",
provider: "Apache Hive",
version: "5.1.0"
},
)),
["airflow", "macros", "hive", "max_partition"] => Some((
qualname.to_string(),
Replacement::ImportPathMoved{
original_path: "airflow.macros.hive.max_partition",
new_path: "airflow.providers.apache.hive.macros.hive.max_partition",
provider: "Apache Hive",
version: "5.1.0"
},
)),
_ => None,
});
if let Some((deprecated, replacement)) = result {

View File

@@ -1,12 +1,8 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::{
name::QualifiedName, Arguments, Expr, ExprAttribute, ExprCall, ExprContext, ExprName,
StmtClassDef,
};
use ruff_python_ast::{name::QualifiedName, Arguments, Expr, ExprAttribute, ExprCall};
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::Modules;
use ruff_python_semantic::ScopeKind;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -127,106 +123,38 @@ fn removed_argument(checker: &mut Checker, qualname: &QualifiedName, arguments:
None::<&str>,
));
}
_ => {
if is_airflow_auth_manager(qualname.segments()) {
if !arguments.is_empty() {
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
// deprecated: (*arguments).to_string(),
deprecated: "appbuilder".to_string(),
replacement: Replacement::Message(
"The constructor takes no parameter now.",
),
},
arguments.range(),
));
}
} else if is_airflow_task_handler(qualname.segments()) {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"filename_template",
None::<&str>,
));
} else if is_airflow_operator(qualname.segments()) {
checker
.diagnostics
.extend(diagnostic_for_argument(arguments, "sla", None::<&str>));
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"task_concurrency",
Some("max_active_tis_per_dag"),
));
match qualname.segments() {
["airflow", .., "operators", "trigger_dagrun", "TriggerDagRunOperator"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"execution_date",
Some("logical_date"),
));
}
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "DayOfWeekSensor"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "BranchDayOfWeekOperator"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
_ => {}
}
}
["airflow", .., "operators", "trigger_dagrun", "TriggerDagRunOperator"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"execution_date",
Some("logical_date"),
));
}
["airflow", .., "operators", "datetime", "BranchDateTimeOperator"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "DayOfWeekSensor"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
["airflow", .., "operators", "weekday", "BranchDayOfWeekOperator"] => {
checker.diagnostics.extend(diagnostic_for_argument(
arguments,
"use_task_execution_day",
Some("use_task_logical_date"),
));
}
_ => {}
};
}
fn removed_class_attribute(checker: &mut Checker, expr: &Expr) {
let Expr::Attribute(ExprAttribute { attr, value, .. }) = expr else {
return;
};
let Some(qualname) = typing::resolve_assignment(value, checker.semantic()) else {
return;
};
let replacement = match *qualname.segments() {
["airflow", "providers_manager", "ProvidersManager"] => match attr.as_str() {
"dataset_factories" => Some(Replacement::Name("asset_factories")),
"dataset_uri_handlers" => Some(Replacement::Name("asset_uri_handlers")),
"dataset_to_openlineage_converters" => {
Some(Replacement::Name("asset_to_openlineage_converters"))
}
&_ => None,
},
["airflow", "lineage", "hook"] => match attr.as_str() {
"dataset" => Some(Replacement::Name("asset")),
&_ => None,
},
_ => None,
};
if let Some(replacement) = replacement {
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: attr.to_string(),
replacement,
},
attr.range(),
));
}
}
fn removed_method(checker: &mut Checker, expr: &Expr) {
let Expr::Call(ExprCall { func, .. }) = expr else {
return;
@@ -268,27 +196,7 @@ fn removed_method(checker: &mut Checker, expr: &Expr) {
)),
&_ => None,
},
["airflow", "datasets", ..] | ["airflow", "Dataset"] => match attr.as_str() {
"iter_datasets" => Some(Replacement::Name("iter_assets")),
"iter_dataset_aliases" => Some(Replacement::Name("iter_asset_aliases")),
&_ => None,
},
_ => {
if is_airflow_secret_backend(qualname.segments()) {
match attr.as_str() {
"get_conn_uri" => Some(Replacement::Name("get_conn_value")),
"get_connections" => Some(Replacement::Name("get_connection")),
&_ => None,
}
} else if is_airflow_hook(qualname.segments()) {
match attr.as_str() {
"get_connections" => Some(Replacement::Name("get_connection")),
&_ => None,
}
} else {
None
}
}
_ => None,
};
if let Some(replacement) = replacement {
checker.diagnostics.push(Diagnostic::new(
@@ -482,11 +390,6 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
qualname.to_string(),
Replacement::Name("airflow.lineage.hook.AssetLineageInfo"),
)),
// airflow.hooks
["airflow", "hooks", "base_hook", "BaseHook"] => Some((
qualname.to_string(),
Replacement::Name("airflow.hooks.base.BaseHook"),
)),
// airflow.operators
["airflow", "operators", "subdag", ..] => {
Some((
@@ -743,37 +646,6 @@ fn removed_name(checker: &mut Checker, expr: &Expr, ranged: impl Ranged) {
}
}
fn removed_airflow_plugin_extension(
checker: &mut Checker,
expr: &Expr,
name: &str,
class_def: &StmtClassDef,
) {
if matches!(name, "executors" | "operators" | "sensors" | "hooks") {
if class_def.bases().iter().any(|expr| {
checker
.semantic()
.resolve_qualified_name(expr)
.is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["airflow", "plugins_manager", "AirflowPlugin"]
)
})
}) {
checker.diagnostics.push(Diagnostic::new(
Airflow3Removal {
deprecated: name.to_string(),
replacement: Replacement::Message(
"This extension should just be imported as a regular python module.",
),
},
expr.range(),
));
}
}
}
/// AIR302
pub(crate) fn removed_in_3(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().seen_module(Modules::AIRFLOW) {
@@ -790,104 +662,8 @@ pub(crate) fn removed_in_3(checker: &mut Checker, expr: &Expr) {
removed_method(checker, expr);
}
Expr::Attribute(ExprAttribute { attr: ranged, .. }) => {
removed_name(checker, expr, ranged);
removed_class_attribute(checker, expr);
}
ranged @ Expr::Name(ExprName { id, ctx, .. }) => {
removed_name(checker, expr, ranged);
if ctx == &ExprContext::Store {
if let ScopeKind::Class(class_def) = &checker.semantic().current_scope().kind {
removed_airflow_plugin_extension(checker, expr, id, class_def);
}
}
}
Expr::Attribute(ExprAttribute { attr: ranged, .. }) => removed_name(checker, expr, ranged),
ranged @ Expr::Name(_) => removed_name(checker, expr, ranged),
_ => {}
}
}
/// Check whether the symbol is coming from the `secrets` builtin or provider module which ends
/// with `Backend`.
fn is_airflow_secret_backend(segments: &[&str]) -> bool {
is_airflow_builtin_or_provider(segments, "secrets", "Backend")
}
/// Check whether the symbol is coming from the `hooks` builtin or provider module which ends
/// with `Hook`.
fn is_airflow_hook(segments: &[&str]) -> bool {
is_airflow_builtin_or_provider(segments, "hooks", "Hook")
}
/// Check whether the symbol is coming from the `operators` builtin or provider module which ends
/// with `Operator`.
fn is_airflow_operator(segments: &[&str]) -> bool {
is_airflow_builtin_or_provider(segments, "operators", "Operator")
}
/// Check whether the symbol is coming from the `log` builtin or provider module which ends
/// with `TaskHandler`.
fn is_airflow_task_handler(segments: &[&str]) -> bool {
is_airflow_builtin_or_provider(segments, "log", "TaskHandler")
}
/// Check whether the segments corresponding to the fully qualified name points to a symbol that's
/// either a builtin or coming from one of the providers in Airflow.
///
/// The pattern it looks for are:
/// - `airflow.providers.**.<module>.**.*<symbol_suffix>` for providers
/// - `airflow.<module>.**.*<symbol_suffix>` for builtins
///
/// where `**` is one or more segments separated by a dot, and `*` is one or more characters.
///
/// Examples for the above patterns:
/// - `airflow.providers.google.cloud.secrets.secret_manager.CloudSecretManagerBackend` (provider)
/// - `airflow.secrets.base_secrets.BaseSecretsBackend` (builtin)
fn is_airflow_builtin_or_provider(segments: &[&str], module: &str, symbol_suffix: &str) -> bool {
match segments {
["airflow", "providers", rest @ ..] => {
if let (Some(pos), Some(last_element)) =
(rest.iter().position(|&s| s == module), rest.last())
{
// Check that the module is not the last element i.e., there's a symbol that's
// being used from the `module` that ends with `symbol_suffix`.
pos + 1 < rest.len() && last_element.ends_with(symbol_suffix)
} else {
false
}
}
["airflow", first, rest @ ..] => {
if let Some(last) = rest.last() {
first == module && last.ends_with(symbol_suffix)
} else {
false
}
}
_ => false,
}
}
fn is_airflow_auth_manager(segments: &[&str]) -> bool {
match segments {
["airflow", "auth", "manager", rest @ ..] => {
if let Some(last_element) = rest.last() {
last_element.ends_with("AuthManager")
} else {
false
}
}
["airflow", "providers", rest @ ..] => {
if let (Some(pos), Some(last_element)) =
(rest.iter().position(|&s| s == "auth_manager"), rest.last())
{
pos + 1 < rest.len() && last_element.ends_with("AuthManager")
} else {
false
}
}
_ => false,
}
}

View File

@@ -1,43 +0,0 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
snapshot_kind: text
---
AIR302_airflow_plugin.py:7:5: AIR302 `operators` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
5 | name = "test_plugin"
6 | # --- Invalid extensions start
7 | operators = [PluginOperator]
| ^^^^^^^^^ AIR302
8 | sensors = [PluginSensorOperator]
9 | hooks = [PluginHook]
|
AIR302_airflow_plugin.py:8:5: AIR302 `sensors` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
6 | # --- Invalid extensions start
7 | operators = [PluginOperator]
8 | sensors = [PluginSensorOperator]
| ^^^^^^^ AIR302
9 | hooks = [PluginHook]
10 | executors = [PluginExecutor]
|
AIR302_airflow_plugin.py:9:5: AIR302 `hooks` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
7 | operators = [PluginOperator]
8 | sensors = [PluginSensorOperator]
9 | hooks = [PluginHook]
| ^^^^^ AIR302
10 | executors = [PluginExecutor]
11 | # --- Invalid extensions end
|
AIR302_airflow_plugin.py:10:5: AIR302 `executors` is removed in Airflow 3.0; This extension should just be imported as a regular python module.
|
8 | sensors = [PluginSensorOperator]
9 | hooks = [PluginHook]
10 | executors = [PluginExecutor]
| ^^^^^^^^^ AIR302
11 | # --- Invalid extensions end
12 | macros = [plugin_macro]
|

View File

@@ -2,251 +2,175 @@
source: crates/ruff_linter/src/rules/airflow/mod.rs
snapshot_kind: text
---
AIR302_args.py:18:39: AIR302 [*] `schedule_interval` is removed in Airflow 3.0
AIR302_args.py:15:39: AIR302 [*] `schedule_interval` is removed in Airflow 3.0
|
16 | DAG(dag_id="class_schedule", schedule="@hourly")
17 |
18 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
13 | DAG(dag_id="class_schedule", schedule="@hourly")
14 |
15 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
| ^^^^^^^^^^^^^^^^^ AIR302
19 |
20 | DAG(dag_id="class_timetable", timetable=NullTimetable())
16 |
17 | DAG(dag_id="class_timetable", timetable=NullTimetable())
|
= help: Use `schedule` instead
Safe fix
15 15 |
16 16 | DAG(dag_id="class_schedule", schedule="@hourly")
17 17 |
18 |-DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
18 |+DAG(dag_id="class_schedule_interval", schedule="@hourly")
19 19 |
20 20 | DAG(dag_id="class_timetable", timetable=NullTimetable())
21 21 |
12 12 |
13 13 | DAG(dag_id="class_schedule", schedule="@hourly")
14 14 |
15 |-DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
15 |+DAG(dag_id="class_schedule_interval", schedule="@hourly")
16 16 |
17 17 | DAG(dag_id="class_timetable", timetable=NullTimetable())
18 18 |
AIR302_args.py:20:31: AIR302 [*] `timetable` is removed in Airflow 3.0
AIR302_args.py:17:31: AIR302 [*] `timetable` is removed in Airflow 3.0
|
18 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
19 |
20 | DAG(dag_id="class_timetable", timetable=NullTimetable())
15 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
16 |
17 | DAG(dag_id="class_timetable", timetable=NullTimetable())
| ^^^^^^^^^ AIR302
|
= help: Use `schedule` instead
Safe fix
17 17 |
18 18 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
14 14 |
15 15 | DAG(dag_id="class_schedule_interval", schedule_interval="@hourly")
16 16 |
17 |-DAG(dag_id="class_timetable", timetable=NullTimetable())
17 |+DAG(dag_id="class_timetable", schedule=NullTimetable())
18 18 |
19 19 |
20 |-DAG(dag_id="class_timetable", timetable=NullTimetable())
20 |+DAG(dag_id="class_timetable", schedule=NullTimetable())
21 21 |
22 22 |
23 23 | def sla_callback(*arg, **kwargs):
20 20 | def sla_callback(*arg, **kwargs):
AIR302_args.py:27:34: AIR302 `sla_miss_callback` is removed in Airflow 3.0
AIR302_args.py:24:34: AIR302 `sla_miss_callback` is removed in Airflow 3.0
|
27 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
24 | DAG(dag_id="class_sla_callback", sla_miss_callback=sla_callback)
| ^^^^^^^^^^^^^^^^^ AIR302
|
AIR302_args.py:35:6: AIR302 [*] `schedule_interval` is removed in Airflow 3.0
AIR302_args.py:32:6: AIR302 [*] `schedule_interval` is removed in Airflow 3.0
|
35 | @dag(schedule_interval="0 * * * *")
32 | @dag(schedule_interval="0 * * * *")
| ^^^^^^^^^^^^^^^^^ AIR302
36 | def decorator_schedule_interval():
37 | pass
33 | def decorator_schedule_interval():
34 | pass
|
= help: Use `schedule` instead
Safe fix
32 32 | pass
33 33 |
34 34 |
35 |-@dag(schedule_interval="0 * * * *")
35 |+@dag(schedule="0 * * * *")
36 36 | def decorator_schedule_interval():
37 37 | pass
38 38 |
29 29 | pass
30 30 |
31 31 |
32 |-@dag(schedule_interval="0 * * * *")
32 |+@dag(schedule="0 * * * *")
33 33 | def decorator_schedule_interval():
34 34 | pass
35 35 |
AIR302_args.py:40:6: AIR302 [*] `timetable` is removed in Airflow 3.0
AIR302_args.py:37:6: AIR302 [*] `timetable` is removed in Airflow 3.0
|
40 | @dag(timetable=NullTimetable())
37 | @dag(timetable=NullTimetable())
| ^^^^^^^^^ AIR302
41 | def decorator_timetable():
42 | pass
38 | def decorator_timetable():
39 | pass
|
= help: Use `schedule` instead
Safe fix
37 37 | pass
38 38 |
39 39 |
40 |-@dag(timetable=NullTimetable())
40 |+@dag(schedule=NullTimetable())
41 41 | def decorator_timetable():
42 42 | pass
43 43 |
34 34 | pass
35 35 |
36 36 |
37 |-@dag(timetable=NullTimetable())
37 |+@dag(schedule=NullTimetable())
38 38 | def decorator_timetable():
39 39 | pass
40 40 |
AIR302_args.py:45:6: AIR302 `sla_miss_callback` is removed in Airflow 3.0
AIR302_args.py:42:6: AIR302 `sla_miss_callback` is removed in Airflow 3.0
|
45 | @dag(sla_miss_callback=sla_callback)
42 | @dag(sla_miss_callback=sla_callback)
| ^^^^^^^^^^^^^^^^^ AIR302
46 | def decorator_sla_callback():
47 | pass
43 | def decorator_sla_callback():
44 | pass
|
AIR302_args.py:50:39: AIR302 [*] `execution_date` is removed in Airflow 3.0
|
48 | def decorator_deprecated_operator_args():
49 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
50 | task_id="trigger_dagrun_op1", execution_date="2024-12-04"
| ^^^^^^^^^^^^^^ AIR302
51 | )
52 | trigger_dagrun_op2 = TriggerDagRunOperator(
|
= help: Use `logical_date` instead
Safe fix
47 47 | @dag()
48 48 | def decorator_deprecated_operator_args():
49 49 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
50 |- task_id="trigger_dagrun_op1", execution_date="2024-12-04"
50 |+ task_id="trigger_dagrun_op1", logical_date="2024-12-04"
51 51 | )
52 52 | trigger_dagrun_op2 = TriggerDagRunOperator(
53 53 | task_id="trigger_dagrun_op2", execution_date="2024-12-04"
AIR302_args.py:53:39: AIR302 [*] `execution_date` is removed in Airflow 3.0
|
51 | def decorator_deprecated_operator_args():
52 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
53 | task_id="trigger_dagrun_op1", execution_date="2024-12-04"
51 | )
52 | trigger_dagrun_op2 = TriggerDagRunOperator(
53 | task_id="trigger_dagrun_op2", execution_date="2024-12-04"
| ^^^^^^^^^^^^^^ AIR302
54 | )
55 | trigger_dagrun_op2 = TriggerDagRunOperator(
|
= help: Use `logical_date` instead
Safe fix
50 50 | @dag()
51 51 | def decorator_deprecated_operator_args():
52 52 | trigger_dagrun_op = trigger_dagrun.TriggerDagRunOperator(
53 |- task_id="trigger_dagrun_op1", execution_date="2024-12-04"
53 |+ task_id="trigger_dagrun_op1", logical_date="2024-12-04"
50 50 | task_id="trigger_dagrun_op1", execution_date="2024-12-04"
51 51 | )
52 52 | trigger_dagrun_op2 = TriggerDagRunOperator(
53 |- task_id="trigger_dagrun_op2", execution_date="2024-12-04"
53 |+ task_id="trigger_dagrun_op2", logical_date="2024-12-04"
54 54 | )
55 55 | trigger_dagrun_op2 = TriggerDagRunOperator(
56 56 | task_id="trigger_dagrun_op2", execution_date="2024-12-04"
55 55 |
56 56 | branch_dt_op = datetime.BranchDateTimeOperator(
AIR302_args.py:56:39: AIR302 [*] `execution_date` is removed in Airflow 3.0
AIR302_args.py:57:33: AIR302 [*] `use_task_execution_day` is removed in Airflow 3.0
|
54 | )
55 | trigger_dagrun_op2 = TriggerDagRunOperator(
56 | task_id="trigger_dagrun_op2", execution_date="2024-12-04"
| ^^^^^^^^^^^^^^ AIR302
57 | )
|
= help: Use `logical_date` instead
Safe fix
53 53 | task_id="trigger_dagrun_op1", execution_date="2024-12-04"
54 54 | )
55 55 | trigger_dagrun_op2 = TriggerDagRunOperator(
56 |- task_id="trigger_dagrun_op2", execution_date="2024-12-04"
56 |+ task_id="trigger_dagrun_op2", logical_date="2024-12-04"
57 57 | )
58 58 |
59 59 | branch_dt_op = datetime.BranchDateTimeOperator(
AIR302_args.py:60:33: AIR302 [*] `use_task_execution_day` is removed in Airflow 3.0
|
59 | branch_dt_op = datetime.BranchDateTimeOperator(
60 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
56 | branch_dt_op = datetime.BranchDateTimeOperator(
57 | task_id="branch_dt_op", use_task_execution_day=True
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
61 | )
62 | branch_dt_op2 = BranchDateTimeOperator(
58 | )
59 | branch_dt_op2 = BranchDateTimeOperator(
|
= help: Use `use_task_logical_date` instead
Safe fix
57 57 | )
58 58 |
59 59 | branch_dt_op = datetime.BranchDateTimeOperator(
60 |- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
60 |+ task_id="branch_dt_op", use_task_logical_date=True, task_concurrency=5
61 61 | )
62 62 | branch_dt_op2 = BranchDateTimeOperator(
63 63 | task_id="branch_dt_op2",
54 54 | )
55 55 |
56 56 | branch_dt_op = datetime.BranchDateTimeOperator(
57 |- task_id="branch_dt_op", use_task_execution_day=True
57 |+ task_id="branch_dt_op", use_task_logical_date=True
58 58 | )
59 59 | branch_dt_op2 = BranchDateTimeOperator(
60 60 | task_id="branch_dt_op2", use_task_execution_day=True
AIR302_args.py:60:62: AIR302 [*] `task_concurrency` is removed in Airflow 3.0
AIR302_args.py:60:34: AIR302 [*] `use_task_execution_day` is removed in Airflow 3.0
|
59 | branch_dt_op = datetime.BranchDateTimeOperator(
60 | task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
| ^^^^^^^^^^^^^^^^ AIR302
58 | )
59 | branch_dt_op2 = BranchDateTimeOperator(
60 | task_id="branch_dt_op2", use_task_execution_day=True
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
61 | )
62 | branch_dt_op2 = BranchDateTimeOperator(
|
= help: Use `max_active_tis_per_dag` instead
Safe fix
57 57 | )
58 58 |
59 59 | branch_dt_op = datetime.BranchDateTimeOperator(
60 |- task_id="branch_dt_op", use_task_execution_day=True, task_concurrency=5
60 |+ task_id="branch_dt_op", use_task_execution_day=True, max_active_tis_per_dag=5
61 61 | )
62 62 | branch_dt_op2 = BranchDateTimeOperator(
63 63 | task_id="branch_dt_op2",
AIR302_args.py:64:9: AIR302 [*] `use_task_execution_day` is removed in Airflow 3.0
|
62 | branch_dt_op2 = BranchDateTimeOperator(
63 | task_id="branch_dt_op2",
64 | use_task_execution_day=True,
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
65 | sla=timedelta(seconds=10),
66 | )
|
= help: Use `use_task_logical_date` instead
Safe fix
57 57 | task_id="branch_dt_op", use_task_execution_day=True
58 58 | )
59 59 | branch_dt_op2 = BranchDateTimeOperator(
60 |- task_id="branch_dt_op2", use_task_execution_day=True
60 |+ task_id="branch_dt_op2", use_task_logical_date=True
61 61 | )
62 62 | branch_dt_op2 = BranchDateTimeOperator(
63 63 | task_id="branch_dt_op2",
64 |- use_task_execution_day=True,
64 |+ use_task_logical_date=True,
65 65 | sla=timedelta(seconds=10),
66 66 | )
67 67 |
AIR302_args.py:65:9: AIR302 `sla` is removed in Airflow 3.0
|
63 | task_id="branch_dt_op2",
64 | use_task_execution_day=True,
65 | sla=timedelta(seconds=10),
| ^^^ AIR302
66 | )
|
AIR302_args.py:87:15: AIR302 `filename_template` is removed in Airflow 3.0
|
86 | # deprecated filename_template arugment in FileTaskHandler
87 | S3TaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^ AIR302
88 | HdfsTaskHandler(filename_template="/tmp/test")
89 | ElasticsearchTaskHandler(filename_template="/tmp/test")
|
AIR302_args.py:88:17: AIR302 `filename_template` is removed in Airflow 3.0
|
86 | # deprecated filename_template arugment in FileTaskHandler
87 | S3TaskHandler(filename_template="/tmp/test")
88 | HdfsTaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^ AIR302
89 | ElasticsearchTaskHandler(filename_template="/tmp/test")
90 | GCSTaskHandler(filename_template="/tmp/test")
|
AIR302_args.py:89:26: AIR302 `filename_template` is removed in Airflow 3.0
|
87 | S3TaskHandler(filename_template="/tmp/test")
88 | HdfsTaskHandler(filename_template="/tmp/test")
89 | ElasticsearchTaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^ AIR302
90 | GCSTaskHandler(filename_template="/tmp/test")
|
AIR302_args.py:90:16: AIR302 `filename_template` is removed in Airflow 3.0
|
88 | HdfsTaskHandler(filename_template="/tmp/test")
89 | ElasticsearchTaskHandler(filename_template="/tmp/test")
90 | GCSTaskHandler(filename_template="/tmp/test")
| ^^^^^^^^^^^^^^^^^ AIR302
91 |
92 | FabAuthManager(None)
|
AIR302_args.py:92:15: AIR302 `appbuilder` is removed in Airflow 3.0; The constructor takes no parameter now.
|
90 | GCSTaskHandler(filename_template="/tmp/test")
91 |
92 | FabAuthManager(None)
| ^^^^^^ AIR302
|
62 62 |
63 63 | dof_task_sensor = weekday.DayOfWeekSensor(

View File

@@ -1,220 +0,0 @@
---
source: crates/ruff_linter/src/rules/airflow/mod.rs
snapshot_kind: text
---
AIR302_class_attribute.py:13:4: AIR302 `register_dataset_change` is removed in Airflow 3.0
|
12 | dm = DatasetManager()
13 | dm.register_dataset_change()
| ^^^^^^^^^^^^^^^^^^^^^^^ AIR302
14 | dm.create_datasets()
15 | dm.notify_dataset_created()
|
= help: Use `register_asset_change` instead
AIR302_class_attribute.py:14:4: AIR302 `create_datasets` is removed in Airflow 3.0
|
12 | dm = DatasetManager()
13 | dm.register_dataset_change()
14 | dm.create_datasets()
| ^^^^^^^^^^^^^^^ AIR302
15 | dm.notify_dataset_created()
16 | dm.notify_dataset_changed()
|
= help: Use `create_assets` instead
AIR302_class_attribute.py:15:4: AIR302 `notify_dataset_created` is removed in Airflow 3.0
|
13 | dm.register_dataset_change()
14 | dm.create_datasets()
15 | dm.notify_dataset_created()
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
16 | dm.notify_dataset_changed()
17 | dm.notify_dataset_alias_created()
|
= help: Use `notify_asset_created` instead
AIR302_class_attribute.py:16:4: AIR302 `notify_dataset_changed` is removed in Airflow 3.0
|
14 | dm.create_datasets()
15 | dm.notify_dataset_created()
16 | dm.notify_dataset_changed()
| ^^^^^^^^^^^^^^^^^^^^^^ AIR302
17 | dm.notify_dataset_alias_created()
|
= help: Use `notify_asset_changed` instead
AIR302_class_attribute.py:17:4: AIR302 `notify_dataset_alias_created` is removed in Airflow 3.0
|
15 | dm.notify_dataset_created()
16 | dm.notify_dataset_changed()
17 | dm.notify_dataset_alias_created()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
18 |
19 | hlc = HookLineageCollector()
|
= help: Use `notify_asset_alias_created` instead
AIR302_class_attribute.py:20:5: AIR302 `create_dataset` is removed in Airflow 3.0
|
19 | hlc = HookLineageCollector()
20 | hlc.create_dataset()
| ^^^^^^^^^^^^^^ AIR302
21 | hlc.add_input_dataset()
22 | hlc.add_output_dataset()
|
= help: Use `create_asset` instead
AIR302_class_attribute.py:21:5: AIR302 `add_input_dataset` is removed in Airflow 3.0
|
19 | hlc = HookLineageCollector()
20 | hlc.create_dataset()
21 | hlc.add_input_dataset()
| ^^^^^^^^^^^^^^^^^ AIR302
22 | hlc.add_output_dataset()
23 | hlc.collected_datasets()
|
= help: Use `add_input_asset` instead
AIR302_class_attribute.py:22:5: AIR302 `add_output_dataset` is removed in Airflow 3.0
|
20 | hlc.create_dataset()
21 | hlc.add_input_dataset()
22 | hlc.add_output_dataset()
| ^^^^^^^^^^^^^^^^^^ AIR302
23 | hlc.collected_datasets()
|
= help: Use `add_output_asset` instead
AIR302_class_attribute.py:23:5: AIR302 `collected_datasets` is removed in Airflow 3.0
|
21 | hlc.add_input_dataset()
22 | hlc.add_output_dataset()
23 | hlc.collected_datasets()
| ^^^^^^^^^^^^^^^^^^ AIR302
24 |
25 | aam = AwsAuthManager()
|
= help: Use `collected_assets` instead
AIR302_class_attribute.py:26:5: AIR302 `is_authorized_dataset` is removed in Airflow 3.0
|
25 | aam = AwsAuthManager()
26 | aam.is_authorized_dataset()
| ^^^^^^^^^^^^^^^^^^^^^ AIR302
27 |
28 | pm = ProvidersManager()
|
= help: Use `is_authorized_asset` instead
AIR302_class_attribute.py:30:4: AIR302 `dataset_factories` is removed in Airflow 3.0
|
28 | pm = ProvidersManager()
29 | pm.initialize_providers_asset_uri_resources()
30 | pm.dataset_factories
| ^^^^^^^^^^^^^^^^^ AIR302
31 |
32 | base_secret_backend = BaseSecretsBackend()
|
= help: Use `asset_factories` instead
AIR302_class_attribute.py:33:21: AIR302 `get_conn_uri` is removed in Airflow 3.0
|
32 | base_secret_backend = BaseSecretsBackend()
33 | base_secret_backend.get_conn_uri()
| ^^^^^^^^^^^^ AIR302
34 | base_secret_backend.get_connections()
|
= help: Use `get_conn_value` instead
AIR302_class_attribute.py:34:21: AIR302 `get_connections` is removed in Airflow 3.0
|
32 | base_secret_backend = BaseSecretsBackend()
33 | base_secret_backend.get_conn_uri()
34 | base_secret_backend.get_connections()
| ^^^^^^^^^^^^^^^ AIR302
35 |
36 | csm_backend = CloudSecretManagerBackend()
|
= help: Use `get_connection` instead
AIR302_class_attribute.py:37:13: AIR302 `get_conn_uri` is removed in Airflow 3.0
|
36 | csm_backend = CloudSecretManagerBackend()
37 | csm_backend.get_conn_uri()
| ^^^^^^^^^^^^ AIR302
38 | csm_backend.get_connections()
|
= help: Use `get_conn_value` instead
AIR302_class_attribute.py:38:13: AIR302 `get_connections` is removed in Airflow 3.0
|
36 | csm_backend = CloudSecretManagerBackend()
37 | csm_backend.get_conn_uri()
38 | csm_backend.get_connections()
| ^^^^^^^^^^^^^^^ AIR302
39 |
40 | vault_backend = VaultBackend()
|
= help: Use `get_connection` instead
AIR302_class_attribute.py:41:15: AIR302 `get_conn_uri` is removed in Airflow 3.0
|
40 | vault_backend = VaultBackend()
41 | vault_backend.get_conn_uri()
| ^^^^^^^^^^^^ AIR302
42 | vault_backend.get_connections()
|
= help: Use `get_conn_value` instead
AIR302_class_attribute.py:42:15: AIR302 `get_connections` is removed in Airflow 3.0
|
40 | vault_backend = VaultBackend()
41 | vault_backend.get_conn_uri()
42 | vault_backend.get_connections()
| ^^^^^^^^^^^^^^^ AIR302
43 |
44 | not_an_error = NotAir302SecretError()
|
= help: Use `get_connection` instead
AIR302_class_attribute.py:54:18: AIR302 `dataset_factories` is removed in Airflow 3.0
|
53 | provider_manager = ProvidersManager()
54 | provider_manager.dataset_factories
| ^^^^^^^^^^^^^^^^^ AIR302
55 | provider_manager.dataset_uri_handlers
56 | provider_manager.dataset_to_openlineage_converters
|
= help: Use `asset_factories` instead
AIR302_class_attribute.py:55:18: AIR302 `dataset_uri_handlers` is removed in Airflow 3.0
|
53 | provider_manager = ProvidersManager()
54 | provider_manager.dataset_factories
55 | provider_manager.dataset_uri_handlers
| ^^^^^^^^^^^^^^^^^^^^ AIR302
56 | provider_manager.dataset_to_openlineage_converters
|
= help: Use `asset_uri_handlers` instead
AIR302_class_attribute.py:56:18: AIR302 `dataset_to_openlineage_converters` is removed in Airflow 3.0
|
54 | provider_manager.dataset_factories
55 | provider_manager.dataset_uri_handlers
56 | provider_manager.dataset_to_openlineage_converters
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR302
57 |
58 | dl_info = DatasetLineageInfo()
|
= help: Use `asset_to_openlineage_converters` instead
AIR302_class_attribute.py:58:11: AIR302 `airflow.lineage.hook.DatasetLineageInfo` is removed in Airflow 3.0
|
56 | provider_manager.dataset_to_openlineage_converters
57 |
58 | dl_info = DatasetLineageInfo()
| ^^^^^^^^^^^^^^^^^^ AIR302
59 | dl_info.dataset
|
= help: Use `airflow.lineage.hook.AssetLineageInfo` instead

View File

@@ -2,163 +2,73 @@
source: crates/ruff_linter/src/rules/airflow/mod.rs
snapshot_kind: text
---
AIR303.py:20:1: AIR303 Import path `airflow.api.auth.backend.basic_auth` is moved into `fab` provider in Airflow 3.0;
AIR303.py:10:1: AIR303 Import path `airflow.api.auth.backend.basic_auth` is moved into `fab` provider in Airflow 3.0;
|
19 | # apache-airflow-providers-fab
20 | basic_auth, kerberos_auth
8 | from airflow.www.security import FabAirflowSecurityManagerOverride
9 |
10 | basic_auth, kerberos_auth
| ^^^^^^^^^^ AIR303
21 | auth_current_user
22 | backend_kerberos_auth
11 | auth_current_user
12 | backend_kerberos_auth
|
= help: Install `apache-airflow-provider-fab>=1.0.0` and import from `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth` instead.
AIR303.py:20:13: AIR303 Import path `airflow.api.auth.backend.kerberos_auth` is moved into `fab` provider in Airflow 3.0;
AIR303.py:10:13: AIR303 Import path `airflow.api.auth.backend.kerberos_auth` is moved into `fab` provider in Airflow 3.0;
|
19 | # apache-airflow-providers-fab
20 | basic_auth, kerberos_auth
8 | from airflow.www.security import FabAirflowSecurityManagerOverride
9 |
10 | basic_auth, kerberos_auth
| ^^^^^^^^^^^^^ AIR303
21 | auth_current_user
22 | backend_kerberos_auth
11 | auth_current_user
12 | backend_kerberos_auth
|
= help: Install `apache-airflow-provider-fab>=1.0.0` and import from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead.
AIR303.py:21:1: AIR303 Import path `airflow.api.auth.backend.basic_auth` is moved into `fab` provider in Airflow 3.0;
AIR303.py:11:1: AIR303 Import path `airflow.api.auth.backend.basic_auth` is moved into `fab` provider in Airflow 3.0;
|
19 | # apache-airflow-providers-fab
20 | basic_auth, kerberos_auth
21 | auth_current_user
10 | basic_auth, kerberos_auth
11 | auth_current_user
| ^^^^^^^^^^^^^^^^^ AIR303
22 | backend_kerberos_auth
23 | fab_override
12 | backend_kerberos_auth
13 | fab_override
|
= help: Install `apache-airflow-provider-fab>=1.0.0` and import from `airflow.providers.fab.auth_manager.api.auth.backend.basic_auth` instead.
AIR303.py:22:1: AIR303 Import path `airflow.auth_manager.api.auth.backend.kerberos_auth` is moved into `fab` provider in Airflow 3.0;
AIR303.py:12:1: AIR303 Import path `airflow.auth_manager.api.auth.backend.kerberos_auth` is moved into `fab` provider in Airflow 3.0;
|
20 | basic_auth, kerberos_auth
21 | auth_current_user
22 | backend_kerberos_auth
10 | basic_auth, kerberos_auth
11 | auth_current_user
12 | backend_kerberos_auth
| ^^^^^^^^^^^^^^^^^^^^^ AIR303
23 | fab_override
13 | fab_override
|
= help: Install `apache-airflow-provider-fab>=1.0.0` and import from `airflow.providers.fab.auth_manager.api.auth.backend.kerberos_auth` instead.
AIR303.py:23:1: AIR303 Import path `airflow.auth.managers.fab.security_managr.override` is moved into `fab` provider in Airflow 3.0;
AIR303.py:13:1: AIR303 Import path `airflow.auth.managers.fab.security_manager.override` is moved into `fab` provider in Airflow 3.0;
|
21 | auth_current_user
22 | backend_kerberos_auth
23 | fab_override
11 | auth_current_user
12 | backend_kerberos_auth
13 | fab_override
| ^^^^^^^^^^^^ AIR303
24 |
25 | FabAuthManager()
14 |
15 | FabAuthManager
|
= help: Install `apache-airflow-provider-fab>=1.0.0` and import from `airflow.providers.fab.auth_manager.security_manager.override` instead.
AIR303.py:25:1: AIR303 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` is moved into `fab` provider in Airflow 3.0;
AIR303.py:15:1: AIR303 `airflow.auth.managers.fab.fab_auth_manager.FabAuthManager` is moved into `fab` provider in Airflow 3.0;
|
23 | fab_override
24 |
25 | FabAuthManager()
13 | fab_override
14 |
15 | FabAuthManager
| ^^^^^^^^^^^^^^ AIR303
26 | FabAirflowSecurityManagerOverride()
16 | FabAirflowSecurityManagerOverride
|
= help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.FabAuthManager` instead.
AIR303.py:26:1: AIR303 `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0;
AIR303.py:16:1: AIR303 `airflow.www.security.FabAirflowSecurityManagerOverride` is moved into `fab` provider in Airflow 3.0;
|
25 | FabAuthManager()
26 | FabAirflowSecurityManagerOverride()
15 | FabAuthManager
16 | FabAirflowSecurityManagerOverride
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AIR303
27 |
28 | # apache-airflow-providers-celery
|
= help: Install `apache-airflow-provider-fab>=1.0.0` and use `airflow.providers.fab.auth_manager.security_manager.override.FabAirflowSecurityManagerOverride` instead.
AIR303.py:29:1: AIR303 Import path `airflow.config_templates.default_celery.DEFAULT_CELERY_CONFIG` is moved into `celery` provider in Airflow 3.0;
|
28 | # apache-airflow-providers-celery
29 | DEFAULT_CELERY_CONFIG
| ^^^^^^^^^^^^^^^^^^^^^ AIR303
30 | app
|
= help: Install `apache-airflow-provider-celery>=3.3.0` and import from `airflow.providers.celery.executors.default_celery.DEFAULT_CELERY_CONFIG` instead.
AIR303.py:30:1: AIR303 Import path `airflow.executors.celery_executor.app` is moved into `celery` provider in Airflow 3.0;
|
28 | # apache-airflow-providers-celery
29 | DEFAULT_CELERY_CONFIG
30 | app
| ^^^ AIR303
31 |
32 | # apache-airflow-providers-common-sql
|
= help: Install `apache-airflow-provider-celery>=3.3.0` and import from `airflow.providers.celery.executors.celery_executor_utils.app` instead.
AIR303.py:33:1: AIR303 Import path `airflow.hooks.dbapi.ConnectorProtocol` is moved into `Common SQL` provider in Airflow 3.0;
|
32 | # apache-airflow-providers-common-sql
33 | ConnectorProtocol()
| ^^^^^^^^^^^^^^^^^ AIR303
34 | DbApiHook()
|
= help: Install `apache-airflow-provider-Common SQL>=1.0.0` and import from `airflow.providers.common.sql.hooks.sql.ConnectorProtocol` instead.
AIR303.py:34:1: AIR303 Import path `airflow.hooks.dbapi.DbApiHook` is moved into `Common SQL` provider in Airflow 3.0;
|
32 | # apache-airflow-providers-common-sql
33 | ConnectorProtocol()
34 | DbApiHook()
| ^^^^^^^^^ AIR303
35 |
36 | # apache-airflow-providers-cncf-kubernetes
|
= help: Install `apache-airflow-provider-Common SQL>=1.0.0` and import from `airflow.providers.common.sql.hooks.sql.DbApiHook` instead.
AIR303.py:37:1: AIR303 Import path `airflow.executors.kubernetes_executor_types.ALL_NAMESPACES` is moved into `Kubernetes` provider in Airflow 3.0;
|
36 | # apache-airflow-providers-cncf-kubernetes
37 | ALL_NAMESPACES
| ^^^^^^^^^^^^^^ AIR303
38 | POD_EXECUTOR_DONE_KEY
|
= help: Install `apache-airflow-provider-Kubernetes>=7.4.0` and import from `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.ALL_NAMESPACES` instead.
AIR303.py:38:1: AIR303 Import path `airflow.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` is moved into `Kubernetes` provider in Airflow 3.0;
|
36 | # apache-airflow-providers-cncf-kubernetes
37 | ALL_NAMESPACES
38 | POD_EXECUTOR_DONE_KEY
| ^^^^^^^^^^^^^^^^^^^^^ AIR303
39 |
40 | # apache-airflow-providers-apache-hive
|
= help: Install `apache-airflow-provider-Kubernetes>=7.4.0` and import from `airflow.providers.cncf.kubernetes.executors.kubernetes_executor_types.POD_EXECUTOR_DONE_KEY` instead.
AIR303.py:41:1: AIR303 Import path `airflow.hooks.hive_hooks.HIVE_QUEUE_PRIORITIES` is moved into `Apache Hive` provider in Airflow 3.0;
|
40 | # apache-airflow-providers-apache-hive
41 | HIVE_QUEUE_PRIORITIES
| ^^^^^^^^^^^^^^^^^^^^^ AIR303
42 | closest_ds_partition()
43 | max_partition()
|
= help: Install `apache-airflow-provider-Apache Hive>=1.0.0` and import from `airflow.providers.apache.hive.hooks.hive.HIVE_QUEUE_PRIORITIES` instead.
AIR303.py:42:1: AIR303 Import path `airflow.macros.hive.closest_ds_partition` is moved into `Apache Hive` provider in Airflow 3.0;
|
40 | # apache-airflow-providers-apache-hive
41 | HIVE_QUEUE_PRIORITIES
42 | closest_ds_partition()
| ^^^^^^^^^^^^^^^^^^^^ AIR303
43 | max_partition()
|
= help: Install `apache-airflow-provider-Apache Hive>=5.1.0` and import from `airflow.providers.apache.hive.macros.hive.closest_ds_partition` instead.
AIR303.py:43:1: AIR303 Import path `airflow.macros.hive.max_partition` is moved into `Apache Hive` provider in Airflow 3.0;
|
41 | HIVE_QUEUE_PRIORITIES
42 | closest_ds_partition()
43 | max_partition()
| ^^^^^^^^^^^^^ AIR303
|
= help: Install `apache-airflow-provider-Apache Hive>=5.1.0` and import from `airflow.providers.apache.hive.macros.hive.max_partition` instead.

View File

@@ -99,15 +99,9 @@ pub(crate) fn fastapi_non_annotated_dependency(
let mut updatable_count = 0;
let mut has_non_updatable_default = false;
let total_params =
function_def.parameters.args.len() + function_def.parameters.kwonlyargs.len();
let total_params = function_def.parameters.args.len();
for parameter in function_def
.parameters
.args
.iter()
.chain(&function_def.parameters.kwonlyargs)
{
for parameter in &function_def.parameters.args {
let needs_update = matches!(
(&parameter.parameter.annotation, &parameter.default),
(Some(_annotation), Some(default)) if is_fastapi_dependency(checker, default)

View File

@@ -7,7 +7,7 @@ mod fastapi_redundant_response_model;
mod fastapi_unused_path_parameter;
use ruff_python_ast as ast;
use ruff_python_semantic::analyze::typing;
use ruff_python_semantic::analyze::typing::resolve_assignment;
use ruff_python_semantic::SemanticModel;
/// Returns `true` if the function is a FastAPI route.
@@ -41,11 +41,11 @@ pub(crate) fn is_fastapi_route_call(call_expr: &ast::ExprCall, semantic: &Semant
) {
return false;
}
let Some(name) = value.as_name_expr() else {
return false;
};
let Some(binding_id) = semantic.resolve_name(name) else {
return false;
};
typing::is_fastapi_route(semantic.binding(binding_id), semantic)
resolve_assignment(value, semantic).is_some_and(|qualified_name| {
matches!(
qualified_name.segments(),
["fastapi", "FastAPI" | "APIRouter"]
)
})
}

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
snapshot_kind: text
---
FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
|
@@ -321,40 +320,13 @@ FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
55 56 | limit: int = 10,
56 57 | ):
FAST002.py:61:25: FAST002 [*] FastAPI dependency without `Annotated`
FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
|
60 | @app.get("/items/{item_id}")
61 | async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
62 | pass
|
= help: Replace with `typing.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
58 59 |
59 60 |
60 61 | @app.get("/items/{item_id}")
61 |-async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
62 |+async def read_items(*, item_id: Annotated[int, Path(title="The ID of the item to get")], q: str):
62 63 | pass
63 64 |
64 65 | # Non fixable errors
FAST002.py:70:5: FAST002 FastAPI dependency without `Annotated`
|
68 | skip: int = 0,
69 | limit: int = 10,
70 | current_user: User = Depends(get_current_user),
65 | skip: int = 0,
66 | limit: int = 10,
67 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
71 | ):
72 | pass
68 | ):
69 | pass
|
= help: Replace with `typing.Annotated`

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
snapshot_kind: text
---
FAST002.py:24:5: FAST002 [*] FastAPI dependency without `Annotated`
|
@@ -321,40 +320,13 @@ FAST002.py:53:5: FAST002 [*] FastAPI dependency without `Annotated`
55 56 | limit: int = 10,
56 57 | ):
FAST002.py:61:25: FAST002 [*] FastAPI dependency without `Annotated`
FAST002.py:67:5: FAST002 FastAPI dependency without `Annotated`
|
60 | @app.get("/items/{item_id}")
61 | async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
62 | pass
|
= help: Replace with `typing_extensions.Annotated`
Unsafe fix
12 12 | Security,
13 13 | )
14 14 | from pydantic import BaseModel
15 |+from typing_extensions import Annotated
15 16 |
16 17 | app = FastAPI()
17 18 | router = APIRouter()
--------------------------------------------------------------------------------
58 59 |
59 60 |
60 61 | @app.get("/items/{item_id}")
61 |-async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):
62 |+async def read_items(*, item_id: Annotated[int, Path(title="The ID of the item to get")], q: str):
62 63 | pass
63 64 |
64 65 | # Non fixable errors
FAST002.py:70:5: FAST002 FastAPI dependency without `Annotated`
|
68 | skip: int = 0,
69 | limit: int = 10,
70 | current_user: User = Depends(get_current_user),
65 | skip: int = 0,
66 | limit: int = 10,
67 | current_user: User = Depends(get_current_user),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FAST002
71 | ):
72 | pass
68 | ):
69 | pass
|
= help: Replace with `typing_extensions.Annotated`

View File

@@ -1,5 +1,6 @@
---
source: crates/ruff_linter/src/rules/fastapi/mod.rs
snapshot_kind: text
---
FAST001.py:17:22: FAST001 [*] FastAPI route with redundant `response_model` argument
|
@@ -171,25 +172,4 @@ FAST001.py:53:24: FAST001 [*] FastAPI route with redundant `response_model` argu
53 |+@router.get("/items/")
54 54 | async def create_item(item: Item) -> Item:
55 55 | return item
56 56 |
FAST001.py:118:23: FAST001 [*] FastAPI route with redundant `response_model` argument
|
116 | def setup_app(app_arg: FastAPI, non_app: str) -> None:
117 | # Error
118 | @app_arg.get("/", response_model=str)
| ^^^^^^^^^^^^^^^^^^ FAST001
119 | async def get_root() -> str:
120 | return "Hello World!"
|
= help: Remove argument
Unsafe fix
115 115 |
116 116 | def setup_app(app_arg: FastAPI, non_app: str) -> None:
117 117 | # Error
118 |- @app_arg.get("/", response_model=str)
118 |+ @app_arg.get("/")
119 119 | async def get_root() -> str:
120 120 | return "Hello World!"
121 121 |
56 56 |

View File

@@ -1,7 +1,7 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::{self as ast, Comprehension, Expr};
use ruff_python_semantic::analyze::typing;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -115,22 +115,16 @@ pub(crate) fn unnecessary_dict_comprehension(
let Expr::Tuple(ast::ExprTuple { elts, .. }) = &generator.target else {
return;
};
let [Expr::Name(ast::ExprName { id: target_key, .. }), Expr::Name(ast::ExprName {
id: target_value, ..
})] = elts.as_slice()
else {
let [target_key, target_value] = elts.as_slice() else {
return;
};
let Expr::Name(ast::ExprName { id: key, .. }) = &key else {
if ComparableExpr::from(key) != ComparableExpr::from(target_key) {
return;
};
let Expr::Name(ast::ExprName { id: value, .. }) = &value else {
return;
};
if target_key == key && target_value == value {
add_diagnostic(checker, expr);
}
if ComparableExpr::from(value) != ComparableExpr::from(target_value) {
return;
}
add_diagnostic(checker, expr);
}
/// C416
@@ -146,83 +140,10 @@ pub(crate) fn unnecessary_list_set_comprehension(
if !generator.ifs.is_empty() || generator.is_async {
return;
}
if is_dict_items(checker, &generator.iter) {
match (&generator.target, elt) {
// [(k, v) for k, v in dict.items()] or [(k, v) for [k, v] in dict.items()]
(
Expr::Tuple(ast::ExprTuple {
elts: target_elts, ..
})
| Expr::List(ast::ExprList {
elts: target_elts, ..
}),
Expr::Tuple(ast::ExprTuple { elts, .. }),
) => {
let [Expr::Name(ast::ExprName { id: target_key, .. }), Expr::Name(ast::ExprName {
id: target_value, ..
})] = target_elts.as_slice()
else {
return;
};
let [Expr::Name(ast::ExprName { id: key, .. }), Expr::Name(ast::ExprName { id: value, .. })] =
elts.as_slice()
else {
return;
};
if target_key == key && target_value == value {
add_diagnostic(checker, expr);
}
}
// [x for x in dict.items()]
(
Expr::Name(ast::ExprName {
id: target_name, ..
}),
Expr::Name(ast::ExprName { id: elt_name, .. }),
) if target_name == elt_name => {
add_diagnostic(checker, expr);
}
_ => {}
}
} else {
// [x for x in iterable]
let Expr::Name(ast::ExprName {
id: target_name, ..
}) = &generator.target
else {
return;
};
let Expr::Name(ast::ExprName { id: elt_name, .. }) = &elt else {
return;
};
if elt_name == target_name {
add_diagnostic(checker, expr);
}
if ComparableExpr::from(elt) != ComparableExpr::from(&generator.target) {
return;
}
}
fn is_dict_items(checker: &Checker, expr: &Expr) -> bool {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return false;
};
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
return false;
};
if attr.as_str() != "items" {
return false;
}
let Expr::Name(name) = value.as_ref() else {
return false;
};
let Some(id) = checker.semantic().resolve_name(name) else {
return false;
};
typing::is_dict(checker.semantic().binding(id), checker.semantic())
add_diagnostic(checker, expr);
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]

View File

@@ -1,6 +1,5 @@
---
source: crates/ruff_linter/src/rules/flake8_comprehensions/mod.rs
snapshot_kind: text
---
C416.py:6:1: C416 [*] Unnecessary list comprehension (rewrite using `list()`)
|
@@ -62,7 +61,7 @@ C416.py:8:1: C416 [*] Unnecessary dict comprehension (rewrite using `dict()`)
8 |+dict(y)
9 9 | {k: v for k, v in d.items()}
10 10 | [(k, v) for k, v in d.items()]
11 11 | [(k, v) for [k, v] in d.items()]
11 11 | {k: (a, b) for k, (a, b) in d.items()}
C416.py:9:1: C416 [*] Unnecessary dict comprehension (rewrite using `dict()`)
|
@@ -71,7 +70,7 @@ C416.py:9:1: C416 [*] Unnecessary dict comprehension (rewrite using `dict()`)
9 | {k: v for k, v in d.items()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C416
10 | [(k, v) for k, v in d.items()]
11 | [(k, v) for [k, v] in d.items()]
11 | {k: (a, b) for k, (a, b) in d.items()}
|
= help: Rewrite using `dict()`
@@ -82,8 +81,8 @@ C416.py:9:1: C416 [*] Unnecessary dict comprehension (rewrite using `dict()`)
9 |-{k: v for k, v in d.items()}
9 |+dict(d.items())
10 10 | [(k, v) for k, v in d.items()]
11 11 | [(k, v) for [k, v] in d.items()]
12 12 | {k: (a, b) for k, (a, b) in d.items()}
11 11 | {k: (a, b) for k, (a, b) in d.items()}
12 12 |
C416.py:10:1: C416 [*] Unnecessary list comprehension (rewrite using `list()`)
|
@@ -91,8 +90,7 @@ C416.py:10:1: C416 [*] Unnecessary list comprehension (rewrite using `list()`)
9 | {k: v for k, v in d.items()}
10 | [(k, v) for k, v in d.items()]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C416
11 | [(k, v) for [k, v] in d.items()]
12 | {k: (a, b) for k, (a, b) in d.items()}
11 | {k: (a, b) for k, (a, b) in d.items()}
|
= help: Rewrite using `list()`
@@ -102,46 +100,42 @@ C416.py:10:1: C416 [*] Unnecessary list comprehension (rewrite using `list()`)
9 9 | {k: v for k, v in d.items()}
10 |-[(k, v) for k, v in d.items()]
10 |+list(d.items())
11 11 | [(k, v) for [k, v] in d.items()]
12 12 | {k: (a, b) for k, (a, b) in d.items()}
13 13 |
11 11 | {k: (a, b) for k, (a, b) in d.items()}
12 12 |
13 13 | [i for i, in z]
C416.py:11:1: C416 [*] Unnecessary list comprehension (rewrite using `list()`)
C416.py:11:1: C416 [*] Unnecessary dict comprehension (rewrite using `dict()`)
|
9 | {k: v for k, v in d.items()}
10 | [(k, v) for k, v in d.items()]
11 | [(k, v) for [k, v] in d.items()]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C416
12 | {k: (a, b) for k, (a, b) in d.items()}
11 | {k: (a, b) for k, (a, b) in d.items()}
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C416
12 |
13 | [i for i, in z]
|
= help: Rewrite using `list()`
= help: Rewrite using `dict()`
Unsafe fix
8 8 | {k: v for k, v in y}
9 9 | {k: v for k, v in d.items()}
10 10 | [(k, v) for k, v in d.items()]
11 |-[(k, v) for [k, v] in d.items()]
11 |+list(d.items())
12 12 | {k: (a, b) for k, (a, b) in d.items()}
13 13 |
14 14 | [i for i, in z]
11 |-{k: (a, b) for k, (a, b) in d.items()}
11 |+dict(d.items())
12 12 |
13 13 | [i for i, in z]
14 14 | [i for i, j in y]
C416.py:25:70: C416 [*] Unnecessary list comprehension (rewrite using `list()`)
C416.py:24:70: C416 [*] Unnecessary list comprehension (rewrite using `list()`)
|
24 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
25 | any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
24 | any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
| ^^^^^^^^^^^^^^^^^^^^^^^ C416
26 |
27 | zz = [[1], [2], [3]]
|
= help: Rewrite using `list()`
Unsafe fix
22 22 | {k: v if v else None for k, v in y}
23 23 |
24 24 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
25 |-any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
25 |+any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in list(SymbolType))
26 26 |
27 27 | zz = [[1], [2], [3]]
28 28 | [(i,) for (i,) in zz] # != list(zz)
21 21 | {k: v if v else None for k, v in y}
22 22 |
23 23 | # Regression test for: https://github.com/astral-sh/ruff/issues/7196
24 |-any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in[t for t in SymbolType])
24 |+any(len(symbol_table.get_by_type(symbol_type)) > 0 for symbol_type in list(SymbolType))

View File

@@ -101,13 +101,13 @@ impl From<&Expr> for ConstantLikelihood {
.map(ConstantLikelihood::from)
.min()
.unwrap_or(ConstantLikelihood::Definitely),
Expr::Dict(dict) => dict
.items
.iter()
.flat_map(|item| std::iter::once(&item.value).chain(item.key.as_ref()))
.map(ConstantLikelihood::from)
.min()
.unwrap_or(ConstantLikelihood::Definitely),
Expr::Dict(dict) => {
if dict.is_empty() {
ConstantLikelihood::Definitely
} else {
ConstantLikelihood::Probably
}
}
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => cmp::min(
ConstantLikelihood::from(&**left),
ConstantLikelihood::from(&**right),

View File

@@ -269,7 +269,7 @@ SIM300.py:14:1: SIM300 [*] Yoda condition detected
14 |+(number - 100) > 0 # SIM300
15 15 | B<A[0][0]or B
16 16 | B or(B)<A[0][0]
17 17 | {"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
17 17 |
SIM300.py:15:1: SIM300 [*] Yoda condition detected
|
@@ -278,7 +278,6 @@ SIM300.py:15:1: SIM300 [*] Yoda condition detected
15 | B<A[0][0]or B
| ^^^^^^^^^ SIM300
16 | B or(B)<A[0][0]
17 | {"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
|
= help: Rewrite as `A[0][0] > B`
@@ -289,8 +288,8 @@ SIM300.py:15:1: SIM300 [*] Yoda condition detected
15 |-B<A[0][0]or B
15 |+A[0][0] > B or B
16 16 | B or(B)<A[0][0]
17 17 | {"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
18 18 |
17 17 |
18 18 | # Errors in preview
SIM300.py:16:5: SIM300 [*] Yoda condition detected
|
@@ -298,7 +297,8 @@ SIM300.py:16:5: SIM300 [*] Yoda condition detected
15 | B<A[0][0]or B
16 | B or(B)<A[0][0]
| ^^^^^^^^^^^ SIM300
17 | {"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
17 |
18 | # Errors in preview
|
= help: Rewrite as `A[0][0] > (B)`
@@ -308,67 +308,46 @@ SIM300.py:16:5: SIM300 [*] Yoda condition detected
15 15 | B<A[0][0]or B
16 |-B or(B)<A[0][0]
16 |+B or A[0][0] > (B)
17 17 | {"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
18 18 |
19 19 | # Errors in preview
17 17 |
18 18 | # Errors in preview
19 19 | ['upper'] == UPPER_LIST
SIM300.py:17:1: SIM300 [*] Yoda condition detected
SIM300.py:19:1: SIM300 [*] Yoda condition detected
|
15 | B<A[0][0]or B
16 | B or(B)<A[0][0]
17 | {"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
18 |
19 | # Errors in preview
|
= help: Rewrite as `DummyHandler.CONFIG == {"non-empty-dict": "is-ok"}`
Safe fix
14 14 | 0 < (number - 100) # SIM300
15 15 | B<A[0][0]or B
16 16 | B or(B)<A[0][0]
17 |-{"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
17 |+DummyHandler.CONFIG == {"non-empty-dict": "is-ok"}
18 18 |
19 19 | # Errors in preview
20 20 | ['upper'] == UPPER_LIST
SIM300.py:20:1: SIM300 [*] Yoda condition detected
|
19 | # Errors in preview
20 | ['upper'] == UPPER_LIST
18 | # Errors in preview
19 | ['upper'] == UPPER_LIST
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM300
21 | {} == DummyHandler.CONFIG
20 | {} == DummyHandler.CONFIG
|
= help: Rewrite as `UPPER_LIST == ['upper']`
Safe fix
17 17 | {"non-empty-dict": "is-ok"} == DummyHandler.CONFIG
18 18 |
19 19 | # Errors in preview
20 |-['upper'] == UPPER_LIST
20 |+UPPER_LIST == ['upper']
21 21 | {} == DummyHandler.CONFIG
22 22 |
23 23 | # Errors in stable
16 16 | B or(B)<A[0][0]
17 17 |
18 18 | # Errors in preview
19 |-['upper'] == UPPER_LIST
19 |+UPPER_LIST == ['upper']
20 20 | {} == DummyHandler.CONFIG
21 21 |
22 22 | # Errors in stable
SIM300.py:21:1: SIM300 [*] Yoda condition detected
SIM300.py:20:1: SIM300 [*] Yoda condition detected
|
19 | # Errors in preview
20 | ['upper'] == UPPER_LIST
21 | {} == DummyHandler.CONFIG
18 | # Errors in preview
19 | ['upper'] == UPPER_LIST
20 | {} == DummyHandler.CONFIG
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
22 |
23 | # Errors in stable
21 |
22 | # Errors in stable
|
= help: Rewrite as `DummyHandler.CONFIG == {}`
Safe fix
18 18 |
19 19 | # Errors in preview
20 20 | ['upper'] == UPPER_LIST
21 |-{} == DummyHandler.CONFIG
21 |+DummyHandler.CONFIG == {}
22 22 |
23 23 | # Errors in stable
24 24 | UPPER_LIST == ['upper']
17 17 |
18 18 | # Errors in preview
19 19 | ['upper'] == UPPER_LIST
20 |-{} == DummyHandler.CONFIG
20 |+DummyHandler.CONFIG == {}
21 21 |
22 22 | # Errors in stable
23 23 | UPPER_LIST == ['upper']

View File

@@ -1,15 +1,8 @@
use std::ops::Range;
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{AstNode, Expr, ExprBinOp, ExprCall, Operator};
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::CommentRanges;
use ruff_text_size::{Ranged, TextRange};
use ruff_python_ast::{self as ast, Expr, ExprCall};
use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
/// ## What it does
/// Checks for `pathlib.Path` objects that are initialized with the current
@@ -50,17 +43,7 @@ impl AlwaysFixableViolation for PathConstructorCurrentDirectory {
}
/// PTH201
pub(crate) fn path_constructor_current_directory(checker: &mut Checker, call: &ExprCall) {
let applicability = |range| {
if checker.comment_ranges().intersects(range) {
Applicability::Unsafe
} else {
Applicability::Safe
}
};
let (func, arguments) = (&call.func, &call.arguments);
pub(crate) fn path_constructor_current_directory(checker: &mut Checker, expr: &Expr, func: &Expr) {
if !checker
.semantic()
.resolve_qualified_name(func)
@@ -71,75 +54,21 @@ pub(crate) fn path_constructor_current_directory(checker: &mut Checker, call: &E
return;
}
let Expr::Call(ExprCall { arguments, .. }) = expr else {
return;
};
if !arguments.keywords.is_empty() {
return;
}
let [Expr::StringLiteral(arg)] = &*arguments.args else {
let [Expr::StringLiteral(ast::ExprStringLiteral { value, range })] = &*arguments.args else {
return;
};
if !matches!(arg.value.to_str(), "" | ".") {
return;
if matches!(value.to_str(), "" | ".") {
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, *range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_deletion(*range)));
checker.diagnostics.push(diagnostic);
}
let mut diagnostic = Diagnostic::new(PathConstructorCurrentDirectory, arg.range());
match parent_and_next_path_fragment_range(
checker.semantic(),
checker.comment_ranges(),
checker.source(),
) {
Some((parent_range, next_fragment_range)) => {
let next_fragment_expr = checker.locator().slice(next_fragment_range);
let call_expr = checker.locator().slice(call.range());
let relative_argument_range: Range<usize> = {
let range = arg.range() - call.start();
range.start().into()..range.end().into()
};
let mut new_call_expr = call_expr.to_string();
new_call_expr.replace_range(relative_argument_range, next_fragment_expr);
let edit = Edit::range_replacement(new_call_expr, parent_range);
diagnostic.set_fix(Fix::applicable_edit(edit, applicability(parent_range)));
}
None => diagnostic.try_set_fix(|| {
let edit = remove_argument(arg, arguments, Parentheses::Preserve, checker.source())?;
Ok(Fix::applicable_edit(edit, applicability(call.range())))
}),
};
checker.diagnostics.push(diagnostic);
}
fn parent_and_next_path_fragment_range(
semantic: &SemanticModel,
comment_ranges: &CommentRanges,
source: &str,
) -> Option<(TextRange, TextRange)> {
let parent = semantic.current_expression_parent()?;
let Expr::BinOp(parent @ ExprBinOp { op, right, .. }) = parent else {
return None;
};
let range = right.range();
if !matches!(op, Operator::Div) {
return None;
}
Some((
parent.range(),
parenthesized_range(
right.into(),
parent.as_any_node_ref(),
comment_ranges,
source,
)
.unwrap_or(range),
))
}

View File

@@ -2,329 +2,84 @@
source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs
snapshot_kind: text
---
PTH201.py:6:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
PTH201.py:5:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
5 | # match
6 | _ = Path(".")
4 | # match
5 | _ = Path(".")
| ^^^ PTH201
7 | _ = pth(".")
8 | _ = PurePath(".")
6 | _ = pth(".")
7 | _ = PurePath(".")
|
= help: Remove the current directory argument
Safe fix
2 2 | from pathlib import Path as pth
3 3 |
4 4 | # match
5 |-_ = Path(".")
5 |+_ = Path()
6 6 | _ = pth(".")
7 7 | _ = PurePath(".")
8 8 | _ = Path("")
PTH201.py:6:9: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
4 | # match
5 | _ = Path(".")
6 | _ = pth(".")
| ^^^ PTH201
7 | _ = PurePath(".")
8 | _ = Path("")
|
= help: Remove the current directory argument
Safe fix
3 3 |
4 4 |
5 5 | # match
6 |-_ = Path(".")
6 |+_ = Path()
7 7 | _ = pth(".")
8 8 | _ = PurePath(".")
9 9 | _ = Path("")
4 4 | # match
5 5 | _ = Path(".")
6 |-_ = pth(".")
6 |+_ = pth()
7 7 | _ = PurePath(".")
8 8 | _ = Path("")
9 9 |
PTH201.py:7:9: PTH201 [*] Do not pass the current directory explicitly to `Path`
PTH201.py:7:14: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
5 | # match
6 | _ = Path(".")
7 | _ = pth(".")
| ^^^ PTH201
8 | _ = PurePath(".")
9 | _ = Path("")
|
= help: Remove the current directory argument
Safe fix
4 4 |
5 5 | # match
6 6 | _ = Path(".")
7 |-_ = pth(".")
7 |+_ = pth()
8 8 | _ = PurePath(".")
9 9 | _ = Path("")
10 10 |
PTH201.py:8:14: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
6 | _ = Path(".")
7 | _ = pth(".")
8 | _ = PurePath(".")
5 | _ = Path(".")
6 | _ = pth(".")
7 | _ = PurePath(".")
| ^^^ PTH201
9 | _ = Path("")
8 | _ = Path("")
|
= help: Remove the current directory argument
Safe fix
5 5 | # match
6 6 | _ = Path(".")
7 7 | _ = pth(".")
8 |-_ = PurePath(".")
8 |+_ = PurePath()
9 9 | _ = Path("")
10 10 |
11 11 | Path('', )
4 4 | # match
5 5 | _ = Path(".")
6 6 | _ = pth(".")
7 |-_ = PurePath(".")
7 |+_ = PurePath()
8 8 | _ = Path("")
9 9 |
10 10 | # no match
PTH201.py:9:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
PTH201.py:8:10: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
7 | _ = pth(".")
8 | _ = PurePath(".")
9 | _ = Path("")
6 | _ = pth(".")
7 | _ = PurePath(".")
8 | _ = Path("")
| ^^ PTH201
10 |
11 | Path('', )
9 |
10 | # no match
|
= help: Remove the current directory argument
Safe fix
6 6 | _ = Path(".")
7 7 | _ = pth(".")
8 8 | _ = PurePath(".")
9 |-_ = Path("")
9 |+_ = Path()
10 10 |
11 11 | Path('', )
12 12 |
PTH201.py:11:6: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
9 | _ = Path("")
10 |
11 | Path('', )
| ^^ PTH201
12 |
13 | Path(
|
= help: Remove the current directory argument
Safe fix
8 8 | _ = PurePath(".")
9 9 | _ = Path("")
10 10 |
11 |-Path('', )
11 |+Path()
12 12 |
13 13 | Path(
14 14 | '',
PTH201.py:14:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
13 | Path(
14 | '',
| ^^ PTH201
15 | )
|
= help: Remove the current directory argument
Safe fix
10 10 |
11 11 | Path('', )
12 12 |
13 |-Path(
14 |- '',
15 |-)
13 |+Path()
16 14 |
17 15 | Path( # Comment before argument
18 16 | '',
PTH201.py:18:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
17 | Path( # Comment before argument
18 | '',
| ^^ PTH201
19 | )
|
= help: Remove the current directory argument
Unsafe fix
14 14 | '',
15 15 | )
16 16 |
17 |-Path( # Comment before argument
18 |- '',
19 |-)
17 |+Path()
20 18 |
21 19 | Path(
22 20 | '', # EOL comment
PTH201.py:22:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
21 | Path(
22 | '', # EOL comment
| ^^ PTH201
23 | )
|
= help: Remove the current directory argument
Unsafe fix
18 18 | '',
19 19 | )
20 20 |
21 |-Path(
22 |- '', # EOL comment
23 |-)
21 |+Path()
24 22 |
25 23 | Path(
26 24 | '' # Comment in the middle of implicitly concatenated string
PTH201.py:26:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
25 | Path(
26 | '' # Comment in the middle of implicitly concatenated string
| _____^
27 | | ".",
| |_______^ PTH201
28 | )
|
= help: Remove the current directory argument
Unsafe fix
22 22 | '', # EOL comment
23 23 | )
24 24 |
25 |-Path(
26 |- '' # Comment in the middle of implicitly concatenated string
27 |- ".",
28 |-)
25 |+Path()
29 26 |
30 27 | Path(
31 28 | '' # Comment before comma
PTH201.py:31:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
30 | Path(
31 | '' # Comment before comma
| ^^ PTH201
32 | ,
33 | )
|
= help: Remove the current directory argument
Unsafe fix
27 27 | ".",
28 28 | )
29 29 |
30 |-Path(
31 |- '' # Comment before comma
32 |- ,
33 |-)
30 |+Path()
34 31 |
35 32 | Path(
36 33 | '',
PTH201.py:36:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
35 | Path(
36 | '',
| ^^ PTH201
37 | ) / "bare"
|
= help: Remove the current directory argument
Safe fix
33 33 | )
34 34 |
35 35 | Path(
36 |- '',
37 |-) / "bare"
36 |+ "bare",
37 |+)
38 38 |
39 39 | Path( # Comment before argument
40 40 | '',
PTH201.py:40:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
39 | Path( # Comment before argument
40 | '',
| ^^ PTH201
41 | ) / ("parenthesized")
|
= help: Remove the current directory argument
Unsafe fix
37 37 | ) / "bare"
38 38 |
39 39 | Path( # Comment before argument
40 |- '',
41 |-) / ("parenthesized")
40 |+ ("parenthesized"),
41 |+)
42 42 |
43 43 | Path(
44 44 | '', # EOL comment
PTH201.py:44:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
43 | Path(
44 | '', # EOL comment
| ^^ PTH201
45 | ) / ( ("double parenthesized" ) )
|
= help: Remove the current directory argument
Unsafe fix
41 41 | ) / ("parenthesized")
42 42 |
43 43 | Path(
44 |- '', # EOL comment
45 |-) / ( ("double parenthesized" ) )
44 |+ ( ("double parenthesized" ) ), # EOL comment
45 |+)
46 46 |
47 47 | ( Path(
48 48 | '' # Comment in the middle of implicitly concatenated string
PTH201.py:48:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
47 | ( Path(
48 | '' # Comment in the middle of implicitly concatenated string
| _____^
49 | | ".",
| |_______^ PTH201
50 | ) )/ (("parenthesized path call")
51 | # Comment between closing parentheses
|
= help: Remove the current directory argument
Unsafe fix
44 44 | '', # EOL comment
45 45 | ) / ( ("double parenthesized" ) )
46 46 |
47 |-( Path(
48 |- '' # Comment in the middle of implicitly concatenated string
49 |- ".",
50 |-) )/ (("parenthesized path call")
47 |+Path(
48 |+ (("parenthesized path call")
51 49 | # Comment between closing parentheses
50 |+),
52 51 | )
53 52 |
54 53 | Path(
PTH201.py:55:5: PTH201 [*] Do not pass the current directory explicitly to `Path`
|
54 | Path(
55 | '' # Comment before comma
| ^^ PTH201
56 | ,
57 | ) / "multiple" / (
|
= help: Remove the current directory argument
Unsafe fix
52 52 | )
53 53 |
54 54 | Path(
55 |- '' # Comment before comma
55 |+ "multiple" # Comment before comma
56 56 | ,
57 |-) / "multiple" / (
57 |+) / (
58 58 | "frag" # Comment
59 59 | 'ment'
60 60 | )
5 5 | _ = Path(".")
6 6 | _ = pth(".")
7 7 | _ = PurePath(".")
8 |-_ = Path("")
8 |+_ = Path()
9 9 |
10 10 | # no match
11 11 | _ = Path()

View File

@@ -1,10 +1,9 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, ViolationMetadata};
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::{
self as ast, AstNode, Expr, ExprEllipsisLiteral, ExprLambda, Identifier, Parameter,
ParameterWithDefault, Parameters, Stmt,
self as ast, Expr, Identifier, Parameter, ParameterWithDefault, Parameters, Stmt,
};
use ruff_python_codegen::Generator;
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::{has_leading_content, has_trailing_content, leading_indentation};
use ruff_source_file::UniversalNewlines;
@@ -66,7 +65,10 @@ pub(crate) fn lambda_assignment(
return;
};
let Expr::Lambda(lambda) = value else {
let Expr::Lambda(ast::ExprLambda {
parameters, body, ..
}) = value
else {
return;
};
@@ -83,9 +85,16 @@ pub(crate) fn lambda_assignment(
let first_line = checker.locator().line_str(stmt.start());
let indentation = leading_indentation(first_line);
let mut indented = String::new();
for (idx, line) in function(id, lambda, annotation, checker)
.universal_newlines()
.enumerate()
for (idx, line) in function(
id,
parameters.as_deref(),
body,
annotation,
checker.semantic(),
checker.generator(),
)
.universal_newlines()
.enumerate()
{
if idx == 0 {
indented.push_str(&line);
@@ -177,21 +186,19 @@ fn extract_types(annotation: &Expr, semantic: &SemanticModel) -> Option<(Vec<Exp
/// Generate a function definition from a `lambda` expression.
fn function(
name: &str,
lambda: &ExprLambda,
parameters: Option<&Parameters>,
body: &Expr,
annotation: Option<&Expr>,
checker: &Checker,
semantic: &SemanticModel,
generator: Generator,
) -> String {
// Use a dummy body. It gets replaced at the end with the actual body.
// This allows preserving the source formatting for the body.
let body = Stmt::Return(ast::StmtReturn {
value: Some(Box::new(Expr::EllipsisLiteral(
ExprEllipsisLiteral::default(),
))),
value: Some(Box::new(body.clone())),
range: TextRange::default(),
});
let parameters = lambda.parameters.as_deref().cloned().unwrap_or_default();
let parameters = parameters.cloned().unwrap_or_default();
if let Some(annotation) = annotation {
if let Some((arg_types, return_type)) = extract_types(annotation, checker.semantic()) {
if let Some((arg_types, return_type)) = extract_types(annotation, semantic) {
// A `lambda` expression can only have positional-only and positional-or-keyword
// arguments. The order is always positional-only first, then positional-or-keyword.
let new_posonlyargs = parameters
@@ -236,12 +243,10 @@ fn function(
type_params: None,
range: TextRange::default(),
});
let generated = checker.generator().stmt(&func);
return replace_trailing_ellipsis_with_original_expr(generated, lambda, checker);
return generator.stmt(&func);
}
}
let function = Stmt::FunctionDef(ast::StmtFunctionDef {
let func = Stmt::FunctionDef(ast::StmtFunctionDef {
is_async: false,
name: Identifier::new(name.to_string(), TextRange::default()),
parameters: Box::new(parameters),
@@ -251,32 +256,5 @@ fn function(
type_params: None,
range: TextRange::default(),
});
let generated = checker.generator().stmt(&function);
replace_trailing_ellipsis_with_original_expr(generated, lambda, checker)
}
fn replace_trailing_ellipsis_with_original_expr(
mut generated: String,
lambda: &ExprLambda,
checker: &Checker,
) -> String {
let original_expr_range = parenthesized_range(
(&lambda.body).into(),
lambda.as_any_node_ref(),
checker.comment_ranges(),
checker.source(),
)
.unwrap_or(lambda.body.range());
let original_expr_in_source = checker.locator().slice(original_expr_range);
let placeholder_ellipsis_start = generated.rfind("...").unwrap();
let placeholder_ellipsis_end = placeholder_ellipsis_start + "...".len();
generated.replace_range(
placeholder_ellipsis_start..placeholder_ellipsis_end,
original_expr_in_source,
);
generated
generator.stmt(&func)
}

View File

@@ -102,7 +102,7 @@ impl<'a> IntoIterator for &'a LogicalLines<'a> {
/// line breaks.
///
/// ## Examples
/// This expression forms one logical line because the array elements are parenthesized.
/// This expression forms one logical line because because the array elements are parenthesized.
///
/// ```python
/// a = [

View File

@@ -279,7 +279,7 @@ E731.py:127:5: E731 [*] Do not assign a `lambda` expression, use a `def`
126 126 |
127 |- f: Callable[[str, int], tuple[str, int]] = lambda a, b: (a, b)
127 |+ def f(a: str, b: int) -> tuple[str, int]:
128 |+ return (a, b)
128 |+ return a, b
128 129 |
129 130 |
130 131 | def scope():
@@ -364,103 +364,7 @@ E731.py:147:5: E731 [*] Do not assign a `lambda` expression, use a `def`
148 |- i := 1,
149 |- )
147 |+ def f():
148 |+ return (
149 |+ i := 1,
150 |+ )
150 151 |
151 152 |
152 153 | from dataclasses import dataclass
E731.py:163:1: E731 [*] Do not assign a `lambda` expression, use a `def`
|
161 | # Regression tests for:
162 | # * https://github.com/astral-sh/ruff/issues/7720
163 | / x = lambda: """
164 | | a
165 | | b
166 | | """
| |___^ E731
167 |
168 | # * https://github.com/astral-sh/ruff/issues/10277
|
= help: Rewrite `x` as a `def`
Unsafe fix
160 160 |
161 161 | # Regression tests for:
162 162 | # * https://github.com/astral-sh/ruff/issues/7720
163 |-x = lambda: """
163 |+def x():
164 |+ return """
164 165 | a
165 166 | b
166 167 | """
E731.py:169:1: E731 [*] Do not assign a `lambda` expression, use a `def`
|
168 | # * https://github.com/astral-sh/ruff/issues/10277
169 | at_least_one_million = lambda _: _ >= 1_000_000
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E731
170 |
171 | x = lambda: (
|
= help: Rewrite `at_least_one_million` as a `def`
Unsafe fix
166 166 | """
167 167 |
168 168 | # * https://github.com/astral-sh/ruff/issues/10277
169 |-at_least_one_million = lambda _: _ >= 1_000_000
169 |+def at_least_one_million(_):
170 |+ return _ >= 1_000_000
170 171 |
171 172 | x = lambda: (
172 173 | # comment
E731.py:171:1: E731 [*] Do not assign a `lambda` expression, use a `def`
|
169 | at_least_one_million = lambda _: _ >= 1_000_000
170 |
171 | / x = lambda: (
172 | | # comment
173 | | 5 + 10
174 | | )
| |_^ E731
175 |
176 | x = lambda: (
|
= help: Rewrite `x` as a `def`
Unsafe fix
168 168 | # * https://github.com/astral-sh/ruff/issues/10277
169 169 | at_least_one_million = lambda _: _ >= 1_000_000
170 170 |
171 |-x = lambda: (
171 |+def x():
172 |+ return (
172 173 | # comment
173 174 | 5 + 10
174 175 | )
E731.py:176:1: E731 [*] Do not assign a `lambda` expression, use a `def`
|
174 | )
175 |
176 | / x = lambda: (
177 | | # comment
178 | | y := 10
179 | | )
| |_^ E731
|
= help: Rewrite `x` as a `def`
Unsafe fix
173 173 | 5 + 10
174 174 | )
175 175 |
176 |-x = lambda: (
176 |+def x():
177 |+ return (
177 178 | # comment
178 179 | y := 10
179 180 | )
148 |+ return (i := 1),
150 149 |
151 150 |
152 151 | from dataclasses import dataclass

View File

@@ -17,41 +17,41 @@ mod tests {
use super::settings::{Convention, Settings};
#[test_case(Rule::MissingBlankLineAfterLastSection, Path::new("sections.py"))]
#[test_case(Rule::BlankLineAfterLastSection, Path::new("sections.py"))]
#[test_case(Rule::NoBlankLineAfterSection, Path::new("sections.py"))]
#[test_case(Rule::MissingBlankLineAfterLastSection, Path::new("D413.py"))]
#[test_case(Rule::MissingBlankLineAfterSummary, Path::new("D.py"))]
#[test_case(Rule::BlankLineAfterLastSection, Path::new("D413.py"))]
#[test_case(Rule::BlankLineAfterSummary, Path::new("D.py"))]
#[test_case(Rule::NoBlankLineBeforeSection, Path::new("sections.py"))]
#[test_case(Rule::NonCapitalizedSectionName, Path::new("sections.py"))]
#[test_case(Rule::MissingDashedUnderlineAfterSection, Path::new("sections.py"))]
#[test_case(Rule::CapitalizeSectionName, Path::new("sections.py"))]
#[test_case(Rule::DashedUnderlineAfterSection, Path::new("sections.py"))]
#[test_case(Rule::UndocumentedParam, Path::new("canonical_google_examples.py"))]
#[test_case(Rule::UndocumentedParam, Path::new("canonical_numpy_examples.py"))]
#[test_case(Rule::UndocumentedParam, Path::new("sections.py"))]
#[test_case(Rule::MissingTrailingPeriod, Path::new("D.py"))]
#[test_case(Rule::MissingTrailingPeriod, Path::new("D400.py"))]
#[test_case(Rule::MissingTrailingPeriod, Path::new("D400_415.py"))]
#[test_case(Rule::MissingTerminalPunctuation, Path::new("D.py"))]
#[test_case(Rule::MissingTerminalPunctuation, Path::new("D400_415.py"))]
#[test_case(Rule::EndsInPeriod, Path::new("D.py"))]
#[test_case(Rule::EndsInPeriod, Path::new("D400.py"))]
#[test_case(Rule::EndsInPeriod, Path::new("D400_415.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D.py"))]
#[test_case(Rule::EndsInPunctuation, Path::new("D400_415.py"))]
#[test_case(Rule::FirstWordUncapitalized, Path::new("D.py"))]
#[test_case(Rule::FirstWordUncapitalized, Path::new("D403.py"))]
#[test_case(Rule::UnnecessaryMultilineDocstring, Path::new("D.py"))]
#[test_case(Rule::DocstringTabIndentation, Path::new("D.py"))]
#[test_case(Rule::FitsOnOneLine, Path::new("D.py"))]
#[test_case(Rule::IndentWithSpaces, Path::new("D.py"))]
#[test_case(Rule::UndocumentedMagicMethod, Path::new("D.py"))]
#[test_case(Rule::MultiLineSummaryFirstLine, Path::new("D.py"))]
#[test_case(Rule::MultiLineSummarySecondLine, Path::new("D.py"))]
#[test_case(Rule::NewLineAfterLastParagraph, Path::new("D.py"))]
#[test_case(Rule::MissingNewLineAfterSectionName, Path::new("sections.py"))]
#[test_case(Rule::BlankLineAfterFunction, Path::new("D.py"))]
#[test_case(Rule::UnnecessaryMultilineDocstring, Path::new("D200.py"))]
#[test_case(Rule::BlankLineAfterFunction, Path::new("D202.py"))]
#[test_case(Rule::NewLineAfterSectionName, Path::new("sections.py"))]
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D.py"))]
#[test_case(Rule::FitsOnOneLine, Path::new("D200.py"))]
#[test_case(Rule::NoBlankLineAfterFunction, Path::new("D202.py"))]
#[test_case(Rule::BlankLineBeforeClass, Path::new("D.py"))]
#[test_case(Rule::BlankLineBeforeFunction, Path::new("D.py"))]
#[test_case(Rule::NoBlankLineBeforeFunction, Path::new("D.py"))]
#[test_case(Rule::BlankLinesBetweenHeaderAndContent, Path::new("sections.py"))]
#[test_case(Rule::BlankLinesBetweenHeaderAndContent, Path::new("sphinx.py"))]
#[test_case(Rule::OverIndentation, Path::new("D.py"))]
#[test_case(Rule::OverIndentation, Path::new("D208.py"))]
#[test_case(Rule::SignatureInDocstring, Path::new("D.py"))]
#[test_case(Rule::SignatureInDocstring, Path::new("D402.py"))]
#[test_case(Rule::NoSignature, Path::new("D.py"))]
#[test_case(Rule::NoSignature, Path::new("D402.py"))]
#[test_case(Rule::SurroundingWhitespace, Path::new("D.py"))]
#[test_case(Rule::DocstringStartsWithThis, Path::new("D.py"))]
#[test_case(Rule::UnderIndentation, Path::new("D.py"))]
@@ -59,8 +59,8 @@ mod tests {
#[test_case(Rule::EmptyDocstringSection, Path::new("sections.py"))]
#[test_case(Rule::NonImperativeMood, Path::new("D401.py"))]
#[test_case(Rule::NoBlankLineAfterSection, Path::new("D410.py"))]
#[test_case(Rule::IncorrectBlankLineAfterClass, Path::new("D.py"))]
#[test_case(Rule::IncorrectBlankLineBeforeClass, Path::new("D.py"))]
#[test_case(Rule::OneBlankLineAfterClass, Path::new("D.py"))]
#[test_case(Rule::OneBlankLineBeforeClass, Path::new("D.py"))]
#[test_case(Rule::UndocumentedPublicClass, Path::new("D.py"))]
#[test_case(Rule::UndocumentedPublicFunction, Path::new("D.py"))]
#[test_case(Rule::UndocumentedPublicInit, Path::new("D.py"))]
@@ -83,13 +83,13 @@ mod tests {
#[test_case(Rule::UndocumentedPublicNestedClass, Path::new("D.py"))]
#[test_case(Rule::UndocumentedPublicPackage, Path::new("D.py"))]
#[test_case(Rule::UndocumentedPublicPackage, Path::new("D104/__init__.py"))]
#[test_case(Rule::MissingSectionNameColon, Path::new("D.py"))]
#[test_case(Rule::OverindentedSection, Path::new("sections.py"))]
#[test_case(Rule::OverindentedSection, Path::new("D214_module.py"))]
#[test_case(Rule::OverindentedSectionUnderline, Path::new("D215.py"))]
#[test_case(Rule::MissingSectionUnderlineAfterName, Path::new("sections.py"))]
#[test_case(Rule::MismatchedSectionUnderlineLength, Path::new("sections.py"))]
#[test_case(Rule::OverindentedSectionUnderline, Path::new("sections.py"))]
#[test_case(Rule::SectionNameEndsInColon, Path::new("D.py"))]
#[test_case(Rule::SectionNotOverIndented, Path::new("sections.py"))]
#[test_case(Rule::SectionNotOverIndented, Path::new("D214_module.py"))]
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("D215.py"))]
#[test_case(Rule::SectionUnderlineAfterName, Path::new("sections.py"))]
#[test_case(Rule::SectionUnderlineMatchesSectionLength, Path::new("sections.py"))]
#[test_case(Rule::SectionUnderlineNotOverIndented, Path::new("sections.py"))]
#[test_case(Rule::OverloadWithDocstring, Path::new("D.py"))]
#[test_case(Rule::EscapeSequenceInDocstring, Path::new("D.py"))]
#[test_case(Rule::EscapeSequenceInDocstring, Path::new("D301.py"))]
@@ -171,7 +171,7 @@ mod tests {
Path::new("pydocstyle/D209_D400.py"),
&settings::LinterSettings::for_rules([
Rule::NewLineAfterLastParagraph,
Rule::MissingTrailingPeriod,
Rule::EndsInPeriod,
]),
)?;
assert_messages!(diagnostics);

View File

@@ -41,16 +41,16 @@ use crate::docstrings::Docstring;
///
/// [PEP 257]: https://peps.python.org/pep-0257/
#[derive(ViolationMetadata)]
pub(crate) struct MissingBlankLineAfterSummary {
pub(crate) struct BlankLineAfterSummary {
num_lines: usize,
}
impl Violation for MissingBlankLineAfterSummary {
impl Violation for BlankLineAfterSummary {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let MissingBlankLineAfterSummary { num_lines } = self;
let BlankLineAfterSummary { num_lines } = self;
if *num_lines == 0 {
"1 blank line required between summary line and description".to_string()
} else {
@@ -85,7 +85,7 @@ pub(crate) fn blank_after_summary(checker: &mut Checker, docstring: &Docstring)
}
if lines_count > 1 && blanks_count != 1 {
let mut diagnostic = Diagnostic::new(
MissingBlankLineAfterSummary {
BlankLineAfterSummary {
num_lines: blanks_count,
},
docstring.range(),

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