Compare commits
24 Commits
v0.4.2
...
cjm/record
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bbe92e429 | ||
|
|
ff9bfad80f | ||
|
|
983a06cec3 | ||
|
|
47692027bf | ||
|
|
ec3243a6e5 | ||
|
|
2d6978f236 | ||
|
|
2490d2d4af | ||
|
|
59b73fabc1 | ||
|
|
61c97a037c | ||
|
|
7cd065e4a2 | ||
|
|
845ba7cf5f | ||
|
|
5994414739 | ||
|
|
632965d0fa | ||
|
|
77a72ecd38 | ||
|
|
16a1f3cbcc | ||
|
|
cd3e319538 | ||
|
|
45725d3275 | ||
|
|
bbca8eb388 | ||
|
|
c8c227dd5d | ||
|
|
b15e9e6e05 | ||
|
|
22d4f11348 | ||
|
|
269014a539 | ||
|
|
dc09f529bc | ||
|
|
3364ef957d |
17
.github/workflows/ci.yaml
vendored
17
.github/workflows/ci.yaml
vendored
@@ -568,23 +568,8 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
# Codspeed comes with a very ancient cargo version (1.66) that resolves features flags differently than what we use now.
|
||||
# This can result in build failures; see https://github.com/astral-sh/ruff/pull/10700.
|
||||
# There's a pending codspeed PR to upgrade to a newer cargo version, but until that's merged, we need to use the workaround below.
|
||||
# https://github.com/CodSpeedHQ/codspeed-rust/pull/31
|
||||
# What we do is to call cargo build manually with the correct feature flags and RUSTC settings. We'll have to
|
||||
# manually maintain the list of benchmarks to run with codspeed (the benefit is that we could detect which benchmarks to run and build based on the changes).
|
||||
# This is inspired by https://github.com/oxc-project/oxc/blob/a0532adc654039a0c7ead7b35216dfa0b0cb8e8f/.github/workflows/benchmark.yml
|
||||
- name: "Build benchmarks"
|
||||
env:
|
||||
RUSTFLAGS: "-C debuginfo=2 -C strip=none -g --cfg codspeed"
|
||||
shell: bash
|
||||
# Build all benchmarks, copy the binary to the codspeed directory, remove any `*.d` files that might have been created.
|
||||
run: |
|
||||
cargo build --release -p ruff_benchmark --bench parser --bench linter --bench formatter --bench lexer --features=codspeed
|
||||
mkdir -p ./target/codspeed/ruff_benchmark
|
||||
cp ./target/release/deps/{lexer,parser,linter,formatter}* target/codspeed/ruff_benchmark/
|
||||
rm -rf ./target/codspeed/ruff_benchmark/*.d
|
||||
run: cargo codspeed build --features codspeed -p ruff_benchmark
|
||||
|
||||
- name: "Run benchmarks"
|
||||
uses: CodSpeedHQ/action@v2
|
||||
|
||||
17
.github/workflows/release.yaml
vendored
17
.github/workflows/release.yaml
vendored
@@ -97,8 +97,8 @@ jobs:
|
||||
*.tar.gz
|
||||
*.sha256
|
||||
|
||||
macos-universal:
|
||||
runs-on: macos-12
|
||||
macos-aarch64:
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -106,16 +106,17 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
architecture: arm64
|
||||
- name: "Prep README.md"
|
||||
run: python scripts/transform_readme.py --target pypi
|
||||
- name: "Build wheels - universal2"
|
||||
- name: "Build wheels - aarch64"
|
||||
uses: PyO3/maturin-action@v1
|
||||
with:
|
||||
args: --release --locked --target universal2-apple-darwin --out dist
|
||||
- name: "Test wheel - universal2"
|
||||
target: aarch64
|
||||
args: --release --locked --out dist
|
||||
- name: "Test wheel - aarch64"
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
ruff --help
|
||||
python -m ruff --help
|
||||
- name: "Upload wheels"
|
||||
@@ -451,7 +452,7 @@ jobs:
|
||||
name: Upload to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- macos-universal
|
||||
- macos-aarch64
|
||||
- macos-x86_64
|
||||
- windows
|
||||
- linux
|
||||
|
||||
349
Cargo.lock
generated
349
Cargo.lock
generated
@@ -16,9 +16,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b79b82693f705137f8fb9b37871d99e4f9a7df12b917eed79c3d3954830a60b"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
@@ -36,6 +36,12 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "allocator-api2"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -145,15 +151,15 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
@@ -183,15 +189,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-automata 0.4.6",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.15.3"
|
||||
version = "3.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b"
|
||||
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
|
||||
|
||||
[[package]]
|
||||
name = "cachedir"
|
||||
@@ -210,9 +216,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.88"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc"
|
||||
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -244,7 +250,7 @@ dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
"windows-targets 0.52.4",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -293,15 +299,15 @@ dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim 0.11.0",
|
||||
"strsim 0.11.1",
|
||||
"terminal_size",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.1"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c"
|
||||
checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e"
|
||||
dependencies = [
|
||||
"clap",
|
||||
]
|
||||
@@ -371,9 +377,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "735f16ee0fc63cb90596cd7b57ce481522adfe1714f95bc04a94d4f4b0a06a6d"
|
||||
checksum = "3a104ac948e0188b921eb3fcbdd55dcf62e542df4c7ab7e660623f6288302089"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"libc",
|
||||
@@ -382,9 +388,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "codspeed-criterion-compat"
|
||||
version = "2.5.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "572ca9c8ad460591b40aad63c99d6746aa3c532f979175344eb015389499860c"
|
||||
checksum = "722c36bdc62d9436d027256ce2627af81ac7a596dfc7d13d849d0d212448d7fe"
|
||||
dependencies = [
|
||||
"codspeed",
|
||||
"colored",
|
||||
@@ -535,6 +541,16 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.8"
|
||||
@@ -570,6 +586,19 @@ dependencies = [
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.3",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
@@ -631,9 +660,9 @@ checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.10.0"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
|
||||
checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
@@ -682,9 +711,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.0.1"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
|
||||
checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
|
||||
|
||||
[[package]]
|
||||
name = "fern"
|
||||
@@ -761,9 +790,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.12"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
|
||||
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -787,15 +816,15 @@ dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-automata 0.4.6",
|
||||
"regex-syntax 0.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "half"
|
||||
version = "2.4.0"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e"
|
||||
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
@@ -812,6 +841,10 @@ name = "hashbrown"
|
||||
version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -901,7 +934,7 @@ dependencies = [
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-automata 0.4.6",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
@@ -1076,9 +1109,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "jod-thread"
|
||||
@@ -1184,9 +1217,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libmimalloc-sys"
|
||||
version = "0.1.35"
|
||||
version = "0.1.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3979b5c37ece694f1f5e51e7ecc871fdb0f517ed04ee45f88d15d6d553cb9664"
|
||||
checksum = "81eb4061c0582dedea1cbc7aff2240300dd6982e0239d1c99e65c1dbf4a30ba7"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -1194,13 +1227,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.0.1"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8"
|
||||
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1215,6 +1247,16 @@ version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
@@ -1275,9 +1317,9 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||
|
||||
[[package]]
|
||||
name = "mimalloc"
|
||||
version = "0.1.39"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa01922b5ea280a911e323e4d2fd24b7fe5cc4042e0d2cda3c40775cdc4bdc9c"
|
||||
checksum = "9f41a2280ded0da56c8cf898babb86e8f10651a34adcfff190ae9a1159c6908d"
|
||||
dependencies = [
|
||||
"libmimalloc-sys",
|
||||
]
|
||||
@@ -1442,6 +1484,29 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
|
||||
dependencies = [
|
||||
"lock_api",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.9.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
@@ -1591,9 +1656,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.13"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
|
||||
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||
|
||||
[[package]]
|
||||
name = "pmutil"
|
||||
@@ -1733,6 +1798,37 @@ dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.5.0",
|
||||
"crossbeam-channel",
|
||||
"ctrlc",
|
||||
"dashmap",
|
||||
"hashbrown 0.14.3",
|
||||
"indexmap",
|
||||
"log",
|
||||
"notify",
|
||||
"parking_lot",
|
||||
"rayon",
|
||||
"ruff_index",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_trivia",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
"smallvec",
|
||||
"smol_str",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"tracing-tree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.4.1"
|
||||
@@ -1744,9 +1840,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4"
|
||||
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"libredox",
|
||||
@@ -1761,8 +1857,8 @@ checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.5",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-automata 0.4.6",
|
||||
"regex-syntax 0.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1776,13 +1872,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd"
|
||||
checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.2",
|
||||
"regex-syntax 0.8.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1793,9 +1889,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
|
||||
|
||||
[[package]]
|
||||
name = "result-like"
|
||||
@@ -2383,9 +2479,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.31"
|
||||
version = "0.38.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
|
||||
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"errno",
|
||||
@@ -2396,9 +2492,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.22.2"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41"
|
||||
checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
@@ -2410,15 +2506,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.3.1"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ede67b28608b4c60685c7d54122d4400d90f62b40caee7700e700380a390fa8"
|
||||
checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.2"
|
||||
version = "0.102.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
|
||||
checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -2427,9 +2523,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
|
||||
checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -2476,6 +2572,12 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
@@ -2537,9 +2639,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.18"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb"
|
||||
checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2566,9 +2668,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.7.0"
|
||||
version = "3.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a"
|
||||
checksum = "2c85f8e96d1d6857f13768fcbd895fcb06225510022a2774ed8b5150581847b0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -2577,9 +2679,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.7.0"
|
||||
version = "3.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655"
|
||||
checksum = "c8b3a576c4eb2924262d5951a3b737ccaf16c931e39a2810c36f9a7e25575557"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -2629,6 +2731,21 @@ version = "1.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
|
||||
|
||||
[[package]]
|
||||
name = "smol_str"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6845563ada680337a52d43bb0b29f396f2d911616f6573012645b9e3d048a49"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
@@ -2658,9 +2775,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
@@ -2780,6 +2897,17 @@ dependencies = [
|
||||
"test-case-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-linebreak",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.59"
|
||||
@@ -2878,9 +3006,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.9"
|
||||
version = "0.22.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4"
|
||||
checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
@@ -3035,6 +3163,12 @@ version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.23"
|
||||
@@ -3086,9 +3220,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "2.9.6"
|
||||
version = "2.9.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11f214ce18d8b2cbe84ed3aa6486ed3f5b285cf8d8fbdbce9f3f767a724adc35"
|
||||
checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"flate2",
|
||||
@@ -3296,9 +3430,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.68"
|
||||
version = "0.3.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446"
|
||||
checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -3352,11 +3486,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
|
||||
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3371,7 +3505,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.4",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3389,7 +3523,7 @@ version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.4",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3409,17 +3543,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.4",
|
||||
"windows_aarch64_msvc 0.52.4",
|
||||
"windows_i686_gnu 0.52.4",
|
||||
"windows_i686_msvc 0.52.4",
|
||||
"windows_x86_64_gnu 0.52.4",
|
||||
"windows_x86_64_gnullvm 0.52.4",
|
||||
"windows_x86_64_msvc 0.52.4",
|
||||
"windows_aarch64_gnullvm 0.52.5",
|
||||
"windows_aarch64_msvc 0.52.5",
|
||||
"windows_i686_gnu 0.52.5",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.5",
|
||||
"windows_x86_64_gnu 0.52.5",
|
||||
"windows_x86_64_gnullvm 0.52.5",
|
||||
"windows_x86_64_msvc 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3430,9 +3565,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
@@ -3442,9 +3577,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
@@ -3454,9 +3589,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
@@ -3466,9 +3607,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
@@ -3478,9 +3619,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
@@ -3490,9 +3631,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
@@ -3502,15 +3643,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.4"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.5"
|
||||
version = "0.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8"
|
||||
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -24,13 +24,14 @@ chrono = { version = "0.4.35", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.5.3", features = ["derive"] }
|
||||
clap_complete_command = { version = "0.5.1" }
|
||||
clearscreen = { version = "3.0.0" }
|
||||
codspeed-criterion-compat = { version = "2.4.0", default-features = false }
|
||||
codspeed-criterion-compat = { version = "2.6.0", default-features = false }
|
||||
colored = { version = "2.1.0" }
|
||||
console_error_panic_hook = { version = "0.1.7" }
|
||||
console_log = { version = "1.0.0" }
|
||||
countme = { version = "3.0.1" }
|
||||
criterion = { version = "0.5.1", default-features = false }
|
||||
crossbeam-channel = { version = "0.5.12" }
|
||||
dashmap = { version = "5.5.3" }
|
||||
dirs = { version = "5.0.0" }
|
||||
drop_bomb = { version = "0.1.5" }
|
||||
env_logger = { version = "0.11.0" }
|
||||
@@ -39,10 +40,12 @@ filetime = { version = "0.2.23" }
|
||||
fs-err = { version = "2.11.0" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
hashbrown = "0.14.3"
|
||||
hexf-parse = { version = "0.2.1" }
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
indexmap = { version = "2.2.6" }
|
||||
indicatif = { version = "0.17.8" }
|
||||
indoc = { version = "2.0.4" }
|
||||
insta = { version = "1.35.1", feature = ["filters", "glob"] }
|
||||
@@ -68,6 +71,7 @@ once_cell = { version = "1.19.0" }
|
||||
path-absolutize = { version = "3.1.1" }
|
||||
path-slash = { version = "0.2.1" }
|
||||
pathdiff = { version = "0.2.1" }
|
||||
parking_lot = "0.12.1"
|
||||
pep440_rs = { version = "0.6.0", features = ["serde"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
proc-macro2 = { version = "1.0.79" }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](LICENSE)
|
||||
[](https://github.com/astral-sh/ruff/blob/main/LICENSE)
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](https://github.com/astral-sh/ruff/actions)
|
||||
[](https://discord.com/invite/astral-sh)
|
||||
@@ -499,7 +499,7 @@ If you're using Ruff, consider adding the Ruff badge to your project's `README.m
|
||||
|
||||
## License
|
||||
|
||||
This repository is licensed under the [MIT License](LICENSE)
|
||||
This repository is licensed under the [MIT License](https://github.com/astral-sh/ruff/blob/main/LICENSE)
|
||||
|
||||
<div align="center">
|
||||
<a target="_blank" href="https://astral.sh" style="background:none">
|
||||
|
||||
45
crates/red_knot/Cargo.toml
Normal file
45
crates/red_knot/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "red_knot"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
ruff_python_parser = { path = "../ruff_python_parser" }
|
||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||
ruff_python_trivia = { path = "../ruff_python_trivia" }
|
||||
ruff_text_size = { path = "../ruff_text_size" }
|
||||
ruff_index = { path = "../ruff_index" }
|
||||
ruff_notebook = { path = "../ruff_notebook" }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
bitflags = { workspace = true }
|
||||
ctrlc = "3.4.4"
|
||||
crossbeam-channel = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
log = { workspace = true }
|
||||
notify = { workspace = true }
|
||||
parking_lot = { workspace = true }
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
smol_str = "0.2.1"
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
tracing-tree = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
textwrap = "0.16.1"
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
415
crates/red_knot/src/ast_ids.rs
Normal file
415
crates/red_knot/src/ast_ids.rs
Normal file
@@ -0,0 +1,415 @@
|
||||
use std::any::type_name;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_index::{Idx, IndexVec};
|
||||
use ruff_python_ast::visitor::preorder;
|
||||
use ruff_python_ast::visitor::preorder::{PreorderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::{
|
||||
AnyNodeRef, AstNode, ExceptHandler, ExceptHandlerExceptHandler, Expr, MatchCase, ModModule,
|
||||
NodeKind, Parameter, Stmt, StmtAnnAssign, StmtAssign, StmtAugAssign, StmtClassDef,
|
||||
StmtFunctionDef, StmtGlobal, StmtImport, StmtImportFrom, StmtNonlocal, StmtTypeAlias,
|
||||
TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple, WithItem,
|
||||
};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
/// A type agnostic ID that uniquely identifies an AST node in a file.
|
||||
#[ruff_index::newtype_index]
|
||||
pub struct AstId;
|
||||
|
||||
/// A typed ID that uniquely identifies an AST node in a file.
|
||||
///
|
||||
/// This is different from [`AstId`] in that it is a combination of ID and the type of the node the ID identifies.
|
||||
/// Typing the ID prevents mixing IDs of different node types and allows to restrict the API to only accept
|
||||
/// nodes for which an ID has been created (not all AST nodes get an ID).
|
||||
pub struct TypedAstId<N: HasAstId> {
|
||||
erased: AstId,
|
||||
_marker: PhantomData<fn() -> N>,
|
||||
}
|
||||
|
||||
impl<N: HasAstId> TypedAstId<N> {
|
||||
/// Upcasts this ID from a more specific node type to a more general node type.
|
||||
pub fn upcast<M: HasAstId>(self) -> TypedAstId<M>
|
||||
where
|
||||
N: Into<M>,
|
||||
{
|
||||
TypedAstId {
|
||||
erased: self.erased,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> Copy for TypedAstId<N> {}
|
||||
impl<N: HasAstId> Clone for TypedAstId<N> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> PartialEq for TypedAstId<N> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.erased == other.erased
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> Eq for TypedAstId<N> {}
|
||||
impl<N: HasAstId> Hash for TypedAstId<N> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.erased.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> Debug for TypedAstId<N> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("TypedAstId")
|
||||
.field(&self.erased)
|
||||
.field(&type_name::<N>())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AstIds {
|
||||
ids: IndexVec<AstId, NodeKey>,
|
||||
reverse: FxHashMap<NodeKey, AstId>,
|
||||
}
|
||||
|
||||
impl AstIds {
|
||||
// TODO rust analyzer doesn't allocate an ID for every node. It only allocates ids for
|
||||
// nodes with a corresponding HIR element, that is nodes that are definitions.
|
||||
pub fn from_module(module: &ModModule) -> Self {
|
||||
let mut visitor = AstIdsVisitor::default();
|
||||
|
||||
// TODO: visit_module?
|
||||
// Make sure we visit the root
|
||||
visitor.create_id(module);
|
||||
visitor.visit_body(&module.body);
|
||||
|
||||
while let Some(deferred) = visitor.deferred.pop() {
|
||||
match deferred {
|
||||
DeferredNode::FunctionDefinition(def) => {
|
||||
def.visit_preorder(&mut visitor);
|
||||
}
|
||||
DeferredNode::ClassDefinition(def) => def.visit_preorder(&mut visitor),
|
||||
}
|
||||
}
|
||||
|
||||
AstIds {
|
||||
ids: visitor.ids,
|
||||
reverse: visitor.reverse,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ID to the root node.
|
||||
pub fn root(&self) -> NodeKey {
|
||||
self.ids[AstId::new(0)]
|
||||
}
|
||||
|
||||
/// Returns the [`TypedAstId`] for a node.
|
||||
pub fn ast_id<N: HasAstId>(&self, node: &N) -> TypedAstId<N> {
|
||||
let key = node.syntax_node_key();
|
||||
TypedAstId {
|
||||
erased: self.reverse.get(&key).copied().unwrap(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [`TypedAstId`] for the node identified with the given [`TypedNodeKey`].
|
||||
pub fn ast_id_for_key<N: HasAstId>(&self, node: &TypedNodeKey<N>) -> TypedAstId<N> {
|
||||
let ast_id = self.ast_id_for_node_key(node.inner);
|
||||
|
||||
TypedAstId {
|
||||
erased: ast_id,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the untyped [`AstId`] for the node identified by the given `node` key.
|
||||
pub fn ast_id_for_node_key(&self, node: NodeKey) -> AstId {
|
||||
self.reverse
|
||||
.get(&node)
|
||||
.copied()
|
||||
.expect("Can't find node in AstIds map.")
|
||||
}
|
||||
|
||||
/// Returns the [`TypedNodeKey`] for the node identified by the given [`TypedAstId`].
|
||||
pub fn key<N: HasAstId>(&self, id: TypedAstId<N>) -> TypedNodeKey<N> {
|
||||
let syntax_key = self.ids[id.erased];
|
||||
|
||||
TypedNodeKey::new(syntax_key).unwrap()
|
||||
}
|
||||
|
||||
pub fn node_key<H: HasAstId>(&self, id: TypedAstId<H>) -> NodeKey {
|
||||
self.ids[id.erased]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AstIds {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
for (key, value) in self.ids.iter_enumerated() {
|
||||
map.entry(&key, &value);
|
||||
}
|
||||
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AstIds {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.ids == other.ids
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for AstIds {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AstIdsVisitor<'a> {
|
||||
ids: IndexVec<AstId, NodeKey>,
|
||||
reverse: FxHashMap<NodeKey, AstId>,
|
||||
deferred: Vec<DeferredNode<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> AstIdsVisitor<'a> {
|
||||
fn create_id<A: HasAstId>(&mut self, node: &A) {
|
||||
let node_key = node.syntax_node_key();
|
||||
|
||||
let id = self.ids.push(node_key);
|
||||
self.reverse.insert(node_key, id);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> PreorderVisitor<'a> for AstIdsVisitor<'a> {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
match stmt {
|
||||
Stmt::FunctionDef(def) => {
|
||||
self.create_id(def);
|
||||
self.deferred.push(DeferredNode::FunctionDefinition(def));
|
||||
return;
|
||||
}
|
||||
// TODO defer visiting the assignment body, type alias parameters etc?
|
||||
Stmt::ClassDef(def) => {
|
||||
self.create_id(def);
|
||||
self.deferred.push(DeferredNode::ClassDefinition(def));
|
||||
return;
|
||||
}
|
||||
Stmt::Expr(_) => {
|
||||
// Skip
|
||||
return;
|
||||
}
|
||||
Stmt::Return(_) => {}
|
||||
Stmt::Delete(_) => {}
|
||||
Stmt::Assign(assignment) => self.create_id(assignment),
|
||||
Stmt::AugAssign(assignment) => {
|
||||
self.create_id(assignment);
|
||||
}
|
||||
Stmt::AnnAssign(assignment) => self.create_id(assignment),
|
||||
Stmt::TypeAlias(assignment) => self.create_id(assignment),
|
||||
Stmt::For(_) => {}
|
||||
Stmt::While(_) => {}
|
||||
Stmt::If(_) => {}
|
||||
Stmt::With(_) => {}
|
||||
Stmt::Match(_) => {}
|
||||
Stmt::Raise(_) => {}
|
||||
Stmt::Try(_) => {}
|
||||
Stmt::Assert(_) => {}
|
||||
Stmt::Import(import) => self.create_id(import),
|
||||
Stmt::ImportFrom(import_from) => self.create_id(import_from),
|
||||
Stmt::Global(global) => self.create_id(global),
|
||||
Stmt::Nonlocal(non_local) => self.create_id(non_local),
|
||||
Stmt::Pass(_) => {}
|
||||
Stmt::Break(_) => {}
|
||||
Stmt::Continue(_) => {}
|
||||
Stmt::IpyEscapeCommand(_) => {}
|
||||
}
|
||||
|
||||
preorder::walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, _expr: &'a Expr) {}
|
||||
|
||||
fn visit_parameter(&mut self, parameter: &'a Parameter) {
|
||||
self.create_id(parameter);
|
||||
preorder::walk_parameter(self, parameter);
|
||||
}
|
||||
|
||||
fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) {
|
||||
match except_handler {
|
||||
ExceptHandler::ExceptHandler(except_handler) => {
|
||||
self.create_id(except_handler);
|
||||
}
|
||||
}
|
||||
|
||||
preorder::walk_except_handler(self, except_handler);
|
||||
}
|
||||
|
||||
fn visit_with_item(&mut self, with_item: &'a WithItem) {
|
||||
self.create_id(with_item);
|
||||
preorder::walk_with_item(self, with_item);
|
||||
}
|
||||
|
||||
fn visit_match_case(&mut self, match_case: &'a MatchCase) {
|
||||
self.create_id(match_case);
|
||||
preorder::walk_match_case(self, match_case);
|
||||
}
|
||||
|
||||
fn visit_type_param(&mut self, type_param: &'a TypeParam) {
|
||||
self.create_id(type_param);
|
||||
}
|
||||
}
|
||||
|
||||
enum DeferredNode<'a> {
|
||||
FunctionDefinition(&'a StmtFunctionDef),
|
||||
ClassDefinition(&'a StmtClassDef),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct TypedNodeKey<N: AstNode> {
|
||||
/// The type erased node key.
|
||||
inner: NodeKey,
|
||||
_marker: PhantomData<fn() -> N>,
|
||||
}
|
||||
|
||||
impl<N: AstNode> TypedNodeKey<N> {
|
||||
pub fn from_node(node: &N) -> Self {
|
||||
let inner = NodeKey {
|
||||
kind: node.as_any_node_ref().kind(),
|
||||
range: node.range(),
|
||||
};
|
||||
Self {
|
||||
inner,
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(node_key: NodeKey) -> Option<Self> {
|
||||
N::can_cast(node_key.kind).then_some(TypedNodeKey {
|
||||
inner: node_key,
|
||||
_marker: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn resolve<'a>(&self, root: AnyNodeRef<'a>) -> Option<N::Ref<'a>> {
|
||||
let node_ref = self.inner.resolve(root)?;
|
||||
|
||||
Some(N::cast_ref(node_ref).unwrap())
|
||||
}
|
||||
|
||||
pub fn resolve_unwrap<'a>(&self, root: AnyNodeRef<'a>) -> N::Ref<'a> {
|
||||
self.resolve(root).expect("node should resolve")
|
||||
}
|
||||
|
||||
pub fn erased(&self) -> &NodeKey {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
struct FindNodeKeyVisitor<'a> {
|
||||
key: NodeKey,
|
||||
result: Option<AnyNodeRef<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> PreorderVisitor<'a> for FindNodeKeyVisitor<'a> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
||||
if self.result.is_some() {
|
||||
return TraversalSignal::Skip;
|
||||
}
|
||||
|
||||
if node.range() == self.key.range && node.kind() == self.key.kind {
|
||||
self.result = Some(node);
|
||||
TraversalSignal::Skip
|
||||
} else if node.range().contains_range(self.key.range) {
|
||||
TraversalSignal::Traverse
|
||||
} else {
|
||||
TraversalSignal::Skip
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_body(&mut self, body: &'a [Stmt]) {
|
||||
// TODO it would be more efficient to use binary search instead of linear
|
||||
for stmt in body {
|
||||
if stmt.range().start() > self.key.range.end() {
|
||||
break;
|
||||
}
|
||||
|
||||
self.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO an alternative to this is to have a `NodeId` on each node (in increasing order depending on the position).
|
||||
// This would allow to reduce the size of this to a u32.
|
||||
// What would be nice if we could use an `Arc::weak_ref` here but that only works if we use
|
||||
// `Arc` internally
|
||||
// TODO: Implement the logic to resolve a node, given a db (and the correct file).
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct NodeKey {
|
||||
kind: NodeKind,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl NodeKey {
|
||||
pub fn resolve<'a>(&self, root: AnyNodeRef<'a>) -> Option<AnyNodeRef<'a>> {
|
||||
// We need to do a binary search here. Only traverse into a node if the range is withint the node
|
||||
let mut visitor = FindNodeKeyVisitor {
|
||||
key: *self,
|
||||
result: None,
|
||||
};
|
||||
|
||||
if visitor.enter_node(root) == TraversalSignal::Traverse {
|
||||
root.visit_preorder(&mut visitor);
|
||||
}
|
||||
|
||||
visitor.result
|
||||
}
|
||||
}
|
||||
|
||||
/// Marker trait implemented by AST nodes for which we extract the `AstId`.
|
||||
pub trait HasAstId: AstNode {
|
||||
fn node_key(&self) -> TypedNodeKey<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
TypedNodeKey {
|
||||
inner: self.syntax_node_key(),
|
||||
_marker: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn syntax_node_key(&self) -> NodeKey {
|
||||
NodeKey {
|
||||
kind: self.as_any_node_ref().kind(),
|
||||
range: self.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HasAstId for StmtFunctionDef {}
|
||||
impl HasAstId for StmtClassDef {}
|
||||
impl HasAstId for StmtAnnAssign {}
|
||||
impl HasAstId for StmtAugAssign {}
|
||||
impl HasAstId for StmtAssign {}
|
||||
impl HasAstId for StmtTypeAlias {}
|
||||
|
||||
impl HasAstId for ModModule {}
|
||||
|
||||
impl HasAstId for StmtImport {}
|
||||
|
||||
impl HasAstId for StmtImportFrom {}
|
||||
|
||||
impl HasAstId for Parameter {}
|
||||
|
||||
impl HasAstId for TypeParam {}
|
||||
impl HasAstId for Stmt {}
|
||||
impl HasAstId for TypeParamTypeVar {}
|
||||
impl HasAstId for TypeParamTypeVarTuple {}
|
||||
impl HasAstId for TypeParamParamSpec {}
|
||||
impl HasAstId for StmtGlobal {}
|
||||
impl HasAstId for StmtNonlocal {}
|
||||
|
||||
impl HasAstId for ExceptHandlerExceptHandler {}
|
||||
impl HasAstId for WithItem {}
|
||||
impl HasAstId for MatchCase {}
|
||||
158
crates/red_knot/src/cache.rs
Normal file
158
crates/red_knot/src/cache.rs
Normal file
@@ -0,0 +1,158 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::hash::Hash;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use dashmap::mapref::entry::Entry;
|
||||
|
||||
use crate::FxDashMap;
|
||||
|
||||
/// Simple key value cache that locks on a per-key level.
|
||||
pub struct KeyValueCache<K, V> {
|
||||
map: FxDashMap<K, V>,
|
||||
statistics: CacheStatistics,
|
||||
}
|
||||
|
||||
impl<K, V> KeyValueCache<K, V>
|
||||
where
|
||||
K: Eq + Hash + Clone,
|
||||
V: Clone,
|
||||
{
|
||||
pub fn try_get(&self, key: &K) -> Option<V> {
|
||||
if let Some(existing) = self.map.get(key) {
|
||||
self.statistics.hit();
|
||||
Some(existing.clone())
|
||||
} else {
|
||||
self.statistics.miss();
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get<F>(&self, key: &K, compute: F) -> V
|
||||
where
|
||||
F: FnOnce(&K) -> V,
|
||||
{
|
||||
match self.map.entry(key.clone()) {
|
||||
Entry::Occupied(cached) => {
|
||||
self.statistics.hit();
|
||||
|
||||
cached.get().clone()
|
||||
}
|
||||
Entry::Vacant(vacant) => {
|
||||
self.statistics.miss();
|
||||
|
||||
let value = compute(key);
|
||||
vacant.insert(value.clone());
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, key: K, value: V) {
|
||||
self.map.insert(key, value);
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, key: &K) -> Option<V> {
|
||||
self.map.remove(key).map(|(_, value)| value)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
self.map.shrink_to_fit();
|
||||
}
|
||||
|
||||
pub fn statistics(&self) -> Option<Statistics> {
|
||||
self.statistics.to_statistics()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> Default for KeyValueCache<K, V>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
V: Clone,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
map: FxDashMap::default(),
|
||||
statistics: CacheStatistics::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, V> std::fmt::Debug for KeyValueCache<K, V>
|
||||
where
|
||||
K: std::fmt::Debug + Eq + Hash,
|
||||
V: std::fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut debug = f.debug_map();
|
||||
|
||||
for entry in &self.map {
|
||||
debug.entry(&entry.value(), &entry.key());
|
||||
}
|
||||
|
||||
debug.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Statistics {
|
||||
pub hits: usize,
|
||||
pub misses: usize,
|
||||
}
|
||||
|
||||
impl Statistics {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
pub fn hit_rate(&self) -> Option<f64> {
|
||||
if self.hits + self.misses == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some((self.hits as f64) / (self.hits + self.misses) as f64)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub type CacheStatistics = DebugStatistics;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub type CacheStatistics = ReleaseStatistics;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DebugStatistics {
|
||||
hits: AtomicUsize,
|
||||
misses: AtomicUsize,
|
||||
}
|
||||
|
||||
impl DebugStatistics {
|
||||
// TODO figure out appropriate Ordering
|
||||
pub fn hit(&self) {
|
||||
self.hits.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn miss(&self) {
|
||||
self.misses.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub fn to_statistics(&self) -> Option<Statistics> {
|
||||
let hits = self.hits.load(Ordering::SeqCst);
|
||||
let misses = self.misses.load(Ordering::SeqCst);
|
||||
|
||||
Some(Statistics { hits, misses })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ReleaseStatistics;
|
||||
|
||||
impl ReleaseStatistics {
|
||||
#[inline]
|
||||
pub const fn hit(&self) {}
|
||||
|
||||
#[inline]
|
||||
pub const fn miss(&self) {}
|
||||
|
||||
#[inline]
|
||||
pub const fn to_statistics(&self) -> Option<Statistics> {
|
||||
None
|
||||
}
|
||||
}
|
||||
66
crates/red_knot/src/cancellation.rs
Normal file
66
crates/red_knot/src/cancellation.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::sync::{Arc, Condvar, Mutex};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CancellationTokenSource {
|
||||
signal: Arc<(Mutex<bool>, Condvar)>,
|
||||
}
|
||||
|
||||
impl CancellationTokenSource {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
signal: Arc::new((Mutex::new(false), Condvar::default())),
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace")]
|
||||
pub fn cancel(&self) {
|
||||
let (cancelled, condvar) = &*self.signal;
|
||||
|
||||
let mut cancelled = cancelled.lock().unwrap();
|
||||
|
||||
if *cancelled {
|
||||
return;
|
||||
}
|
||||
|
||||
*cancelled = true;
|
||||
condvar.notify_all();
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
let (cancelled, _) = &*self.signal;
|
||||
|
||||
*cancelled.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn token(&self) -> CancellationToken {
|
||||
CancellationToken {
|
||||
signal: self.signal.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct CancellationToken {
|
||||
signal: Arc<(Mutex<bool>, Condvar)>,
|
||||
}
|
||||
|
||||
impl CancellationToken {
|
||||
/// Returns `true` if cancellation has been requested.
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
let (cancelled, _) = &*self.signal;
|
||||
|
||||
*cancelled.lock().unwrap()
|
||||
}
|
||||
|
||||
pub fn wait(&self) {
|
||||
let (bool, condvar) = &*self.signal;
|
||||
|
||||
let lock = condvar
|
||||
.wait_while(bool.lock().unwrap(), |bool| !*bool)
|
||||
.unwrap();
|
||||
|
||||
debug_assert!(*lock);
|
||||
|
||||
drop(lock);
|
||||
}
|
||||
}
|
||||
178
crates/red_knot/src/db.rs
Normal file
178
crates/red_knot/src/db.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::files::FileId;
|
||||
use crate::lint::{Diagnostics, LintSyntaxStorage};
|
||||
use crate::module::{Module, ModuleData, ModuleName, ModuleResolver, ModuleSearchPath};
|
||||
use crate::parse::{Parsed, ParsedStorage};
|
||||
use crate::source::{Source, SourceStorage};
|
||||
use crate::symbols::{SymbolId, SymbolTable, SymbolTablesStorage};
|
||||
use crate::types::{Type, TypeStore};
|
||||
|
||||
pub trait SourceDb {
|
||||
// queries
|
||||
fn file_id(&self, path: &std::path::Path) -> FileId;
|
||||
|
||||
fn file_path(&self, file_id: FileId) -> Arc<std::path::Path>;
|
||||
|
||||
fn source(&self, file_id: FileId) -> Source;
|
||||
|
||||
fn parse(&self, file_id: FileId) -> Parsed;
|
||||
|
||||
fn lint_syntax(&self, file_id: FileId) -> Diagnostics;
|
||||
}
|
||||
|
||||
pub trait SemanticDb: SourceDb {
|
||||
// queries
|
||||
fn resolve_module(&self, name: ModuleName) -> Option<Module>;
|
||||
|
||||
fn file_to_module(&self, file_id: FileId) -> Option<Module>;
|
||||
|
||||
fn path_to_module(&self, path: &Path) -> Option<Module>;
|
||||
|
||||
fn symbol_table(&self, file_id: FileId) -> Arc<SymbolTable>;
|
||||
|
||||
fn infer_symbol_type(&self, file_id: FileId, symbol_id: SymbolId) -> Type;
|
||||
|
||||
// mutations
|
||||
|
||||
fn add_module(&mut self, path: &Path) -> Option<(Module, Vec<Arc<ModuleData>>)>;
|
||||
|
||||
fn set_module_search_paths(&mut self, paths: Vec<ModuleSearchPath>);
|
||||
}
|
||||
|
||||
pub trait Db: SemanticDb {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SourceJar {
|
||||
pub sources: SourceStorage,
|
||||
pub parsed: ParsedStorage,
|
||||
pub lint_syntax: LintSyntaxStorage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SemanticJar {
|
||||
pub module_resolver: ModuleResolver,
|
||||
pub symbol_tables: SymbolTablesStorage,
|
||||
pub type_store: TypeStore,
|
||||
}
|
||||
|
||||
/// Gives access to a specific jar in the database.
|
||||
///
|
||||
/// Nope, the terminology isn't borrowed from Java but from Salsa <https://salsa-rs.github.io/salsa/>,
|
||||
/// which is an analogy to storing the salsa in different jars.
|
||||
///
|
||||
/// The basic idea is that each crate can define its own jar and the jars can be combined to a single
|
||||
/// database in the top level crate. Each crate also defines its own `Database` trait. The combination of
|
||||
/// `Database` trait and the jar allows to write queries in isolation without having to know how they get composed at the upper levels.
|
||||
///
|
||||
/// Salsa further defines a `HasIngredient` trait which slices the jar to a specific storage (e.g. a specific cache).
|
||||
/// We don't need this just jet because we write our queries by hand. We may want a similar trait if we decide
|
||||
/// to use a macro to generate the queries.
|
||||
pub trait HasJar<T> {
|
||||
/// Gives a read-only reference to the jar.
|
||||
fn jar(&self) -> &T;
|
||||
|
||||
/// Gives a mutable reference to the jar.
|
||||
fn jar_mut(&mut self) -> &mut T;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use crate::db::{HasJar, SourceDb, SourceJar};
|
||||
use crate::files::{FileId, Files};
|
||||
use crate::lint::{lint_syntax, Diagnostics};
|
||||
use crate::module::{
|
||||
add_module, file_to_module, path_to_module, resolve_module, set_module_search_paths,
|
||||
Module, ModuleData, ModuleName, ModuleSearchPath,
|
||||
};
|
||||
use crate::parse::{parse, Parsed};
|
||||
use crate::source::{source_text, Source};
|
||||
use crate::symbols::{symbol_table, SymbolId, SymbolTable};
|
||||
use crate::types::{infer_symbol_type, Type};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{SemanticDb, SemanticJar};
|
||||
|
||||
// This can be a partial database used in a single crate for testing.
|
||||
// It would hold fewer data than the full database.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct TestDb {
|
||||
files: Files,
|
||||
source: SourceJar,
|
||||
semantic: SemanticJar,
|
||||
}
|
||||
|
||||
impl HasJar<SourceJar> for TestDb {
|
||||
fn jar(&self) -> &SourceJar {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn jar_mut(&mut self) -> &mut SourceJar {
|
||||
&mut self.source
|
||||
}
|
||||
}
|
||||
|
||||
impl HasJar<SemanticJar> for TestDb {
|
||||
fn jar(&self) -> &SemanticJar {
|
||||
&self.semantic
|
||||
}
|
||||
|
||||
fn jar_mut(&mut self) -> &mut SemanticJar {
|
||||
&mut self.semantic
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceDb for TestDb {
|
||||
fn file_id(&self, path: &Path) -> FileId {
|
||||
self.files.intern(path)
|
||||
}
|
||||
|
||||
fn file_path(&self, file_id: FileId) -> Arc<Path> {
|
||||
self.files.path(file_id)
|
||||
}
|
||||
|
||||
fn source(&self, file_id: FileId) -> Source {
|
||||
source_text(self, file_id)
|
||||
}
|
||||
|
||||
fn parse(&self, file_id: FileId) -> Parsed {
|
||||
parse(self, file_id)
|
||||
}
|
||||
|
||||
fn lint_syntax(&self, file_id: FileId) -> Diagnostics {
|
||||
lint_syntax(self, file_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl SemanticDb for TestDb {
|
||||
fn resolve_module(&self, name: ModuleName) -> Option<Module> {
|
||||
resolve_module(self, name)
|
||||
}
|
||||
|
||||
fn file_to_module(&self, file_id: FileId) -> Option<Module> {
|
||||
file_to_module(self, file_id)
|
||||
}
|
||||
|
||||
fn path_to_module(&self, path: &Path) -> Option<Module> {
|
||||
path_to_module(self, path)
|
||||
}
|
||||
|
||||
fn infer_symbol_type(&self, file_id: FileId, symbol_id: SymbolId) -> Type {
|
||||
infer_symbol_type(self, file_id, symbol_id)
|
||||
}
|
||||
|
||||
fn symbol_table(&self, file_id: FileId) -> Arc<SymbolTable> {
|
||||
symbol_table(self, file_id)
|
||||
}
|
||||
|
||||
fn add_module(&mut self, path: &Path) -> Option<(Module, Vec<Arc<ModuleData>>)> {
|
||||
add_module(self, path)
|
||||
}
|
||||
|
||||
fn set_module_search_paths(&mut self, paths: Vec<ModuleSearchPath>) {
|
||||
set_module_search_paths(self, paths);
|
||||
}
|
||||
}
|
||||
}
|
||||
148
crates/red_knot/src/files.rs
Normal file
148
crates/red_knot/src/files.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use hashbrown::hash_map::RawEntryMut;
|
||||
use parking_lot::RwLock;
|
||||
use rustc_hash::FxHasher;
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
|
||||
type Map<K, V> = hashbrown::HashMap<K, V, ()>;
|
||||
|
||||
#[newtype_index]
|
||||
pub struct FileId;
|
||||
|
||||
// TODO we'll need a higher level virtual file system abstraction that allows testing if a file exists
|
||||
// or retrieving its content (ideally lazily and in a way that the memory can be retained later)
|
||||
// I suspect that we'll end up with a FileSystem trait and our own Path abstraction.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Files {
|
||||
inner: Arc<RwLock<FilesInner>>,
|
||||
}
|
||||
|
||||
impl Files {
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub fn intern(&self, path: &Path) -> FileId {
|
||||
self.inner.write().intern(path)
|
||||
}
|
||||
|
||||
pub fn try_get(&self, path: &Path) -> Option<FileId> {
|
||||
self.inner.read().try_get(path)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub fn path(&self, id: FileId) -> Arc<Path> {
|
||||
self.inner.read().path(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Files {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let files = self.inner.read();
|
||||
let mut debug = f.debug_map();
|
||||
for item in files.iter() {
|
||||
debug.entry(&item.0, &item.1);
|
||||
}
|
||||
|
||||
debug.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Files {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.inner.read().eq(&other.inner.read())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Files {}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FilesInner {
|
||||
by_path: Map<FileId, ()>,
|
||||
// TODO should we use a map here to reclaim the space for removed files?
|
||||
// TODO I think we should use our own path abstraction here to avoid having to normalize paths
|
||||
// and dealing with non-utf paths everywhere.
|
||||
by_id: IndexVec<FileId, Arc<Path>>,
|
||||
}
|
||||
|
||||
impl FilesInner {
|
||||
/// Inserts the path and returns a new id for it or returns the id if it is an existing path.
|
||||
// TODO should this accept Path or PathBuf?
|
||||
pub(crate) fn intern(&mut self, path: &Path) -> FileId {
|
||||
let mut hasher = FxHasher::default();
|
||||
path.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
let entry = self
|
||||
.by_path
|
||||
.raw_entry_mut()
|
||||
.from_hash(hash, |existing_file| &*self.by_id[*existing_file] == path);
|
||||
|
||||
match entry {
|
||||
RawEntryMut::Occupied(entry) => *entry.key(),
|
||||
RawEntryMut::Vacant(entry) => {
|
||||
let id = self.by_id.push(Arc::from(path));
|
||||
entry.insert_with_hasher(hash, id, (), |_| hash);
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn try_get(&self, path: &Path) -> Option<FileId> {
|
||||
let mut hasher = FxHasher::default();
|
||||
path.hash(&mut hasher);
|
||||
let hash = hasher.finish();
|
||||
|
||||
Some(
|
||||
*self
|
||||
.by_path
|
||||
.raw_entry()
|
||||
.from_hash(hash, |existing_file| &*self.by_id[*existing_file] == path)?
|
||||
.0,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns the path for the file with the given id.
|
||||
pub(crate) fn path(&self, id: FileId) -> Arc<Path> {
|
||||
self.by_id[id].clone()
|
||||
}
|
||||
|
||||
pub(crate) fn iter(&self) -> impl Iterator<Item = (FileId, Arc<Path>)> + '_ {
|
||||
self.by_path.keys().map(|id| (*id, self.by_id[*id].clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for FilesInner {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.by_id == other.by_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for FilesInner {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn insert_path_twice_same_id() {
|
||||
let files = Files::default();
|
||||
let path = PathBuf::from("foo/bar");
|
||||
let id1 = files.intern(&path);
|
||||
let id2 = files.intern(&path);
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_different_paths_different_ids() {
|
||||
let files = Files::default();
|
||||
let path1 = PathBuf::from("foo/bar");
|
||||
let path2 = PathBuf::from("foo/bar/baz");
|
||||
let id1 = files.intern(&path1);
|
||||
let id2 = files.intern(&path2);
|
||||
assert_ne!(id1, id2);
|
||||
}
|
||||
}
|
||||
67
crates/red_knot/src/hir.rs
Normal file
67
crates/red_knot/src/hir.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! Key observations
|
||||
//!
|
||||
//! The HIR avoids allocations to large extends by:
|
||||
//! * Using an arena per node type
|
||||
//! * using ids and id ranges to reference items.
|
||||
//!
|
||||
//! Using separate arena per node type has the advantage that the IDs are relatively stable, because
|
||||
//! they only change when a node of the same kind has been added or removed. (What's unclear is if that matters or if
|
||||
//! it still triggers a re-compute because the AST-id in the node has changed).
|
||||
//!
|
||||
//! The HIR does not store all details. It mainly stores the *public* interface. There's a reference
|
||||
//! back to the AST node to get more details.
|
||||
//!
|
||||
//!
|
||||
|
||||
use crate::ast_ids::{HasAstId, TypedAstId};
|
||||
use crate::files::FileId;
|
||||
use std::fmt::Formatter;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
pub struct HirAstId<N: HasAstId> {
|
||||
file_id: FileId,
|
||||
node_id: TypedAstId<N>,
|
||||
}
|
||||
|
||||
impl<N: HasAstId> Copy for HirAstId<N> {}
|
||||
impl<N: HasAstId> Clone for HirAstId<N> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> PartialEq for HirAstId<N> {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.file_id == other.file_id && self.node_id == other.node_id
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> Eq for HirAstId<N> {}
|
||||
|
||||
impl<N: HasAstId> std::fmt::Debug for HirAstId<N> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("HirAstId")
|
||||
.field("file_id", &self.file_id)
|
||||
.field("node_id", &self.node_id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> Hash for HirAstId<N> {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.file_id.hash(state);
|
||||
self.node_id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<N: HasAstId> HirAstId<N> {
|
||||
pub fn upcast<M: HasAstId>(self) -> HirAstId<M>
|
||||
where
|
||||
N: Into<M>,
|
||||
{
|
||||
HirAstId {
|
||||
file_id: self.file_id,
|
||||
node_id: self.node_id.upcast(),
|
||||
}
|
||||
}
|
||||
}
|
||||
556
crates/red_knot/src/hir/definition.rs
Normal file
556
crates/red_knot/src/hir/definition.rs
Normal file
@@ -0,0 +1,556 @@
|
||||
use std::ops::{Index, Range};
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_ast::visitor::preorder;
|
||||
use ruff_python_ast::visitor::preorder::PreorderVisitor;
|
||||
use ruff_python_ast::{
|
||||
Decorator, ExceptHandler, ExceptHandlerExceptHandler, Expr, MatchCase, ModModule, Stmt,
|
||||
StmtAnnAssign, StmtAssign, StmtClassDef, StmtFunctionDef, StmtGlobal, StmtImport,
|
||||
StmtImportFrom, StmtNonlocal, StmtTypeAlias, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
|
||||
TypeParamTypeVarTuple, WithItem,
|
||||
};
|
||||
|
||||
use crate::ast_ids::{AstIds, HasAstId};
|
||||
use crate::files::FileId;
|
||||
use crate::hir::HirAstId;
|
||||
use crate::Name;
|
||||
|
||||
#[newtype_index]
|
||||
pub struct FunctionId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Function {
|
||||
ast_id: HirAstId<StmtFunctionDef>,
|
||||
name: Name,
|
||||
parameters: Range<ParameterId>,
|
||||
type_parameters: Range<TypeParameterId>, // TODO: type_parameters, return expression, decorators
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct ParameterId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Parameter {
|
||||
kind: ParameterKind,
|
||||
name: Name,
|
||||
default: Option<()>, // TODO use expression HIR
|
||||
ast_id: HirAstId<ruff_python_ast::Parameter>,
|
||||
}
|
||||
|
||||
// TODO or should `Parameter` be an enum?
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum ParameterKind {
|
||||
PositionalOnly,
|
||||
Arguments,
|
||||
Vararg,
|
||||
KeywordOnly,
|
||||
Kwarg,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct ClassId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Class {
|
||||
name: Name,
|
||||
ast_id: HirAstId<StmtClassDef>,
|
||||
// TODO type parameters, inheritance, decorators, members
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct AssignmentId;
|
||||
|
||||
// This can have more than one name...
|
||||
// but that means we can't implement `name()` on `ModuleItem`.
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Assignment {
|
||||
// TODO: Handle multiple names / targets
|
||||
name: Name,
|
||||
ast_id: HirAstId<StmtAssign>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct AnnotatedAssignment {
|
||||
name: Name,
|
||||
ast_id: HirAstId<StmtAnnAssign>,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct AnnotatedAssignmentId;
|
||||
|
||||
#[newtype_index]
|
||||
pub struct TypeAliasId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct TypeAlias {
|
||||
name: Name,
|
||||
ast_id: HirAstId<StmtTypeAlias>,
|
||||
parameters: Range<TypeParameterId>,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct TypeParameterId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum TypeParameter {
|
||||
TypeVar(TypeParameterTypeVar),
|
||||
ParamSpec(TypeParameterParamSpec),
|
||||
TypeVarTuple(TypeParameterTypeVarTuple),
|
||||
}
|
||||
|
||||
impl TypeParameter {
|
||||
pub fn ast_id(&self) -> HirAstId<TypeParam> {
|
||||
match self {
|
||||
TypeParameter::TypeVar(type_var) => type_var.ast_id.upcast(),
|
||||
TypeParameter::ParamSpec(param_spec) => param_spec.ast_id.upcast(),
|
||||
TypeParameter::TypeVarTuple(type_var_tuple) => type_var_tuple.ast_id.upcast(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct TypeParameterTypeVar {
|
||||
name: Name,
|
||||
ast_id: HirAstId<TypeParamTypeVar>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct TypeParameterParamSpec {
|
||||
name: Name,
|
||||
ast_id: HirAstId<TypeParamParamSpec>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct TypeParameterTypeVarTuple {
|
||||
name: Name,
|
||||
ast_id: HirAstId<TypeParamTypeVarTuple>,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct GlobalId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Global {
|
||||
// TODO track names
|
||||
ast_id: HirAstId<StmtGlobal>,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct NonLocalId;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct NonLocal {
|
||||
// TODO track names
|
||||
ast_id: HirAstId<StmtNonlocal>,
|
||||
}
|
||||
|
||||
pub enum DefinitionId {
|
||||
Function(FunctionId),
|
||||
Parameter(ParameterId),
|
||||
Class(ClassId),
|
||||
Assignment(AssignmentId),
|
||||
AnnotatedAssignment(AnnotatedAssignmentId),
|
||||
Global(GlobalId),
|
||||
NonLocal(NonLocalId),
|
||||
TypeParameter(TypeParameterId),
|
||||
TypeAlias(TypeAlias),
|
||||
}
|
||||
|
||||
pub enum DefinitionItem {
|
||||
Function(Function),
|
||||
Parameter(Parameter),
|
||||
Class(Class),
|
||||
Assignment(Assignment),
|
||||
AnnotatedAssignment(AnnotatedAssignment),
|
||||
Global(Global),
|
||||
NonLocal(NonLocal),
|
||||
TypeParameter(TypeParameter),
|
||||
TypeAlias(TypeAlias),
|
||||
}
|
||||
|
||||
// The closest is rust-analyzers item-tree. It only represents "Items" which make the public interface of a module
|
||||
// (it excludes any other statement or expressions). rust-analyzer uses it as the main input to the name resolution
|
||||
// algorithm
|
||||
// > It is the input to the name resolution algorithm, as well as to the queries defined in `adt.rs`,
|
||||
// > `data.rs`, and most things in `attr.rs`.
|
||||
//
|
||||
// > One important purpose of this layer is to provide an "invalidation barrier" for incremental
|
||||
// > computations: when typing inside an item body, the `ItemTree` of the modified file is typically
|
||||
// > unaffected, so we don't have to recompute name resolution results or item data (see `data.rs`).
|
||||
//
|
||||
// I haven't fully figured this out but I think that this composes the "public" interface of a module?
|
||||
// But maybe that's too optimistic.
|
||||
//
|
||||
//
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq)]
|
||||
pub struct Definitions {
|
||||
functions: IndexVec<FunctionId, Function>,
|
||||
parameters: IndexVec<ParameterId, Parameter>,
|
||||
classes: IndexVec<ClassId, Class>,
|
||||
assignments: IndexVec<AssignmentId, Assignment>,
|
||||
annotated_assignments: IndexVec<AnnotatedAssignmentId, AnnotatedAssignment>,
|
||||
type_aliases: IndexVec<TypeAliasId, TypeAlias>,
|
||||
type_parameters: IndexVec<TypeParameterId, TypeParameter>,
|
||||
globals: IndexVec<GlobalId, Global>,
|
||||
non_locals: IndexVec<NonLocalId, NonLocal>,
|
||||
}
|
||||
|
||||
impl Definitions {
|
||||
pub fn from_module(module: &ModModule, ast_ids: &AstIds, file_id: FileId) -> Self {
|
||||
let mut visitor = DefinitionsVisitor {
|
||||
definitions: Definitions::default(),
|
||||
ast_ids,
|
||||
file_id,
|
||||
};
|
||||
|
||||
visitor.visit_body(&module.body);
|
||||
|
||||
visitor.definitions
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<FunctionId> for Definitions {
|
||||
type Output = Function;
|
||||
|
||||
fn index(&self, index: FunctionId) -> &Self::Output {
|
||||
&self.functions[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<ParameterId> for Definitions {
|
||||
type Output = Parameter;
|
||||
|
||||
fn index(&self, index: ParameterId) -> &Self::Output {
|
||||
&self.parameters[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<ClassId> for Definitions {
|
||||
type Output = Class;
|
||||
|
||||
fn index(&self, index: ClassId) -> &Self::Output {
|
||||
&self.classes[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<AssignmentId> for Definitions {
|
||||
type Output = Assignment;
|
||||
|
||||
fn index(&self, index: AssignmentId) -> &Self::Output {
|
||||
&self.assignments[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<AnnotatedAssignmentId> for Definitions {
|
||||
type Output = AnnotatedAssignment;
|
||||
|
||||
fn index(&self, index: AnnotatedAssignmentId) -> &Self::Output {
|
||||
&self.annotated_assignments[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<TypeAliasId> for Definitions {
|
||||
type Output = TypeAlias;
|
||||
|
||||
fn index(&self, index: TypeAliasId) -> &Self::Output {
|
||||
&self.type_aliases[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<GlobalId> for Definitions {
|
||||
type Output = Global;
|
||||
|
||||
fn index(&self, index: GlobalId) -> &Self::Output {
|
||||
&self.globals[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<NonLocalId> for Definitions {
|
||||
type Output = NonLocal;
|
||||
|
||||
fn index(&self, index: NonLocalId) -> &Self::Output {
|
||||
&self.non_locals[index]
|
||||
}
|
||||
}
|
||||
|
||||
impl Index<TypeParameterId> for Definitions {
|
||||
type Output = TypeParameter;
|
||||
|
||||
fn index(&self, index: TypeParameterId) -> &Self::Output {
|
||||
&self.type_parameters[index]
|
||||
}
|
||||
}
|
||||
|
||||
struct DefinitionsVisitor<'a> {
|
||||
definitions: Definitions,
|
||||
ast_ids: &'a AstIds,
|
||||
file_id: FileId,
|
||||
}
|
||||
|
||||
impl DefinitionsVisitor<'_> {
|
||||
fn ast_id<N: HasAstId>(&self, node: &N) -> HirAstId<N> {
|
||||
HirAstId {
|
||||
file_id: self.file_id,
|
||||
node_id: self.ast_ids.ast_id(node),
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_function_def(&mut self, function: &StmtFunctionDef) -> FunctionId {
|
||||
let name = Name::new(&function.name);
|
||||
|
||||
let first_type_parameter_id = self.definitions.type_parameters.next_index();
|
||||
let mut last_type_parameter_id = first_type_parameter_id;
|
||||
|
||||
if let Some(type_params) = &function.type_params {
|
||||
for parameter in &type_params.type_params {
|
||||
let id = self.lower_type_parameter(parameter);
|
||||
last_type_parameter_id = id;
|
||||
}
|
||||
}
|
||||
|
||||
let parameters = self.lower_parameters(&function.parameters);
|
||||
|
||||
self.definitions.functions.push(Function {
|
||||
name,
|
||||
ast_id: self.ast_id(function),
|
||||
parameters,
|
||||
type_parameters: first_type_parameter_id..last_type_parameter_id,
|
||||
})
|
||||
}
|
||||
|
||||
fn lower_parameters(&mut self, parameters: &ruff_python_ast::Parameters) -> Range<ParameterId> {
|
||||
let first_parameter_id = self.definitions.parameters.next_index();
|
||||
let mut last_parameter_id = first_parameter_id;
|
||||
|
||||
for parameter in ¶meters.posonlyargs {
|
||||
last_parameter_id = self.definitions.parameters.push(Parameter {
|
||||
kind: ParameterKind::PositionalOnly,
|
||||
name: Name::new(¶meter.parameter.name),
|
||||
default: None,
|
||||
ast_id: self.ast_id(¶meter.parameter),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(vararg) = ¶meters.vararg {
|
||||
last_parameter_id = self.definitions.parameters.push(Parameter {
|
||||
kind: ParameterKind::Vararg,
|
||||
name: Name::new(&vararg.name),
|
||||
default: None,
|
||||
ast_id: self.ast_id(vararg),
|
||||
});
|
||||
}
|
||||
|
||||
for parameter in ¶meters.kwonlyargs {
|
||||
last_parameter_id = self.definitions.parameters.push(Parameter {
|
||||
kind: ParameterKind::KeywordOnly,
|
||||
name: Name::new(¶meter.parameter.name),
|
||||
default: None,
|
||||
ast_id: self.ast_id(¶meter.parameter),
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(kwarg) = ¶meters.kwarg {
|
||||
last_parameter_id = self.definitions.parameters.push(Parameter {
|
||||
kind: ParameterKind::KeywordOnly,
|
||||
name: Name::new(&kwarg.name),
|
||||
default: None,
|
||||
ast_id: self.ast_id(kwarg),
|
||||
});
|
||||
}
|
||||
|
||||
first_parameter_id..last_parameter_id
|
||||
}
|
||||
|
||||
fn lower_class_def(&mut self, class: &StmtClassDef) -> ClassId {
|
||||
let name = Name::new(&class.name);
|
||||
|
||||
self.definitions.classes.push(Class {
|
||||
name,
|
||||
ast_id: self.ast_id(class),
|
||||
})
|
||||
}
|
||||
|
||||
fn lower_assignment(&mut self, assignment: &StmtAssign) {
|
||||
// FIXME handle multiple names
|
||||
if let Some(Expr::Name(name)) = assignment.targets.first() {
|
||||
self.definitions.assignments.push(Assignment {
|
||||
name: Name::new(&name.id),
|
||||
ast_id: self.ast_id(assignment),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_annotated_assignment(&mut self, annotated_assignment: &StmtAnnAssign) {
|
||||
if let Expr::Name(name) = &*annotated_assignment.target {
|
||||
self.definitions
|
||||
.annotated_assignments
|
||||
.push(AnnotatedAssignment {
|
||||
name: Name::new(&name.id),
|
||||
ast_id: self.ast_id(annotated_assignment),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_type_alias(&mut self, type_alias: &StmtTypeAlias) {
|
||||
if let Expr::Name(name) = &*type_alias.name {
|
||||
let name = Name::new(&name.id);
|
||||
|
||||
let lower_parameters_id = self.definitions.type_parameters.next_index();
|
||||
let mut last_parameter_id = lower_parameters_id;
|
||||
|
||||
if let Some(type_params) = &type_alias.type_params {
|
||||
for type_parameter in &type_params.type_params {
|
||||
let id = self.lower_type_parameter(type_parameter);
|
||||
last_parameter_id = id;
|
||||
}
|
||||
}
|
||||
|
||||
self.definitions.type_aliases.push(TypeAlias {
|
||||
name,
|
||||
ast_id: self.ast_id(type_alias),
|
||||
parameters: lower_parameters_id..last_parameter_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_type_parameter(&mut self, type_parameter: &TypeParam) -> TypeParameterId {
|
||||
match type_parameter {
|
||||
TypeParam::TypeVar(type_var) => {
|
||||
self.definitions
|
||||
.type_parameters
|
||||
.push(TypeParameter::TypeVar(TypeParameterTypeVar {
|
||||
name: Name::new(&type_var.name),
|
||||
ast_id: self.ast_id(type_var),
|
||||
}))
|
||||
}
|
||||
TypeParam::ParamSpec(param_spec) => {
|
||||
self.definitions
|
||||
.type_parameters
|
||||
.push(TypeParameter::ParamSpec(TypeParameterParamSpec {
|
||||
name: Name::new(¶m_spec.name),
|
||||
ast_id: self.ast_id(param_spec),
|
||||
}))
|
||||
}
|
||||
TypeParam::TypeVarTuple(type_var_tuple) => {
|
||||
self.definitions
|
||||
.type_parameters
|
||||
.push(TypeParameter::TypeVarTuple(TypeParameterTypeVarTuple {
|
||||
name: Name::new(&type_var_tuple.name),
|
||||
ast_id: self.ast_id(type_var_tuple),
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lower_import(&mut self, _import: &StmtImport) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fn lower_import_from(&mut self, _import_from: &StmtImportFrom) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fn lower_global(&mut self, global: &StmtGlobal) -> GlobalId {
|
||||
self.definitions.globals.push(Global {
|
||||
ast_id: self.ast_id(global),
|
||||
})
|
||||
}
|
||||
|
||||
fn lower_non_local(&mut self, non_local: &StmtNonlocal) -> NonLocalId {
|
||||
self.definitions.non_locals.push(NonLocal {
|
||||
ast_id: self.ast_id(non_local),
|
||||
})
|
||||
}
|
||||
|
||||
fn lower_except_handler(&mut self, _except_handler: &ExceptHandlerExceptHandler) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fn lower_with_item(&mut self, _with_item: &WithItem) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
fn lower_match_case(&mut self, _match_case: &MatchCase) {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
impl PreorderVisitor<'_> for DefinitionsVisitor<'_> {
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
match stmt {
|
||||
// Definition statements
|
||||
Stmt::FunctionDef(definition) => {
|
||||
self.lower_function_def(definition);
|
||||
self.visit_body(&definition.body);
|
||||
}
|
||||
Stmt::ClassDef(definition) => {
|
||||
self.lower_class_def(definition);
|
||||
self.visit_body(&definition.body);
|
||||
}
|
||||
Stmt::Assign(assignment) => {
|
||||
self.lower_assignment(assignment);
|
||||
}
|
||||
Stmt::AnnAssign(annotated_assignment) => {
|
||||
self.lower_annotated_assignment(annotated_assignment);
|
||||
}
|
||||
Stmt::TypeAlias(type_alias) => {
|
||||
self.lower_type_alias(type_alias);
|
||||
}
|
||||
|
||||
Stmt::Import(import) => self.lower_import(import),
|
||||
Stmt::ImportFrom(import_from) => self.lower_import_from(import_from),
|
||||
Stmt::Global(global) => {
|
||||
self.lower_global(global);
|
||||
}
|
||||
Stmt::Nonlocal(non_local) => {
|
||||
self.lower_non_local(non_local);
|
||||
}
|
||||
|
||||
// Visit the compound statement bodies because they can contain other definitions.
|
||||
Stmt::For(_)
|
||||
| Stmt::While(_)
|
||||
| Stmt::If(_)
|
||||
| Stmt::With(_)
|
||||
| Stmt::Match(_)
|
||||
| Stmt::Try(_) => {
|
||||
preorder::walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
// Skip over simple statements because they can't contain any other definitions.
|
||||
Stmt::Return(_)
|
||||
| Stmt::Delete(_)
|
||||
| Stmt::AugAssign(_)
|
||||
| Stmt::Raise(_)
|
||||
| Stmt::Assert(_)
|
||||
| Stmt::Expr(_)
|
||||
| Stmt::Pass(_)
|
||||
| Stmt::Break(_)
|
||||
| Stmt::Continue(_)
|
||||
| Stmt::IpyEscapeCommand(_) => {
|
||||
// No op
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, _: &'_ Expr) {}
|
||||
|
||||
fn visit_decorator(&mut self, _decorator: &'_ Decorator) {}
|
||||
|
||||
fn visit_except_handler(&mut self, except_handler: &'_ ExceptHandler) {
|
||||
match except_handler {
|
||||
ExceptHandler::ExceptHandler(except_handler) => {
|
||||
self.lower_except_handler(except_handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_with_item(&mut self, with_item: &'_ WithItem) {
|
||||
self.lower_with_item(with_item);
|
||||
}
|
||||
|
||||
fn visit_match_case(&mut self, match_case: &'_ MatchCase) {
|
||||
self.lower_match_case(match_case);
|
||||
self.visit_body(&match_case.body);
|
||||
}
|
||||
}
|
||||
102
crates/red_knot/src/lib.rs
Normal file
102
crates/red_knot/src/lib.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::hash::BuildHasherDefault;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use rustc_hash::{FxHashSet, FxHasher};
|
||||
|
||||
use crate::files::FileId;
|
||||
|
||||
pub mod ast_ids;
|
||||
pub mod cache;
|
||||
pub mod cancellation;
|
||||
pub mod db;
|
||||
pub mod files;
|
||||
pub mod hir;
|
||||
pub mod lint;
|
||||
pub mod module;
|
||||
mod parse;
|
||||
pub mod program;
|
||||
pub mod source;
|
||||
mod symbols;
|
||||
mod types;
|
||||
pub mod watch;
|
||||
|
||||
pub(crate) type FxDashMap<K, V> = dashmap::DashMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
#[allow(unused)]
|
||||
pub(crate) type FxDashSet<V> = dashmap::DashSet<V, BuildHasherDefault<FxHasher>>;
|
||||
pub(crate) type FxIndexSet<V> = indexmap::set::IndexSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Workspace {
|
||||
/// TODO this should be a resolved path. We should probably use a newtype wrapper that guarantees that
|
||||
/// PATH is a UTF-8 path and is normalized.
|
||||
root: PathBuf,
|
||||
/// The files that are open in the workspace.
|
||||
///
|
||||
/// * Editor: The files that are actively being edited in the editor (the user has a tab open with the file).
|
||||
/// * CLI: The resolved files passed as arguments to the CLI.
|
||||
open_files: FxHashSet<FileId>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(root: PathBuf) -> Self {
|
||||
Self {
|
||||
root,
|
||||
open_files: FxHashSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &Path {
|
||||
self.root.as_path()
|
||||
}
|
||||
|
||||
// TODO having the content in workspace feels wrong.
|
||||
pub fn open_file(&mut self, file_id: FileId) {
|
||||
self.open_files.insert(file_id);
|
||||
}
|
||||
|
||||
pub fn close_file(&mut self, file_id: FileId) {
|
||||
self.open_files.remove(&file_id);
|
||||
}
|
||||
|
||||
// TODO introduce an `OpenFile` type instead of using an anonymous tuple.
|
||||
pub fn open_files(&self) -> impl Iterator<Item = FileId> + '_ {
|
||||
self.open_files.iter().copied()
|
||||
}
|
||||
|
||||
pub fn is_file_open(&self, file_id: FileId) -> bool {
|
||||
self.open_files.contains(&file_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Name(smol_str::SmolStr);
|
||||
|
||||
impl Name {
|
||||
#[inline]
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self(smol_str::SmolStr::new(name))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Name {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Name
|
||||
where
|
||||
T: Into<smol_str::SmolStr>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
125
crates/red_knot/src/lint.rs
Normal file
125
crates/red_knot/src/lint.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::StringLiteral;
|
||||
|
||||
use crate::cache::KeyValueCache;
|
||||
use crate::db::{HasJar, SourceDb, SourceJar};
|
||||
use crate::files::FileId;
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub(crate) fn lint_syntax<Db>(db: &Db, file_id: FileId) -> Diagnostics
|
||||
where
|
||||
Db: SourceDb + HasJar<SourceJar>,
|
||||
{
|
||||
let storage = &db.jar().lint_syntax;
|
||||
|
||||
storage.get(&file_id, |file_id| {
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let source = db.source(*file_id);
|
||||
lint_lines(source.text(), &mut diagnostics);
|
||||
|
||||
let parsed = db.parse(*file_id);
|
||||
|
||||
if parsed.errors().is_empty() {
|
||||
let ast = parsed.ast();
|
||||
|
||||
let mut visitor = SyntaxLintVisitor {
|
||||
diagnostics,
|
||||
source: source.text(),
|
||||
};
|
||||
visitor.visit_body(&ast.body);
|
||||
diagnostics = visitor.diagnostics;
|
||||
} else {
|
||||
diagnostics.extend(parsed.errors().iter().map(std::string::ToString::to_string));
|
||||
}
|
||||
|
||||
Diagnostics::from(diagnostics)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn lint_lines(source: &str, diagnostics: &mut Vec<String>) {
|
||||
for (line_number, line) in source.lines().enumerate() {
|
||||
if line.len() < 88 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let char_count = line.chars().count();
|
||||
if char_count > 88 {
|
||||
diagnostics.push(format!(
|
||||
"Line {} is too long ({} characters)",
|
||||
line_number + 1,
|
||||
char_count
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SyntaxLintVisitor<'a> {
|
||||
diagnostics: Vec<String>,
|
||||
source: &'a str,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for SyntaxLintVisitor<'_> {
|
||||
fn visit_string_literal(&mut self, string_literal: &'_ StringLiteral) {
|
||||
// A very naive implementation of use double quotes
|
||||
let text = &self.source[string_literal.range];
|
||||
|
||||
if text.starts_with('\'') {
|
||||
self.diagnostics
|
||||
.push("Use double quotes for strings".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Diagnostics {
|
||||
Empty,
|
||||
List(Arc<Vec<String>>),
|
||||
}
|
||||
|
||||
impl Diagnostics {
|
||||
pub fn as_slice(&self) -> &[String] {
|
||||
match self {
|
||||
Diagnostics::Empty => &[],
|
||||
Diagnostics::List(list) => list.as_slice(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Diagnostics {
|
||||
type Target = [String];
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for Diagnostics {
|
||||
fn from(value: Vec<String>) -> Self {
|
||||
if value.is_empty() {
|
||||
Diagnostics::Empty
|
||||
} else {
|
||||
Diagnostics::List(Arc::new(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LintSyntaxStorage(KeyValueCache<FileId, Diagnostics>);
|
||||
|
||||
impl Deref for LintSyntaxStorage {
|
||||
type Target = KeyValueCache<FileId, Diagnostics>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for LintSyntaxStorage {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
469
crates/red_knot/src/main.rs
Normal file
469
crates/red_knot/src/main.rs
Normal file
@@ -0,0 +1,469 @@
|
||||
#![allow(clippy::dbg_macro)]
|
||||
|
||||
use std::collections::hash_map::Entry;
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use tracing::subscriber::Interest;
|
||||
use tracing::{Level, Metadata};
|
||||
use tracing_subscriber::filter::LevelFilter;
|
||||
use tracing_subscriber::layer::{Context, Filter, SubscriberExt};
|
||||
use tracing_subscriber::{Layer, Registry};
|
||||
use tracing_tree::time::Uptime;
|
||||
|
||||
use red_knot::cancellation::CancellationTokenSource;
|
||||
use red_knot::db::{HasJar, SourceDb, SourceJar};
|
||||
use red_knot::files::FileId;
|
||||
use red_knot::module::{ModuleSearchPath, ModuleSearchPathKind};
|
||||
use red_knot::program::check::{CheckError, RayonCheckScheduler};
|
||||
use red_knot::program::{FileChange, FileChangeKind, Program};
|
||||
use red_knot::watch::FileWatcher;
|
||||
use red_knot::Workspace;
|
||||
|
||||
#[allow(clippy::print_stdout, clippy::unnecessary_wraps, clippy::print_stderr)]
|
||||
fn main() -> anyhow::Result<()> {
|
||||
setup_tracing();
|
||||
|
||||
let arguments: Vec<_> = std::env::args().collect();
|
||||
|
||||
if arguments.len() < 2 {
|
||||
eprintln!("Usage: red_knot <path>");
|
||||
return Err(anyhow::anyhow!("Invalid arguments"));
|
||||
}
|
||||
|
||||
let entry_point = Path::new(&arguments[1]);
|
||||
|
||||
if !entry_point.exists() {
|
||||
eprintln!("The entry point does not exist.");
|
||||
return Err(anyhow::anyhow!("Invalid arguments"));
|
||||
}
|
||||
|
||||
if !entry_point.is_file() {
|
||||
eprintln!("The entry point is not a file.");
|
||||
return Err(anyhow::anyhow!("Invalid arguments"));
|
||||
}
|
||||
|
||||
let workspace_folder = entry_point.parent().unwrap();
|
||||
let workspace = Workspace::new(workspace_folder.to_path_buf());
|
||||
|
||||
let workspace_search_path = ModuleSearchPath::new(
|
||||
workspace.root().to_path_buf(),
|
||||
ModuleSearchPathKind::FirstParty,
|
||||
);
|
||||
let mut program = Program::new(workspace, vec![workspace_search_path]);
|
||||
|
||||
let entry_id = program.file_id(entry_point);
|
||||
program.workspace_mut().open_file(entry_id);
|
||||
|
||||
let (main_loop, main_loop_cancellation_token) = MainLoop::new();
|
||||
|
||||
// Listen to Ctrl+C and abort the watch mode.
|
||||
let main_loop_cancellation_token = Mutex::new(Some(main_loop_cancellation_token));
|
||||
ctrlc::set_handler(move || {
|
||||
let mut lock = main_loop_cancellation_token.lock().unwrap();
|
||||
|
||||
if let Some(token) = lock.take() {
|
||||
token.stop();
|
||||
}
|
||||
})?;
|
||||
|
||||
let file_changes_notifier = main_loop.file_changes_notifier();
|
||||
|
||||
// Watch for file changes and re-trigger the analysis.
|
||||
let mut file_watcher = FileWatcher::new(
|
||||
move |changes| {
|
||||
file_changes_notifier.notify(changes);
|
||||
},
|
||||
program.files().clone(),
|
||||
)?;
|
||||
|
||||
file_watcher.watch_folder(workspace_folder)?;
|
||||
|
||||
main_loop.run(&mut program);
|
||||
|
||||
let source_jar: &SourceJar = program.jar();
|
||||
|
||||
dbg!(source_jar.parsed.statistics());
|
||||
dbg!(source_jar.sources.statistics());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct MainLoop {
|
||||
orchestrator_sender: crossbeam_channel::Sender<OrchestratorMessage>,
|
||||
main_loop_receiver: crossbeam_channel::Receiver<MainLoopMessage>,
|
||||
}
|
||||
|
||||
impl MainLoop {
|
||||
fn new() -> (Self, MainLoopCancellationToken) {
|
||||
let (orchestrator_sender, orchestrator_receiver) = crossbeam_channel::bounded(1);
|
||||
let (main_loop_sender, main_loop_receiver) = crossbeam_channel::bounded(1);
|
||||
|
||||
let mut orchestrator = Orchestrator {
|
||||
pending_analysis: None,
|
||||
receiver: orchestrator_receiver,
|
||||
sender: main_loop_sender.clone(),
|
||||
aggregated_changes: AggregatedChanges::default(),
|
||||
};
|
||||
|
||||
std::thread::spawn(move || {
|
||||
orchestrator.run();
|
||||
});
|
||||
|
||||
(
|
||||
Self {
|
||||
orchestrator_sender,
|
||||
main_loop_receiver,
|
||||
},
|
||||
MainLoopCancellationToken {
|
||||
sender: main_loop_sender,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn file_changes_notifier(&self) -> FileChangesNotifier {
|
||||
FileChangesNotifier {
|
||||
sender: self.orchestrator_sender.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(self, program: &mut Program) {
|
||||
self.orchestrator_sender
|
||||
.send(OrchestratorMessage::Run)
|
||||
.unwrap();
|
||||
|
||||
for message in &self.main_loop_receiver {
|
||||
tracing::trace!("Main Loop: Tick");
|
||||
|
||||
match message {
|
||||
MainLoopMessage::CheckProgram => {
|
||||
// Remove mutability from program.
|
||||
let program = &*program;
|
||||
let run_cancellation_token_source = CancellationTokenSource::new();
|
||||
let run_cancellation_token = run_cancellation_token_source.token();
|
||||
let sender = &self.orchestrator_sender;
|
||||
|
||||
sender
|
||||
.send(OrchestratorMessage::CheckProgramStarted {
|
||||
cancellation_token: run_cancellation_token_source,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
rayon::in_place_scope(|scope| {
|
||||
let scheduler = RayonCheckScheduler::new(program, scope);
|
||||
|
||||
let result = program.check(&scheduler, run_cancellation_token);
|
||||
match result {
|
||||
Ok(result) => sender
|
||||
.send(OrchestratorMessage::CheckProgramCompleted(result))
|
||||
.unwrap(),
|
||||
Err(CheckError::Cancelled) => sender
|
||||
.send(OrchestratorMessage::CheckProgramCancelled)
|
||||
.unwrap(),
|
||||
}
|
||||
});
|
||||
}
|
||||
MainLoopMessage::ApplyChanges(changes) => {
|
||||
program.apply_changes(changes.iter());
|
||||
}
|
||||
MainLoopMessage::CheckCompleted(diagnostics) => {
|
||||
dbg!(diagnostics);
|
||||
}
|
||||
MainLoopMessage::Exit => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for MainLoop {
|
||||
fn drop(&mut self) {
|
||||
self.orchestrator_sender
|
||||
.send(OrchestratorMessage::Shutdown)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FileChangesNotifier {
|
||||
sender: crossbeam_channel::Sender<OrchestratorMessage>,
|
||||
}
|
||||
|
||||
impl FileChangesNotifier {
|
||||
fn notify(&self, changes: Vec<FileChange>) {
|
||||
self.sender
|
||||
.send(OrchestratorMessage::FileChanges(changes))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct MainLoopCancellationToken {
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
}
|
||||
|
||||
impl MainLoopCancellationToken {
|
||||
fn stop(self) {
|
||||
self.sender.send(MainLoopMessage::Exit).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
struct Orchestrator {
|
||||
aggregated_changes: AggregatedChanges,
|
||||
pending_analysis: Option<PendingAnalysisState>,
|
||||
|
||||
/// Sends messages to the main loop.
|
||||
sender: crossbeam_channel::Sender<MainLoopMessage>,
|
||||
/// Receives messages from the main loop.
|
||||
receiver: crossbeam_channel::Receiver<OrchestratorMessage>,
|
||||
}
|
||||
|
||||
impl Orchestrator {
|
||||
fn run(&mut self) {
|
||||
while let Ok(message) = self.receiver.recv() {
|
||||
match message {
|
||||
OrchestratorMessage::Run => {
|
||||
self.pending_analysis = None;
|
||||
self.sender.send(MainLoopMessage::CheckProgram).unwrap();
|
||||
}
|
||||
|
||||
OrchestratorMessage::CheckProgramStarted { cancellation_token } => {
|
||||
debug_assert!(self.pending_analysis.is_none());
|
||||
|
||||
self.pending_analysis = Some(PendingAnalysisState { cancellation_token });
|
||||
}
|
||||
|
||||
OrchestratorMessage::CheckProgramCompleted(diagnostics) => {
|
||||
self.pending_analysis
|
||||
.take()
|
||||
.expect("Expected a pending analysis.");
|
||||
|
||||
self.sender
|
||||
.send(MainLoopMessage::CheckCompleted(diagnostics))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
OrchestratorMessage::CheckProgramCancelled => {
|
||||
self.pending_analysis
|
||||
.take()
|
||||
.expect("Expected a pending analysis.");
|
||||
|
||||
self.debounce_changes();
|
||||
}
|
||||
|
||||
OrchestratorMessage::FileChanges(changes) => {
|
||||
// Request cancellation, but wait until all analysis tasks have completed to
|
||||
// avoid stale messages in the next main loop.
|
||||
let pending = if let Some(pending_state) = self.pending_analysis.as_ref() {
|
||||
pending_state.cancellation_token.cancel();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.aggregated_changes.extend(changes);
|
||||
|
||||
// If there are no pending analysis tasks, apply the file changes. Otherwise
|
||||
// keep running until all file checks have completed.
|
||||
if !pending {
|
||||
self.debounce_changes();
|
||||
}
|
||||
}
|
||||
OrchestratorMessage::Shutdown => {
|
||||
return self.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn debounce_changes(&mut self) {
|
||||
debug_assert!(self.pending_analysis.is_none());
|
||||
|
||||
loop {
|
||||
// Consume possibly incoming file change messages before running a new analysis, but don't wait for more than 100ms.
|
||||
crossbeam_channel::select! {
|
||||
recv(self.receiver) -> message => {
|
||||
match message {
|
||||
Ok(OrchestratorMessage::Shutdown) => {
|
||||
return self.shutdown();
|
||||
}
|
||||
Ok(OrchestratorMessage::FileChanges(file_changes)) => {
|
||||
self.aggregated_changes.extend(file_changes);
|
||||
}
|
||||
|
||||
Ok(OrchestratorMessage::CheckProgramStarted {..}| OrchestratorMessage::CheckProgramCompleted(_) | OrchestratorMessage::CheckProgramCancelled) => unreachable!("No program check should be running while debouncing changes."),
|
||||
Ok(OrchestratorMessage::Run) => unreachable!("The orchestrator is already running."),
|
||||
|
||||
Err(_) => {
|
||||
// There are no more senders, no point in waiting for more messages
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
default(std::time::Duration::from_millis(100)) => {
|
||||
// No more file changes after 100 ms, send the changes and schedule a new analysis
|
||||
self.sender.send(MainLoopMessage::ApplyChanges(std::mem::take(&mut self.aggregated_changes))).unwrap();
|
||||
self.sender.send(MainLoopMessage::CheckProgram).unwrap();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_self)]
|
||||
fn shutdown(&self) {
|
||||
tracing::trace!("Shutting down orchestrator.");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PendingAnalysisState {
|
||||
cancellation_token: CancellationTokenSource,
|
||||
}
|
||||
|
||||
/// Message sent from the orchestrator to the main loop.
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckProgram,
|
||||
CheckCompleted(Vec<String>),
|
||||
ApplyChanges(AggregatedChanges),
|
||||
Exit,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum OrchestratorMessage {
|
||||
Run,
|
||||
Shutdown,
|
||||
|
||||
CheckProgramStarted {
|
||||
cancellation_token: CancellationTokenSource,
|
||||
},
|
||||
CheckProgramCompleted(Vec<String>),
|
||||
CheckProgramCancelled,
|
||||
|
||||
FileChanges(Vec<FileChange>),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct AggregatedChanges {
|
||||
changes: FxHashMap<FileId, FileChangeKind>,
|
||||
}
|
||||
|
||||
impl AggregatedChanges {
|
||||
fn add(&mut self, change: FileChange) {
|
||||
match self.changes.entry(change.file_id()) {
|
||||
Entry::Occupied(mut entry) => {
|
||||
let merged = entry.get_mut();
|
||||
|
||||
match (merged, change.kind()) {
|
||||
(FileChangeKind::Created, FileChangeKind::Deleted) => {
|
||||
// Deletion after creations means that ruff never saw the file.
|
||||
entry.remove();
|
||||
}
|
||||
(FileChangeKind::Created, FileChangeKind::Modified) => {
|
||||
// No-op, for ruff, modifying a file that it doesn't yet know that it exists is still considered a creation.
|
||||
}
|
||||
|
||||
(FileChangeKind::Modified, FileChangeKind::Created) => {
|
||||
// Uhh, that should probably not happen. Continue considering it a modification.
|
||||
}
|
||||
|
||||
(FileChangeKind::Modified, FileChangeKind::Deleted) => {
|
||||
*entry.get_mut() = FileChangeKind::Deleted;
|
||||
}
|
||||
|
||||
(FileChangeKind::Deleted, FileChangeKind::Created) => {
|
||||
*entry.get_mut() = FileChangeKind::Modified;
|
||||
}
|
||||
|
||||
(FileChangeKind::Deleted, FileChangeKind::Modified) => {
|
||||
// That's weird, but let's consider it a modification.
|
||||
*entry.get_mut() = FileChangeKind::Modified;
|
||||
}
|
||||
|
||||
(FileChangeKind::Created, FileChangeKind::Created)
|
||||
| (FileChangeKind::Modified, FileChangeKind::Modified)
|
||||
| (FileChangeKind::Deleted, FileChangeKind::Deleted) => {
|
||||
// No-op transitions. Some of them should be impossible but we handle them anyway.
|
||||
}
|
||||
}
|
||||
}
|
||||
Entry::Vacant(entry) => {
|
||||
entry.insert(change.kind());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend<I>(&mut self, changes: I)
|
||||
where
|
||||
I: IntoIterator<Item = FileChange>,
|
||||
I::IntoIter: ExactSizeIterator,
|
||||
{
|
||||
let iter = changes.into_iter();
|
||||
self.changes.reserve(iter.len());
|
||||
|
||||
for change in iter {
|
||||
self.add(change);
|
||||
}
|
||||
}
|
||||
|
||||
fn iter(&self) -> impl Iterator<Item = FileChange> + '_ {
|
||||
self.changes
|
||||
.iter()
|
||||
.map(|(id, kind)| FileChange::new(*id, *kind))
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_tracing() {
|
||||
let subscriber = Registry::default().with(
|
||||
tracing_tree::HierarchicalLayer::default()
|
||||
.with_indent_lines(true)
|
||||
.with_indent_amount(2)
|
||||
.with_bracketed_fields(true)
|
||||
.with_thread_ids(true)
|
||||
.with_targets(true)
|
||||
.with_writer(|| Box::new(std::io::stderr()))
|
||||
.with_timer(Uptime::default())
|
||||
.with_filter(LoggingFilter {
|
||||
trace_level: Level::TRACE,
|
||||
}),
|
||||
);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
}
|
||||
|
||||
struct LoggingFilter {
|
||||
trace_level: Level,
|
||||
}
|
||||
|
||||
impl LoggingFilter {
|
||||
fn is_enabled(&self, meta: &Metadata<'_>) -> bool {
|
||||
let filter = if meta.target().starts_with("red_knot") || meta.target().starts_with("ruff") {
|
||||
self.trace_level
|
||||
} else {
|
||||
Level::INFO
|
||||
};
|
||||
|
||||
meta.level() <= &filter
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Filter<S> for LoggingFilter {
|
||||
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
|
||||
self.is_enabled(meta)
|
||||
}
|
||||
|
||||
fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
|
||||
if self.is_enabled(meta) {
|
||||
Interest::always()
|
||||
} else {
|
||||
Interest::never()
|
||||
}
|
||||
}
|
||||
|
||||
fn max_level_hint(&self) -> Option<LevelFilter> {
|
||||
Some(LevelFilter::from_level(self.trace_level))
|
||||
}
|
||||
}
|
||||
1034
crates/red_knot/src/module.rs
Normal file
1034
crates/red_knot/src/module.rs
Normal file
File diff suppressed because it is too large
Load Diff
95
crates/red_knot/src/parse.rs
Normal file
95
crates/red_knot/src/parse.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_parser::{Mode, ParseError};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::cache::KeyValueCache;
|
||||
use crate::db::{HasJar, SourceDb, SourceJar};
|
||||
use crate::files::FileId;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Parsed {
|
||||
inner: Arc<ParsedInner>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct ParsedInner {
|
||||
ast: ast::ModModule,
|
||||
errors: Vec<ParseError>,
|
||||
}
|
||||
|
||||
impl Parsed {
|
||||
fn new(ast: ast::ModModule, errors: Vec<ParseError>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(ParsedInner { ast, errors }),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_text(text: &str) -> Self {
|
||||
let result = ruff_python_parser::parse(text, Mode::Module);
|
||||
|
||||
let (module, errors) = match result {
|
||||
Ok(ast::Mod::Module(module)) => (module, vec![]),
|
||||
Ok(ast::Mod::Expression(expression)) => (
|
||||
ast::ModModule {
|
||||
range: expression.range(),
|
||||
body: vec![ast::Stmt::Expr(ast::StmtExpr {
|
||||
range: expression.range(),
|
||||
value: expression.body,
|
||||
})],
|
||||
},
|
||||
vec![],
|
||||
),
|
||||
Err(errors) => (
|
||||
ast::ModModule {
|
||||
range: TextRange::default(),
|
||||
body: Vec::new(),
|
||||
},
|
||||
vec![errors],
|
||||
),
|
||||
};
|
||||
|
||||
Parsed::new(module, errors)
|
||||
}
|
||||
|
||||
pub fn ast(&self) -> &ast::ModModule {
|
||||
&self.inner.ast
|
||||
}
|
||||
|
||||
pub fn errors(&self) -> &[ParseError] {
|
||||
&self.inner.errors
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub(crate) fn parse<Db>(db: &Db, file_id: FileId) -> Parsed
|
||||
where
|
||||
Db: SourceDb + HasJar<SourceJar>,
|
||||
{
|
||||
let parsed = db.jar();
|
||||
|
||||
parsed.parsed.get(&file_id, |file_id| {
|
||||
let source = db.source(*file_id);
|
||||
|
||||
Parsed::from_text(source.text())
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ParsedStorage(KeyValueCache<FileId, Parsed>);
|
||||
|
||||
impl Deref for ParsedStorage {
|
||||
type Target = KeyValueCache<FileId, Parsed>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for ParsedStorage {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
306
crates/red_knot/src/program/check.rs
Normal file
306
crates/red_knot/src/program/check.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
use crate::cancellation::CancellationToken;
|
||||
use crate::db::{SemanticDb, SourceDb};
|
||||
use crate::files::FileId;
|
||||
use crate::lint::Diagnostics;
|
||||
use crate::program::Program;
|
||||
use rayon::max_num_threads;
|
||||
use rustc_hash::FxHashSet;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
impl Program {
|
||||
/// Checks all open files in the workspace and its dependencies.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
pub fn check(
|
||||
&self,
|
||||
scheduler: &dyn CheckScheduler,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<Vec<String>, CheckError> {
|
||||
let check_loop = CheckFilesLoop::new(scheduler, cancellation_token);
|
||||
|
||||
check_loop.run(self.workspace().open_files.iter().copied())
|
||||
}
|
||||
|
||||
/// Checks a single file and its dependencies.
|
||||
#[tracing::instrument(level = "debug", skip(self, scheduler, cancellation_token))]
|
||||
pub fn check_file(
|
||||
&self,
|
||||
file: FileId,
|
||||
scheduler: &dyn CheckScheduler,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<Vec<String>, CheckError> {
|
||||
let check_loop = CheckFilesLoop::new(scheduler, cancellation_token);
|
||||
|
||||
check_loop.run([file].into_iter())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, context))]
|
||||
fn do_check_file(
|
||||
&self,
|
||||
file: FileId,
|
||||
context: &CheckContext,
|
||||
) -> Result<Diagnostics, CheckError> {
|
||||
context.cancelled_ok()?;
|
||||
|
||||
let symbol_table = self.symbol_table(file);
|
||||
let dependencies = symbol_table.dependencies();
|
||||
|
||||
if !dependencies.is_empty() {
|
||||
let module = self.file_to_module(file);
|
||||
|
||||
// TODO scheduling all dependencies here is wasteful if we don't infer any types on them
|
||||
// but I think that's unlikely, so it is okay?
|
||||
// Anyway, we need to figure out a way to retrieve the dependencies of a module
|
||||
// from the persistent cache. So maybe it should be a separate query after all.
|
||||
for dependency in dependencies {
|
||||
let dependency_name = dependency.module_name(self, module);
|
||||
|
||||
if let Some(dependency_name) = dependency_name {
|
||||
// TODO We may want to have a different check functions for non-first-party
|
||||
// files because we only need to index them and not check them.
|
||||
// Supporting non-first-party code also requires supporting typing stubs.
|
||||
if let Some(dependency) = self.resolve_module(dependency_name) {
|
||||
if dependency.path(self).root().kind().is_first_party() {
|
||||
context.schedule_check_file(dependency.path(self).file());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
if self.workspace().is_file_open(file) {
|
||||
diagnostics.extend_from_slice(&self.lint_syntax(file));
|
||||
}
|
||||
|
||||
Ok(Diagnostics::from(diagnostics))
|
||||
}
|
||||
}
|
||||
|
||||
/// Schedules checks for files.
|
||||
pub trait CheckScheduler {
|
||||
/// Schedules a check for a file.
|
||||
///
|
||||
/// The check can either be run immediately on the current thread or the check can be queued
|
||||
/// in a thread pool and ran asynchronously.
|
||||
///
|
||||
/// The order in which scheduled checks are executed is not guaranteed.
|
||||
///
|
||||
/// The implementation should call [`CheckFileTask::run`] to execute the check.
|
||||
fn check_file(&self, file_task: CheckFileTask);
|
||||
|
||||
/// The maximum number of checks that can be run concurrently.
|
||||
///
|
||||
/// Returns `None` if the checks run on the current thread (no concurrency).
|
||||
fn max_concurrency(&self) -> Option<NonZeroUsize>;
|
||||
}
|
||||
|
||||
/// Scheduler that runs checks on a rayon thread pool.
|
||||
pub struct RayonCheckScheduler<'program, 'scope_ref, 'scope> {
|
||||
program: &'program Program,
|
||||
scope: &'scope_ref rayon::Scope<'scope>,
|
||||
}
|
||||
|
||||
impl<'program, 'scope_ref, 'scope> RayonCheckScheduler<'program, 'scope_ref, 'scope> {
|
||||
pub fn new(program: &'program Program, scope: &'scope_ref rayon::Scope<'scope>) -> Self {
|
||||
Self { program, scope }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'program, 'scope_ref, 'scope> CheckScheduler
|
||||
for RayonCheckScheduler<'program, 'scope_ref, 'scope>
|
||||
where
|
||||
'program: 'scope,
|
||||
{
|
||||
fn check_file(&self, check_file_task: CheckFileTask) {
|
||||
let child_span =
|
||||
tracing::trace_span!("check_file", file_id = check_file_task.file_id.as_u32());
|
||||
let program = self.program;
|
||||
|
||||
self.scope
|
||||
.spawn(move |_| child_span.in_scope(|| check_file_task.run(program)));
|
||||
}
|
||||
|
||||
fn max_concurrency(&self) -> Option<NonZeroUsize> {
|
||||
Some(NonZeroUsize::new(max_num_threads()).unwrap_or(NonZeroUsize::MIN))
|
||||
}
|
||||
}
|
||||
|
||||
/// Scheduler that runs all checks on the current thread.
|
||||
pub struct SameThreadCheckScheduler<'a> {
|
||||
program: &'a Program,
|
||||
}
|
||||
|
||||
impl<'a> SameThreadCheckScheduler<'a> {
|
||||
pub fn new(program: &'a Program) -> Self {
|
||||
Self { program }
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckScheduler for SameThreadCheckScheduler<'_> {
|
||||
fn check_file(&self, task: CheckFileTask) {
|
||||
task.run(self.program);
|
||||
}
|
||||
|
||||
fn max_concurrency(&self) -> Option<NonZeroUsize> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum CheckError {
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CheckFileTask {
|
||||
file_id: FileId,
|
||||
context: CheckContext,
|
||||
}
|
||||
|
||||
impl CheckFileTask {
|
||||
/// Runs the check and communicates the result to the orchestrator.
|
||||
pub fn run(self, program: &Program) {
|
||||
match program.do_check_file(self.file_id, &self.context) {
|
||||
Ok(diagnostics) => self
|
||||
.context
|
||||
.sender
|
||||
.send(CheckFileMessage::Completed(diagnostics))
|
||||
.unwrap(),
|
||||
Err(CheckError::Cancelled) => self
|
||||
.context
|
||||
.sender
|
||||
.send(CheckFileMessage::Cancelled)
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CheckContext {
|
||||
cancellation_token: CancellationToken,
|
||||
sender: crossbeam_channel::Sender<CheckFileMessage>,
|
||||
}
|
||||
|
||||
impl CheckContext {
|
||||
fn new(
|
||||
cancellation_token: CancellationToken,
|
||||
sender: crossbeam_channel::Sender<CheckFileMessage>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cancellation_token,
|
||||
sender,
|
||||
}
|
||||
}
|
||||
|
||||
/// Queues a new file for checking using the [`CheckScheduler`].
|
||||
#[allow(unused)]
|
||||
fn schedule_check_file(&self, file_id: FileId) {
|
||||
self.sender.send(CheckFileMessage::Queue(file_id)).unwrap();
|
||||
}
|
||||
|
||||
/// Returns `true` if the check has been cancelled.
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.cancellation_token.is_cancelled()
|
||||
}
|
||||
|
||||
fn cancelled_ok(&self) -> Result<(), CheckError> {
|
||||
if self.is_cancelled() {
|
||||
Err(CheckError::Cancelled)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CheckFilesLoop<'a> {
|
||||
scheduler: &'a dyn CheckScheduler,
|
||||
cancellation_token: CancellationToken,
|
||||
pending: usize,
|
||||
queued_files: FxHashSet<FileId>,
|
||||
}
|
||||
|
||||
impl<'a> CheckFilesLoop<'a> {
|
||||
fn new(scheduler: &'a dyn CheckScheduler, cancellation_token: CancellationToken) -> Self {
|
||||
Self {
|
||||
scheduler,
|
||||
cancellation_token,
|
||||
|
||||
queued_files: FxHashSet::default(),
|
||||
pending: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, files: impl Iterator<Item = FileId>) -> Result<Vec<String>, CheckError> {
|
||||
let (sender, receiver) = if let Some(max_concurrency) = self.scheduler.max_concurrency() {
|
||||
crossbeam_channel::bounded(max_concurrency.get())
|
||||
} else {
|
||||
// The checks run on the current thread. That means it is necessary to store all messages
|
||||
// or we risk deadlocking when the main loop never gets a chance to read the messages.
|
||||
crossbeam_channel::unbounded()
|
||||
};
|
||||
|
||||
let context = CheckContext::new(self.cancellation_token.clone(), sender.clone());
|
||||
|
||||
for file in files {
|
||||
self.queue_file(file, context.clone())?;
|
||||
}
|
||||
|
||||
self.run_impl(receiver, &context)
|
||||
}
|
||||
|
||||
fn run_impl(
|
||||
mut self,
|
||||
receiver: crossbeam_channel::Receiver<CheckFileMessage>,
|
||||
context: &CheckContext,
|
||||
) -> Result<Vec<String>, CheckError> {
|
||||
if self.cancellation_token.is_cancelled() {
|
||||
return Err(CheckError::Cancelled);
|
||||
}
|
||||
|
||||
let mut result = Vec::default();
|
||||
|
||||
for message in receiver {
|
||||
match message {
|
||||
CheckFileMessage::Completed(diagnostics) => {
|
||||
result.extend_from_slice(&diagnostics);
|
||||
|
||||
self.pending -= 1;
|
||||
|
||||
if self.pending == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
CheckFileMessage::Queue(id) => {
|
||||
self.queue_file(id, context.clone())?;
|
||||
}
|
||||
CheckFileMessage::Cancelled => {
|
||||
return Err(CheckError::Cancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn queue_file(&mut self, file_id: FileId, context: CheckContext) -> Result<(), CheckError> {
|
||||
if context.is_cancelled() {
|
||||
return Err(CheckError::Cancelled);
|
||||
}
|
||||
|
||||
if self.queued_files.insert(file_id) {
|
||||
self.pending += 1;
|
||||
|
||||
self.scheduler
|
||||
.check_file(CheckFileTask { file_id, context });
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
enum CheckFileMessage {
|
||||
Completed(Diagnostics),
|
||||
Queue(FileId),
|
||||
Cancelled,
|
||||
}
|
||||
176
crates/red_knot/src/program/mod.rs
Normal file
176
crates/red_knot/src/program/mod.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
pub mod check;
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::{Db, HasJar, SemanticDb, SemanticJar, SourceDb, SourceJar};
|
||||
use crate::files::{FileId, Files};
|
||||
use crate::lint::{lint_syntax, Diagnostics, LintSyntaxStorage};
|
||||
use crate::module::{
|
||||
add_module, file_to_module, path_to_module, resolve_module, set_module_search_paths, Module,
|
||||
ModuleData, ModuleName, ModuleResolver, ModuleSearchPath,
|
||||
};
|
||||
use crate::parse::{parse, Parsed, ParsedStorage};
|
||||
use crate::source::{source_text, Source, SourceStorage};
|
||||
use crate::symbols::{symbol_table, SymbolId, SymbolTable, SymbolTablesStorage};
|
||||
use crate::types::{infer_symbol_type, Type, TypeStore};
|
||||
use crate::Workspace;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Program {
|
||||
files: Files,
|
||||
source: SourceJar,
|
||||
semantic: SemanticJar,
|
||||
workspace: Workspace,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn new(workspace: Workspace, module_search_paths: Vec<ModuleSearchPath>) -> Self {
|
||||
Self {
|
||||
source: SourceJar {
|
||||
sources: SourceStorage::default(),
|
||||
parsed: ParsedStorage::default(),
|
||||
lint_syntax: LintSyntaxStorage::default(),
|
||||
},
|
||||
semantic: SemanticJar {
|
||||
module_resolver: ModuleResolver::new(module_search_paths),
|
||||
symbol_tables: SymbolTablesStorage::default(),
|
||||
type_store: TypeStore::default(),
|
||||
},
|
||||
files: Files::default(),
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_changes<I>(&mut self, changes: I)
|
||||
where
|
||||
I: IntoIterator<Item = FileChange>,
|
||||
{
|
||||
for change in changes {
|
||||
self.semantic
|
||||
.module_resolver
|
||||
.remove_module(&self.file_path(change.id));
|
||||
self.semantic.symbol_tables.remove(&change.id);
|
||||
self.source.sources.remove(&change.id);
|
||||
self.source.parsed.remove(&change.id);
|
||||
self.source.lint_syntax.remove(&change.id);
|
||||
// TODO: remove all dependent modules as well
|
||||
self.semantic.type_store.remove_module(change.id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
|
||||
pub fn workspace(&self) -> &Workspace {
|
||||
&self.workspace
|
||||
}
|
||||
|
||||
pub fn workspace_mut(&mut self) -> &mut Workspace {
|
||||
&mut self.workspace
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceDb for Program {
|
||||
fn file_id(&self, path: &Path) -> FileId {
|
||||
self.files.intern(path)
|
||||
}
|
||||
|
||||
fn file_path(&self, file_id: FileId) -> Arc<Path> {
|
||||
self.files.path(file_id)
|
||||
}
|
||||
|
||||
fn source(&self, file_id: FileId) -> Source {
|
||||
source_text(self, file_id)
|
||||
}
|
||||
|
||||
fn parse(&self, file_id: FileId) -> Parsed {
|
||||
parse(self, file_id)
|
||||
}
|
||||
|
||||
fn lint_syntax(&self, file_id: FileId) -> Diagnostics {
|
||||
lint_syntax(self, file_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl SemanticDb for Program {
|
||||
fn resolve_module(&self, name: ModuleName) -> Option<Module> {
|
||||
resolve_module(self, name)
|
||||
}
|
||||
|
||||
fn file_to_module(&self, file_id: FileId) -> Option<Module> {
|
||||
file_to_module(self, file_id)
|
||||
}
|
||||
|
||||
fn path_to_module(&self, path: &Path) -> Option<Module> {
|
||||
path_to_module(self, path)
|
||||
}
|
||||
|
||||
fn symbol_table(&self, file_id: FileId) -> Arc<SymbolTable> {
|
||||
symbol_table(self, file_id)
|
||||
}
|
||||
|
||||
fn infer_symbol_type(&self, file_id: FileId, symbol_id: SymbolId) -> Type {
|
||||
infer_symbol_type(self, file_id, symbol_id)
|
||||
}
|
||||
|
||||
// Mutations
|
||||
|
||||
fn add_module(&mut self, path: &Path) -> Option<(Module, Vec<Arc<ModuleData>>)> {
|
||||
add_module(self, path)
|
||||
}
|
||||
|
||||
fn set_module_search_paths(&mut self, paths: Vec<ModuleSearchPath>) {
|
||||
set_module_search_paths(self, paths);
|
||||
}
|
||||
}
|
||||
|
||||
impl Db for Program {}
|
||||
|
||||
impl HasJar<SourceJar> for Program {
|
||||
fn jar(&self) -> &SourceJar {
|
||||
&self.source
|
||||
}
|
||||
|
||||
fn jar_mut(&mut self) -> &mut SourceJar {
|
||||
&mut self.source
|
||||
}
|
||||
}
|
||||
|
||||
impl HasJar<SemanticJar> for Program {
|
||||
fn jar(&self) -> &SemanticJar {
|
||||
&self.semantic
|
||||
}
|
||||
|
||||
fn jar_mut(&mut self) -> &mut SemanticJar {
|
||||
&mut self.semantic
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct FileChange {
|
||||
id: FileId,
|
||||
kind: FileChangeKind,
|
||||
}
|
||||
|
||||
impl FileChange {
|
||||
pub fn new(file_id: FileId, kind: FileChangeKind) -> Self {
|
||||
Self { id: file_id, kind }
|
||||
}
|
||||
|
||||
pub fn file_id(&self) -> FileId {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> FileChangeKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum FileChangeKind {
|
||||
Created,
|
||||
Modified,
|
||||
Deleted,
|
||||
}
|
||||
96
crates/red_knot/src/source.rs
Normal file
96
crates/red_knot/src/source.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use crate::cache::KeyValueCache;
|
||||
use crate::db::{HasJar, SourceDb, SourceJar};
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::files::FileId;
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub(crate) fn source_text<Db>(db: &Db, file_id: FileId) -> Source
|
||||
where
|
||||
Db: SourceDb + HasJar<SourceJar>,
|
||||
{
|
||||
let sources = &db.jar().sources;
|
||||
|
||||
sources.get(&file_id, |file_id| {
|
||||
let path = db.file_path(*file_id);
|
||||
|
||||
let source_text = std::fs::read_to_string(&path).unwrap_or_else(|err| {
|
||||
tracing::error!("Failed to read file '{path:?}: {err}'. Falling back to empty text");
|
||||
String::new()
|
||||
});
|
||||
|
||||
let python_ty = PySourceType::from(&path);
|
||||
|
||||
let kind = match python_ty {
|
||||
PySourceType::Python => {
|
||||
SourceKind::Python(Arc::from(source_text))
|
||||
}
|
||||
PySourceType::Stub => SourceKind::Stub(Arc::from(source_text)),
|
||||
PySourceType::Ipynb => {
|
||||
let notebook = Notebook::from_source_code(&source_text).unwrap_or_else(|err| {
|
||||
// TODO should this be changed to never fail?
|
||||
// or should we instead add a diagnostic somewhere? But what would we return in this case?
|
||||
tracing::error!(
|
||||
"Failed to parse notebook '{path:?}: {err}'. Falling back to an empty notebook"
|
||||
);
|
||||
Notebook::from_source_code("").unwrap()
|
||||
});
|
||||
|
||||
SourceKind::IpyNotebook(Arc::new(notebook))
|
||||
}
|
||||
};
|
||||
|
||||
Source { kind }
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SourceKind {
|
||||
Python(Arc<str>),
|
||||
Stub(Arc<str>),
|
||||
IpyNotebook(Arc<Notebook>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Source {
|
||||
kind: SourceKind,
|
||||
}
|
||||
|
||||
impl Source {
|
||||
pub fn python<T: Into<Arc<str>>>(source: T) -> Self {
|
||||
Self {
|
||||
kind: SourceKind::Python(source.into()),
|
||||
}
|
||||
}
|
||||
pub fn kind(&self) -> &SourceKind {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
match &self.kind {
|
||||
SourceKind::Python(text) => text,
|
||||
SourceKind::Stub(text) => text,
|
||||
SourceKind::IpyNotebook(notebook) => notebook.source_code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SourceStorage(pub(crate) KeyValueCache<FileId, Source>);
|
||||
|
||||
impl Deref for SourceStorage {
|
||||
type Target = KeyValueCache<FileId, Source>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for SourceStorage {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
816
crates/red_knot/src/symbols.rs
Normal file
816
crates/red_knot/src/symbols.rs
Normal file
@@ -0,0 +1,816 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::iter::{Copied, DoubleEndedIterator, FusedIterator};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
use hashbrown::hash_map::{Keys, RawEntryMut};
|
||||
use rustc_hash::{FxHashMap, FxHasher};
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::visitor::preorder::PreorderVisitor;
|
||||
|
||||
use crate::ast_ids::TypedNodeKey;
|
||||
use crate::cache::KeyValueCache;
|
||||
use crate::db::{HasJar, SemanticDb, SemanticJar};
|
||||
use crate::files::FileId;
|
||||
use crate::module::{Module, ModuleName};
|
||||
use crate::Name;
|
||||
|
||||
#[allow(unreachable_pub)]
|
||||
#[tracing::instrument(level = "debug", skip(db))]
|
||||
pub fn symbol_table<Db>(db: &Db, file_id: FileId) -> Arc<SymbolTable>
|
||||
where
|
||||
Db: SemanticDb + HasJar<SemanticJar>,
|
||||
{
|
||||
let jar = db.jar();
|
||||
|
||||
jar.symbol_tables.get(&file_id, |_| {
|
||||
let parsed = db.parse(file_id);
|
||||
Arc::from(SymbolTable::from_ast(parsed.ast()))
|
||||
})
|
||||
}
|
||||
|
||||
type Map<K, V> = hashbrown::HashMap<K, V, ()>;
|
||||
|
||||
#[newtype_index]
|
||||
pub(crate) struct ScopeId;
|
||||
|
||||
impl ScopeId {
|
||||
pub(crate) fn scope(self, table: &SymbolTable) -> &Scope {
|
||||
&table.scopes_by_id[self]
|
||||
}
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
pub struct SymbolId;
|
||||
|
||||
impl SymbolId {
|
||||
pub(crate) fn symbol(self, table: &SymbolTable) -> &Symbol {
|
||||
&table.symbols_by_id[self]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub(crate) enum ScopeKind {
|
||||
Module,
|
||||
Annotation,
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Scope {
|
||||
name: Name,
|
||||
kind: ScopeKind,
|
||||
child_scopes: Vec<ScopeId>,
|
||||
// symbol IDs, hashed by symbol name
|
||||
symbols_by_name: Map<SymbolId, ()>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub(crate) fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
|
||||
pub(crate) fn kind(&self) -> ScopeKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Symbol {
|
||||
name: Name,
|
||||
}
|
||||
|
||||
impl Symbol {
|
||||
pub(crate) fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO storing TypedNodeKey for definitions means we have to search to find them again in the AST;
|
||||
// this is at best O(log n). If looking up definitions is a bottleneck we should look for
|
||||
// alternatives here.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Definition {
|
||||
// For the import cases, we don't need reference to any arbitrary AST subtrees (annotations,
|
||||
// RHS), and referencing just the import statement node is imprecise (a single import statement
|
||||
// can assign many symbols, we'd have to re-search for the one we care about), so we just copy
|
||||
// the small amount of information we need from the AST.
|
||||
Import(ImportDefinition),
|
||||
ImportFrom(ImportFromDefinition),
|
||||
ClassDef(TypedNodeKey<ast::StmtClassDef>),
|
||||
FunctionDef(TypedNodeKey<ast::StmtFunctionDef>),
|
||||
Assignment(TypedNodeKey<ast::StmtAssign>),
|
||||
AnnotatedAssignment(TypedNodeKey<ast::StmtAnnAssign>),
|
||||
// TODO with statements, except handlers, function args...
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ImportDefinition {
|
||||
pub(crate) module: Name,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ImportFromDefinition {
|
||||
pub(crate) module: Option<Name>,
|
||||
pub(crate) name: Name,
|
||||
pub(crate) level: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Dependency {
|
||||
Module(Name),
|
||||
Relative { level: u32, module: Option<Name> },
|
||||
}
|
||||
|
||||
impl Dependency {
|
||||
pub(crate) fn module_name<Db>(&self, db: &Db, relative_to: Option<Module>) -> Option<ModuleName>
|
||||
where
|
||||
Db: SemanticDb + HasJar<SemanticJar>,
|
||||
{
|
||||
match self {
|
||||
Dependency::Module(name) => Some(ModuleName::new(name.as_str())),
|
||||
Dependency::Relative { level, module } => {
|
||||
relative_to?.relative_name(db, *level, module.as_deref())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Table of all symbols in all scopes for a module.
|
||||
#[derive(Debug)]
|
||||
pub struct SymbolTable {
|
||||
scopes_by_id: IndexVec<ScopeId, Scope>,
|
||||
symbols_by_id: IndexVec<SymbolId, Symbol>,
|
||||
defs: FxHashMap<SymbolId, Vec<Definition>>,
|
||||
dependencies: Vec<Dependency>,
|
||||
}
|
||||
|
||||
impl SymbolTable {
|
||||
pub(crate) fn from_ast(module: &ast::ModModule) -> Self {
|
||||
let root_scope_id = SymbolTable::root_scope_id();
|
||||
let mut builder = SymbolTableBuilder {
|
||||
table: SymbolTable::new(),
|
||||
scopes: vec![root_scope_id],
|
||||
};
|
||||
builder.visit_body(&module.body);
|
||||
builder.table
|
||||
}
|
||||
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut table = SymbolTable {
|
||||
scopes_by_id: IndexVec::new(),
|
||||
symbols_by_id: IndexVec::new(),
|
||||
defs: FxHashMap::default(),
|
||||
dependencies: Vec::new(),
|
||||
};
|
||||
table.scopes_by_id.push(Scope {
|
||||
name: Name::new("<module>"),
|
||||
kind: ScopeKind::Module,
|
||||
child_scopes: Vec::new(),
|
||||
symbols_by_name: Map::default(),
|
||||
});
|
||||
table
|
||||
}
|
||||
|
||||
pub(crate) fn dependencies(&self) -> &[Dependency] {
|
||||
&self.dependencies
|
||||
}
|
||||
|
||||
pub(crate) const fn root_scope_id() -> ScopeId {
|
||||
ScopeId::from_usize(0)
|
||||
}
|
||||
|
||||
pub(crate) fn root_scope(&self) -> &Scope {
|
||||
&self.scopes_by_id[SymbolTable::root_scope_id()]
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_ids_for_scope(&self, scope_id: ScopeId) -> Copied<Keys<SymbolId, ()>> {
|
||||
self.scopes_by_id[scope_id].symbols_by_name.keys().copied()
|
||||
}
|
||||
|
||||
pub(crate) fn symbols_for_scope(
|
||||
&self,
|
||||
scope_id: ScopeId,
|
||||
) -> SymbolIterator<Copied<Keys<SymbolId, ()>>> {
|
||||
SymbolIterator {
|
||||
table: self,
|
||||
ids: self.symbol_ids_for_scope(scope_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root_symbol_ids(&self) -> Copied<Keys<SymbolId, ()>> {
|
||||
self.symbol_ids_for_scope(SymbolTable::root_scope_id())
|
||||
}
|
||||
|
||||
pub(crate) fn root_symbols(&self) -> SymbolIterator<Copied<Keys<SymbolId, ()>>> {
|
||||
self.symbols_for_scope(SymbolTable::root_scope_id())
|
||||
}
|
||||
|
||||
pub(crate) fn child_scope_ids_of(&self, scope_id: ScopeId) -> &[ScopeId] {
|
||||
&self.scopes_by_id[scope_id].child_scopes
|
||||
}
|
||||
|
||||
pub(crate) fn child_scopes_of(&self, scope_id: ScopeId) -> ScopeIterator<&[ScopeId]> {
|
||||
ScopeIterator {
|
||||
table: self,
|
||||
ids: self.child_scope_ids_of(scope_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn root_child_scope_ids(&self) -> &[ScopeId] {
|
||||
self.child_scope_ids_of(SymbolTable::root_scope_id())
|
||||
}
|
||||
|
||||
pub(crate) fn root_child_scopes(&self) -> ScopeIterator<&[ScopeId]> {
|
||||
self.child_scopes_of(SymbolTable::root_scope_id())
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_id_by_name(&self, scope_id: ScopeId, name: &str) -> Option<SymbolId> {
|
||||
let scope = &self.scopes_by_id[scope_id];
|
||||
let hash = SymbolTable::hash_name(name);
|
||||
let name = Name::new(name);
|
||||
scope
|
||||
.symbols_by_name
|
||||
.raw_entry()
|
||||
.from_hash(hash, |symid| self.symbols_by_id[*symid].name == name)
|
||||
.map(|(symbol_id, ())| *symbol_id)
|
||||
}
|
||||
|
||||
pub(crate) fn symbol_by_name(&self, scope_id: ScopeId, name: &str) -> Option<&Symbol> {
|
||||
Some(&self.symbols_by_id[self.symbol_id_by_name(scope_id, name)?])
|
||||
}
|
||||
|
||||
pub(crate) fn root_symbol_id_by_name(&self, name: &str) -> Option<SymbolId> {
|
||||
self.symbol_id_by_name(SymbolTable::root_scope_id(), name)
|
||||
}
|
||||
|
||||
pub(crate) fn root_symbol_by_name(&self, name: &str) -> Option<&Symbol> {
|
||||
self.symbol_by_name(SymbolTable::root_scope_id(), name)
|
||||
}
|
||||
|
||||
pub(crate) fn defs(&self, symbol_id: SymbolId) -> &[Definition] {
|
||||
self.defs
|
||||
.get(&symbol_id)
|
||||
.map(std::vec::Vec::as_slice)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn add_symbol_to_scope(&mut self, scope_id: ScopeId, name: &str) -> SymbolId {
|
||||
let hash = SymbolTable::hash_name(name);
|
||||
let scope = &mut self.scopes_by_id[scope_id];
|
||||
let name = Name::new(name);
|
||||
|
||||
let entry = scope
|
||||
.symbols_by_name
|
||||
.raw_entry_mut()
|
||||
.from_hash(hash, |existing| self.symbols_by_id[*existing].name == name);
|
||||
|
||||
match entry {
|
||||
RawEntryMut::Occupied(entry) => *entry.key(),
|
||||
RawEntryMut::Vacant(entry) => {
|
||||
let id = self.symbols_by_id.push(Symbol { name });
|
||||
entry.insert_with_hasher(hash, id, (), |_| hash);
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_child_scope(
|
||||
&mut self,
|
||||
parent_scope_id: ScopeId,
|
||||
name: &str,
|
||||
kind: ScopeKind,
|
||||
) -> ScopeId {
|
||||
let new_scope_id = self.scopes_by_id.push(Scope {
|
||||
name: Name::new(name),
|
||||
kind,
|
||||
child_scopes: Vec::new(),
|
||||
symbols_by_name: Map::default(),
|
||||
});
|
||||
let parent_scope = &mut self.scopes_by_id[parent_scope_id];
|
||||
parent_scope.child_scopes.push(new_scope_id);
|
||||
new_scope_id
|
||||
}
|
||||
|
||||
fn hash_name(name: &str) -> u64 {
|
||||
let mut hasher = FxHasher::default();
|
||||
name.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SymbolIterator<'a, I> {
|
||||
table: &'a SymbolTable,
|
||||
ids: I,
|
||||
}
|
||||
|
||||
impl<'a, I> Iterator for SymbolIterator<'a, I>
|
||||
where
|
||||
I: Iterator<Item = SymbolId>,
|
||||
{
|
||||
type Item = &'a Symbol;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let id = self.ids.next()?;
|
||||
Some(&self.table.symbols_by_id[id])
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.ids.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, I> FusedIterator for SymbolIterator<'a, I> where
|
||||
I: Iterator<Item = SymbolId> + FusedIterator
|
||||
{
|
||||
}
|
||||
|
||||
impl<'a, I> DoubleEndedIterator for SymbolIterator<'a, I>
|
||||
where
|
||||
I: Iterator<Item = SymbolId> + DoubleEndedIterator,
|
||||
{
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let id = self.ids.next_back()?;
|
||||
Some(&self.table.symbols_by_id[id])
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ScopeIterator<'a, I> {
|
||||
table: &'a SymbolTable,
|
||||
ids: I,
|
||||
}
|
||||
|
||||
impl<'a, I> Iterator for ScopeIterator<'a, I>
|
||||
where
|
||||
I: Iterator<Item = ScopeId>,
|
||||
{
|
||||
type Item = &'a Scope;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let id = self.ids.next()?;
|
||||
Some(&self.table.scopes_by_id[id])
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
self.ids.size_hint()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, I> FusedIterator for ScopeIterator<'a, I> where I: Iterator<Item = ScopeId> + FusedIterator {}
|
||||
|
||||
impl<'a, I> DoubleEndedIterator for ScopeIterator<'a, I>
|
||||
where
|
||||
I: Iterator<Item = ScopeId> + DoubleEndedIterator,
|
||||
{
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
let id = self.ids.next_back()?;
|
||||
Some(&self.table.scopes_by_id[id])
|
||||
}
|
||||
}
|
||||
|
||||
struct SymbolTableBuilder {
|
||||
table: SymbolTable,
|
||||
scopes: Vec<ScopeId>,
|
||||
}
|
||||
|
||||
impl SymbolTableBuilder {
|
||||
fn add_symbol(&mut self, identifier: &str) -> SymbolId {
|
||||
self.table.add_symbol_to_scope(self.cur_scope(), identifier)
|
||||
}
|
||||
|
||||
fn add_symbol_with_def(&mut self, identifier: &str, definition: Definition) -> SymbolId {
|
||||
let symbol_id = self.add_symbol(identifier);
|
||||
self.table
|
||||
.defs
|
||||
.entry(symbol_id)
|
||||
.or_default()
|
||||
.push(definition);
|
||||
symbol_id
|
||||
}
|
||||
|
||||
fn push_scope(&mut self, child_of: ScopeId, name: &str, kind: ScopeKind) -> ScopeId {
|
||||
let scope_id = self.table.add_child_scope(child_of, name, kind);
|
||||
self.scopes.push(scope_id);
|
||||
scope_id
|
||||
}
|
||||
|
||||
fn pop_scope(&mut self) -> ScopeId {
|
||||
self.scopes
|
||||
.pop()
|
||||
.expect("Scope stack should never be empty")
|
||||
}
|
||||
|
||||
fn cur_scope(&self) -> ScopeId {
|
||||
*self
|
||||
.scopes
|
||||
.last()
|
||||
.expect("Scope stack should never be empty")
|
||||
}
|
||||
|
||||
fn with_type_params(
|
||||
&mut self,
|
||||
name: &str,
|
||||
params: &Option<Box<ast::TypeParams>>,
|
||||
nested: impl FnOnce(&mut Self),
|
||||
) {
|
||||
if let Some(type_params) = params {
|
||||
self.push_scope(self.cur_scope(), name, ScopeKind::Annotation);
|
||||
for type_param in &type_params.type_params {
|
||||
let name = match type_param {
|
||||
ast::TypeParam::TypeVar(ast::TypeParamTypeVar { name, .. }) => name,
|
||||
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { name, .. }) => name,
|
||||
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { name, .. }) => name,
|
||||
};
|
||||
self.add_symbol(name);
|
||||
}
|
||||
}
|
||||
nested(self);
|
||||
if params.is_some() {
|
||||
self.pop_scope();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PreorderVisitor<'_> for SymbolTableBuilder {
|
||||
fn visit_expr(&mut self, expr: &ast::Expr) {
|
||||
if let ast::Expr::Name(ast::ExprName { id, .. }) = expr {
|
||||
self.add_symbol(id);
|
||||
}
|
||||
ast::visitor::preorder::walk_expr(self, expr);
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &ast::Stmt) {
|
||||
// TODO need to capture more definition statements here
|
||||
match stmt {
|
||||
ast::Stmt::ClassDef(node) => {
|
||||
let def = Definition::ClassDef(TypedNodeKey::from_node(node));
|
||||
self.add_symbol_with_def(&node.name, def);
|
||||
self.with_type_params(&node.name, &node.type_params, |builder| {
|
||||
builder.push_scope(builder.cur_scope(), &node.name, ScopeKind::Class);
|
||||
ast::visitor::preorder::walk_stmt(builder, stmt);
|
||||
builder.pop_scope();
|
||||
});
|
||||
}
|
||||
ast::Stmt::FunctionDef(node) => {
|
||||
let def = Definition::FunctionDef(TypedNodeKey::from_node(node));
|
||||
self.add_symbol_with_def(&node.name, def);
|
||||
self.with_type_params(&node.name, &node.type_params, |builder| {
|
||||
builder.push_scope(builder.cur_scope(), &node.name, ScopeKind::Function);
|
||||
ast::visitor::preorder::walk_stmt(builder, stmt);
|
||||
builder.pop_scope();
|
||||
});
|
||||
}
|
||||
ast::Stmt::Import(ast::StmtImport { names, .. }) => {
|
||||
for alias in names {
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
asname.id.as_str()
|
||||
} else {
|
||||
alias.name.id.split('.').next().unwrap()
|
||||
};
|
||||
|
||||
let module = Name::new(&alias.name.id);
|
||||
|
||||
let def = Definition::Import(ImportDefinition {
|
||||
module: module.clone(),
|
||||
});
|
||||
self.add_symbol_with_def(symbol_name, def);
|
||||
self.table.dependencies.push(Dependency::Module(module));
|
||||
}
|
||||
}
|
||||
ast::Stmt::ImportFrom(ast::StmtImportFrom {
|
||||
module,
|
||||
names,
|
||||
level,
|
||||
..
|
||||
}) => {
|
||||
let module = module.as_ref().map(|m| Name::new(&m.id));
|
||||
|
||||
for alias in names {
|
||||
let symbol_name = if let Some(asname) = &alias.asname {
|
||||
asname.id.as_str()
|
||||
} else {
|
||||
alias.name.id.as_str()
|
||||
};
|
||||
let def = Definition::ImportFrom(ImportFromDefinition {
|
||||
module: module.clone(),
|
||||
name: Name::new(&alias.name.id),
|
||||
level: *level,
|
||||
});
|
||||
self.add_symbol_with_def(symbol_name, def);
|
||||
}
|
||||
|
||||
let dependency = if let Some(module) = module {
|
||||
if *level == 0 {
|
||||
Dependency::Module(module)
|
||||
} else {
|
||||
Dependency::Relative {
|
||||
level: *level,
|
||||
module: Some(module),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Dependency::Relative {
|
||||
level: *level,
|
||||
module,
|
||||
}
|
||||
};
|
||||
|
||||
self.table.dependencies.push(dependency);
|
||||
}
|
||||
_ => {
|
||||
ast::visitor::preorder::walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SymbolTablesStorage(KeyValueCache<FileId, Arc<SymbolTable>>);
|
||||
|
||||
impl Deref for SymbolTablesStorage {
|
||||
type Target = KeyValueCache<FileId, Arc<SymbolTable>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for SymbolTablesStorage {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use textwrap::dedent;
|
||||
|
||||
use crate::parse::Parsed;
|
||||
use crate::symbols::ScopeKind;
|
||||
|
||||
use super::{SymbolId, SymbolIterator, SymbolTable};
|
||||
|
||||
mod from_ast {
|
||||
use super::*;
|
||||
|
||||
fn parse(code: &str) -> Parsed {
|
||||
Parsed::from_text(&dedent(code))
|
||||
}
|
||||
|
||||
fn names<I>(it: SymbolIterator<I>) -> Vec<&str>
|
||||
where
|
||||
I: Iterator<Item = SymbolId>,
|
||||
{
|
||||
let mut symbols: Vec<_> = it.map(|sym| sym.name.as_str()).collect();
|
||||
symbols.sort_unstable();
|
||||
symbols
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let parsed = parse("");
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()).len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
let parsed = parse("x");
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["x"]);
|
||||
assert_eq!(
|
||||
table.defs(table.root_symbol_id_by_name("x").unwrap()).len(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn annotation_only() {
|
||||
let parsed = parse("x: int");
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["int", "x"]);
|
||||
// TODO record definition
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import() {
|
||||
let parsed = parse("import foo");
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["foo"]);
|
||||
assert_eq!(
|
||||
table
|
||||
.defs(table.root_symbol_id_by_name("foo").unwrap())
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_sub() {
|
||||
let parsed = parse("import foo.bar");
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["foo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_as() {
|
||||
let parsed = parse("import foo.bar as baz");
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["baz"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_from() {
|
||||
let parsed = parse("from bar import foo");
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["foo"]);
|
||||
assert_eq!(
|
||||
table
|
||||
.defs(table.root_symbol_id_by_name("foo").unwrap())
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_scope() {
|
||||
let parsed = parse(
|
||||
"
|
||||
class C:
|
||||
x = 1
|
||||
y = 2
|
||||
",
|
||||
);
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["C", "y"]);
|
||||
let scopes = table.root_child_scope_ids();
|
||||
assert_eq!(scopes.len(), 1);
|
||||
let c_scope = scopes[0].scope(&table);
|
||||
assert_eq!(c_scope.kind(), ScopeKind::Class);
|
||||
assert_eq!(c_scope.name(), "C");
|
||||
assert_eq!(names(table.symbols_for_scope(scopes[0])), vec!["x"]);
|
||||
assert_eq!(
|
||||
table.defs(table.root_symbol_id_by_name("C").unwrap()).len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn func_scope() {
|
||||
let parsed = parse(
|
||||
"
|
||||
def func():
|
||||
x = 1
|
||||
y = 2
|
||||
",
|
||||
);
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["func", "y"]);
|
||||
let scopes = table.root_child_scope_ids();
|
||||
assert_eq!(scopes.len(), 1);
|
||||
let func_scope = scopes[0].scope(&table);
|
||||
assert_eq!(func_scope.kind(), ScopeKind::Function);
|
||||
assert_eq!(func_scope.name(), "func");
|
||||
assert_eq!(names(table.symbols_for_scope(scopes[0])), vec!["x"]);
|
||||
assert_eq!(
|
||||
table
|
||||
.defs(table.root_symbol_id_by_name("func").unwrap())
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dupes() {
|
||||
let parsed = parse(
|
||||
"
|
||||
def func():
|
||||
x = 1
|
||||
def func():
|
||||
y = 2
|
||||
",
|
||||
);
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["func"]);
|
||||
let scopes = table.root_child_scope_ids();
|
||||
assert_eq!(scopes.len(), 2);
|
||||
let func_scope_1 = scopes[0].scope(&table);
|
||||
let func_scope_2 = scopes[1].scope(&table);
|
||||
assert_eq!(func_scope_1.kind(), ScopeKind::Function);
|
||||
assert_eq!(func_scope_1.name(), "func");
|
||||
assert_eq!(func_scope_2.kind(), ScopeKind::Function);
|
||||
assert_eq!(func_scope_2.name(), "func");
|
||||
assert_eq!(names(table.symbols_for_scope(scopes[0])), vec!["x"]);
|
||||
assert_eq!(names(table.symbols_for_scope(scopes[1])), vec!["y"]);
|
||||
assert_eq!(
|
||||
table
|
||||
.defs(table.root_symbol_id_by_name("func").unwrap())
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_func() {
|
||||
let parsed = parse(
|
||||
"
|
||||
def func[T]():
|
||||
x = 1
|
||||
",
|
||||
);
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["func"]);
|
||||
let scopes = table.root_child_scope_ids();
|
||||
assert_eq!(scopes.len(), 1);
|
||||
let ann_scope_id = scopes[0];
|
||||
let ann_scope = ann_scope_id.scope(&table);
|
||||
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
|
||||
assert_eq!(ann_scope.name(), "func");
|
||||
assert_eq!(names(table.symbols_for_scope(ann_scope_id)), vec!["T"]);
|
||||
let scopes = table.child_scope_ids_of(ann_scope_id);
|
||||
assert_eq!(scopes.len(), 1);
|
||||
let func_scope_id = scopes[0];
|
||||
let func_scope = func_scope_id.scope(&table);
|
||||
assert_eq!(func_scope.kind(), ScopeKind::Function);
|
||||
assert_eq!(func_scope.name(), "func");
|
||||
assert_eq!(names(table.symbols_for_scope(func_scope_id)), vec!["x"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_class() {
|
||||
let parsed = parse(
|
||||
"
|
||||
class C[T]:
|
||||
x = 1
|
||||
",
|
||||
);
|
||||
let table = SymbolTable::from_ast(parsed.ast());
|
||||
assert_eq!(names(table.root_symbols()), vec!["C"]);
|
||||
let scopes = table.root_child_scope_ids();
|
||||
assert_eq!(scopes.len(), 1);
|
||||
let ann_scope_id = scopes[0];
|
||||
let ann_scope = ann_scope_id.scope(&table);
|
||||
assert_eq!(ann_scope.kind(), ScopeKind::Annotation);
|
||||
assert_eq!(ann_scope.name(), "C");
|
||||
assert_eq!(names(table.symbols_for_scope(ann_scope_id)), vec!["T"]);
|
||||
let scopes = table.child_scope_ids_of(ann_scope_id);
|
||||
assert_eq!(scopes.len(), 1);
|
||||
let func_scope_id = scopes[0];
|
||||
let func_scope = func_scope_id.scope(&table);
|
||||
assert_eq!(func_scope.kind(), ScopeKind::Class);
|
||||
assert_eq!(func_scope.name(), "C");
|
||||
assert_eq!(names(table.symbols_for_scope(func_scope_id)), vec!["x"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_same_name_symbol_twice() {
|
||||
let mut table = SymbolTable::new();
|
||||
let root_scope_id = SymbolTable::root_scope_id();
|
||||
let symbol_id_1 = table.add_symbol_to_scope(root_scope_id, "foo");
|
||||
let symbol_id_2 = table.add_symbol_to_scope(root_scope_id, "foo");
|
||||
assert_eq!(symbol_id_1, symbol_id_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_different_named_symbols() {
|
||||
let mut table = SymbolTable::new();
|
||||
let root_scope_id = SymbolTable::root_scope_id();
|
||||
let symbol_id_1 = table.add_symbol_to_scope(root_scope_id, "foo");
|
||||
let symbol_id_2 = table.add_symbol_to_scope(root_scope_id, "bar");
|
||||
assert_ne!(symbol_id_1, symbol_id_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_child_scope_with_symbol() {
|
||||
let mut table = SymbolTable::new();
|
||||
let root_scope_id = SymbolTable::root_scope_id();
|
||||
let foo_symbol_top = table.add_symbol_to_scope(root_scope_id, "foo");
|
||||
let c_scope = table.add_child_scope(root_scope_id, "C", ScopeKind::Class);
|
||||
let foo_symbol_inner = table.add_symbol_to_scope(c_scope, "foo");
|
||||
assert_ne!(foo_symbol_top, foo_symbol_inner);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scope_from_id() {
|
||||
let table = SymbolTable::new();
|
||||
let root_scope_id = SymbolTable::root_scope_id();
|
||||
let scope = root_scope_id.scope(&table);
|
||||
assert_eq!(scope.name.as_str(), "<module>");
|
||||
assert_eq!(scope.kind, ScopeKind::Module);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_from_id() {
|
||||
let mut table = SymbolTable::new();
|
||||
let root_scope_id = SymbolTable::root_scope_id();
|
||||
let foo_symbol_id = table.add_symbol_to_scope(root_scope_id, "foo");
|
||||
let symbol = foo_symbol_id.symbol(&table);
|
||||
assert_eq!(symbol.name.as_str(), "foo");
|
||||
}
|
||||
}
|
||||
554
crates/red_knot/src/types.rs
Normal file
554
crates/red_knot/src/types.rs
Normal file
@@ -0,0 +1,554 @@
|
||||
#![allow(dead_code)]
|
||||
use crate::ast_ids::NodeKey;
|
||||
use crate::files::FileId;
|
||||
use crate::module::ModuleName;
|
||||
use crate::symbols::SymbolId;
|
||||
use crate::{FxDashMap, FxHashSet, FxIndexSet, Name};
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
pub(crate) mod infer;
|
||||
|
||||
pub(crate) use infer::infer_symbol_type;
|
||||
|
||||
/// unique ID for a type
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum Type {
|
||||
/// the dynamic or gradual type: a statically-unknown set of values
|
||||
Any,
|
||||
/// the empty set of values
|
||||
Never,
|
||||
/// unknown type (no annotation)
|
||||
/// equivalent to Any, or to object in strict mode
|
||||
Unknown,
|
||||
/// name is not bound to any value
|
||||
Unbound,
|
||||
/// a specific function object
|
||||
Function(FunctionTypeId),
|
||||
/// a specific class object
|
||||
Class(ClassTypeId),
|
||||
/// the set of Python objects with the given class in their __class__'s method resolution order
|
||||
Instance(ClassTypeId),
|
||||
Union(UnionTypeId),
|
||||
Intersection(IntersectionTypeId),
|
||||
// TODO protocols, callable types, overloads, generics, type vars
|
||||
}
|
||||
|
||||
impl Type {
|
||||
fn display<'a>(&'a self, store: &'a TypeStore) -> DisplayType<'a> {
|
||||
DisplayType { ty: self, store }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: currently calling `get_function` et al and holding on to the `FunctionTypeRef` will lock a
|
||||
// shard of this dashmap, for as long as you hold the reference. This may be a problem. We could
|
||||
// switch to having all the arenas hold Arc, or we could see if we can split up ModuleTypeStore,
|
||||
// and/or give it inner mutability and finer-grained internal locking.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TypeStore {
|
||||
modules: FxDashMap<FileId, ModuleTypeStore>,
|
||||
}
|
||||
|
||||
impl TypeStore {
|
||||
pub fn remove_module(&self, file_id: FileId) {
|
||||
self.modules.remove(&file_id);
|
||||
}
|
||||
|
||||
pub fn cache_symbol_type(&self, file_id: FileId, symbol_id: SymbolId, ty: Type) {
|
||||
self.add_or_get_module(file_id)
|
||||
.symbol_types
|
||||
.insert(symbol_id, ty);
|
||||
}
|
||||
|
||||
pub fn cache_node_type(&self, file_id: FileId, node_key: NodeKey, ty: Type) {
|
||||
self.add_or_get_module(file_id)
|
||||
.node_types
|
||||
.insert(node_key, ty);
|
||||
}
|
||||
|
||||
pub fn get_cached_symbol_type(&self, file_id: FileId, symbol_id: SymbolId) -> Option<Type> {
|
||||
self.try_get_module(file_id)?
|
||||
.symbol_types
|
||||
.get(&symbol_id)
|
||||
.copied()
|
||||
}
|
||||
|
||||
pub fn get_cached_node_type(&self, file_id: FileId, node_key: &NodeKey) -> Option<Type> {
|
||||
self.try_get_module(file_id)?
|
||||
.node_types
|
||||
.get(node_key)
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn add_or_get_module(&self, file_id: FileId) -> ModuleStoreRefMut {
|
||||
self.modules
|
||||
.entry(file_id)
|
||||
.or_insert_with(|| ModuleTypeStore::new(file_id))
|
||||
}
|
||||
|
||||
fn get_module(&self, file_id: FileId) -> ModuleStoreRef {
|
||||
self.try_get_module(file_id).expect("module should exist")
|
||||
}
|
||||
|
||||
fn try_get_module(&self, file_id: FileId) -> Option<ModuleStoreRef> {
|
||||
self.modules.get(&file_id)
|
||||
}
|
||||
|
||||
fn add_function(&self, file_id: FileId, name: &str) -> FunctionTypeId {
|
||||
self.add_or_get_module(file_id).add_function(name)
|
||||
}
|
||||
|
||||
fn add_class(&self, file_id: FileId, name: &str) -> ClassTypeId {
|
||||
self.add_or_get_module(file_id).add_class(name)
|
||||
}
|
||||
|
||||
fn add_union(&self, file_id: FileId, elems: &[Type]) -> UnionTypeId {
|
||||
self.add_or_get_module(file_id).add_union(elems)
|
||||
}
|
||||
|
||||
fn add_intersection(
|
||||
&self,
|
||||
file_id: FileId,
|
||||
positive: &[Type],
|
||||
negative: &[Type],
|
||||
) -> IntersectionTypeId {
|
||||
self.add_or_get_module(file_id)
|
||||
.add_intersection(positive, negative)
|
||||
}
|
||||
|
||||
fn get_function(&self, id: FunctionTypeId) -> FunctionTypeRef {
|
||||
FunctionTypeRef {
|
||||
module_store: self.get_module(id.file_id),
|
||||
function_id: id.func_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_class(&self, id: ClassTypeId) -> ClassTypeRef {
|
||||
ClassTypeRef {
|
||||
module_store: self.get_module(id.file_id),
|
||||
class_id: id.class_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_union(&self, id: UnionTypeId) -> UnionTypeRef {
|
||||
UnionTypeRef {
|
||||
module_store: self.get_module(id.file_id),
|
||||
union_id: id.union_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_intersection(&self, id: IntersectionTypeId) -> IntersectionTypeRef {
|
||||
IntersectionTypeRef {
|
||||
module_store: self.get_module(id.file_id),
|
||||
intersection_id: id.intersection_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_symbol_dependency(&self, from: (FileId, SymbolId), to: (FileId, SymbolId)) {
|
||||
let (from_file_id, from_symbol_id) = from;
|
||||
self.add_or_get_module(from_file_id)
|
||||
.symbol_dependencies
|
||||
.entry(from_symbol_id)
|
||||
.or_default()
|
||||
.insert(to);
|
||||
}
|
||||
|
||||
fn record_module_dependency(&self, from: (FileId, SymbolId), to: ModuleName) {
|
||||
let (from_file_id, from_symbol_id) = from;
|
||||
self.add_or_get_module(from_file_id)
|
||||
.module_dependencies
|
||||
.entry(from_symbol_id)
|
||||
.or_default()
|
||||
.insert(to);
|
||||
}
|
||||
}
|
||||
|
||||
type ModuleStoreRef<'a> = dashmap::mapref::one::Ref<
|
||||
'a,
|
||||
FileId,
|
||||
ModuleTypeStore,
|
||||
std::hash::BuildHasherDefault<rustc_hash::FxHasher>,
|
||||
>;
|
||||
|
||||
type ModuleStoreRefMut<'a> = dashmap::mapref::one::RefMut<
|
||||
'a,
|
||||
FileId,
|
||||
ModuleTypeStore,
|
||||
std::hash::BuildHasherDefault<rustc_hash::FxHasher>,
|
||||
>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FunctionTypeRef<'a> {
|
||||
module_store: ModuleStoreRef<'a>,
|
||||
function_id: ModuleFunctionTypeId,
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for FunctionTypeRef<'a> {
|
||||
type Target = FunctionType;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.module_store.get_function(self.function_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ClassTypeRef<'a> {
|
||||
module_store: ModuleStoreRef<'a>,
|
||||
class_id: ModuleClassTypeId,
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for ClassTypeRef<'a> {
|
||||
type Target = ClassType;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.module_store.get_class(self.class_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnionTypeRef<'a> {
|
||||
module_store: ModuleStoreRef<'a>,
|
||||
union_id: ModuleUnionTypeId,
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for UnionTypeRef<'a> {
|
||||
type Target = UnionType;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.module_store.get_union(self.union_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct IntersectionTypeRef<'a> {
|
||||
module_store: ModuleStoreRef<'a>,
|
||||
intersection_id: ModuleIntersectionTypeId,
|
||||
}
|
||||
|
||||
impl<'a> std::ops::Deref for IntersectionTypeRef<'a> {
|
||||
type Target = IntersectionType;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.module_store.get_intersection(self.intersection_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct FunctionTypeId {
|
||||
file_id: FileId,
|
||||
func_id: ModuleFunctionTypeId,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct ClassTypeId {
|
||||
file_id: FileId,
|
||||
class_id: ModuleClassTypeId,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct UnionTypeId {
|
||||
file_id: FileId,
|
||||
union_id: ModuleUnionTypeId,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
pub struct IntersectionTypeId {
|
||||
file_id: FileId,
|
||||
intersection_id: ModuleIntersectionTypeId,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
struct ModuleFunctionTypeId;
|
||||
|
||||
#[newtype_index]
|
||||
struct ModuleClassTypeId;
|
||||
|
||||
#[newtype_index]
|
||||
struct ModuleUnionTypeId;
|
||||
|
||||
#[newtype_index]
|
||||
struct ModuleIntersectionTypeId;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ModuleTypeStore {
|
||||
file_id: FileId,
|
||||
/// arena of all function types defined in this module
|
||||
functions: IndexVec<ModuleFunctionTypeId, FunctionType>,
|
||||
/// arena of all class types defined in this module
|
||||
classes: IndexVec<ModuleClassTypeId, ClassType>,
|
||||
/// arenda of all union types created in this module
|
||||
unions: IndexVec<ModuleUnionTypeId, UnionType>,
|
||||
/// arena of all intersection types created in this module
|
||||
intersections: IndexVec<ModuleIntersectionTypeId, IntersectionType>,
|
||||
/// cached types of symbols in this module
|
||||
symbol_types: FxHashMap<SymbolId, Type>,
|
||||
/// cached types of AST nodes in this module
|
||||
node_types: FxHashMap<NodeKey, Type>,
|
||||
// the inferred type for symbol K depends on the type of symbols in V
|
||||
symbol_dependencies: FxHashMap<SymbolId, FxHashSet<(FileId, SymbolId)>>,
|
||||
// the inferred type for symbol K depends on the modules in V; this type of dependency is
|
||||
// recorded when e.g. the target symbol doesn't exist in the module, so we can't record a
|
||||
// dependency on a symbol, but if the module changes it could still change our resolution)
|
||||
module_dependencies: FxHashMap<SymbolId, FxHashSet<ModuleName>>,
|
||||
}
|
||||
|
||||
impl ModuleTypeStore {
|
||||
fn new(file_id: FileId) -> Self {
|
||||
Self {
|
||||
file_id,
|
||||
functions: IndexVec::default(),
|
||||
classes: IndexVec::default(),
|
||||
unions: IndexVec::default(),
|
||||
intersections: IndexVec::default(),
|
||||
symbol_types: FxHashMap::default(),
|
||||
node_types: FxHashMap::default(),
|
||||
symbol_dependencies: FxHashMap::default(),
|
||||
module_dependencies: FxHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_function(&mut self, name: &str) -> FunctionTypeId {
|
||||
let func_id = self.functions.push(FunctionType {
|
||||
name: Name::new(name),
|
||||
});
|
||||
FunctionTypeId {
|
||||
file_id: self.file_id,
|
||||
func_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_class(&mut self, name: &str) -> ClassTypeId {
|
||||
let class_id = self.classes.push(ClassType {
|
||||
name: Name::new(name),
|
||||
});
|
||||
ClassTypeId {
|
||||
file_id: self.file_id,
|
||||
class_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_union(&mut self, elems: &[Type]) -> UnionTypeId {
|
||||
let union_id = self.unions.push(UnionType {
|
||||
elements: elems.iter().copied().collect(),
|
||||
});
|
||||
UnionTypeId {
|
||||
file_id: self.file_id,
|
||||
union_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_intersection(&mut self, positive: &[Type], negative: &[Type]) -> IntersectionTypeId {
|
||||
let intersection_id = self.intersections.push(IntersectionType {
|
||||
positive: positive.iter().copied().collect(),
|
||||
negative: negative.iter().copied().collect(),
|
||||
});
|
||||
IntersectionTypeId {
|
||||
file_id: self.file_id,
|
||||
intersection_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_function(&self, func_id: ModuleFunctionTypeId) -> &FunctionType {
|
||||
&self.functions[func_id]
|
||||
}
|
||||
|
||||
fn get_class(&self, class_id: ModuleClassTypeId) -> &ClassType {
|
||||
&self.classes[class_id]
|
||||
}
|
||||
|
||||
fn get_union(&self, union_id: ModuleUnionTypeId) -> &UnionType {
|
||||
&self.unions[union_id]
|
||||
}
|
||||
|
||||
fn get_intersection(&self, intersection_id: ModuleIntersectionTypeId) -> &IntersectionType {
|
||||
&self.intersections[intersection_id]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
struct DisplayType<'a> {
|
||||
ty: &'a Type,
|
||||
store: &'a TypeStore,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DisplayType<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.ty {
|
||||
Type::Any => f.write_str("Any"),
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Unbound => f.write_str("Unbound"),
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::Class(class_id) => {
|
||||
f.write_str("Literal[")?;
|
||||
f.write_str(self.store.get_class(*class_id).name())?;
|
||||
f.write_str("]")
|
||||
}
|
||||
Type::Instance(class_id) => f.write_str(self.store.get_class(*class_id).name()),
|
||||
Type::Function(func_id) => f.write_str(self.store.get_function(*func_id).name()),
|
||||
Type::Union(union_id) => self
|
||||
.store
|
||||
.get_module(union_id.file_id)
|
||||
.get_union(union_id.union_id)
|
||||
.display(f, self.store),
|
||||
Type::Intersection(int_id) => self
|
||||
.store
|
||||
.get_module(int_id.file_id)
|
||||
.get_intersection(int_id.intersection_id)
|
||||
.display(f, self.store),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ClassType {
|
||||
name: Name,
|
||||
}
|
||||
|
||||
impl ClassType {
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FunctionType {
|
||||
name: Name,
|
||||
}
|
||||
|
||||
impl FunctionType {
|
||||
fn name(&self) -> &str {
|
||||
self.name.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnionType {
|
||||
// the union type includes values in any of these types
|
||||
elements: FxIndexSet<Type>,
|
||||
}
|
||||
|
||||
impl UnionType {
|
||||
fn display(&self, f: &mut std::fmt::Formatter<'_>, store: &TypeStore) -> std::fmt::Result {
|
||||
f.write_str("(")?;
|
||||
let mut first = true;
|
||||
for ty in &self.elements {
|
||||
if !first {
|
||||
f.write_str(" | ")?;
|
||||
};
|
||||
first = false;
|
||||
write!(f, "{}", ty.display(store))?;
|
||||
}
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
// Negation types aren't expressible in annotations, and are most likely to arise from type
|
||||
// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them
|
||||
// directly in intersections rather than as a separate type. This sacrifices some efficiency in the
|
||||
// case where a Not appears outside an intersection (unclear when that could even happen, but we'd
|
||||
// have to represent it as a single-element intersection if it did) in exchange for better
|
||||
// efficiency in the not-within-intersection case.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct IntersectionType {
|
||||
// the intersection type includes only values in all of these types
|
||||
positive: FxIndexSet<Type>,
|
||||
// negated elements of the intersection, e.g.
|
||||
negative: FxIndexSet<Type>,
|
||||
}
|
||||
|
||||
impl IntersectionType {
|
||||
fn display(&self, f: &mut std::fmt::Formatter<'_>, store: &TypeStore) -> std::fmt::Result {
|
||||
f.write_str("(")?;
|
||||
let mut first = true;
|
||||
for (neg, ty) in self
|
||||
.positive
|
||||
.iter()
|
||||
.map(|ty| (false, ty))
|
||||
.chain(self.negative.iter().map(|ty| (true, ty)))
|
||||
{
|
||||
if !first {
|
||||
f.write_str(" & ")?;
|
||||
};
|
||||
first = false;
|
||||
if neg {
|
||||
f.write_str("~")?;
|
||||
};
|
||||
write!(f, "{}", ty.display(store))?;
|
||||
}
|
||||
f.write_str(")")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::files::Files;
|
||||
use crate::types::{Type, TypeStore};
|
||||
use crate::FxIndexSet;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn add_class() {
|
||||
let store = TypeStore::default();
|
||||
let files = Files::default();
|
||||
let file_id = files.intern(Path::new("/foo"));
|
||||
let id = store.add_class(file_id, "C");
|
||||
assert_eq!(store.get_class(id).name(), "C");
|
||||
let inst = Type::Instance(id);
|
||||
assert_eq!(format!("{}", inst.display(&store)), "C");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_function() {
|
||||
let store = TypeStore::default();
|
||||
let files = Files::default();
|
||||
let file_id = files.intern(Path::new("/foo"));
|
||||
let id = store.add_function(file_id, "func");
|
||||
assert_eq!(store.get_function(id).name(), "func");
|
||||
let func = Type::Function(id);
|
||||
assert_eq!(format!("{}", func.display(&store)), "func");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_union() {
|
||||
let store = TypeStore::default();
|
||||
let files = Files::default();
|
||||
let file_id = files.intern(Path::new("/foo"));
|
||||
let c1 = store.add_class(file_id, "C1");
|
||||
let c2 = store.add_class(file_id, "C2");
|
||||
let elems = vec![Type::Instance(c1), Type::Instance(c2)];
|
||||
let id = store.add_union(file_id, &elems);
|
||||
assert_eq!(
|
||||
store.get_union(id).elements,
|
||||
elems.into_iter().collect::<FxIndexSet<_>>()
|
||||
);
|
||||
let union = Type::Union(id);
|
||||
assert_eq!(format!("{}", union.display(&store)), "(C1 | C2)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_intersection() {
|
||||
let store = TypeStore::default();
|
||||
let files = Files::default();
|
||||
let file_id = files.intern(Path::new("/foo"));
|
||||
let c1 = store.add_class(file_id, "C1");
|
||||
let c2 = store.add_class(file_id, "C2");
|
||||
let c3 = store.add_class(file_id, "C3");
|
||||
let pos = vec![Type::Instance(c1), Type::Instance(c2)];
|
||||
let neg = vec![Type::Instance(c3)];
|
||||
let id = store.add_intersection(file_id, &pos, &neg);
|
||||
assert_eq!(
|
||||
store.get_intersection(id).positive,
|
||||
pos.into_iter().collect::<FxIndexSet<_>>()
|
||||
);
|
||||
assert_eq!(
|
||||
store.get_intersection(id).negative,
|
||||
neg.into_iter().collect::<FxIndexSet<_>>()
|
||||
);
|
||||
let intersection = Type::Intersection(id);
|
||||
assert_eq!(
|
||||
format!("{}", intersection.display(&store)),
|
||||
"(C1 & C2 & ~C3)"
|
||||
);
|
||||
}
|
||||
}
|
||||
164
crates/red_knot/src/types/infer.rs
Normal file
164
crates/red_knot/src/types/infer.rs
Normal file
@@ -0,0 +1,164 @@
|
||||
#![allow(dead_code)]
|
||||
use crate::db::{HasJar, SemanticDb, SemanticJar};
|
||||
use crate::module::ModuleName;
|
||||
use crate::symbols::{Definition, ImportFromDefinition, SymbolId};
|
||||
use crate::types::Type;
|
||||
use crate::FileId;
|
||||
use ruff_python_ast::AstNode;
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(db))]
|
||||
pub fn infer_symbol_type<Db>(db: &Db, file_id: FileId, symbol_id: SymbolId) -> Type
|
||||
where
|
||||
Db: SemanticDb + HasJar<SemanticJar>,
|
||||
{
|
||||
let symbols = db.symbol_table(file_id);
|
||||
let defs = symbols.defs(symbol_id);
|
||||
|
||||
if let Some(ty) = db
|
||||
.jar()
|
||||
.type_store
|
||||
.get_cached_symbol_type(file_id, symbol_id)
|
||||
{
|
||||
return ty;
|
||||
}
|
||||
|
||||
// TODO handle multiple defs, conditional defs...
|
||||
assert_eq!(defs.len(), 1);
|
||||
|
||||
let ty = match &defs[0] {
|
||||
Definition::ImportFrom(ImportFromDefinition {
|
||||
module,
|
||||
name,
|
||||
level,
|
||||
}) => {
|
||||
// TODO relative imports
|
||||
assert!(matches!(level, 0));
|
||||
let module_name = ModuleName::new(module.as_ref().expect("TODO relative imports"));
|
||||
if let Some(module) = db.resolve_module(module_name.clone()) {
|
||||
let remote_file_id = module.path(db).file();
|
||||
let remote_symbols = db.symbol_table(remote_file_id);
|
||||
if let Some(remote_symbol_id) = remote_symbols.root_symbol_id_by_name(name) {
|
||||
// TODO integrate this into module and symbol-resolution APIs (requiring a
|
||||
// "requester" argument) so that it doesn't have to be remembered
|
||||
db.jar().type_store.record_symbol_dependency(
|
||||
(file_id, symbol_id),
|
||||
(remote_file_id, remote_symbol_id),
|
||||
);
|
||||
db.infer_symbol_type(remote_file_id, remote_symbol_id)
|
||||
} else {
|
||||
db.jar()
|
||||
.type_store
|
||||
.record_module_dependency((file_id, symbol_id), module_name);
|
||||
Type::Unknown
|
||||
}
|
||||
} else {
|
||||
db.jar()
|
||||
.type_store
|
||||
.record_module_dependency((file_id, symbol_id), module_name);
|
||||
Type::Unknown
|
||||
}
|
||||
}
|
||||
Definition::ClassDef(node_key) => {
|
||||
if let Some(ty) = db
|
||||
.jar()
|
||||
.type_store
|
||||
.get_cached_node_type(file_id, node_key.erased())
|
||||
{
|
||||
ty
|
||||
} else {
|
||||
let parsed = db.parse(file_id);
|
||||
let ast = parsed.ast();
|
||||
let node = node_key.resolve_unwrap(ast.as_any_node_ref());
|
||||
|
||||
let store = &db.jar().type_store;
|
||||
let ty = Type::Class(store.add_class(file_id, &node.name.id));
|
||||
store.cache_node_type(file_id, *node_key.erased(), ty);
|
||||
ty
|
||||
}
|
||||
}
|
||||
_ => todo!("other kinds of definitions"),
|
||||
};
|
||||
|
||||
db.jar()
|
||||
.type_store
|
||||
.cache_symbol_type(file_id, symbol_id, ty);
|
||||
ty
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::db::{HasJar, SemanticDb, SemanticJar};
|
||||
use crate::module::{ModuleName, ModuleSearchPath, ModuleSearchPathKind};
|
||||
use crate::types::Type;
|
||||
|
||||
// TODO with virtual filesystem we shouldn't have to write files to disk for these
|
||||
// tests
|
||||
|
||||
struct TestCase {
|
||||
temp_dir: tempfile::TempDir,
|
||||
db: TestDb,
|
||||
|
||||
src: ModuleSearchPath,
|
||||
}
|
||||
|
||||
fn create_test() -> std::io::Result<TestCase> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
|
||||
let src = temp_dir.path().join("src");
|
||||
std::fs::create_dir(&src)?;
|
||||
let src = ModuleSearchPath::new(src.canonicalize()?, ModuleSearchPathKind::FirstParty);
|
||||
|
||||
let roots = vec![src.clone()];
|
||||
|
||||
let mut db = TestDb::default();
|
||||
db.set_module_search_paths(roots);
|
||||
|
||||
Ok(TestCase { temp_dir, db, src })
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn follow_import_to_class() -> std::io::Result<()> {
|
||||
let TestCase {
|
||||
src,
|
||||
db,
|
||||
temp_dir: _temp_dir,
|
||||
} = create_test()?;
|
||||
|
||||
let a_path = src.path().join("a.py");
|
||||
let b_path = src.path().join("b.py");
|
||||
std::fs::write(a_path, "from b import C as D")?;
|
||||
std::fs::write(b_path, "class C: pass")?;
|
||||
let a_file = db
|
||||
.resolve_module(ModuleName::new("a"))
|
||||
.expect("module should be found")
|
||||
.path(&db)
|
||||
.file();
|
||||
let a_syms = db.symbol_table(a_file);
|
||||
let d_sym = a_syms
|
||||
.root_symbol_id_by_name("D")
|
||||
.expect("D symbol should be found");
|
||||
|
||||
let ty = db.infer_symbol_type(a_file, d_sym);
|
||||
|
||||
let b_file = db
|
||||
.resolve_module(ModuleName::new("b"))
|
||||
.expect("module should be found")
|
||||
.path(&db)
|
||||
.file();
|
||||
let b_syms = db.symbol_table(b_file);
|
||||
let c_sym = b_syms
|
||||
.root_symbol_id_by_name("C")
|
||||
.expect("C symbol should be found");
|
||||
|
||||
let jar = HasJar::<SemanticJar>::jar(&db);
|
||||
|
||||
assert!(matches!(ty, Type::Class(_)));
|
||||
assert_eq!(format!("{}", ty.display(&jar.type_store)), "Literal[C]");
|
||||
assert_eq!(
|
||||
jar.type_store.get_module(a_file).symbol_dependencies[&d_sym],
|
||||
[(b_file, c_sym)].iter().copied().collect()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
78
crates/red_knot/src/watch.rs
Normal file
78
crates/red_knot/src/watch.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use anyhow::Context;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::files::Files;
|
||||
use crate::program::{FileChange, FileChangeKind};
|
||||
use notify::event::{CreateKind, RemoveKind};
|
||||
use notify::{recommended_watcher, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
pub struct FileWatcher {
|
||||
watcher: RecommendedWatcher,
|
||||
}
|
||||
|
||||
pub trait EventHandler: Send + 'static {
|
||||
fn handle(&self, changes: Vec<FileChange>);
|
||||
}
|
||||
|
||||
impl<F> EventHandler for F
|
||||
where
|
||||
F: Fn(Vec<FileChange>) + Send + 'static,
|
||||
{
|
||||
fn handle(&self, changes: Vec<FileChange>) {
|
||||
let f = self;
|
||||
f(changes);
|
||||
}
|
||||
}
|
||||
|
||||
impl FileWatcher {
|
||||
pub fn new<E>(handler: E, files: Files) -> anyhow::Result<Self>
|
||||
where
|
||||
E: EventHandler,
|
||||
{
|
||||
Self::from_handler(Box::new(handler), files)
|
||||
}
|
||||
|
||||
fn from_handler(handler: Box<dyn EventHandler>, files: Files) -> anyhow::Result<Self> {
|
||||
let watcher = recommended_watcher(move |changes: notify::Result<Event>| {
|
||||
match changes {
|
||||
Ok(event) => {
|
||||
// TODO verify that this handles all events correctly
|
||||
let change_kind = match event.kind {
|
||||
EventKind::Create(CreateKind::File) => FileChangeKind::Created,
|
||||
EventKind::Modify(_) => FileChangeKind::Modified,
|
||||
EventKind::Remove(RemoveKind::File) => FileChangeKind::Deleted,
|
||||
_ => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut changes = Vec::new();
|
||||
|
||||
for path in event.paths {
|
||||
if path.is_file() {
|
||||
let id = files.intern(&path);
|
||||
changes.push(FileChange::new(id, change_kind));
|
||||
}
|
||||
}
|
||||
|
||||
if !changes.is_empty() {
|
||||
handler.handle(changes);
|
||||
}
|
||||
}
|
||||
// TODO proper error handling
|
||||
Err(err) => {
|
||||
panic!("Error: {err}");
|
||||
}
|
||||
}
|
||||
})
|
||||
.context("Failed to create file watcher.")?;
|
||||
|
||||
Ok(Self { watcher })
|
||||
}
|
||||
|
||||
pub fn watch_folder(&mut self, path: &Path) -> anyhow::Result<()> {
|
||||
self.watcher.watch(path, RecursiveMode::Recursive)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1284,3 +1284,49 @@ fn negated_per_file_ignores_overlap() -> Result<()> {
|
||||
"###);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_interaction() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let ruff_toml = tempdir.path().join("ruff.toml");
|
||||
fs::write(
|
||||
&ruff_toml,
|
||||
r#"
|
||||
[lint]
|
||||
select = ["F"]
|
||||
"#,
|
||||
)?;
|
||||
|
||||
insta::with_settings!({
|
||||
filters => vec![(tempdir_filter(&tempdir).as_str(), "[TMP]/")]
|
||||
}, {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(&ruff_toml)
|
||||
.args(["--stdin-filename", "test.py"])
|
||||
.arg("--fix")
|
||||
.arg("-")
|
||||
.pass_stdin(r#"
|
||||
import os # F401
|
||||
|
||||
def function():
|
||||
import os # F811
|
||||
print(os.name)
|
||||
"#), @r###"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
import os # F401
|
||||
|
||||
def function():
|
||||
print(os.name)
|
||||
|
||||
----- stderr -----
|
||||
Found 1 error (1 fixed, 0 remaining).
|
||||
"###);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
itertools = { workspace = true }
|
||||
glob = { workspace = true }
|
||||
globset = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
filetime = { workspace = true }
|
||||
seahash = { workspace = true }
|
||||
|
||||
@@ -7,6 +7,9 @@ logging.log(logging.INFO, f"Hello {name}")
|
||||
_LOGGER = logging.getLogger()
|
||||
_LOGGER.info(f"{__name__}")
|
||||
|
||||
logging.getLogger().info(f"{name}")
|
||||
|
||||
from logging import info
|
||||
|
||||
info(f"{name}")
|
||||
info(f"{__name__}")
|
||||
|
||||
@@ -53,7 +53,7 @@ class WithinBody[T](list[T]): # OK
|
||||
|
||||
def foo(self, x: T) -> T: # OK
|
||||
return x
|
||||
|
||||
|
||||
def foo(self):
|
||||
T # OK
|
||||
|
||||
@@ -76,6 +76,16 @@ type Foo[T: (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNot
|
||||
def foo[T: (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
|
||||
# Same in defaults
|
||||
|
||||
type Foo[T = DoesNotExist] = T # F821: Undefined name `DoesNotExist`
|
||||
def foo[T = DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist`
|
||||
class Foo[T = DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist`
|
||||
|
||||
type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
def foo[T = (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
class Foo[T = (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
|
||||
# Type parameters in nested classes
|
||||
|
||||
class Parent[T]:
|
||||
@@ -83,21 +93,21 @@ class Parent[T]:
|
||||
|
||||
def can_use_class_variable(self, x: t) -> t: # OK
|
||||
return x
|
||||
|
||||
|
||||
class Child:
|
||||
def can_access_parent_type_parameter(self, x: T) -> T: # OK
|
||||
T # OK
|
||||
return x
|
||||
|
||||
|
||||
def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T`
|
||||
t # F821: Undefined name `t`
|
||||
return x
|
||||
|
||||
|
||||
# Type parameters in nested functions
|
||||
|
||||
def can_access_inside_nested[T](t: T) -> T: # OK
|
||||
def bar(x: T) -> T: # OK
|
||||
T # OK
|
||||
return x
|
||||
|
||||
|
||||
bar(t)
|
||||
|
||||
1
crates/ruff_linter/resources/test/fixtures/pygrep_hooks/PGH004_1.py
vendored
Normal file
1
crates/ruff_linter/resources/test/fixtures/pygrep_hooks/PGH004_1.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
#noqa
|
||||
19
crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py
vendored
Normal file
19
crates/ruff_linter/resources/test/fixtures/refurb/FURB116.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
num = 1337
|
||||
|
||||
def return_num() -> int:
|
||||
return num
|
||||
|
||||
print(oct(num)[2:]) # FURB116
|
||||
print(hex(num)[2:]) # FURB116
|
||||
print(bin(num)[2:]) # FURB116
|
||||
|
||||
print(oct(1337)[2:]) # FURB116
|
||||
print(hex(1337)[2:]) # FURB116
|
||||
print(bin(1337)[2:]) # FURB116
|
||||
|
||||
print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
|
||||
## invalid
|
||||
print(oct(0o1337)[1:])
|
||||
print(hex(0x1337)[3:])
|
||||
@@ -26,10 +26,10 @@ def f() -> None:
|
||||
|
||||
# fmt: off
|
||||
# Invalid - no space before #
|
||||
d = 1# noqa: E501
|
||||
d = 1 # noqa: E501
|
||||
|
||||
# Invalid - many spaces before #
|
||||
d = 1 # noqa: E501
|
||||
d = 1 # noqa: E501
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -104,5 +104,28 @@ def f():
|
||||
|
||||
def f():
|
||||
# Invalid - nonexistant error code with multibyte character
|
||||
d = 1 #
noqa: F841, E50
|
||||
e = 1 #
noqa: E50
|
||||
d = 1 #
noqa: F841, E50
|
||||
e = 1 #
noqa: E50
|
||||
|
||||
|
||||
def f():
|
||||
# Disabled - check redirects are reported correctly
|
||||
eval(command) # noqa: PGH001
|
||||
|
||||
|
||||
# Check duplicate code detection
|
||||
def f():
|
||||
x = 2 # noqa: F841, F841, X200
|
||||
|
||||
y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
|
||||
z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
|
||||
return
|
||||
|
||||
|
||||
# Allow code redirects
|
||||
x = eval(command) # noqa: PGH001, S307
|
||||
x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
||||
6
crates/ruff_linter/resources/test/fixtures/ruff/RUF101.py
vendored
Normal file
6
crates/ruff_linter/resources/test/fixtures/ruff/RUF101.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
x = 2 # noqa: RUF940
|
||||
x = 2 # noqa: RUF950
|
||||
x = 2 # noqa: RUF940, RUF950
|
||||
x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
@@ -128,6 +128,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SortedMinMax) {
|
||||
refurb::rules::sorted_min_max(checker, subscript);
|
||||
}
|
||||
if checker.enabled(Rule::FStringNumberFormat) {
|
||||
refurb::rules::fstring_number_format(checker, subscript);
|
||||
}
|
||||
|
||||
pandas_vet::rules::subscript(checker, value, expr);
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
let level = *level;
|
||||
|
||||
// Mark the top-level module as "seen" by the semantic model.
|
||||
if level.map_or(true, |level| level == 0) {
|
||||
if level == 0 {
|
||||
if let Some(module) = module.and_then(|module| module.split('.').next()) {
|
||||
self.semantic.add_module(module);
|
||||
}
|
||||
@@ -1568,8 +1568,8 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
// Step 1: Binding
|
||||
match type_param {
|
||||
ast::TypeParam::TypeVar(ast::TypeParamTypeVar { name, range, .. })
|
||||
| ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { name, range })
|
||||
| ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { name, range }) => {
|
||||
| ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { name, range, .. })
|
||||
| ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { name, range, .. }) => {
|
||||
self.add_binding(
|
||||
name.as_str(),
|
||||
*range,
|
||||
@@ -1579,13 +1579,46 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
}
|
||||
}
|
||||
// Step 2: Traversal
|
||||
if let ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
|
||||
bound: Some(bound), ..
|
||||
}) = type_param
|
||||
{
|
||||
self.visit
|
||||
.type_param_definitions
|
||||
.push((bound, self.semantic.snapshot()));
|
||||
match type_param {
|
||||
ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
|
||||
bound,
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = bound {
|
||||
self.visit
|
||||
.type_param_definitions
|
||||
.push((expr, self.semantic.snapshot()));
|
||||
}
|
||||
if let Some(expr) = default {
|
||||
self.visit
|
||||
.type_param_definitions
|
||||
.push((expr, self.semantic.snapshot()));
|
||||
}
|
||||
}
|
||||
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple {
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = default {
|
||||
self.visit
|
||||
.type_param_definitions
|
||||
.push((expr, self.semantic.snapshot()));
|
||||
}
|
||||
}
|
||||
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec {
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = default {
|
||||
self.visit
|
||||
.type_param_definitions
|
||||
.push((expr, self.semantic.snapshot()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ fn extract_import_map(path: &Path, package: Option<&Path>, blocks: &[&Block]) ->
|
||||
level,
|
||||
range: _,
|
||||
}) => {
|
||||
let level = level.unwrap_or_default() as usize;
|
||||
let level = *level as usize;
|
||||
let module = if let Some(module) = module {
|
||||
let module: &String = module.as_ref();
|
||||
if level == 0 {
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
use std::path::Path;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ruff_text_size::Ranged;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::fix::edits::delete_comment;
|
||||
use crate::noqa;
|
||||
use crate::noqa::{Directive, FileExemption, NoqaDirectives, NoqaMapping};
|
||||
use crate::noqa::{Code, Directive, FileExemption, NoqaDirectives, NoqaMapping};
|
||||
use crate::registry::{AsRule, Rule, RuleSet};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::rules::pygrep_hooks;
|
||||
use crate::rules::ruff;
|
||||
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
@@ -85,7 +86,7 @@ pub(crate) fn check_noqa(
|
||||
true
|
||||
}
|
||||
Directive::Codes(directive) => {
|
||||
if noqa::includes(diagnostic.kind.rule(), directive.codes()) {
|
||||
if directive.includes(diagnostic.kind.rule()) {
|
||||
directive_line
|
||||
.matches
|
||||
.push(diagnostic.kind.rule().noqa_code());
|
||||
@@ -128,33 +129,37 @@ pub(crate) fn check_noqa(
|
||||
}
|
||||
Directive::Codes(directive) => {
|
||||
let mut disabled_codes = vec![];
|
||||
let mut duplicated_codes = vec![];
|
||||
let mut unknown_codes = vec![];
|
||||
let mut unmatched_codes = vec![];
|
||||
let mut valid_codes = vec![];
|
||||
let mut seen_codes = FxHashSet::default();
|
||||
let mut self_ignore = false;
|
||||
for code in directive.codes() {
|
||||
let code = get_redirect_target(code).unwrap_or(code);
|
||||
for original_code in directive.iter().map(Code::as_str) {
|
||||
let code = get_redirect_target(original_code).unwrap_or(original_code);
|
||||
if Rule::UnusedNOQA.noqa_code() == code {
|
||||
self_ignore = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if line.matches.iter().any(|match_| *match_ == code)
|
||||
if !seen_codes.insert(original_code) {
|
||||
duplicated_codes.push(original_code);
|
||||
} else if line.matches.iter().any(|match_| *match_ == code)
|
||||
|| settings
|
||||
.external
|
||||
.iter()
|
||||
.any(|external| code.starts_with(external))
|
||||
{
|
||||
valid_codes.push(code);
|
||||
valid_codes.push(original_code);
|
||||
} else {
|
||||
if let Ok(rule) = Rule::from_code(code) {
|
||||
if settings.rules.enabled(rule) {
|
||||
unmatched_codes.push(code);
|
||||
unmatched_codes.push(original_code);
|
||||
} else {
|
||||
disabled_codes.push(code);
|
||||
disabled_codes.push(original_code);
|
||||
}
|
||||
} else {
|
||||
unknown_codes.push(code);
|
||||
unknown_codes.push(original_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,6 +169,7 @@ pub(crate) fn check_noqa(
|
||||
}
|
||||
|
||||
if !(disabled_codes.is_empty()
|
||||
&& duplicated_codes.is_empty()
|
||||
&& unknown_codes.is_empty()
|
||||
&& unmatched_codes.is_empty())
|
||||
{
|
||||
@@ -174,6 +180,10 @@ pub(crate) fn check_noqa(
|
||||
.iter()
|
||||
.map(|code| (*code).to_string())
|
||||
.collect(),
|
||||
duplicated: duplicated_codes
|
||||
.iter()
|
||||
.map(|code| (*code).to_string())
|
||||
.collect(),
|
||||
unknown: unknown_codes
|
||||
.iter()
|
||||
.map(|code| (*code).to_string())
|
||||
@@ -208,6 +218,10 @@ pub(crate) fn check_noqa(
|
||||
pygrep_hooks::rules::blanket_noqa(diagnostics, &noqa_directives, locator);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::RedirectedNOQA) {
|
||||
ruff::rules::redirected_noqa(diagnostics, &noqa_directives);
|
||||
}
|
||||
|
||||
ignored_diagnostics.sort_unstable();
|
||||
ignored_diagnostics
|
||||
}
|
||||
|
||||
@@ -969,6 +969,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "028") => (RuleGroup::Preview, rules::ruff::rules::InvalidFormatterSuppressionComment),
|
||||
(Ruff, "029") => (RuleGroup::Preview, rules::ruff::rules::UnusedAsync),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Preview, rules::ruff::rules::RedirectedNOQA),
|
||||
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
|
||||
#[cfg(feature = "test-rules")]
|
||||
(Ruff, "900") => (RuleGroup::Stable, rules::ruff::rules::StableTestRule),
|
||||
@@ -1050,6 +1051,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "110") => (RuleGroup::Preview, rules::refurb::rules::IfExpInsteadOfOrOperator),
|
||||
#[allow(deprecated)]
|
||||
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
|
||||
(Refurb, "116") => (RuleGroup::Preview, rules::refurb::rules::FStringNumberFormat),
|
||||
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
|
||||
(Refurb, "129") => (RuleGroup::Preview, rules::refurb::rules::ReadlinesInFor),
|
||||
#[allow(deprecated)]
|
||||
|
||||
@@ -129,21 +129,31 @@ fn apply_fixes<'a>(
|
||||
|
||||
/// Compare two fixes.
|
||||
fn cmp_fix(rule1: Rule, rule2: Rule, fix1: &Fix, fix2: &Fix) -> std::cmp::Ordering {
|
||||
fix1.min_start()
|
||||
.cmp(&fix2.min_start())
|
||||
.then_with(|| match (&rule1, &rule2) {
|
||||
// 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
|
||||
}
|
||||
(Rule::IfElseBlockInsteadOfIfExp, Rule::IfElseBlockInsteadOfDictGet) => {
|
||||
std::cmp::Ordering::Greater
|
||||
}
|
||||
// Always apply `RedefinedWhileUnused` before `UnusedImport`, as the latter can end up fixing
|
||||
// the former.
|
||||
{
|
||||
match (rule1, rule2) {
|
||||
(Rule::RedefinedWhileUnused, Rule::UnusedImport) => return std::cmp::Ordering::Less,
|
||||
(Rule::UnusedImport, Rule::RedefinedWhileUnused) => return std::cmp::Ordering::Greater,
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
})
|
||||
}
|
||||
}
|
||||
// Apply fixes in order of their start position.
|
||||
.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 `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
|
||||
}
|
||||
(Rule::IfElseBlockInsteadOfIfExp, Rule::IfElseBlockInsteadOfDictGet) => {
|
||||
std::cmp::Ordering::Greater
|
||||
}
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -405,7 +405,7 @@ impl<'a> Importer<'a> {
|
||||
range: _,
|
||||
}) = stmt
|
||||
{
|
||||
if level.map_or(true, |level| level == 0)
|
||||
if *level == 0
|
||||
&& name.as_ref().is_some_and(|name| name == module)
|
||||
&& names.iter().all(|alias| alias.name.as_str() != "*")
|
||||
{
|
||||
|
||||
@@ -85,8 +85,16 @@ impl<'a> Directive<'a> {
|
||||
let mut codes_end = codes_start;
|
||||
let mut leading_space = 0;
|
||||
while let Some(code) = Self::lex_code(&text[codes_end + leading_space..]) {
|
||||
codes.push(code);
|
||||
codes_end += leading_space;
|
||||
codes.push(Code {
|
||||
code,
|
||||
range: TextRange::at(
|
||||
TextSize::try_from(codes_end).unwrap(),
|
||||
code.text_len(),
|
||||
)
|
||||
.add(offset),
|
||||
});
|
||||
|
||||
codes_end += code.len();
|
||||
|
||||
// Codes can be comma- or whitespace-delimited. Compute the length of the
|
||||
@@ -175,16 +183,51 @@ impl Ranged for All {
|
||||
}
|
||||
}
|
||||
|
||||
/// An individual rule code in a `noqa` directive (e.g., `F401`).
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Code<'a> {
|
||||
code: &'a str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl<'a> Code<'a> {
|
||||
/// The code that is ignored by the `noqa` directive.
|
||||
pub(crate) fn as_str(&self) -> &'a str {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Code<'_> {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
fmt.write_str(self.code)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Ranged for Code<'a> {
|
||||
/// The range of the rule code.
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Codes<'a> {
|
||||
range: TextRange,
|
||||
codes: Vec<&'a str>,
|
||||
codes: Vec<Code<'a>>,
|
||||
}
|
||||
|
||||
impl Codes<'_> {
|
||||
/// The codes that are ignored by the `noqa` directive.
|
||||
pub(crate) fn codes(&self) -> &[&str] {
|
||||
&self.codes
|
||||
impl<'a> Codes<'a> {
|
||||
/// Returns an iterator over the [`Code`]s in the `noqa` directive.
|
||||
pub(crate) fn iter(&self) -> std::slice::Iter<Code> {
|
||||
self.codes.iter()
|
||||
}
|
||||
|
||||
/// Returns `true` if the string list of `codes` includes `code` (or an alias
|
||||
/// thereof).
|
||||
pub(crate) fn includes(&self, needle: Rule) -> bool {
|
||||
let needle = needle.noqa_code();
|
||||
self.iter()
|
||||
.any(|code| needle == get_redirect_target(code.as_str()).unwrap_or(code.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,15 +238,6 @@ impl Ranged for Codes<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the string list of `codes` includes `code` (or an alias
|
||||
/// thereof).
|
||||
pub(crate) fn includes(needle: Rule, haystack: &[&str]) -> bool {
|
||||
let needle = needle.noqa_code();
|
||||
haystack
|
||||
.iter()
|
||||
.any(|candidate| needle == get_redirect_target(candidate).unwrap_or(candidate))
|
||||
}
|
||||
|
||||
/// Returns `true` if the given [`Rule`] is ignored at the specified `lineno`.
|
||||
pub(crate) fn rule_is_ignored(
|
||||
code: Rule,
|
||||
@@ -215,7 +249,7 @@ pub(crate) fn rule_is_ignored(
|
||||
let line_range = locator.line_range(offset);
|
||||
match Directive::try_extract(locator.slice(line_range), line_range.start()) {
|
||||
Ok(Some(Directive::All(_))) => true,
|
||||
Ok(Some(Directive::Codes(Codes { codes, range: _ }))) => includes(code, &codes),
|
||||
Ok(Some(Directive::Codes(codes))) => codes.includes(code),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
@@ -525,8 +559,8 @@ fn add_noqa_inner(
|
||||
Directive::All(_) => {
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(Codes { codes, range: _ }) => {
|
||||
if includes(diagnostic.kind.rule(), codes) {
|
||||
Directive::Codes(codes) => {
|
||||
if codes.includes(diagnostic.kind.rule()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -542,9 +576,9 @@ fn add_noqa_inner(
|
||||
Directive::All(_) => {
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(Codes { codes, range: _ }) => {
|
||||
Directive::Codes(codes) => {
|
||||
let rule = diagnostic.kind.rule();
|
||||
if !includes(rule, codes) {
|
||||
if !codes.includes(rule) {
|
||||
matches_by_line
|
||||
.entry(directive_line.start())
|
||||
.or_insert_with(|| {
|
||||
@@ -591,7 +625,7 @@ fn add_noqa_inner(
|
||||
Some(Directive::All(_)) => {
|
||||
// Does not get inserted into the map.
|
||||
}
|
||||
Some(Directive::Codes(Codes { range, codes })) => {
|
||||
Some(Directive::Codes(codes)) => {
|
||||
// Reconstruct the line based on the preserved rule codes.
|
||||
// This enables us to tally the number of edits.
|
||||
let output_start = output.len();
|
||||
@@ -599,7 +633,7 @@ fn add_noqa_inner(
|
||||
// Add existing content.
|
||||
output.push_str(
|
||||
locator
|
||||
.slice(TextRange::new(offset, range.start()))
|
||||
.slice(TextRange::new(offset, codes.start()))
|
||||
.trim_end(),
|
||||
);
|
||||
|
||||
@@ -652,6 +686,8 @@ pub(crate) struct NoqaDirectiveLine<'a> {
|
||||
pub(crate) directive: Directive<'a>,
|
||||
/// The codes that are ignored by the directive.
|
||||
pub(crate) matches: Vec<NoqaCode>,
|
||||
// Whether the directive applies to range.end
|
||||
pub(crate) includes_end: bool,
|
||||
}
|
||||
|
||||
impl Ranged for NoqaDirectiveLine<'_> {
|
||||
@@ -684,23 +720,18 @@ impl<'a> NoqaDirectives<'a> {
|
||||
}
|
||||
Ok(Some(directive)) => {
|
||||
// noqa comments are guaranteed to be single line.
|
||||
let range = locator.line_range(range.start());
|
||||
directives.push(NoqaDirectiveLine {
|
||||
range: locator.line_range(range.start()),
|
||||
range,
|
||||
directive,
|
||||
matches: Vec::new(),
|
||||
includes_end: range.end() == locator.contents().text_len(),
|
||||
});
|
||||
}
|
||||
Ok(None) => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Extend a mapping at the end of the file to also include the EOF token.
|
||||
if let Some(last) = directives.last_mut() {
|
||||
if last.range.end() == locator.contents().text_len() {
|
||||
last.range = last.range.add_end(TextSize::from(1));
|
||||
}
|
||||
}
|
||||
|
||||
Self { inner: directives }
|
||||
}
|
||||
|
||||
@@ -724,11 +755,15 @@ impl<'a> NoqaDirectives<'a> {
|
||||
.binary_search_by(|directive| {
|
||||
if directive.range.end() < offset {
|
||||
std::cmp::Ordering::Less
|
||||
} else if directive.range.contains(offset) {
|
||||
std::cmp::Ordering::Equal
|
||||
} else {
|
||||
} else if directive.range.start() > offset {
|
||||
std::cmp::Ordering::Greater
|
||||
}
|
||||
// At this point, end >= offset, start <= offset
|
||||
else if !directive.includes_end && directive.range.end() == offset {
|
||||
std::cmp::Ordering::Less
|
||||
} else {
|
||||
std::cmp::Ordering::Equal
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ impl Rule {
|
||||
pub const fn lint_source(&self) -> LintSource {
|
||||
match self {
|
||||
Rule::InvalidPyprojectToml => LintSource::PyprojectToml,
|
||||
Rule::BlanketNOQA | Rule::UnusedNOQA => LintSource::Noqa,
|
||||
Rule::BlanketNOQA | Rule::RedirectedNOQA | Rule::UnusedNOQA => LintSource::Noqa,
|
||||
Rule::BidirectionalUnicode
|
||||
| Rule::BlankLineWithWhitespace
|
||||
| Rule::DocLineTooLong
|
||||
|
||||
@@ -25,23 +25,31 @@ G004.py:8:14: G004 Logging statement uses f-string
|
||||
8 | _LOGGER.info(f"{__name__}")
|
||||
| ^^^^^^^^^^^^^ G004
|
||||
9 |
|
||||
10 | from logging import info
|
||||
10 | logging.getLogger().info(f"{name}")
|
||||
|
|
||||
|
||||
G004.py:11:6: G004 Logging statement uses f-string
|
||||
G004.py:10:26: G004 Logging statement uses f-string
|
||||
|
|
||||
10 | from logging import info
|
||||
11 | info(f"{name}")
|
||||
8 | _LOGGER.info(f"{__name__}")
|
||||
9 |
|
||||
10 | logging.getLogger().info(f"{name}")
|
||||
| ^^^^^^^^^ G004
|
||||
11 |
|
||||
12 | from logging import info
|
||||
|
|
||||
|
||||
G004.py:14:6: G004 Logging statement uses f-string
|
||||
|
|
||||
12 | from logging import info
|
||||
13 |
|
||||
14 | info(f"{name}")
|
||||
| ^^^^^^^^^ G004
|
||||
12 | info(f"{__name__}")
|
||||
15 | info(f"{__name__}")
|
||||
|
|
||||
|
||||
G004.py:12:6: G004 Logging statement uses f-string
|
||||
G004.py:15:6: G004 Logging statement uses f-string
|
||||
|
|
||||
10 | from logging import info
|
||||
11 | info(f"{name}")
|
||||
12 | info(f"{__name__}")
|
||||
14 | info(f"{name}")
|
||||
15 | info(f"{__name__}")
|
||||
| ^^^^^^^^^^^^^ G004
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -54,13 +54,11 @@ pub(crate) fn import(import_from: &Stmt, name: &str, asname: Option<&str>) -> Op
|
||||
pub(crate) fn import_from(
|
||||
import_from: &Stmt,
|
||||
module: Option<&str>,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
) -> Option<Diagnostic> {
|
||||
// If level is not zero or module is none, return
|
||||
if let Some(level) = level {
|
||||
if level != 0 {
|
||||
return None;
|
||||
}
|
||||
if level != 0 {
|
||||
return None;
|
||||
};
|
||||
|
||||
if let Some(module) = module {
|
||||
|
||||
@@ -76,7 +76,7 @@ impl Violation for RelativeImports {
|
||||
|
||||
fn fix_banned_relative_import(
|
||||
stmt: &Stmt,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
module: Option<&str>,
|
||||
module_path: Option<&[String]>,
|
||||
generator: Generator,
|
||||
@@ -99,7 +99,7 @@ fn fix_banned_relative_import(
|
||||
TextRange::default(),
|
||||
)),
|
||||
names: names.clone(),
|
||||
level: Some(0),
|
||||
level: 0,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
let content = generator.stmt(&node.into());
|
||||
@@ -113,7 +113,7 @@ fn fix_banned_relative_import(
|
||||
pub(crate) fn banned_relative_import(
|
||||
checker: &Checker,
|
||||
stmt: &Stmt,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
module: Option<&str>,
|
||||
module_path: Option<&[String]>,
|
||||
strictness: Strictness,
|
||||
@@ -122,7 +122,7 @@ pub(crate) fn banned_relative_import(
|
||||
Strictness::All => 0,
|
||||
Strictness::Parents => 1,
|
||||
};
|
||||
if level? > strictness_level {
|
||||
if level > strictness_level {
|
||||
let mut diagnostic = Diagnostic::new(RelativeImports { strictness }, stmt.range());
|
||||
if let Some(fix) =
|
||||
fix_banned_relative_import(stmt, level, module, module_path, checker.generator())
|
||||
|
||||
@@ -300,7 +300,7 @@ pub(crate) fn typing_only_runtime_import(
|
||||
// Categorize the import, using coarse-grained categorization.
|
||||
let import_type = match categorize(
|
||||
&qualified_name.to_string(),
|
||||
None,
|
||||
0,
|
||||
&checker.settings.src,
|
||||
checker.package(),
|
||||
checker.settings.isort.detect_same_package,
|
||||
|
||||
@@ -92,7 +92,7 @@ enum Reason<'a> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn categorize<'a>(
|
||||
module_name: &str,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
src: &[PathBuf],
|
||||
package: Option<&Path>,
|
||||
detect_same_package: bool,
|
||||
@@ -104,14 +104,14 @@ pub(crate) fn categorize<'a>(
|
||||
) -> &'a ImportSection {
|
||||
let module_base = module_name.split('.').next().unwrap();
|
||||
let (mut import_type, mut reason) = {
|
||||
if matches!(level, None | Some(0)) && module_base == "__future__" {
|
||||
if level == 0 && module_base == "__future__" {
|
||||
(&ImportSection::Known(ImportType::Future), Reason::Future)
|
||||
} else if no_sections {
|
||||
(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::NoSections,
|
||||
)
|
||||
} else if level.is_some_and(|level| level > 0) {
|
||||
} else if level > 0 {
|
||||
(
|
||||
&ImportSection::Known(ImportType::LocalFolder),
|
||||
Reason::NonZeroLevel,
|
||||
@@ -133,7 +133,7 @@ pub(crate) fn categorize<'a>(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::SourceMatch(src),
|
||||
)
|
||||
} else if matches!(level, None | Some(0)) && module_name == "__main__" {
|
||||
} else if level == 0 && module_name == "__main__" {
|
||||
(
|
||||
&ImportSection::Known(ImportType::FirstParty),
|
||||
Reason::KnownFirstParty,
|
||||
@@ -191,7 +191,7 @@ pub(crate) fn categorize_imports<'a>(
|
||||
for (alias, comments) in block.import {
|
||||
let import_type = categorize(
|
||||
&alias.module_name(),
|
||||
None,
|
||||
0,
|
||||
src,
|
||||
package,
|
||||
detect_same_package,
|
||||
|
||||
@@ -52,7 +52,7 @@ pub(crate) enum AnnotatedImport<'a> {
|
||||
ImportFrom {
|
||||
module: Option<&'a str>,
|
||||
names: Vec<AnnotatedAliasData<'a>>,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
atop: Vec<Comment<'a>>,
|
||||
inline: Vec<Comment<'a>>,
|
||||
trailing_comma: TrailingComma,
|
||||
|
||||
@@ -73,7 +73,7 @@ pub(crate) fn order_imports<'a>(
|
||||
ModuleKey::from_module(
|
||||
Some(alias.name),
|
||||
alias.asname,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
ImportStyle::Straight,
|
||||
settings,
|
||||
@@ -90,7 +90,7 @@ pub(crate) fn order_imports<'a>(
|
||||
Import((alias, _)) => ModuleKey::from_module(
|
||||
Some(alias.name),
|
||||
alias.asname,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
ImportStyle::Straight,
|
||||
settings,
|
||||
@@ -110,7 +110,7 @@ pub(crate) fn order_imports<'a>(
|
||||
ModuleKey::from_module(
|
||||
Some(alias.name),
|
||||
alias.asname,
|
||||
None,
|
||||
0,
|
||||
None,
|
||||
ImportStyle::Straight,
|
||||
settings,
|
||||
|
||||
@@ -97,7 +97,7 @@ impl<'a> ModuleKey<'a> {
|
||||
pub(crate) fn from_module(
|
||||
name: Option<&'a str>,
|
||||
asname: Option<&'a str>,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
first_alias: Option<(&'a str, Option<&'a str>)>,
|
||||
style: ImportStyle,
|
||||
settings: &Settings,
|
||||
@@ -106,13 +106,11 @@ impl<'a> ModuleKey<'a> {
|
||||
|
||||
let maybe_length = (settings.length_sort
|
||||
|| (settings.length_sort_straight && style == ImportStyle::Straight))
|
||||
.then_some(
|
||||
name.map(str::width).unwrap_or_default() + level.unwrap_or_default() as usize,
|
||||
);
|
||||
.then_some(name.map(str::width).unwrap_or_default() + level as usize);
|
||||
|
||||
let distance = match level {
|
||||
None | Some(0) => Distance::None,
|
||||
Some(level) => match settings.relative_imports_order {
|
||||
0 => Distance::None,
|
||||
_ => match settings.relative_imports_order {
|
||||
RelativeImportsOrder::ClosestToFurthest => Distance::Nearest(level),
|
||||
RelativeImportsOrder::FurthestToClosest => Distance::Furthest(Reverse(level)),
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ pub(crate) enum TrailingComma {
|
||||
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq, Clone)]
|
||||
pub(crate) struct ImportFromData<'a> {
|
||||
pub(crate) module: Option<&'a str>,
|
||||
pub(crate) level: Option<u32>,
|
||||
pub(crate) level: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
|
||||
|
||||
@@ -196,9 +196,7 @@ fn function(
|
||||
value: Some(Box::new(body.clone())),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
let parameters = parameters
|
||||
.cloned()
|
||||
.unwrap_or_else(|| Parameters::empty(TextRange::default()));
|
||||
let parameters = parameters.cloned().unwrap_or_default();
|
||||
if let Some(annotation) = annotation {
|
||||
if let Some((arg_types, return_type)) = extract_types(annotation, semantic) {
|
||||
// A `lambda` expression can only have positional and positional-only
|
||||
|
||||
@@ -10,8 +10,9 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python 2, the `print` statement can be used with the `>>` syntax to
|
||||
/// print to a file-like object. This `print >> sys.stderr` syntax is
|
||||
/// deprecated in Python 3.
|
||||
/// print to a file-like object. This `print >> sys.stderr` syntax no
|
||||
/// longer exists in Python 3, where `print` is only a function, not a
|
||||
/// statement.
|
||||
///
|
||||
/// Instead, use the `file` keyword argument to the `print` function, the
|
||||
/// `sys.stderr.write` function, or the `logging` module.
|
||||
|
||||
@@ -134,7 +134,7 @@ F821_17.py:77:15: F821 Undefined name `DoesNotExist1`
|
||||
77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
78 |
|
||||
79 | # Type parameters in nested classes
|
||||
79 | # Same in defaults
|
||||
|
|
||||
|
||||
F821_17.py:77:30: F821 Undefined name `DoesNotExist2`
|
||||
@@ -144,35 +144,117 @@ F821_17.py:77:30: F821 Undefined name `DoesNotExist2`
|
||||
77 | class Foo[T: (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
78 |
|
||||
79 | # Type parameters in nested classes
|
||||
79 | # Same in defaults
|
||||
|
|
||||
|
||||
F821_17.py:92:52: F821 Undefined name `t`
|
||||
F821_17.py:81:14: F821 Undefined name `DoesNotExist`
|
||||
|
|
||||
90 | return x
|
||||
91 |
|
||||
92 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T`
|
||||
| ^ F821
|
||||
93 | t # F821: Undefined name `t`
|
||||
94 | return x
|
||||
79 | # Same in defaults
|
||||
80 |
|
||||
81 | type Foo[T = DoesNotExist] = T # F821: Undefined name `DoesNotExist`
|
||||
| ^^^^^^^^^^^^ F821
|
||||
82 | def foo[T = DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist`
|
||||
83 | class Foo[T = DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist`
|
||||
|
|
||||
|
||||
F821_17.py:92:58: F821 Undefined name `t`
|
||||
F821_17.py:82:13: F821 Undefined name `DoesNotExist`
|
||||
|
|
||||
90 | return x
|
||||
91 |
|
||||
92 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T`
|
||||
| ^ F821
|
||||
93 | t # F821: Undefined name `t`
|
||||
94 | return x
|
||||
81 | type Foo[T = DoesNotExist] = T # F821: Undefined name `DoesNotExist`
|
||||
82 | def foo[T = DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist`
|
||||
| ^^^^^^^^^^^^ F821
|
||||
83 | class Foo[T = DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist`
|
||||
|
|
||||
|
||||
F821_17.py:93:17: F821 Undefined name `t`
|
||||
F821_17.py:83:15: F821 Undefined name `DoesNotExist`
|
||||
|
|
||||
92 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T`
|
||||
93 | t # F821: Undefined name `t`
|
||||
| ^ F821
|
||||
94 | return x
|
||||
81 | type Foo[T = DoesNotExist] = T # F821: Undefined name `DoesNotExist`
|
||||
82 | def foo[T = DoesNotExist](t: T) -> T: return t # F821: Undefined name `DoesNotExist`
|
||||
83 | class Foo[T = DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist`
|
||||
| ^^^^^^^^^^^^ F821
|
||||
84 |
|
||||
85 | type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
|
|
||||
|
||||
F821_17.py:85:15: F821 Undefined name `DoesNotExist1`
|
||||
|
|
||||
83 | class Foo[T = DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist`
|
||||
84 |
|
||||
85 | type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
86 | def foo[T = (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
87 | class Foo[T = (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
|
|
||||
|
||||
F821_17.py:85:30: F821 Undefined name `DoesNotExist2`
|
||||
|
|
||||
83 | class Foo[T = DoesNotExist](list[T]): ... # F821: Undefined name `DoesNotExist`
|
||||
84 |
|
||||
85 | type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
86 | def foo[T = (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
87 | class Foo[T = (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
|
|
||||
|
||||
F821_17.py:86:14: F821 Undefined name `DoesNotExist1`
|
||||
|
|
||||
85 | type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
86 | def foo[T = (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
87 | class Foo[T = (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
|
|
||||
|
||||
F821_17.py:86:29: F821 Undefined name `DoesNotExist2`
|
||||
|
|
||||
85 | type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
86 | def foo[T = (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
87 | class Foo[T = (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
|
|
||||
|
||||
F821_17.py:87:16: F821 Undefined name `DoesNotExist1`
|
||||
|
|
||||
85 | type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
86 | def foo[T = (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
87 | class Foo[T = (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
88 |
|
||||
89 | # Type parameters in nested classes
|
||||
|
|
||||
|
||||
F821_17.py:87:31: F821 Undefined name `DoesNotExist2`
|
||||
|
|
||||
85 | type Foo[T = (DoesNotExist1, DoesNotExist2)] = T # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
86 | def foo[T = (DoesNotExist1, DoesNotExist2)](t: T) -> T: return t # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
87 | class Foo[T = (DoesNotExist1, DoesNotExist2)](list[T]): ... # F821: Undefined name `DoesNotExist1`, Undefined name `DoesNotExist2`
|
||||
| ^^^^^^^^^^^^^ F821
|
||||
88 |
|
||||
89 | # Type parameters in nested classes
|
||||
|
|
||||
|
||||
F821_17.py:102:52: F821 Undefined name `t`
|
||||
|
|
||||
100 | return x
|
||||
101 |
|
||||
102 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T`
|
||||
| ^ F821
|
||||
103 | t # F821: Undefined name `t`
|
||||
104 | return x
|
||||
|
|
||||
|
||||
F821_17.py:102:58: F821 Undefined name `t`
|
||||
|
|
||||
100 | return x
|
||||
101 |
|
||||
102 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T`
|
||||
| ^ F821
|
||||
103 | t # F821: Undefined name `t`
|
||||
104 | return x
|
||||
|
|
||||
|
||||
F821_17.py:103:17: F821 Undefined name `t`
|
||||
|
|
||||
102 | def cannot_access_parent_variable(self, x: t) -> t: # F821: Undefined name `T`
|
||||
103 | t # F821: Undefined name `t`
|
||||
| ^ F821
|
||||
104 | return x
|
||||
|
|
||||
|
||||
@@ -16,6 +16,7 @@ mod tests {
|
||||
#[test_case(Rule::BlanketTypeIgnore, Path::new("PGH003_0.py"))]
|
||||
#[test_case(Rule::BlanketTypeIgnore, Path::new("PGH003_1.py"))]
|
||||
#[test_case(Rule::BlanketNOQA, Path::new("PGH004_0.py"))]
|
||||
#[test_case(Rule::BlanketNOQA, Path::new("PGH004_1.py"))]
|
||||
#[test_case(Rule::InvalidMockAccess, Path::new("PGH005_0.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
|
||||
@@ -88,9 +88,8 @@ pub(crate) fn blanket_noqa(
|
||||
) {
|
||||
for directive_line in noqa_directives.lines() {
|
||||
if let Directive::All(all) = &directive_line.directive {
|
||||
let line = locator.slice(directive_line.range);
|
||||
let offset = directive_line.range.start();
|
||||
let noqa_end = all.end() - offset;
|
||||
let line = locator.slice(directive_line);
|
||||
let noqa_end = all.end() - directive_line.start();
|
||||
|
||||
// Skip the `# noqa`, plus any trailing whitespace.
|
||||
let mut cursor = Cursor::new(&line[noqa_end.to_usize()..]);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pygrep_hooks/mod.rs
|
||||
---
|
||||
PGH004_1.py:1:1: PGH004 Use specific rule codes when using `noqa`
|
||||
|
|
||||
1 | #noqa
|
||||
| ^^^^^ PGH004
|
||||
|
|
||||
@@ -51,7 +51,7 @@ pub(crate) fn import_self(alias: &Alias, module_path: Option<&[String]>) -> Opti
|
||||
|
||||
/// PLW0406
|
||||
pub(crate) fn import_from_self(
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
module: Option<&str>,
|
||||
names: &[Alias],
|
||||
module_path: Option<&[String]>,
|
||||
|
||||
@@ -78,7 +78,7 @@ pub(crate) fn manual_from_import(
|
||||
asname: None,
|
||||
range: TextRange::default(),
|
||||
}],
|
||||
level: Some(0),
|
||||
level: 0,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::identifier::Identifier;
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
use ruff_python_ast::{self as ast, ParameterWithDefault};
|
||||
use ruff_python_semantic::{
|
||||
analyze::{function_type, visibility},
|
||||
Scope, ScopeId, ScopeKind,
|
||||
@@ -102,9 +102,8 @@ pub(crate) fn no_self_use(
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.chain(¶meters.args)
|
||||
.chain(¶meters.kwonlyargs)
|
||||
.next()
|
||||
.map(ParameterWithDefault::as_parameter)
|
||||
.map(|param| ¶m.parameter)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -68,7 +68,7 @@ pub(crate) fn deprecated_c_element_tree(checker: &mut Checker, stmt: &Stmt) {
|
||||
level,
|
||||
range: _,
|
||||
}) => {
|
||||
if level.is_some_and(|level| level > 0) {
|
||||
if *level > 0 {
|
||||
// Ex) `import .xml.etree.cElementTree as ET`
|
||||
} else if let Some(module) = module {
|
||||
if module == "xml.etree.cElementTree" {
|
||||
|
||||
@@ -643,10 +643,10 @@ pub(crate) fn deprecated_import(
|
||||
stmt: &Stmt,
|
||||
names: &[Alias],
|
||||
module: Option<&str>,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
) {
|
||||
// Avoid relative and star imports.
|
||||
if level.is_some_and(|level| level > 0) {
|
||||
if level > 0 {
|
||||
return;
|
||||
}
|
||||
if names.first().is_some_and(|name| &name.name == "*") {
|
||||
|
||||
@@ -319,7 +319,7 @@ pub(crate) fn deprecated_mock_import(checker: &mut Checker, stmt: &Stmt) {
|
||||
level,
|
||||
..
|
||||
}) => {
|
||||
if level.is_some_and(|level| level > 0) {
|
||||
if *level > 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,9 @@ pub(crate) fn non_pep695_type_alias(checker: &mut Checker, stmt: &StmtAnnAssign)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
// We don't handle defaults here yet. Should perhaps be a different rule since
|
||||
// defaults are only valid in 3.13+.
|
||||
default: None,
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
|
||||
@@ -42,6 +42,7 @@ mod tests {
|
||||
#[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))]
|
||||
#[test_case(Rule::ListReverseCopy, Path::new("FURB187.py"))]
|
||||
#[test_case(Rule::WriteWholeFile, Path::new("FURB103.py"))]
|
||||
#[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))]
|
||||
#[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::snippet::SourceCodeSnippet;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `bin(...)[2:]` (or `hex`, or `oct`) to convert
|
||||
/// an integer into a string.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When converting an integer to a baseless binary, hexadecimal, or octal
|
||||
/// string, using f-strings is more concise and readable than using the
|
||||
/// `bin`, `hex`, or `oct` functions followed by a slice.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// print(bin(1337)[2:])
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// print(f"{1337:b}")
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct FStringNumberFormat {
|
||||
replacement: Option<SourceCodeSnippet>,
|
||||
base: Base,
|
||||
}
|
||||
|
||||
impl Violation for FStringNumberFormat {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let FStringNumberFormat { replacement, base } = self;
|
||||
let function_name = base.function_name();
|
||||
|
||||
if let Some(display) = replacement
|
||||
.as_ref()
|
||||
.and_then(SourceCodeSnippet::full_display)
|
||||
{
|
||||
format!("Replace `{function_name}` call with `{display}`")
|
||||
} else {
|
||||
format!("Replace `{function_name}` call with f-string")
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let FStringNumberFormat { replacement, .. } = self;
|
||||
if let Some(display) = replacement
|
||||
.as_ref()
|
||||
.and_then(SourceCodeSnippet::full_display)
|
||||
{
|
||||
Some(format!("Replace with `{display}`"))
|
||||
} else {
|
||||
Some(format!("Replace with f-string"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB116
|
||||
pub(crate) fn fstring_number_format(checker: &mut Checker, subscript: &ast::ExprSubscript) {
|
||||
// The slice must be exactly `[2:]`.
|
||||
let Expr::Slice(ast::ExprSlice {
|
||||
lower: Some(lower),
|
||||
upper: None,
|
||||
step: None,
|
||||
..
|
||||
}) = subscript.slice.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(int),
|
||||
..
|
||||
}) = lower.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if *int != 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// The call must be exactly `hex(...)`, `bin(...)`, or `oct(...)`.
|
||||
let Expr::Call(ExprCall {
|
||||
func, arguments, ..
|
||||
}) = subscript.value.as_ref()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !arguments.keywords.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let [arg] = &*arguments.args else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(id) = checker.semantic().resolve_builtin_symbol(func) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(base) = Base::from_str(id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Generate a replacement, if possible.
|
||||
let replacement = if matches!(
|
||||
arg,
|
||||
Expr::NumberLiteral(_) | Expr::Name(_) | Expr::Attribute(_)
|
||||
) {
|
||||
let inner_source = checker.locator().slice(arg);
|
||||
|
||||
let quote = checker.stylist().quote();
|
||||
let shorthand = base.shorthand();
|
||||
|
||||
Some(format!("f{quote}{{{inner_source}:{shorthand}}}{quote}"))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
FStringNumberFormat {
|
||||
replacement: replacement.as_deref().map(SourceCodeSnippet::from_str),
|
||||
base,
|
||||
},
|
||||
subscript.range(),
|
||||
);
|
||||
|
||||
if let Some(replacement) = replacement {
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
replacement,
|
||||
subscript.range(),
|
||||
)));
|
||||
}
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum Base {
|
||||
Hex,
|
||||
Bin,
|
||||
Oct,
|
||||
}
|
||||
|
||||
impl Base {
|
||||
/// Returns the shorthand for the base.
|
||||
fn shorthand(self) -> &'static str {
|
||||
match self {
|
||||
Base::Hex => "x",
|
||||
Base::Bin => "b",
|
||||
Base::Oct => "o",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the builtin function name for the base.
|
||||
fn function_name(self) -> &'static str {
|
||||
match self {
|
||||
Base::Hex => "hex",
|
||||
Base::Bin => "bin",
|
||||
Base::Oct => "oct",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the base from a string.
|
||||
fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"hex" => Some(Base::Hex),
|
||||
"bin" => Some(Base::Bin),
|
||||
"oct" => Some(Base::Oct),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ pub(crate) use bit_count::*;
|
||||
pub(crate) use check_and_remove_from_set::*;
|
||||
pub(crate) use delete_full_slice::*;
|
||||
pub(crate) use for_loop_set_mutations::*;
|
||||
pub(crate) use fstring_number_format::*;
|
||||
pub(crate) use hashlib_digest_hex::*;
|
||||
pub(crate) use if_exp_instead_of_or_operator::*;
|
||||
pub(crate) use if_expr_min_max::*;
|
||||
@@ -32,6 +33,7 @@ mod bit_count;
|
||||
mod check_and_remove_from_set;
|
||||
mod delete_full_slice;
|
||||
mod for_loop_set_mutations;
|
||||
mod fstring_number_format;
|
||||
mod hashlib_digest_hex;
|
||||
mod if_exp_instead_of_or_operator;
|
||||
mod if_expr_min_max;
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB116.py:6:7: FURB116 [*] Replace `oct` call with `f"{num:o}"`
|
||||
|
|
||||
4 | return num
|
||||
5 |
|
||||
6 | print(oct(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
7 | print(hex(num)[2:]) # FURB116
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:o}"`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | def return_num() -> int:
|
||||
4 4 | return num
|
||||
5 5 |
|
||||
6 |-print(oct(num)[2:]) # FURB116
|
||||
6 |+print(f"{num:o}") # FURB116
|
||||
7 7 | print(hex(num)[2:]) # FURB116
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
|
||||
FURB116.py:7:7: FURB116 [*] Replace `hex` call with `f"{num:x}"`
|
||||
|
|
||||
6 | print(oct(num)[2:]) # FURB116
|
||||
7 | print(hex(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:x}"`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | return num
|
||||
5 5 |
|
||||
6 6 | print(oct(num)[2:]) # FURB116
|
||||
7 |-print(hex(num)[2:]) # FURB116
|
||||
7 |+print(f"{num:x}") # FURB116
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
|
||||
FURB116.py:8:7: FURB116 [*] Replace `bin` call with `f"{num:b}"`
|
||||
|
|
||||
6 | print(oct(num)[2:]) # FURB116
|
||||
7 | print(hex(num)[2:]) # FURB116
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^ FURB116
|
||||
9 |
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{num:b}"`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 |
|
||||
6 6 | print(oct(num)[2:]) # FURB116
|
||||
7 7 | print(hex(num)[2:]) # FURB116
|
||||
8 |-print(bin(num)[2:]) # FURB116
|
||||
8 |+print(f"{num:b}") # FURB116
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
11 11 | print(hex(1337)[2:]) # FURB116
|
||||
|
||||
FURB116.py:10:7: FURB116 [*] Replace `oct` call with `f"{1337:o}"`
|
||||
|
|
||||
8 | print(bin(num)[2:]) # FURB116
|
||||
9 |
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^^ FURB116
|
||||
11 | print(hex(1337)[2:]) # FURB116
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{1337:o}"`
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 | print(hex(num)[2:]) # FURB116
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
10 |-print(oct(1337)[2:]) # FURB116
|
||||
10 |+print(f"{1337:o}") # FURB116
|
||||
11 11 | print(hex(1337)[2:]) # FURB116
|
||||
12 12 | print(bin(1337)[2:]) # FURB116
|
||||
13 13 |
|
||||
|
||||
FURB116.py:11:7: FURB116 [*] Replace `hex` call with `f"{1337:x}"`
|
||||
|
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
11 | print(hex(1337)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^^ FURB116
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
|
|
||||
= help: Replace with `f"{1337:x}"`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | print(bin(num)[2:]) # FURB116
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
11 |-print(hex(1337)[2:]) # FURB116
|
||||
11 |+print(f"{1337:x}") # FURB116
|
||||
12 12 | print(bin(1337)[2:]) # FURB116
|
||||
13 13 |
|
||||
14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
|
||||
FURB116.py:12:7: FURB116 [*] Replace `bin` call with `f"{1337:b}"`
|
||||
|
|
||||
10 | print(oct(1337)[2:]) # FURB116
|
||||
11 | print(hex(1337)[2:]) # FURB116
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
| ^^^^^^^^^^^^^ FURB116
|
||||
13 |
|
||||
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
|
|
||||
= help: Replace with `f"{1337:b}"`
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 |
|
||||
10 10 | print(oct(1337)[2:]) # FURB116
|
||||
11 11 | print(hex(1337)[2:]) # FURB116
|
||||
12 |-print(bin(1337)[2:]) # FURB116
|
||||
12 |+print(f"{1337:b}") # FURB116
|
||||
13 13 |
|
||||
14 14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
15 15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
|
||||
FURB116.py:14:7: FURB116 Replace `bin` call with f-string
|
||||
|
|
||||
12 | print(bin(1337)[2:]) # FURB116
|
||||
13 |
|
||||
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ FURB116
|
||||
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
|
|
||||
= help: Replace with f-string
|
||||
|
||||
FURB116.py:15:7: FURB116 Replace `bin` call with f-string
|
||||
|
|
||||
14 | print(bin(return_num())[2:]) # FURB116 (no autofix)
|
||||
15 | print(bin(int(f"{num}"))[2:]) # FURB116 (no autofix)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ FURB116
|
||||
16 |
|
||||
17 | ## invalid
|
||||
|
|
||||
= help: Replace with f-string
|
||||
@@ -54,6 +54,7 @@ mod tests {
|
||||
#[test_case(Rule::MissingFStringSyntax, Path::new("RUF027_2.py"))]
|
||||
#[test_case(Rule::InvalidFormatterSuppressionComment, Path::new("RUF028.py"))]
|
||||
#[test_case(Rule::UnusedAsync, Path::new("RUF029.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
@@ -141,6 +142,8 @@ mod tests {
|
||||
Rule::UnusedImport,
|
||||
Rule::UnusedVariable,
|
||||
Rule::TabIndentation,
|
||||
Rule::YodaConditions,
|
||||
Rule::SuspiciousEvalUsage,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
@@ -160,6 +163,8 @@ mod tests {
|
||||
Rule::UnusedImport,
|
||||
Rule::UnusedVariable,
|
||||
Rule::TabIndentation,
|
||||
Rule::YodaConditions,
|
||||
Rule::SuspiciousEvalUsage,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -17,6 +17,7 @@ pub(crate) use never_union::*;
|
||||
pub(crate) use pairwise_over_zipped::*;
|
||||
pub(crate) use parenthesize_logical_operators::*;
|
||||
pub(crate) use quadratic_list_summation::*;
|
||||
pub(crate) use redirected_noqa::*;
|
||||
pub(crate) use sort_dunder_all::*;
|
||||
pub(crate) use sort_dunder_slots::*;
|
||||
pub(crate) use static_key_dict_comprehension::*;
|
||||
@@ -49,6 +50,7 @@ mod never_union;
|
||||
mod pairwise_over_zipped;
|
||||
mod parenthesize_logical_operators;
|
||||
mod quadratic_list_summation;
|
||||
mod redirected_noqa;
|
||||
mod sequence_sorting;
|
||||
mod sort_dunder_all;
|
||||
mod sort_dunder_slots;
|
||||
|
||||
69
crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs
Normal file
69
crates/ruff_linter/src/rules/ruff/rules/redirected_noqa.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::noqa::{Directive, NoqaDirectives};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `noqa` directives that use redirected rule codes.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// When a rule code has been redirected, the implication is that the rule has
|
||||
/// been deprecated in favor of another rule or code. To keep the codebase
|
||||
/// consistent and up-to-date, prefer the canonical rule code over the deprecated
|
||||
/// code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// x = eval(command) # noqa: PGH001
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// x = eval(command) # noqa: S307
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct RedirectedNOQA {
|
||||
original: String,
|
||||
target: String,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for RedirectedNOQA {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let RedirectedNOQA { original, target } = self;
|
||||
format!("`{original}` is a redirect to `{target}`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let RedirectedNOQA { target, .. } = self;
|
||||
format!("Replace with `{target}`")
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF101
|
||||
pub(crate) fn redirected_noqa(diagnostics: &mut Vec<Diagnostic>, noqa_directives: &NoqaDirectives) {
|
||||
for line in noqa_directives.lines() {
|
||||
let Directive::Codes(directive) = &line.directive else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for code in directive.iter() {
|
||||
if let Some(redirected) = get_redirect_target(code.as_str()) {
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
RedirectedNOQA {
|
||||
original: code.to_string(),
|
||||
target: redirected.to_string(),
|
||||
},
|
||||
code.range(),
|
||||
);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
redirected.to_string(),
|
||||
code.range(),
|
||||
)));
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,9 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct UnusedCodes {
|
||||
pub unknown: Vec<String>,
|
||||
pub disabled: Vec<String>,
|
||||
pub duplicated: Vec<String>,
|
||||
pub unknown: Vec<String>,
|
||||
pub unmatched: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -72,6 +73,16 @@ impl AlwaysFixableViolation for UnusedNOQA {
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
if !codes.duplicated.is_empty() {
|
||||
codes_by_reason.push(format!(
|
||||
"duplicated: {}",
|
||||
codes
|
||||
.duplicated
|
||||
.iter()
|
||||
.map(|code| format!("`{code}`"))
|
||||
.join(", ")
|
||||
));
|
||||
}
|
||||
if !codes.unknown.is_empty() {
|
||||
codes_by_reason.push(format!(
|
||||
"unknown: {}",
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF101.py:1:15: RUF101 [*] `RUF940` is a redirect to `RUF950`
|
||||
|
|
||||
1 | x = 2 # noqa: RUF940
|
||||
| ^^^^^^ RUF101
|
||||
2 | x = 2 # noqa: RUF950
|
||||
3 | x = 2 # noqa: RUF940, RUF950
|
||||
|
|
||||
= help: Replace with `RUF950`
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-x = 2 # noqa: RUF940
|
||||
2 1 | x = 2 # noqa: RUF950
|
||||
2 |+x = 2 # noqa: RUF950
|
||||
3 3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
|
||||
RUF101.py:3:15: RUF101 [*] `RUF940` is a redirect to `RUF950`
|
||||
|
|
||||
1 | x = 2 # noqa: RUF940
|
||||
2 | x = 2 # noqa: RUF950
|
||||
3 | x = 2 # noqa: RUF940, RUF950
|
||||
| ^^^^^^ RUF101
|
||||
4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
|
|
||||
= help: Replace with `RUF950`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | x = 2 # noqa: RUF940
|
||||
2 2 | x = 2 # noqa: RUF950
|
||||
3 |-x = 2 # noqa: RUF940, RUF950
|
||||
3 |+x = 2 # noqa: RUF950, RUF950
|
||||
4 4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
6 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
|
||||
RUF101.py:4:23: RUF101 [*] `RUF940` is a redirect to `RUF950`
|
||||
|
|
||||
2 | x = 2 # noqa: RUF950
|
||||
3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
| ^^^^^^ RUF101
|
||||
5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
|
|
||||
= help: Replace with `RUF950`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | x = 2 # noqa: RUF940
|
||||
2 2 | x = 2 # noqa: RUF950
|
||||
3 3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 |-x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
4 |+x = 2 # noqa: RUF950, RUF950, RUF950, RUF950, RUF950
|
||||
5 5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
6 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
|
||||
RUF101.py:5:15: RUF101 [*] `RUF940` is a redirect to `RUF950`
|
||||
|
|
||||
3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
| ^^^^^^ RUF101
|
||||
6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
|
|
||||
= help: Replace with `RUF950`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | x = 2 # noqa: RUF950
|
||||
3 3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 |-x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
5 |+x = 2 # noqa: RUF950, RUF950, RUF940
|
||||
6 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
|
||||
RUF101.py:5:31: RUF101 [*] `RUF940` is a redirect to `RUF950`
|
||||
|
|
||||
3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
| ^^^^^^ RUF101
|
||||
6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
|
|
||||
= help: Replace with `RUF950`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | x = 2 # noqa: RUF950
|
||||
3 3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 |-x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
5 |+x = 2 # noqa: RUF940, RUF950, RUF950
|
||||
6 6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
|
||||
RUF101.py:6:15: RUF101 [*] `RUF940` is a redirect to `RUF950`
|
||||
|
|
||||
4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
| ^^^^^^ RUF101
|
||||
|
|
||||
= help: Replace with `RUF950`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
6 |-x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
6 |+x = 2 # noqa: RUF950, RUF950, RUF940, RUF950
|
||||
|
||||
RUF101.py:6:31: RUF101 [*] `RUF940` is a redirect to `RUF950`
|
||||
|
|
||||
4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
6 | x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
| ^^^^^^ RUF101
|
||||
|
|
||||
= help: Replace with `RUF950`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | x = 2 # noqa: RUF940, RUF950
|
||||
4 4 | x = 2 # noqa: RUF950, RUF940, RUF950, RUF950, RUF950
|
||||
5 5 | x = 2 # noqa: RUF940, RUF950, RUF940
|
||||
6 |-x = 2 # noqa: RUF940, RUF950, RUF940, RUF950
|
||||
6 |+x = 2 # noqa: RUF940, RUF950, RUF950, RUF950
|
||||
@@ -120,12 +120,12 @@ RUF100_0.py:25:12: RUF100 [*] Unused `noqa` directive (unknown: `V500`)
|
||||
27 27 | # fmt: off
|
||||
28 28 | # Invalid - no space before #
|
||||
|
||||
RUF100_0.py:29:10: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
RUF100_0.py:29:12: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
|
|
||||
27 | # fmt: off
|
||||
28 | # Invalid - no space before #
|
||||
29 | d = 1# noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
29 | d = 1 # noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
30 |
|
||||
31 | # Invalid - many spaces before #
|
||||
|
|
||||
@@ -135,44 +135,44 @@ RUF100_0.py:29:10: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
26 26 |
|
||||
27 27 | # fmt: off
|
||||
28 28 | # Invalid - no space before #
|
||||
29 |- d = 1# noqa: E501
|
||||
29 |- d = 1 # noqa: E501
|
||||
29 |+ d = 1
|
||||
30 30 |
|
||||
31 31 | # Invalid - many spaces before #
|
||||
32 32 | d = 1 # noqa: E501
|
||||
32 32 | d = 1 # noqa: E501
|
||||
|
||||
RUF100_0.py:32:5: F841 [*] Local variable `d` is assigned to but never used
|
||||
|
|
||||
31 | # Invalid - many spaces before #
|
||||
32 | d = 1 # noqa: E501
|
||||
32 | d = 1 # noqa: E501
|
||||
| ^ F841
|
||||
33 | # fmt: on
|
||||
|
|
||||
= help: Remove assignment to unused variable `d`
|
||||
|
||||
ℹ Unsafe fix
|
||||
29 29 | d = 1# noqa: E501
|
||||
29 29 | d = 1 # noqa: E501
|
||||
30 30 |
|
||||
31 31 | # Invalid - many spaces before #
|
||||
32 |- d = 1 # noqa: E501
|
||||
32 |- d = 1 # noqa: E501
|
||||
33 32 | # fmt: on
|
||||
34 33 |
|
||||
35 34 |
|
||||
|
||||
RUF100_0.py:32:33: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
RUF100_0.py:32:12: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
|
|
||||
31 | # Invalid - many spaces before #
|
||||
32 | d = 1 # noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
32 | d = 1 # noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
33 | # fmt: on
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
29 29 | d = 1# noqa: E501
|
||||
29 29 | d = 1 # noqa: E501
|
||||
30 30 |
|
||||
31 31 | # Invalid - many spaces before #
|
||||
32 |- d = 1 # noqa: E501
|
||||
32 |- d = 1 # noqa: E501
|
||||
32 |+ d = 1
|
||||
33 33 | # fmt: on
|
||||
34 34 |
|
||||
@@ -288,9 +288,9 @@ RUF100_0.py:107:12: RUF100 [*] Unused `noqa` directive (unknown: `E50`)
|
||||
|
|
||||
105 | def f():
|
||||
106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
| ^^^^^^^^^^^^^^^^ RUF100
|
||||
108 | e = 1 #
noqa: E50
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
| ^^^^^^^^^^^^^^^^^ RUF100
|
||||
108 | e = 1 #
noqa: E50
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
@@ -298,15 +298,17 @@ RUF100_0.py:107:12: RUF100 [*] Unused `noqa` directive (unknown: `E50`)
|
||||
104 104 |
|
||||
105 105 | def f():
|
||||
106 106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 |- d = 1 #
noqa: F841, E50
|
||||
107 |- d = 1 #
noqa: F841, E50
|
||||
107 |+ d = 1 # noqa: F841
|
||||
108 108 | e = 1 #
noqa: E50
|
||||
108 108 | e = 1 #
noqa: E50
|
||||
109 109 |
|
||||
110 110 |
|
||||
|
||||
RUF100_0.py:108:5: F841 [*] Local variable `e` is assigned to but never used
|
||||
|
|
||||
106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
| ^ F841
|
||||
|
|
||||
= help: Remove assignment to unused variable `e`
|
||||
@@ -314,21 +316,144 @@ RUF100_0.py:108:5: F841 [*] Local variable `e` is assigned to but never used
|
||||
ℹ Unsafe fix
|
||||
105 105 | def f():
|
||||
106 106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
109 108 |
|
||||
110 109 |
|
||||
111 110 | def f():
|
||||
|
||||
RUF100_0.py:108:12: RUF100 [*] Unused `noqa` directive (unknown: `E50`)
|
||||
|
|
||||
106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
| ^^^^^^^^^^ RUF100
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
| ^^^^^^^^^^^ RUF100
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
105 105 | def f():
|
||||
106 106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
108 |+ e = 1
|
||||
109 109 |
|
||||
110 110 |
|
||||
111 111 | def f():
|
||||
|
||||
RUF100_0.py:118:12: RUF100 [*] Unused `noqa` directive (duplicated: `F841`; unknown: `X200`)
|
||||
|
|
||||
116 | # Check duplicate code detection
|
||||
117 | def f():
|
||||
118 | x = 2 # noqa: F841, F841, X200
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
119 |
|
||||
120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
115 115 |
|
||||
116 116 | # Check duplicate code detection
|
||||
117 117 | def f():
|
||||
118 |- x = 2 # noqa: F841, F841, X200
|
||||
118 |+ x = 2 # noqa: F841
|
||||
119 119 |
|
||||
120 120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
121 121 |
|
||||
|
||||
RUF100_0.py:120:19: RUF100 [*] Unused `noqa` directive (duplicated: `SIM300`, `SIM300`)
|
||||
|
|
||||
118 | x = 2 # noqa: F841, F841, X200
|
||||
119 |
|
||||
120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
121 |
|
||||
122 | z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
117 117 | def f():
|
||||
118 118 | x = 2 # noqa: F841, F841, X200
|
||||
119 119 |
|
||||
120 |- y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
120 |+ y = 2 == bar # noqa: SIM300, F841
|
||||
121 121 |
|
||||
122 122 | z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
123 123 |
|
||||
|
||||
RUF100_0.py:122:12: RUF100 [*] Unused `noqa` directive (duplicated: `F841`, `F841`, `F841`, `F841`)
|
||||
|
|
||||
120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
121 |
|
||||
122 | z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
123 |
|
||||
124 | return
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
119 119 |
|
||||
120 120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
121 121 |
|
||||
122 |- z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
122 |+ z = 2 # noqa: F841
|
||||
123 123 |
|
||||
124 124 | return
|
||||
125 125 |
|
||||
|
||||
RUF100_0.py:129:20: RUF100 [*] Unused `noqa` directive (duplicated: `S307`, `S307`, `S307`)
|
||||
|
|
||||
127 | # Allow code redirects
|
||||
128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
126 126 |
|
||||
127 127 | # Allow code redirects
|
||||
128 128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 |-x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
129 |+x = eval(command) # noqa: S307, PGH001
|
||||
130 130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
||||
RUF100_0.py:130:20: RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`)
|
||||
|
|
||||
128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
127 127 | # Allow code redirects
|
||||
128 128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 |-x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
130 |+x = eval(command) # noqa: PGH001, S307
|
||||
131 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
||||
RUF100_0.py:131:20: RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`, `S307`)
|
||||
|
|
||||
129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
128 128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 |-x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
131 |+x = eval(command) # noqa: PGH001, S307
|
||||
|
||||
@@ -100,12 +100,12 @@ RUF100_0.py:22:12: RUF100 [*] Unused `noqa` directive (unused: `F841`)
|
||||
24 24 | # Invalid (but external)
|
||||
25 25 | d = 1 # noqa: V500
|
||||
|
||||
RUF100_0.py:29:10: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
RUF100_0.py:29:12: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
|
|
||||
27 | # fmt: off
|
||||
28 | # Invalid - no space before #
|
||||
29 | d = 1# noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
29 | d = 1 # noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
30 |
|
||||
31 | # Invalid - many spaces before #
|
||||
|
|
||||
@@ -115,44 +115,44 @@ RUF100_0.py:29:10: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
26 26 |
|
||||
27 27 | # fmt: off
|
||||
28 28 | # Invalid - no space before #
|
||||
29 |- d = 1# noqa: E501
|
||||
29 |- d = 1 # noqa: E501
|
||||
29 |+ d = 1
|
||||
30 30 |
|
||||
31 31 | # Invalid - many spaces before #
|
||||
32 32 | d = 1 # noqa: E501
|
||||
32 32 | d = 1 # noqa: E501
|
||||
|
||||
RUF100_0.py:32:5: F841 [*] Local variable `d` is assigned to but never used
|
||||
|
|
||||
31 | # Invalid - many spaces before #
|
||||
32 | d = 1 # noqa: E501
|
||||
32 | d = 1 # noqa: E501
|
||||
| ^ F841
|
||||
33 | # fmt: on
|
||||
|
|
||||
= help: Remove assignment to unused variable `d`
|
||||
|
||||
ℹ Unsafe fix
|
||||
29 29 | d = 1# noqa: E501
|
||||
29 29 | d = 1 # noqa: E501
|
||||
30 30 |
|
||||
31 31 | # Invalid - many spaces before #
|
||||
32 |- d = 1 # noqa: E501
|
||||
32 |- d = 1 # noqa: E501
|
||||
33 32 | # fmt: on
|
||||
34 33 |
|
||||
35 34 |
|
||||
|
||||
RUF100_0.py:32:33: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
RUF100_0.py:32:12: RUF100 [*] Unused `noqa` directive (unused: `E501`)
|
||||
|
|
||||
31 | # Invalid - many spaces before #
|
||||
32 | d = 1 # noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
32 | d = 1 # noqa: E501
|
||||
| ^^^^^^^^^^^^ RUF100
|
||||
33 | # fmt: on
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
29 29 | d = 1# noqa: E501
|
||||
29 29 | d = 1 # noqa: E501
|
||||
30 30 |
|
||||
31 31 | # Invalid - many spaces before #
|
||||
32 |- d = 1 # noqa: E501
|
||||
32 |- d = 1 # noqa: E501
|
||||
32 |+ d = 1
|
||||
33 33 | # fmt: on
|
||||
34 34 |
|
||||
@@ -268,9 +268,9 @@ RUF100_0.py:107:12: RUF100 [*] Unused `noqa` directive (unknown: `E50`)
|
||||
|
|
||||
105 | def f():
|
||||
106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
| ^^^^^^^^^^^^^^^^ RUF100
|
||||
108 | e = 1 #
noqa: E50
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
| ^^^^^^^^^^^^^^^^^ RUF100
|
||||
108 | e = 1 #
noqa: E50
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
@@ -278,15 +278,17 @@ RUF100_0.py:107:12: RUF100 [*] Unused `noqa` directive (unknown: `E50`)
|
||||
104 104 |
|
||||
105 105 | def f():
|
||||
106 106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 |- d = 1 #
noqa: F841, E50
|
||||
107 |- d = 1 #
noqa: F841, E50
|
||||
107 |+ d = 1 # noqa: F841
|
||||
108 108 | e = 1 #
noqa: E50
|
||||
108 108 | e = 1 #
noqa: E50
|
||||
109 109 |
|
||||
110 110 |
|
||||
|
||||
RUF100_0.py:108:5: F841 [*] Local variable `e` is assigned to but never used
|
||||
|
|
||||
106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
| ^ F841
|
||||
|
|
||||
= help: Remove assignment to unused variable `e`
|
||||
@@ -294,21 +296,144 @@ RUF100_0.py:108:5: F841 [*] Local variable `e` is assigned to but never used
|
||||
ℹ Unsafe fix
|
||||
105 105 | def f():
|
||||
106 106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
109 108 |
|
||||
110 109 |
|
||||
111 110 | def f():
|
||||
|
||||
RUF100_0.py:108:12: RUF100 [*] Unused `noqa` directive (unknown: `E50`)
|
||||
|
|
||||
106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
| ^^^^^^^^^^ RUF100
|
||||
107 | d = 1 #
noqa: F841, E50
|
||||
108 | e = 1 #
noqa: E50
|
||||
| ^^^^^^^^^^^ RUF100
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
105 105 | def f():
|
||||
106 106 | # Invalid - nonexistant error code with multibyte character
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
107 107 | d = 1 #
noqa: F841, E50
|
||||
108 |- e = 1 #
noqa: E50
|
||||
108 |+ e = 1
|
||||
109 109 |
|
||||
110 110 |
|
||||
111 111 | def f():
|
||||
|
||||
RUF100_0.py:118:12: RUF100 [*] Unused `noqa` directive (duplicated: `F841`; unknown: `X200`)
|
||||
|
|
||||
116 | # Check duplicate code detection
|
||||
117 | def f():
|
||||
118 | x = 2 # noqa: F841, F841, X200
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
119 |
|
||||
120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
115 115 |
|
||||
116 116 | # Check duplicate code detection
|
||||
117 117 | def f():
|
||||
118 |- x = 2 # noqa: F841, F841, X200
|
||||
118 |+ x = 2 # noqa: F841
|
||||
119 119 |
|
||||
120 120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
121 121 |
|
||||
|
||||
RUF100_0.py:120:19: RUF100 [*] Unused `noqa` directive (duplicated: `SIM300`, `SIM300`)
|
||||
|
|
||||
118 | x = 2 # noqa: F841, F841, X200
|
||||
119 |
|
||||
120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
121 |
|
||||
122 | z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
117 117 | def f():
|
||||
118 118 | x = 2 # noqa: F841, F841, X200
|
||||
119 119 |
|
||||
120 |- y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
120 |+ y = 2 == bar # noqa: SIM300, F841
|
||||
121 121 |
|
||||
122 122 | z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
123 123 |
|
||||
|
||||
RUF100_0.py:122:12: RUF100 [*] Unused `noqa` directive (duplicated: `F841`, `F841`, `F841`, `F841`)
|
||||
|
|
||||
120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
121 |
|
||||
122 | z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
123 |
|
||||
124 | return
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
119 119 |
|
||||
120 120 | y = 2 == bar # noqa: SIM300, F841, SIM300, SIM300
|
||||
121 121 |
|
||||
122 |- z = 2 # noqa: F841 F841 F841, F841, F841
|
||||
122 |+ z = 2 # noqa: F841
|
||||
123 123 |
|
||||
124 124 | return
|
||||
125 125 |
|
||||
|
||||
RUF100_0.py:129:20: RUF100 [*] Unused `noqa` directive (duplicated: `S307`, `S307`, `S307`)
|
||||
|
|
||||
127 | # Allow code redirects
|
||||
128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
126 126 |
|
||||
127 127 | # Allow code redirects
|
||||
128 128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 |-x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
129 |+x = eval(command) # noqa: S307, PGH001
|
||||
130 130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
||||
RUF100_0.py:130:20: RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`)
|
||||
|
|
||||
128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
127 127 | # Allow code redirects
|
||||
128 128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 |-x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
130 |+x = eval(command) # noqa: PGH001, S307
|
||||
131 131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
|
||||
RUF100_0.py:131:20: RUF100 [*] Unused `noqa` directive (duplicated: `PGH001`, `S307`)
|
||||
|
|
||||
129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 | x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF100
|
||||
|
|
||||
= help: Remove unused `noqa` directive
|
||||
|
||||
ℹ Safe fix
|
||||
128 128 | x = eval(command) # noqa: PGH001, S307
|
||||
129 129 | x = eval(command) # noqa: S307, PGH001, S307, S307, S307
|
||||
130 130 | x = eval(command) # noqa: PGH001, S307, PGH001
|
||||
131 |-x = eval(command) # noqa: PGH001, S307, PGH001, S307
|
||||
131 |+x = eval(command) # noqa: PGH001, S307
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 0..12,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 8..12,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 0..12,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 8..12,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 35..47,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 43..47,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 0..13,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 9..13,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 0..10,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 6..10,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 0..12,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 8..12,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,8 +8,14 @@ Ok(
|
||||
Codes {
|
||||
range: 0..18,
|
||||
codes: [
|
||||
"F401",
|
||||
"F841",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 8..12,
|
||||
},
|
||||
Code {
|
||||
code: "F841",
|
||||
range: 14..18,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,8 +8,14 @@ Ok(
|
||||
Codes {
|
||||
range: 0..18,
|
||||
codes: [
|
||||
"F401",
|
||||
"F841",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 8..12,
|
||||
},
|
||||
Code {
|
||||
code: "F841",
|
||||
range: 14..18,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,8 +8,14 @@ Ok(
|
||||
Codes {
|
||||
range: 35..53,
|
||||
codes: [
|
||||
"F401",
|
||||
"F841",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 43..47,
|
||||
},
|
||||
Code {
|
||||
code: "F841",
|
||||
range: 49..53,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,8 +8,14 @@ Ok(
|
||||
Codes {
|
||||
range: 0..20,
|
||||
codes: [
|
||||
"F401",
|
||||
"F841",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 9..13,
|
||||
},
|
||||
Code {
|
||||
code: "F841",
|
||||
range: 16..20,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,8 +8,14 @@ Ok(
|
||||
Codes {
|
||||
range: 0..15,
|
||||
codes: [
|
||||
"F401",
|
||||
"F841",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 6..10,
|
||||
},
|
||||
Code {
|
||||
code: "F841",
|
||||
range: 11..15,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,8 +8,14 @@ Ok(
|
||||
Codes {
|
||||
range: 0..18,
|
||||
codes: [
|
||||
"F401",
|
||||
"F841",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 8..12,
|
||||
},
|
||||
Code {
|
||||
code: "F841",
|
||||
range: 14..18,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 4..16,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 12..16,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -8,7 +8,10 @@ Ok(
|
||||
Codes {
|
||||
range: 0..12,
|
||||
codes: [
|
||||
"F401",
|
||||
Code {
|
||||
code: "F401",
|
||||
range: 8..12,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1173,21 +1173,29 @@ impl<'a> From<&'a ast::TypeParam> for ComparableTypeParam<'a> {
|
||||
ast::TypeParam::TypeVar(ast::TypeParamTypeVar {
|
||||
name,
|
||||
bound,
|
||||
default,
|
||||
range: _,
|
||||
}) => Self::TypeVar(TypeParamTypeVar {
|
||||
name: name.as_str(),
|
||||
bound: bound.as_ref().map(Into::into),
|
||||
default: default.as_ref().map(Into::into),
|
||||
}),
|
||||
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple {
|
||||
name,
|
||||
default,
|
||||
range: _,
|
||||
}) => Self::TypeVarTuple(TypeParamTypeVarTuple {
|
||||
name: name.as_str(),
|
||||
default: default.as_ref().map(Into::into),
|
||||
}),
|
||||
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec {
|
||||
name,
|
||||
default,
|
||||
range: _,
|
||||
}) => Self::ParamSpec(TypeParamParamSpec {
|
||||
name: name.as_str(),
|
||||
default: default.as_ref().map(Into::into),
|
||||
}),
|
||||
ast::TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { name, range: _ }) => {
|
||||
Self::TypeVarTuple(TypeParamTypeVarTuple {
|
||||
name: name.as_str(),
|
||||
})
|
||||
}
|
||||
ast::TypeParam::ParamSpec(ast::TypeParamParamSpec { name, range: _ }) => {
|
||||
Self::ParamSpec(TypeParamParamSpec {
|
||||
name: name.as_str(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1196,16 +1204,19 @@ impl<'a> From<&'a ast::TypeParam> for ComparableTypeParam<'a> {
|
||||
pub struct TypeParamTypeVar<'a> {
|
||||
pub name: &'a str,
|
||||
pub bound: Option<Box<ComparableExpr<'a>>>,
|
||||
pub default: Option<Box<ComparableExpr<'a>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct TypeParamParamSpec<'a> {
|
||||
pub name: &'a str,
|
||||
pub default: Option<Box<ComparableExpr<'a>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct TypeParamTypeVarTuple<'a> {
|
||||
pub name: &'a str,
|
||||
pub default: Option<Box<ComparableExpr<'a>>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
@@ -1295,7 +1306,7 @@ pub struct StmtImport<'a> {
|
||||
pub struct StmtImportFrom<'a> {
|
||||
module: Option<&'a str>,
|
||||
names: Vec<ComparableAlias<'a>>,
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
|
||||
@@ -264,11 +264,20 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
|
||||
|
||||
pub fn any_over_type_param(type_param: &TypeParam, func: &dyn Fn(&Expr) -> bool) -> bool {
|
||||
match type_param {
|
||||
TypeParam::TypeVar(ast::TypeParamTypeVar { bound, .. }) => bound
|
||||
TypeParam::TypeVar(ast::TypeParamTypeVar { bound, default, .. }) => {
|
||||
bound
|
||||
.as_ref()
|
||||
.is_some_and(|value| any_over_expr(value, func))
|
||||
|| default
|
||||
.as_ref()
|
||||
.is_some_and(|value| any_over_expr(value, func))
|
||||
}
|
||||
TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { default, .. }) => default
|
||||
.as_ref()
|
||||
.is_some_and(|value| any_over_expr(value, func)),
|
||||
TypeParam::ParamSpec(ast::TypeParamParamSpec { default, .. }) => default
|
||||
.as_ref()
|
||||
.is_some_and(|value| any_over_expr(value, func)),
|
||||
TypeParam::TypeVarTuple(ast::TypeParamTypeVarTuple { .. }) => false,
|
||||
TypeParam::ParamSpec(ast::TypeParamParamSpec { .. }) => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -723,13 +732,13 @@ where
|
||||
/// ```rust
|
||||
/// # use ruff_python_ast::helpers::format_import_from;
|
||||
///
|
||||
/// assert_eq!(format_import_from(None, None), "".to_string());
|
||||
/// assert_eq!(format_import_from(Some(1), None), ".".to_string());
|
||||
/// assert_eq!(format_import_from(Some(1), Some("foo")), ".foo".to_string());
|
||||
/// assert_eq!(format_import_from(0, None), "".to_string());
|
||||
/// assert_eq!(format_import_from(1, None), ".".to_string());
|
||||
/// assert_eq!(format_import_from(1, Some("foo")), ".foo".to_string());
|
||||
/// ```
|
||||
pub fn format_import_from(level: Option<u32>, module: Option<&str>) -> String {
|
||||
pub fn format_import_from(level: u32, module: Option<&str>) -> String {
|
||||
let mut module_name = String::with_capacity(16);
|
||||
if let Some(level) = level {
|
||||
if level > 0 {
|
||||
for _ in 0..level {
|
||||
module_name.push('.');
|
||||
}
|
||||
@@ -747,18 +756,15 @@ pub fn format_import_from(level: Option<u32>, module: Option<&str>) -> String {
|
||||
/// ```rust
|
||||
/// # use ruff_python_ast::helpers::format_import_from_member;
|
||||
///
|
||||
/// assert_eq!(format_import_from_member(None, None, "bar"), "bar".to_string());
|
||||
/// assert_eq!(format_import_from_member(Some(1), None, "bar"), ".bar".to_string());
|
||||
/// assert_eq!(format_import_from_member(Some(1), Some("foo"), "bar"), ".foo.bar".to_string());
|
||||
/// assert_eq!(format_import_from_member(0, None, "bar"), "bar".to_string());
|
||||
/// assert_eq!(format_import_from_member(1, None, "bar"), ".bar".to_string());
|
||||
/// assert_eq!(format_import_from_member(1, Some("foo"), "bar"), ".foo.bar".to_string());
|
||||
/// ```
|
||||
pub fn format_import_from_member(level: Option<u32>, module: Option<&str>, member: &str) -> String {
|
||||
pub fn format_import_from_member(level: u32, module: Option<&str>, member: &str) -> String {
|
||||
let mut qualified_name = String::with_capacity(
|
||||
(level.unwrap_or(0) as usize)
|
||||
+ module.as_ref().map_or(0, |module| module.len())
|
||||
+ 1
|
||||
+ member.len(),
|
||||
(level as usize) + module.as_ref().map_or(0, |module| module.len()) + 1 + member.len(),
|
||||
);
|
||||
if let Some(level) = level {
|
||||
if level > 0 {
|
||||
for _ in 0..level {
|
||||
qualified_name.push('.');
|
||||
}
|
||||
@@ -792,17 +798,17 @@ pub fn to_module_path(package: &Path, path: &Path) -> Option<Vec<String>> {
|
||||
/// ```rust
|
||||
/// # use ruff_python_ast::helpers::collect_import_from_member;
|
||||
///
|
||||
/// assert_eq!(collect_import_from_member(None, None, "bar").segments(), ["bar"]);
|
||||
/// assert_eq!(collect_import_from_member(Some(1), None, "bar").segments(), [".", "bar"]);
|
||||
/// assert_eq!(collect_import_from_member(Some(1), Some("foo"), "bar").segments(), [".", "foo", "bar"]);
|
||||
/// assert_eq!(collect_import_from_member(0, None, "bar").segments(), ["bar"]);
|
||||
/// assert_eq!(collect_import_from_member(1, None, "bar").segments(), [".", "bar"]);
|
||||
/// assert_eq!(collect_import_from_member(1, Some("foo"), "bar").segments(), [".", "foo", "bar"]);
|
||||
/// ```
|
||||
pub fn collect_import_from_member<'a>(
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
module: Option<&'a str>,
|
||||
member: &'a str,
|
||||
) -> QualifiedName<'a> {
|
||||
let mut qualified_name_builder = QualifiedNameBuilder::with_capacity(
|
||||
level.unwrap_or_default() as usize
|
||||
level as usize
|
||||
+ module
|
||||
.map(|module| module.split('.').count())
|
||||
.unwrap_or_default()
|
||||
@@ -810,11 +816,9 @@ pub fn collect_import_from_member<'a>(
|
||||
);
|
||||
|
||||
// Include the dots as standalone segments.
|
||||
if let Some(level) = level {
|
||||
if level > 0 {
|
||||
for _ in 0..level {
|
||||
qualified_name_builder.push(".");
|
||||
}
|
||||
if level > 0 {
|
||||
for _ in 0..level {
|
||||
qualified_name_builder.push(".");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,14 +870,10 @@ pub fn from_relative_import<'a>(
|
||||
/// Given an imported module (based on its relative import level and module name), return the
|
||||
/// fully-qualified module path.
|
||||
pub fn resolve_imported_module_path<'a>(
|
||||
level: Option<u32>,
|
||||
level: u32,
|
||||
module: Option<&'a str>,
|
||||
module_path: Option<&[String]>,
|
||||
) -> Option<Cow<'a, str>> {
|
||||
let Some(level) = level else {
|
||||
return Some(Cow::Borrowed(module.unwrap_or("")));
|
||||
};
|
||||
|
||||
if level == 0 {
|
||||
return Some(Cow::Borrowed(module.unwrap_or("")));
|
||||
}
|
||||
@@ -1563,14 +1563,14 @@ mod tests {
|
||||
fn resolve_import() {
|
||||
// Return the module directly.
|
||||
assert_eq!(
|
||||
resolve_imported_module_path(None, Some("foo"), None),
|
||||
resolve_imported_module_path(0, Some("foo"), None),
|
||||
Some(Cow::Borrowed("foo"))
|
||||
);
|
||||
|
||||
// Construct the module path from the calling module's path.
|
||||
assert_eq!(
|
||||
resolve_imported_module_path(
|
||||
Some(1),
|
||||
1,
|
||||
Some("foo"),
|
||||
Some(&["bar".to_string(), "baz".to_string()])
|
||||
),
|
||||
@@ -1579,19 +1579,16 @@ mod tests {
|
||||
|
||||
// We can't return the module if it's a relative import, and we don't know the calling
|
||||
// module's path.
|
||||
assert_eq!(
|
||||
resolve_imported_module_path(Some(1), Some("foo"), None),
|
||||
None
|
||||
);
|
||||
assert_eq!(resolve_imported_module_path(1, Some("foo"), None), None);
|
||||
|
||||
// We can't return the module if it's a relative import, and the path goes beyond the
|
||||
// calling module's path.
|
||||
assert_eq!(
|
||||
resolve_imported_module_path(Some(1), Some("foo"), Some(&["bar".to_string()])),
|
||||
resolve_imported_module_path(1, Some("foo"), Some(&["bar".to_string()])),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
resolve_imported_module_path(Some(2), Some("foo"), Some(&["bar".to_string()])),
|
||||
resolve_imported_module_path(2, Some("foo"), Some(&["bar".to_string()])),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -1619,11 +1616,13 @@ mod tests {
|
||||
let type_var_one = TypeParam::TypeVar(TypeParamTypeVar {
|
||||
range: TextRange::default(),
|
||||
bound: Some(Box::new(constant_one.clone())),
|
||||
default: None,
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
});
|
||||
let type_var_two = TypeParam::TypeVar(TypeParamTypeVar {
|
||||
range: TextRange::default(),
|
||||
bound: Some(Box::new(constant_two.clone())),
|
||||
bound: None,
|
||||
default: Some(Box::new(constant_two.clone())),
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
});
|
||||
let type_alias = Stmt::TypeAlias(StmtTypeAlias {
|
||||
@@ -1650,30 +1649,49 @@ mod tests {
|
||||
let type_var_no_bound = TypeParam::TypeVar(TypeParamTypeVar {
|
||||
range: TextRange::default(),
|
||||
bound: None,
|
||||
default: None,
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
});
|
||||
assert!(!any_over_type_param(&type_var_no_bound, &|_expr| true));
|
||||
|
||||
let bound = Expr::NumberLiteral(ExprNumberLiteral {
|
||||
let constant = Expr::NumberLiteral(ExprNumberLiteral {
|
||||
value: Number::Int(Int::ONE),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
let type_var_with_bound = TypeParam::TypeVar(TypeParamTypeVar {
|
||||
range: TextRange::default(),
|
||||
bound: Some(Box::new(bound.clone())),
|
||||
bound: Some(Box::new(constant.clone())),
|
||||
default: None,
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
});
|
||||
assert!(
|
||||
any_over_type_param(&type_var_with_bound, &|expr| {
|
||||
assert_eq!(
|
||||
*expr, bound,
|
||||
*expr, constant,
|
||||
"the received expression should be the unwrapped bound"
|
||||
);
|
||||
true
|
||||
}),
|
||||
"if true is returned from `func` it should be respected"
|
||||
);
|
||||
|
||||
let type_var_with_default = TypeParam::TypeVar(TypeParamTypeVar {
|
||||
range: TextRange::default(),
|
||||
default: Some(Box::new(constant.clone())),
|
||||
bound: None,
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
});
|
||||
assert!(
|
||||
any_over_type_param(&type_var_with_default, &|expr| {
|
||||
assert_eq!(
|
||||
*expr, constant,
|
||||
"the received expression should be the unwrapped default"
|
||||
);
|
||||
true
|
||||
}),
|
||||
"if true is returned from `func` it should be respected"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1681,10 +1699,32 @@ mod tests {
|
||||
let type_var_tuple = TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
|
||||
range: TextRange::default(),
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
default: None,
|
||||
});
|
||||
assert!(
|
||||
!any_over_type_param(&type_var_tuple, &|_expr| true),
|
||||
"type var tuples have no expressions to visit"
|
||||
"this TypeVarTuple has no expressions to visit"
|
||||
);
|
||||
|
||||
let constant = Expr::NumberLiteral(ExprNumberLiteral {
|
||||
value: Number::Int(Int::ONE),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
let type_var_tuple_with_default = TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
|
||||
range: TextRange::default(),
|
||||
default: Some(Box::new(constant.clone())),
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
});
|
||||
assert!(
|
||||
any_over_type_param(&type_var_tuple_with_default, &|expr| {
|
||||
assert_eq!(
|
||||
*expr, constant,
|
||||
"the received expression should be the unwrapped default"
|
||||
);
|
||||
true
|
||||
}),
|
||||
"if true is returned from `func` it should be respected"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1693,10 +1733,32 @@ mod tests {
|
||||
let type_param_spec = TypeParam::ParamSpec(TypeParamParamSpec {
|
||||
range: TextRange::default(),
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
default: None,
|
||||
});
|
||||
assert!(
|
||||
!any_over_type_param(&type_param_spec, &|_expr| true),
|
||||
"param specs have no expressions to visit"
|
||||
"this ParamSpec has no expressions to visit"
|
||||
);
|
||||
|
||||
let constant = Expr::NumberLiteral(ExprNumberLiteral {
|
||||
value: Number::Int(Int::ONE),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
let param_spec_with_default = TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
|
||||
range: TextRange::default(),
|
||||
default: Some(Box::new(constant.clone())),
|
||||
name: Identifier::new("x", TextRange::default()),
|
||||
});
|
||||
assert!(
|
||||
any_over_type_param(¶m_spec_with_default, &|expr| {
|
||||
assert_eq!(
|
||||
*expr, constant,
|
||||
"the received expression should be the unwrapped default"
|
||||
);
|
||||
true
|
||||
}),
|
||||
"if true is returned from `func` it should be respected"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct Import<'a> {
|
||||
pub struct ImportFrom<'a> {
|
||||
pub module: Option<&'a str>,
|
||||
pub name: Alias<'a>,
|
||||
pub level: Option<u32>,
|
||||
pub level: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -51,7 +51,7 @@ impl<'a> ImportFrom<'a> {
|
||||
name,
|
||||
as_name: None,
|
||||
},
|
||||
level: None,
|
||||
level: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,8 +78,8 @@ impl std::fmt::Display for Import<'_> {
|
||||
impl std::fmt::Display for ImportFrom<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "from ")?;
|
||||
if let Some(level) = self.level {
|
||||
write!(f, "{}", ".".repeat(level as usize))?;
|
||||
if self.level > 0 {
|
||||
write!(f, "{}", ".".repeat(self.level as usize))?;
|
||||
}
|
||||
if let Some(module) = self.module {
|
||||
write!(f, "{module}")?;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,10 @@
|
||||
#![allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
||||
use std::cell::OnceCell;
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
use std::slice::{Iter, IterMut};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use itertools::Itertools;
|
||||
@@ -471,7 +470,7 @@ pub struct StmtImportFrom {
|
||||
pub range: TextRange,
|
||||
pub module: Option<Identifier>,
|
||||
pub names: Vec<Alias>,
|
||||
pub level: Option<u32>,
|
||||
pub level: u32,
|
||||
}
|
||||
|
||||
impl From<StmtImportFrom> for Stmt {
|
||||
@@ -1420,7 +1419,7 @@ impl StringLiteralValue {
|
||||
Self {
|
||||
inner: StringLiteralValueInner::Concatenated(ConcatenatedStringLiteral {
|
||||
strings,
|
||||
value: OnceCell::new(),
|
||||
value: OnceLock::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -1782,7 +1781,7 @@ struct ConcatenatedStringLiteral {
|
||||
/// Each string literal that makes up the concatenated string.
|
||||
strings: Vec<StringLiteral>,
|
||||
/// The concatenated string value.
|
||||
value: OnceCell<Box<str>>,
|
||||
value: OnceLock<Box<str>>,
|
||||
}
|
||||
|
||||
impl ConcatenatedStringLiteral {
|
||||
@@ -3132,6 +3131,7 @@ pub struct TypeParamTypeVar {
|
||||
pub range: TextRange,
|
||||
pub name: Identifier,
|
||||
pub bound: Option<Box<Expr>>,
|
||||
pub default: Option<Box<Expr>>,
|
||||
}
|
||||
|
||||
impl From<TypeParamTypeVar> for TypeParam {
|
||||
@@ -3145,6 +3145,7 @@ impl From<TypeParamTypeVar> for TypeParam {
|
||||
pub struct TypeParamParamSpec {
|
||||
pub range: TextRange,
|
||||
pub name: Identifier,
|
||||
pub default: Option<Box<Expr>>,
|
||||
}
|
||||
|
||||
impl From<TypeParamParamSpec> for TypeParam {
|
||||
@@ -3158,6 +3159,7 @@ impl From<TypeParamParamSpec> for TypeParam {
|
||||
pub struct TypeParamTypeVarTuple {
|
||||
pub range: TextRange,
|
||||
pub name: Identifier,
|
||||
pub default: Option<Box<Expr>>,
|
||||
}
|
||||
|
||||
impl From<TypeParamTypeVarTuple> for TypeParam {
|
||||
@@ -3183,7 +3185,7 @@ pub struct Decorator {
|
||||
///
|
||||
/// NOTE: This type differs from the original Python AST. See: [arguments](https://docs.python.org/3/library/ast.html#ast.arguments).
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, Default)]
|
||||
pub struct Parameters {
|
||||
pub range: TextRange,
|
||||
pub posonlyargs: Vec<ParameterWithDefault>,
|
||||
@@ -3408,48 +3410,6 @@ impl Deref for TypeParams {
|
||||
|
||||
pub type Suite = Vec<Stmt>;
|
||||
|
||||
impl Parameters {
|
||||
pub fn empty(range: TextRange) -> Self {
|
||||
Self {
|
||||
range,
|
||||
posonlyargs: Vec::new(),
|
||||
args: Vec::new(),
|
||||
vararg: None,
|
||||
kwonlyargs: Vec::new(),
|
||||
kwarg: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParameterWithDefault {
|
||||
pub fn as_parameter(&self) -> &Parameter {
|
||||
&self.parameter
|
||||
}
|
||||
}
|
||||
|
||||
impl Parameters {
|
||||
pub fn defaults(&self) -> impl std::iter::Iterator<Item = &Expr> {
|
||||
self.posonlyargs
|
||||
.iter()
|
||||
.chain(self.args.iter())
|
||||
.filter_map(|arg| arg.default.as_ref().map(std::convert::AsRef::as_ref))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn split_kwonlyargs(&self) -> (Vec<&Parameter>, Vec<(&Parameter, &Expr)>) {
|
||||
let mut args = Vec::new();
|
||||
let mut with_defaults = Vec::new();
|
||||
for arg in &self.kwonlyargs {
|
||||
if let Some(ref default) = arg.default {
|
||||
with_defaults.push((arg.as_parameter(), &**default));
|
||||
} else {
|
||||
args.push(arg.as_parameter());
|
||||
}
|
||||
}
|
||||
(args, with_defaults)
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of escape command as defined in [IPython Syntax] in the IPython codebase.
|
||||
///
|
||||
/// [IPython Syntax]: https://github.com/ipython/ipython/blob/635815e8f1ded5b764d66cacc80bbe25e9e2587f/IPython/core/inputtransformer2.py#L335-L343
|
||||
@@ -4207,7 +4167,7 @@ mod tests {
|
||||
assert_eq!(std::mem::size_of::<ExprSetComp>(), 40);
|
||||
assert_eq!(std::mem::size_of::<ExprSlice>(), 32);
|
||||
assert_eq!(std::mem::size_of::<ExprStarred>(), 24);
|
||||
assert_eq!(std::mem::size_of::<ExprStringLiteral>(), 48);
|
||||
assert_eq!(std::mem::size_of::<ExprStringLiteral>(), 56);
|
||||
assert_eq!(std::mem::size_of::<ExprSubscript>(), 32);
|
||||
assert_eq!(std::mem::size_of::<ExprTuple>(), 40);
|
||||
assert_eq!(std::mem::size_of::<ExprUnaryOp>(), 24);
|
||||
|
||||
@@ -7,7 +7,8 @@ use crate::{
|
||||
self as ast, Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator,
|
||||
ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement, FStringPart,
|
||||
Keyword, MatchCase, Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword,
|
||||
Stmt, StringLiteral, TypeParam, TypeParamTypeVar, TypeParams, UnaryOp, WithItem,
|
||||
Stmt, StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
|
||||
TypeParams, UnaryOp, WithItem,
|
||||
};
|
||||
|
||||
/// A trait for AST visitors. Visits all nodes in the AST recursively in evaluation-order.
|
||||
@@ -666,14 +667,35 @@ pub fn walk_type_param<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, type_param:
|
||||
match type_param {
|
||||
TypeParam::TypeVar(TypeParamTypeVar {
|
||||
bound,
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = bound {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(expr) = default {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = default {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
TypeParam::ParamSpec(TypeParamParamSpec {
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = default {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
TypeParam::TypeVarTuple(_) | TypeParam::ParamSpec(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ use crate::{
|
||||
self as ast, Alias, Arguments, BoolOp, BytesLiteral, CmpOp, Comprehension, Decorator,
|
||||
ElifElseClause, ExceptHandler, Expr, ExprContext, FString, FStringElement, Keyword, MatchCase,
|
||||
Operator, Parameter, Parameters, Pattern, PatternArguments, PatternKeyword, Stmt,
|
||||
StringLiteral, TypeParam, TypeParamTypeVar, TypeParams, UnaryOp, WithItem,
|
||||
StringLiteral, TypeParam, TypeParamParamSpec, TypeParamTypeVar, TypeParamTypeVarTuple,
|
||||
TypeParams, UnaryOp, WithItem,
|
||||
};
|
||||
|
||||
/// A trait for transforming ASTs. Visits all nodes in the AST recursively in evaluation-order.
|
||||
@@ -652,14 +653,35 @@ pub fn walk_type_param<V: Transformer + ?Sized>(visitor: &V, type_param: &mut Ty
|
||||
match type_param {
|
||||
TypeParam::TypeVar(TypeParamTypeVar {
|
||||
bound,
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = bound {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(expr) = default {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
TypeParam::TypeVarTuple(TypeParamTypeVarTuple {
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = default {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
TypeParam::ParamSpec(TypeParamParamSpec {
|
||||
default,
|
||||
name: _,
|
||||
range: _,
|
||||
}) => {
|
||||
if let Some(expr) = default {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
TypeParam::TypeVarTuple(_) | TypeParam::ParamSpec(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -571,7 +571,7 @@ impl<'a> Generator<'a> {
|
||||
}) => {
|
||||
statement!({
|
||||
self.p("from ");
|
||||
if let Some(level) = level {
|
||||
if *level > 0 {
|
||||
for _ in 0..*level {
|
||||
self.p(".");
|
||||
}
|
||||
@@ -756,20 +756,37 @@ impl<'a> Generator<'a> {
|
||||
|
||||
pub(crate) fn unparse_type_param(&mut self, ast: &TypeParam) {
|
||||
match ast {
|
||||
TypeParam::TypeVar(TypeParamTypeVar { name, bound, .. }) => {
|
||||
TypeParam::TypeVar(TypeParamTypeVar {
|
||||
name,
|
||||
bound,
|
||||
default,
|
||||
..
|
||||
}) => {
|
||||
self.p_id(name);
|
||||
if let Some(expr) = bound {
|
||||
self.p(": ");
|
||||
self.unparse_expr(expr, precedence::MAX);
|
||||
}
|
||||
if let Some(expr) = default {
|
||||
self.p(" = ");
|
||||
self.unparse_expr(expr, precedence::MAX);
|
||||
}
|
||||
}
|
||||
TypeParam::TypeVarTuple(TypeParamTypeVarTuple { name, .. }) => {
|
||||
TypeParam::TypeVarTuple(TypeParamTypeVarTuple { name, default, .. }) => {
|
||||
self.p("*");
|
||||
self.p_id(name);
|
||||
if let Some(expr) = default {
|
||||
self.p(" = ");
|
||||
self.unparse_expr(expr, precedence::MAX);
|
||||
}
|
||||
}
|
||||
TypeParam::ParamSpec(TypeParamParamSpec { name, .. }) => {
|
||||
TypeParam::ParamSpec(TypeParamParamSpec { name, default, .. }) => {
|
||||
self.p("**");
|
||||
self.p_id(name);
|
||||
if let Some(expr) = default {
|
||||
self.p(" = ");
|
||||
self.unparse_expr(expr, precedence::MAX);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1676,6 +1693,10 @@ class Foo:
|
||||
assert_round_trip!(r"type Foo[T] = list[T]");
|
||||
assert_round_trip!(r"type Foo[*Ts] = ...");
|
||||
assert_round_trip!(r"type Foo[**P] = ...");
|
||||
assert_round_trip!(r"type Foo[T = int] = list[T]");
|
||||
assert_round_trip!(r"type Foo[*Ts = int] = ...");
|
||||
assert_round_trip!(r"type Foo[*Ts = *int] = ...");
|
||||
assert_round_trip!(r"type Foo[**P = int] = ...");
|
||||
assert_round_trip!(r"type Foo[T, U, *Ts, **P] = ...");
|
||||
// https://github.com/astral-sh/ruff/issues/6498
|
||||
assert_round_trip!(r"f(a=1, *args, **kwargs)");
|
||||
|
||||
@@ -129,6 +129,10 @@ x = f"{ # comment 12
|
||||
{'x': 1, 'y': 2} }"
|
||||
x = f"{ # comment 13
|
||||
{'x': 1, 'y': 2} = }"
|
||||
# But, if there's a format specifier or a conversion flag then we don't need to add
|
||||
# any whitespace at the end
|
||||
x = f"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}!s} bbbbbb"
|
||||
x = f"aaaaa { {'aaaaaa', 'bbbbbbb', 'ccccccccc'}:.3f} bbbbbb"
|
||||
|
||||
# But, in this case, we would split the expression itself because it exceeds the line
|
||||
# length limit so we need not add the extra space.
|
||||
@@ -207,6 +211,29 @@ f"{ # comment 15
|
||||
}" # comment 19
|
||||
# comment 20
|
||||
|
||||
# Single-quoted f-strings with a format specificer can be multiline
|
||||
f"aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
|
||||
variable:.3f} ddddddddddddddd eeeeeeee"
|
||||
|
||||
# But, if it's triple-quoted then we can't or the format specificer will have a
|
||||
# trailing newline
|
||||
f"""aaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb ccccccccccc {
|
||||
variable:.3f} ddddddddddddddd eeeeeeee"""
|
||||
|
||||
# But, we can break the ones which don't have a format specifier
|
||||
f"""fooooooooooooooooooo barrrrrrrrrrrrrrrrrrr {
|
||||
xxxxxxxxxxxxxxx:.3f} aaaaaaaaaaaaaaaaa { xxxxxxxxxxxxxxxxxxxx } bbbbbbbbbbbb"""
|
||||
|
||||
# Throw in a random comment in it but surpise, this is not a comment but just a text
|
||||
# which is part of the format specifier
|
||||
aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
|
||||
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
|
||||
# comment
|
||||
} cccccccccc"""
|
||||
aaaaaaaaaaa = f"""asaaaaaaaaaaaaaaaa {
|
||||
aaaaaaaaaaaa + bbbbbbbbbbbb + ccccccccccccccc + dddddddd:.3f
|
||||
# comment} cccccccccc"""
|
||||
|
||||
# Conversion flags
|
||||
#
|
||||
# This is not a valid Python code because of the additional whitespace between the `!`
|
||||
|
||||
@@ -433,3 +433,6 @@ def function_with_one_argument_and_a_keyword_separator(
|
||||
# https://peps.python.org/pep-0646/#change-2-args-as-a-typevartuple
|
||||
def function_with_variadic_generics(*args: *tuple[int]): ...
|
||||
def function_with_variadic_generics(*args: *tuple[int],): ...
|
||||
|
||||
# Generic arguments (PEP 695)
|
||||
def func[T](lotsoflongargs: T, lotsoflongargs2: T, lotsoflongargs3: T, lotsoflongargs4: T, lotsoflongargs5: T) -> T: ...
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
# basic usage
|
||||
|
||||
type X = int
|
||||
type X = int | str
|
||||
type X = int | "ForwardRefY"
|
||||
type X[T] = T | list[X[T]] # recursive
|
||||
type X[T] = int
|
||||
type X[T] = list[T] | set[T]
|
||||
type X[T=int]=int
|
||||
type X[T:int=int]=int
|
||||
type X[**P=int]=int
|
||||
type X[*Ts=int]=int
|
||||
type X[*Ts=*int]=int
|
||||
type X[T, *Ts, **P] = (T, Ts, P)
|
||||
type X[T: int, *Ts, **P] = (T, Ts, P)
|
||||
type X[T: (int, str), *Ts, **P] = (T, Ts, P)
|
||||
@@ -49,6 +53,18 @@ type X \
|
||||
[T] = T
|
||||
type X[T] \
|
||||
= T
|
||||
type X[T
|
||||
] = T
|
||||
|
||||
# bounds and defaults with multiline definitions
|
||||
type X[T
|
||||
:int ] = int
|
||||
type X[T:
|
||||
int] = int
|
||||
type X[T
|
||||
= int] = int
|
||||
type X[T=
|
||||
int] = int
|
||||
|
||||
# type leading comment
|
||||
type X = ( # trailing open paren comment
|
||||
@@ -94,3 +110,43 @@ type bounds_single_line[T: (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, cccc
|
||||
type bounds_arguments_on_their_own_line[T: (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee)] = T
|
||||
type bounds_argument_per_line[T: (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc, ddddddddddddd, eeeeeeeeeeeeeeee, ffffffffffff)] = T
|
||||
type bounds_trailing_comma[T: (a, b,)] = T
|
||||
|
||||
# bounds plus comments
|
||||
type comment_before_colon[T # comment
|
||||
: int] = T
|
||||
type comment_after_colon[T: # comment
|
||||
int] = T
|
||||
type comment_on_its_own_line[T
|
||||
# comment
|
||||
:
|
||||
# another comment
|
||||
int
|
||||
# why not another
|
||||
] = T
|
||||
|
||||
# type variable defaults
|
||||
type defaults_single_line[T= (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc)] = T
|
||||
type defaults_on_their_own_line[T= (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee)] = T
|
||||
type defaults_one_per_line[T= (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccccccccc, ddddddddddddd, eeeeeeeeeeeeeeee, ffffffffffff)] = T
|
||||
type defaults_trailing_comma[T= (a, b,)] = T
|
||||
|
||||
# defaults plus comments
|
||||
type comment_before_colon[T # comment
|
||||
= int] = T
|
||||
type comment_after_colon[T = # comment
|
||||
int] = T
|
||||
type comment_on_its_own_line[T
|
||||
# comment
|
||||
=
|
||||
# another comment
|
||||
int
|
||||
# why not another
|
||||
] = T
|
||||
type after_star[*Ts = *
|
||||
# comment
|
||||
int] = int
|
||||
|
||||
# both bounds and defaults
|
||||
type bound_and_default[T:int=int] = int
|
||||
type long_bound_short_default[T: (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee)=a]=int
|
||||
type short_bound_long_default[T:a= (aaaaaaaaaaaaaaaaaaaaaaaaaaaaa, bbbbbbbbbbbbbbb, ccccccccccc, ddddddddddddd, eeeeeee)]=int
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user