Compare commits
62 Commits
0.6.9
...
david/more
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d440cad340 | ||
|
|
9169e35175 | ||
|
|
79f809cd13 | ||
|
|
b219c6fc2c | ||
|
|
edfaf896e4 | ||
|
|
da842bbc38 | ||
|
|
b425e6a34a | ||
|
|
a95b4d57c1 | ||
|
|
e069381a23 | ||
|
|
749f176ae5 | ||
|
|
e241c42212 | ||
|
|
09eeee99d4 | ||
|
|
04b636cba2 | ||
|
|
6048f331d9 | ||
|
|
93097f1c53 | ||
|
|
9bb4722ebf | ||
|
|
5caabe54b6 | ||
|
|
814ab47582 | ||
|
|
c3a3622e30 | ||
|
|
4ef422d3b4 | ||
|
|
58bc981677 | ||
|
|
dd5018ac55 | ||
|
|
63df94b521 | ||
|
|
e4c0dd6f96 | ||
|
|
3111dce5b4 | ||
|
|
8445e4725c | ||
|
|
defdc4dd8e | ||
|
|
46bc69d1d4 | ||
|
|
3209953276 | ||
|
|
6ae833e0c7 | ||
|
|
a3dc5c0529 | ||
|
|
d6b24b690a | ||
|
|
5b4afd30ca | ||
|
|
b9827a4122 | ||
|
|
93eff7f174 | ||
|
|
fc661e193a | ||
|
|
42fcbef876 | ||
|
|
71b52b83e4 | ||
|
|
fb90f5a13d | ||
|
|
d7484e6942 | ||
|
|
14ee5dbfde | ||
|
|
27ac34d683 | ||
|
|
31ca1c3064 | ||
|
|
646e4136d7 | ||
|
|
58a11b33da | ||
|
|
7856e90a2c | ||
|
|
98878c9bf2 | ||
|
|
73aa6ea417 | ||
|
|
38d872ea4c | ||
|
|
824def2194 | ||
|
|
2ab78dd6a5 | ||
|
|
03fa7f64dd | ||
|
|
43330225be | ||
|
|
383d9d9f6e | ||
|
|
8108f83810 | ||
|
|
f1205177fd | ||
|
|
1c2cafc101 | ||
|
|
7c5a7d909c | ||
|
|
2a365bb278 | ||
|
|
020f4d4a54 | ||
|
|
888930b7d3 | ||
|
|
d726f09cf0 |
12
.github/workflows/ci.yaml
vendored
12
.github/workflows/ci.yaml
vendored
@@ -148,7 +148,7 @@ jobs:
|
||||
# sync, not just public items. Eventually we should do this for all
|
||||
# crates; for now add crates here as they are warning-clean to prevent
|
||||
# regression.
|
||||
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p ruff_db --document-private-items
|
||||
- run: cargo doc --no-deps -p red_knot_python_semantic -p red_knot -p red_knot_test -p ruff_db --document-private-items
|
||||
env:
|
||||
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
|
||||
RUSTDOCFLAGS: "-D warnings"
|
||||
@@ -518,6 +518,8 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: "Add SSH key"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
uses: webfactory/ssh-agent@v0.9.0
|
||||
@@ -525,13 +527,15 @@ jobs:
|
||||
ssh-private-key: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY }}
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Insiders dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
|
||||
run: pip install -r docs/requirements-insiders.txt
|
||||
run: uv pip install -r docs/requirements-insiders.txt --system
|
||||
- name: "Install dependencies"
|
||||
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS != 'true' }}
|
||||
run: pip install -r docs/requirements.txt
|
||||
run: uv pip install -r docs/requirements.txt --system
|
||||
- name: "Update README File"
|
||||
run: python scripts/transform_readme.py --target mkdocs
|
||||
- name: "Generate docs"
|
||||
@@ -608,7 +612,7 @@ jobs:
|
||||
just test
|
||||
|
||||
benchmarks:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-22.04
|
||||
needs: determine_changes
|
||||
if: ${{ github.repository == 'astral-sh/ruff' && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
|
||||
@@ -28,6 +28,7 @@ repos:
|
||||
additional_dependencies:
|
||||
- mdformat-mkdocs
|
||||
- mdformat-admon
|
||||
- mdformat-footnote
|
||||
exclude: |
|
||||
(?x)^(
|
||||
docs/formatter/black\.md
|
||||
@@ -45,7 +46,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.24.6
|
||||
rev: v1.25.0
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -59,7 +60,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.8
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
188
Cargo.lock
generated
188
Cargo.lock
generated
@@ -36,12 +36,6 @@ 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"
|
||||
@@ -353,9 +347,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.18"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -363,9 +357,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.18"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -714,7 +708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
@@ -728,7 +722,7 @@ checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
@@ -1026,16 +1020,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"allocator-api2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1127,7 +1126,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc9da1a252bd44cd341657203722352efc9bc0c847d06ea6d2dc1cd1135e0a01"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1147,7 +1146,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.14.5",
|
||||
"serde",
|
||||
]
|
||||
|
||||
@@ -1312,9 +1311,9 @@ checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.70"
|
||||
version = "0.3.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -1353,9 +1352,9 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
|
||||
|
||||
[[package]]
|
||||
name = "libcst"
|
||||
version = "1.4.0"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10293a04a48e8b0cb2cc825a93b83090e527bffd3c897a0255ad7bc96079e920"
|
||||
checksum = "1586dd7a857d8a61a577afde1a24cc9573ff549eff092d5ce968b1ec93cc61b6"
|
||||
dependencies = [
|
||||
"chic",
|
||||
"libcst_derive",
|
||||
@@ -1626,9 +1625,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.19.0"
|
||||
version = "1.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -1691,9 +1690,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.14"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "path-absolutize"
|
||||
@@ -1721,15 +1720,15 @@ checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
||||
|
||||
[[package]]
|
||||
name = "pathdiff"
|
||||
version = "0.2.1"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
|
||||
checksum = "d61c5ce1153ab5b689d0c074c4e7fc613e942dfb7dd9eea5ab202d2ad91fe361"
|
||||
|
||||
[[package]]
|
||||
name = "peg"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "400bcab7d219c38abf8bd7cc2054eb9bbbd4312d66f6a5557d572a203f646f61"
|
||||
checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f"
|
||||
dependencies = [
|
||||
"peg-macros",
|
||||
"peg-runtime",
|
||||
@@ -1737,9 +1736,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "peg-macros"
|
||||
version = "0.8.2"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46e61cce859b76d19090f62da50a9fe92bab7c2a5f09e183763559a2ac392c90"
|
||||
checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426"
|
||||
dependencies = [
|
||||
"peg-runtime",
|
||||
"proc-macro2",
|
||||
@@ -1748,9 +1747,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "peg-runtime"
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36bae92c60fa2398ce4678b98b2c4b5a7c61099961ca1fa305aec04a9ad28922"
|
||||
checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a"
|
||||
|
||||
[[package]]
|
||||
name = "pep440_rs"
|
||||
@@ -1944,9 +1943,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.86"
|
||||
version = "1.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
|
||||
checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -2081,10 +2080,14 @@ dependencies = [
|
||||
"camino",
|
||||
"compact_str",
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.0",
|
||||
"insta",
|
||||
"itertools 0.13.0",
|
||||
"memchr",
|
||||
"ordermap",
|
||||
"red_knot_test",
|
||||
"red_knot_vendored",
|
||||
"rstest",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_ast",
|
||||
@@ -2127,6 +2130,26 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_test"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"colored",
|
||||
"once_cell",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
"regex",
|
||||
"ruff_db",
|
||||
"ruff_index",
|
||||
"ruff_python_trivia",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "red_knot_vendored"
|
||||
version = "0.0.0"
|
||||
@@ -2247,6 +2270,12 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "relative-path"
|
||||
version = "1.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
@@ -2262,6 +2291,33 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936"
|
||||
dependencies = [
|
||||
"rstest_macros",
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rstest_macros"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"glob",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"relative-path",
|
||||
"rustc_version",
|
||||
"syn",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.9"
|
||||
@@ -2412,7 +2468,6 @@ dependencies = [
|
||||
"ruff_python_codegen",
|
||||
"ruff_python_formatter",
|
||||
"ruff_python_parser",
|
||||
"ruff_python_stdlib",
|
||||
"ruff_python_trivia",
|
||||
"ruff_workspace",
|
||||
"schemars",
|
||||
@@ -2885,6 +2940,15 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||
dependencies = [
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "0.38.37"
|
||||
@@ -3030,6 +3094,12 @@ version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
@@ -3115,9 +3185,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "3.9.0"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
|
||||
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -3126,9 +3196,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.9.0"
|
||||
version = "3.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
|
||||
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
@@ -3281,12 +3351,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
|
||||
checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3788,9 +3858,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||
checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -3799,9 +3869,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||
checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -3814,9 +3884,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.43"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
|
||||
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
@@ -3826,9 +3896,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||
checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -3836,9 +3906,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -3849,15 +3919,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.93"
|
||||
version = "0.2.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||
checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test"
|
||||
version = "0.3.43"
|
||||
version = "0.3.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9"
|
||||
checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -3870,9 +3940,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-test-macro"
|
||||
version = "0.3.43"
|
||||
version = "0.3.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021"
|
||||
checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
69
Cargo.toml
69
Cargo.toml
@@ -39,6 +39,7 @@ ruff_workspace = { path = "crates/ruff_workspace" }
|
||||
|
||||
red_knot_python_semantic = { path = "crates/red_knot_python_semantic" }
|
||||
red_knot_server = { path = "crates/red_knot_server" }
|
||||
red_knot_test = { path = "crates/red_knot_test" }
|
||||
red_knot_workspace = { path = "crates/red_knot_workspace", default-features = false }
|
||||
|
||||
aho-corasick = { version = "1.1.3" }
|
||||
@@ -72,7 +73,10 @@ filetime = { version = "0.2.23" }
|
||||
glob = { version = "0.3.1" }
|
||||
globset = { version = "0.4.14" }
|
||||
globwalk = { version = "0.9.1" }
|
||||
hashbrown = "0.14.3"
|
||||
hashbrown = { version = "0.15.0", default-features = false, features = [
|
||||
"raw-entry",
|
||||
"inline-more",
|
||||
] }
|
||||
ignore = { version = "0.4.22" }
|
||||
imara-diff = { version = "0.1.5" }
|
||||
imperative = { version = "1.0.4" }
|
||||
@@ -90,7 +94,7 @@ libcst = { version = "1.1.0", default-features = false }
|
||||
log = { version = "0.4.17" }
|
||||
lsp-server = { version = "0.7.6" }
|
||||
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
|
||||
"proposed",
|
||||
"proposed",
|
||||
] }
|
||||
matchit = { version = "0.8.1" }
|
||||
memchr = { version = "2.7.1" }
|
||||
@@ -111,6 +115,7 @@ quote = { version = "1.0.23" }
|
||||
rand = { version = "0.8.5" }
|
||||
rayon = { version = "1.10.0" }
|
||||
regex = { version = "1.10.2" }
|
||||
rstest = { version = "0.22.0", default-features = false }
|
||||
rustc-hash = { version = "2.0.0" }
|
||||
salsa = { git = "https://github.com/salsa-rs/salsa.git", rev = "4a7c955255e707e64e43f3ce5eabb771ae067768" }
|
||||
schemars = { version = "0.8.16" }
|
||||
@@ -120,7 +125,7 @@ serde-wasm-bindgen = { version = "0.6.4" }
|
||||
serde_json = { version = "1.0.113" }
|
||||
serde_test = { version = "1.0.152" }
|
||||
serde_with = { version = "3.6.0", default-features = false, features = [
|
||||
"macros",
|
||||
"macros",
|
||||
] }
|
||||
shellexpand = { version = "3.0.0" }
|
||||
similar = { version = "2.4.0", features = ["inline"] }
|
||||
@@ -137,7 +142,10 @@ toml = { version = "0.8.11" }
|
||||
tracing = { version = "0.1.40" }
|
||||
tracing-flame = { version = "0.2.0" }
|
||||
tracing-indicatif = { version = "0.3.6" }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = ["env-filter", "fmt"] }
|
||||
tracing-subscriber = { version = "0.3.18", default-features = false, features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
] }
|
||||
tracing-tree = { version = "0.4.0" }
|
||||
typed-arena = { version = "2.0.2" }
|
||||
unic-ucd-category = { version = "0.9" }
|
||||
@@ -148,10 +156,10 @@ unicode-normalization = { version = "0.1.23" }
|
||||
ureq = { version = "2.9.6" }
|
||||
url = { version = "2.5.0" }
|
||||
uuid = { version = "1.6.1", features = [
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"js",
|
||||
"v4",
|
||||
"fast-rng",
|
||||
"macro-diagnostics",
|
||||
"js",
|
||||
] }
|
||||
walkdir = { version = "2.3.2" }
|
||||
wasm-bindgen = { version = "0.2.92" }
|
||||
@@ -162,7 +170,10 @@ zip = { version = "0.6.6", default-features = false }
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(fuzzing)", "cfg(codspeed)"] }
|
||||
unexpected_cfgs = { level = "warn", check-cfg = [
|
||||
"cfg(fuzzing)",
|
||||
"cfg(codspeed)",
|
||||
] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -2 }
|
||||
@@ -245,23 +256,23 @@ windows-archive = ".zip"
|
||||
unix-archive = ".tar.gz"
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = [
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"arm-unknown-linux-musleabihf",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"i686-unknown-linux-gnu",
|
||||
"i686-unknown-linux-musl",
|
||||
"powerpc64-unknown-linux-gnu",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"arm-unknown-linux-musleabihf",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"armv7-unknown-linux-musleabihf",
|
||||
"i686-pc-windows-msvc",
|
||||
"i686-unknown-linux-gnu",
|
||||
"i686-unknown-linux-musl",
|
||||
"powerpc64-unknown-linux-gnu",
|
||||
"powerpc64le-unknown-linux-gnu",
|
||||
"s390x-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
]
|
||||
# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)
|
||||
auto-includes = false
|
||||
@@ -280,7 +291,11 @@ local-artifacts-jobs = ["./build-binaries", "./build-docker"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish-pypi", "./publish-wasm"]
|
||||
# Post-announce jobs to run in CI
|
||||
post-announce-jobs = ["./notify-dependents", "./publish-docs", "./publish-playground"]
|
||||
post-announce-jobs = [
|
||||
"./notify-dependents",
|
||||
"./publish-docs",
|
||||
"./publish-playground",
|
||||
]
|
||||
# Custom permissions for GitHub Jobs
|
||||
github-custom-job-permissions = { "build-docker" = { packages = "write", contents = "read" }, "publish-wasm" = { contents = "read", id-token = "write", packages = "write" } }
|
||||
# Whether to install an updater program
|
||||
|
||||
@@ -24,6 +24,7 @@ bitflags = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
compact_str = { workspace = true }
|
||||
countme = { workspace = true }
|
||||
itertools = { workspace = true}
|
||||
ordermap = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
@@ -33,14 +34,17 @@ hashbrown = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
memchr = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ruff_db = { workspace = true, features = ["os", "testing"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
red_knot_test = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
insta = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
|
||||
4
crates/red_knot_python_semantic/build.rs
Normal file
4
crates/red_knot_python_semantic/build.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
/// Rebuild the crate if a test file is added or removed from
|
||||
pub fn main() {
|
||||
println!("cargo:rerun-if-changed=resources/mdtest");
|
||||
}
|
||||
4
crates/red_knot_python_semantic/resources/README.md
Normal file
4
crates/red_knot_python_semantic/resources/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
Markdown files within the `mdtest/` subdirectory are tests of type inference and type checking;
|
||||
executed by the `tests/mdtest.rs` integration test.
|
||||
|
||||
See `crates/red_knot_test/README.md` for documentation of this test format.
|
||||
@@ -0,0 +1,43 @@
|
||||
### Comparison: Byte literals
|
||||
|
||||
These tests assert that we infer precise `Literal` types for comparisons between objects
|
||||
inferred as having `Literal` bytes types:
|
||||
|
||||
```py
|
||||
reveal_type(b"abc" == b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" == b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" != b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"abc" != b"ab") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" < b"abd") # revealed: Literal[True]
|
||||
reveal_type(b"abc" < b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" <= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" <= b"abb") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" > b"abd") # revealed: Literal[False]
|
||||
reveal_type(b"abc" > b"abb") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" >= b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" >= b"abd") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"" in b"") # revealed: Literal[True]
|
||||
reveal_type(b"" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"") # revealed: Literal[False]
|
||||
reveal_type(b"ab" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"abc" in b"abc") # revealed: Literal[True]
|
||||
reveal_type(b"d" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"\x81\x82" in b"\x80\x81\x82") # revealed: Literal[True]
|
||||
reveal_type(b"\x82\x83" in b"\x80\x81\x82") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"ab" not in b"abc") # revealed: Literal[False]
|
||||
reveal_type(b"ac" not in b"abc") # revealed: Literal[True]
|
||||
|
||||
reveal_type(b"abc" is b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is b"ab") # revealed: Literal[False]
|
||||
|
||||
reveal_type(b"abc" is not b"abc") # revealed: bool
|
||||
reveal_type(b"abc" is not b"ab") # revealed: Literal[True]
|
||||
```
|
||||
@@ -0,0 +1,29 @@
|
||||
# Narrowing for `is` conditionals
|
||||
|
||||
## `is None`
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
|
||||
if x is None:
|
||||
# TODO the following should be simplified to 'None'
|
||||
reveal_type(x) # revealed: None | Literal[1] & None
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is` for other types
|
||||
|
||||
```py
|
||||
class A:
|
||||
...
|
||||
|
||||
x = A()
|
||||
y = x if flag else None
|
||||
|
||||
if y is x:
|
||||
# TODO the following should be simplified to 'A'
|
||||
reveal_type(y) # revealed: A | None & A
|
||||
|
||||
reveal_type(y) # revealed: A | None
|
||||
```
|
||||
@@ -0,0 +1,40 @@
|
||||
# Narrowing for `is not` conditionals
|
||||
|
||||
## `is not None`
|
||||
|
||||
The type guard removes `None` from the union type:
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
|
||||
if x is not None:
|
||||
reveal_type(x) # revealed: Literal[1]
|
||||
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
```
|
||||
|
||||
## `is not` for other singleton types
|
||||
|
||||
```py
|
||||
x = True if flag else False
|
||||
reveal_type(x) # revealed: bool
|
||||
|
||||
if x is not False:
|
||||
# TODO the following should be `Literal[True]`
|
||||
reveal_type(x) # revealed: bool & ~Literal[False]
|
||||
```
|
||||
|
||||
## `is not` for non-singleton types
|
||||
|
||||
Non-singleton types should *not* narrow the type: two instances of a
|
||||
non-singleton class may occupy different addresses in memory even if
|
||||
they compare equal.
|
||||
|
||||
```py
|
||||
x = [1]
|
||||
y = [1]
|
||||
|
||||
if x is not y:
|
||||
# TODO: should include type parameter: list[int]
|
||||
reveal_type(x) # revealed: list
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
# Narrowing for `match` statements
|
||||
|
||||
## Single `match` pattern
|
||||
|
||||
```py
|
||||
x = None if flag else 1
|
||||
reveal_type(x) # revealed: None | Literal[1]
|
||||
|
||||
y = 0
|
||||
|
||||
match x:
|
||||
case None:
|
||||
y = x
|
||||
|
||||
# TODO intersection simplification: should be just Literal[0] | None
|
||||
reveal_type(y) # revealed: Literal[0] | None | Literal[1] & None
|
||||
```
|
||||
35
crates/red_knot_python_semantic/resources/mdtest/numbers.md
Normal file
35
crates/red_knot_python_semantic/resources/mdtest/numbers.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Numbers
|
||||
|
||||
## Integers
|
||||
|
||||
### Literals
|
||||
|
||||
We can infer an integer literal type:
|
||||
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Overflow
|
||||
|
||||
We only track integer literals within the range of an i64:
|
||||
|
||||
```py
|
||||
reveal_type(9223372036854775808) # revealed: int
|
||||
```
|
||||
|
||||
## Floats
|
||||
|
||||
There aren't literal float types, but we infer the general float type:
|
||||
|
||||
```py
|
||||
reveal_type(1.0) # revealed: float
|
||||
```
|
||||
|
||||
## Complex
|
||||
|
||||
Same for complex:
|
||||
|
||||
```py
|
||||
reveal_type(2j) # revealed: complex
|
||||
```
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::iter::FusedIterator;
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
use salsa::plumbing::AsId;
|
||||
|
||||
use ruff_db::files::File;
|
||||
@@ -31,7 +31,7 @@ pub(crate) use self::use_def::{
|
||||
BindingWithConstraints, BindingWithConstraintsIterator, DeclarationsIterator,
|
||||
};
|
||||
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
|
||||
type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), FxBuildHasher>;
|
||||
|
||||
/// Returns the semantic index for `file`.
|
||||
///
|
||||
|
||||
@@ -14,10 +14,10 @@ use crate::stdlib::{
|
||||
builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty, typing_extensions_symbol_ty,
|
||||
};
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet};
|
||||
use crate::{Db, FxOrderSet, Module};
|
||||
|
||||
pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
pub(crate) use self::diagnostic::TypeCheckDiagnostics;
|
||||
pub use self::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics};
|
||||
pub(crate) use self::display::TypeArrayDisplay;
|
||||
pub(crate) use self::infer::{
|
||||
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
|
||||
@@ -385,14 +385,6 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builtin_str_instance(db: &'db dyn Db) -> Self {
|
||||
builtins_symbol_ty(db, "str").to_instance(db)
|
||||
}
|
||||
|
||||
pub fn builtin_int_instance(db: &'db dyn Db) -> Self {
|
||||
builtins_symbol_ty(db, "int").to_instance(db)
|
||||
}
|
||||
|
||||
pub fn is_stdlib_symbol(&self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
|
||||
match self {
|
||||
Type::Class(class) => class.is_stdlib_symbol(db, module_name, name),
|
||||
@@ -423,19 +415,17 @@ impl<'db> Type<'db> {
|
||||
(_, Type::Unknown | Type::Any | Type::Todo) => false,
|
||||
(Type::Never, _) => true,
|
||||
(_, Type::Never) => false,
|
||||
(Type::IntLiteral(_), Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "int") =>
|
||||
{
|
||||
(Type::IntLiteral(_), Type::Instance(class)) if class.is_known(db, KnownClass::Int) => {
|
||||
true
|
||||
}
|
||||
(Type::StringLiteral(_), Type::LiteralString) => true,
|
||||
(Type::StringLiteral(_) | Type::LiteralString, Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "str") =>
|
||||
if class.is_known(db, KnownClass::Str) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::BytesLiteral(_), Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "bytes") =>
|
||||
if class.is_known(db, KnownClass::Bytes) =>
|
||||
{
|
||||
true
|
||||
}
|
||||
@@ -443,8 +433,8 @@ impl<'db> Type<'db> {
|
||||
.elements(db)
|
||||
.iter()
|
||||
.any(|&elem_ty| ty.is_subtype_of(db, elem_ty)),
|
||||
(_, Type::Instance(class)) if class.is_stdlib_symbol(db, "builtins", "object") => true,
|
||||
(Type::Instance(class), _) if class.is_stdlib_symbol(db, "builtins", "object") => false,
|
||||
(_, Type::Instance(class)) if class.is_known(db, KnownClass::Object) => true,
|
||||
(Type::Instance(class), _) if class.is_known(db, KnownClass::Object) => false,
|
||||
// TODO
|
||||
_ => false,
|
||||
}
|
||||
@@ -473,6 +463,61 @@ impl<'db> Type<'db> {
|
||||
self == other
|
||||
}
|
||||
|
||||
/// Return true if there is just a single inhabitant for this type.
|
||||
///
|
||||
/// Note: This function aims to have no false positives, but might return `false`
|
||||
/// for more complicated types that are actually singletons.
|
||||
pub(crate) fn is_singleton(self, db: &'db dyn Db) -> bool {
|
||||
match self {
|
||||
Type::Any
|
||||
| Type::Never
|
||||
| Type::Unknown
|
||||
| Type::Todo
|
||||
| Type::Unbound
|
||||
| Type::IntLiteral(..)
|
||||
| Type::StringLiteral(..)
|
||||
| Type::BytesLiteral(..)
|
||||
| Type::LiteralString => {
|
||||
// Note: The literal types included in this pattern are not true singletons.
|
||||
// There can be multiple Python objects (at different memory locations) that
|
||||
// are both of type Literal[345], for example.
|
||||
false
|
||||
}
|
||||
Type::None
|
||||
| Type::BooleanLiteral(_)
|
||||
| Type::Function(..)
|
||||
| Type::Class(..)
|
||||
| Type::Module(..) => true,
|
||||
Type::Instance(class) => class.is_known(db, KnownClass::EllipsisType),
|
||||
Type::Tuple(tuple) => {
|
||||
// We deliberately deviate from the language specification [1] here and claim
|
||||
// that the empty tuple type is a singleton type. The reasoning is that `()`
|
||||
// is often used as a sentinel value in user code. Declaring the empty tuple to
|
||||
// be of singleton type allows us to narrow types in `is not ()` conditionals.
|
||||
//
|
||||
// [1] https://docs.python.org/3/reference/expressions.html#parenthesized-forms
|
||||
tuple.elements(db).is_empty()
|
||||
}
|
||||
Type::Union(..) => {
|
||||
// A single-element union, where the sole element was a singleton, would itself
|
||||
// be a singleton type. However, unions with length < 2 should never appear in
|
||||
// our model due to [`UnionBuilder::build`].
|
||||
false
|
||||
}
|
||||
Type::Intersection(..) => {
|
||||
// Intersection types are hard to analyze. The following types are technically
|
||||
// all singleton types, but it is not straightforward to compute this. Again,
|
||||
// we simply return false.
|
||||
//
|
||||
// bool & ~Literal[False]`
|
||||
// None & (None | int)
|
||||
// (A | B) & (B | C) with A, B, C disjunct and B a singleton
|
||||
//
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a member access of a type.
|
||||
///
|
||||
/// For example, if `foo` is `Type::Instance(<Bar>)`,
|
||||
@@ -600,9 +645,9 @@ impl<'db> Type<'db> {
|
||||
fn call(self, db: &'db dyn Db, arg_types: &[Type<'db>]) -> CallOutcome<'db> {
|
||||
match self {
|
||||
// TODO validate typed call arguments vs callable signature
|
||||
Type::Function(function_type) => match function_type.kind(db) {
|
||||
FunctionKind::Ordinary => CallOutcome::callable(function_type.return_type(db)),
|
||||
FunctionKind::RevealType => CallOutcome::revealed(
|
||||
Type::Function(function_type) => match function_type.known(db) {
|
||||
None => CallOutcome::callable(function_type.return_type(db)),
|
||||
Some(KnownFunction::RevealType) => CallOutcome::revealed(
|
||||
function_type.return_type(db),
|
||||
*arg_types.first().unwrap_or(&Type::Unknown),
|
||||
),
|
||||
@@ -610,16 +655,15 @@ impl<'db> Type<'db> {
|
||||
|
||||
// TODO annotated return type on `__new__` or metaclass `__call__`
|
||||
Type::Class(class) => {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to return
|
||||
// the specific truthiness value of the input arg, `Literal[True]` for the example above.
|
||||
let is_bool = class.is_stdlib_symbol(db, "builtins", "bool");
|
||||
CallOutcome::callable(if is_bool {
|
||||
arg_types
|
||||
CallOutcome::callable(match class.known(db) {
|
||||
// If the class is the builtin-bool class (for example `bool(1)`), we try to
|
||||
// return the specific truthiness value of the input arg, `Literal[True]` for
|
||||
// the example above.
|
||||
Some(KnownClass::Bool) => arg_types
|
||||
.first()
|
||||
.map(|arg| arg.bool(db).into_type(db))
|
||||
.unwrap_or(Type::BooleanLiteral(false))
|
||||
} else {
|
||||
Type::Instance(class)
|
||||
.unwrap_or(Type::BooleanLiteral(false)),
|
||||
_ => Type::Instance(class),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -714,7 +758,7 @@ impl<'db> Type<'db> {
|
||||
let dunder_get_item_method = iterable_meta_type.member(db, "__getitem__");
|
||||
|
||||
dunder_get_item_method
|
||||
.call(db, &[self, builtins_symbol_ty(db, "int").to_instance(db)])
|
||||
.call(db, &[self, KnownClass::Int.to_instance(db)])
|
||||
.return_ty(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
@@ -758,17 +802,17 @@ impl<'db> Type<'db> {
|
||||
Type::Never => Type::Never,
|
||||
Type::Instance(class) => Type::Class(*class),
|
||||
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
|
||||
Type::BooleanLiteral(_) => builtins_symbol_ty(db, "bool"),
|
||||
Type::BytesLiteral(_) => builtins_symbol_ty(db, "bytes"),
|
||||
Type::IntLiteral(_) => builtins_symbol_ty(db, "int"),
|
||||
Type::Function(_) => types_symbol_ty(db, "FunctionType"),
|
||||
Type::Module(_) => types_symbol_ty(db, "ModuleType"),
|
||||
Type::Tuple(_) => builtins_symbol_ty(db, "tuple"),
|
||||
Type::None => typeshed_symbol_ty(db, "NoneType"),
|
||||
Type::BooleanLiteral(_) => KnownClass::Bool.to_class(db),
|
||||
Type::BytesLiteral(_) => KnownClass::Bytes.to_class(db),
|
||||
Type::IntLiteral(_) => KnownClass::Int.to_class(db),
|
||||
Type::Function(_) => KnownClass::FunctionType.to_class(db),
|
||||
Type::Module(_) => KnownClass::ModuleType.to_class(db),
|
||||
Type::Tuple(_) => KnownClass::Tuple.to_class(db),
|
||||
Type::None => KnownClass::NoneType.to_class(db),
|
||||
// TODO not accurate if there's a custom metaclass...
|
||||
Type::Class(_) => builtins_symbol_ty(db, "type"),
|
||||
Type::Class(_) => KnownClass::Type.to_class(db),
|
||||
// TODO can we do better here? `type[LiteralString]`?
|
||||
Type::StringLiteral(_) | Type::LiteralString => builtins_symbol_ty(db, "str"),
|
||||
Type::StringLiteral(_) | Type::LiteralString => KnownClass::Str.to_class(db),
|
||||
// TODO: `type[Any]`?
|
||||
Type::Any => Type::Todo,
|
||||
// TODO: `type[Unknown]`?
|
||||
@@ -790,7 +834,7 @@ impl<'db> Type<'db> {
|
||||
Type::IntLiteral(_) | Type::BooleanLiteral(_) => self.repr(db),
|
||||
Type::StringLiteral(_) | Type::LiteralString => *self,
|
||||
// TODO: handle more complex types
|
||||
_ => Type::builtin_str_instance(db),
|
||||
_ => KnownClass::Str.to_instance(db),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,7 +857,7 @@ impl<'db> Type<'db> {
|
||||
})),
|
||||
Type::LiteralString => Type::LiteralString,
|
||||
// TODO: handle more complex types
|
||||
_ => Type::builtin_str_instance(db),
|
||||
_ => KnownClass::Str.to_instance(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -824,6 +868,143 @@ impl<'db> From<&Type<'db>> for Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-exhaustive enumeration of known classes (e.g. `builtins.int`, `typing.Any`, ...) to allow
|
||||
/// for easier syntax when interacting with very common classes.
|
||||
///
|
||||
/// Feel free to expand this enum if you ever find yourself using the same class in multiple
|
||||
/// places.
|
||||
/// Note: good candidates are any classes in `[crate::stdlib::CoreStdlibModule]`
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum KnownClass {
|
||||
// To figure out where an stdlib symbol is defined, you can go into `crates/red_knot_vendored`
|
||||
// and grep for the symbol name in any `.pyi` file.
|
||||
|
||||
// Builtins
|
||||
Bool,
|
||||
Object,
|
||||
Bytes,
|
||||
Type,
|
||||
Int,
|
||||
Float,
|
||||
Str,
|
||||
List,
|
||||
Tuple,
|
||||
Set,
|
||||
Dict,
|
||||
// Types
|
||||
GenericAlias,
|
||||
ModuleType,
|
||||
FunctionType,
|
||||
EllipsisType,
|
||||
// Typeshed
|
||||
NoneType, // Part of `types` for Python >= 3.10
|
||||
}
|
||||
|
||||
impl<'db> KnownClass {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Bool => "bool",
|
||||
Self::Object => "object",
|
||||
Self::Bytes => "bytes",
|
||||
Self::Type => "type",
|
||||
Self::Int => "int",
|
||||
Self::Float => "float",
|
||||
Self::Str => "str",
|
||||
Self::List => "list",
|
||||
Self::Tuple => "tuple",
|
||||
Self::Set => "set",
|
||||
Self::Dict => "dict",
|
||||
Self::GenericAlias => "GenericAlias",
|
||||
Self::ModuleType => "ModuleType",
|
||||
Self::FunctionType => "FunctionType",
|
||||
Self::EllipsisType => "EllipsisType",
|
||||
Self::NoneType => "NoneType",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
self.to_class(db).to_instance(db)
|
||||
}
|
||||
|
||||
pub fn to_class(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
| Self::Set
|
||||
| Self::Dict => builtins_symbol_ty(db, self.as_str()),
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType | Self::EllipsisType => {
|
||||
types_symbol_ty(db, self.as_str())
|
||||
}
|
||||
Self::NoneType => typeshed_symbol_ty(db, self.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_from_module(module: &Module, class_name: &str) -> Option<Self> {
|
||||
let candidate = Self::from_name(class_name)?;
|
||||
if candidate.check_module(module) {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn from_name(name: &str) -> Option<Self> {
|
||||
// Note: if this becomes hard to maintain (as rust can't ensure at compile time that all
|
||||
// variants of `Self` are covered), we might use a macro (in-house or dependency)
|
||||
// See: https://stackoverflow.com/q/39070244
|
||||
match name {
|
||||
"bool" => Some(Self::Bool),
|
||||
"object" => Some(Self::Object),
|
||||
"bytes" => Some(Self::Bytes),
|
||||
"tuple" => Some(Self::Tuple),
|
||||
"type" => Some(Self::Type),
|
||||
"int" => Some(Self::Int),
|
||||
"float" => Some(Self::Float),
|
||||
"str" => Some(Self::Str),
|
||||
"set" => Some(Self::Set),
|
||||
"dict" => Some(Self::Dict),
|
||||
"list" => Some(Self::List),
|
||||
"GenericAlias" => Some(Self::GenericAlias),
|
||||
"NoneType" => Some(Self::NoneType),
|
||||
"ModuleType" => Some(Self::ModuleType),
|
||||
"FunctionType" => Some(Self::FunctionType),
|
||||
"EllipsisType" => Some(Self::EllipsisType),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Private method checking if known class can be defined in the given module.
|
||||
fn check_module(self, module: &Module) -> bool {
|
||||
if !module.search_path().is_standard_library() {
|
||||
return false;
|
||||
}
|
||||
match self {
|
||||
Self::Bool
|
||||
| Self::Object
|
||||
| Self::Bytes
|
||||
| Self::Type
|
||||
| Self::Int
|
||||
| Self::Float
|
||||
| Self::Str
|
||||
| Self::List
|
||||
| Self::Tuple
|
||||
| Self::Set
|
||||
| Self::Dict => module.name() == "builtins",
|
||||
Self::GenericAlias | Self::ModuleType | Self::FunctionType | Self::EllipsisType => {
|
||||
module.name() == "types"
|
||||
}
|
||||
Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum CallOutcome<'db> {
|
||||
Callable {
|
||||
@@ -1128,7 +1309,7 @@ impl Truthiness {
|
||||
match self {
|
||||
Self::AlwaysTrue => Type::BooleanLiteral(true),
|
||||
Self::AlwaysFalse => Type::BooleanLiteral(false),
|
||||
Self::Ambiguous => builtins_symbol_ty(db, "bool").to_instance(db),
|
||||
Self::Ambiguous => KnownClass::Bool.to_instance(db),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1150,7 +1331,7 @@ pub struct FunctionType<'db> {
|
||||
pub name: ast::name::Name,
|
||||
|
||||
/// Is this a function that we special-case somehow? If so, which one?
|
||||
kind: FunctionKind,
|
||||
known: Option<KnownFunction>,
|
||||
|
||||
definition: Definition<'db>,
|
||||
|
||||
@@ -1202,11 +1383,10 @@ impl<'db> FunctionType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Hash)]
|
||||
pub enum FunctionKind {
|
||||
/// Just a normal function for which we have no particular special casing
|
||||
#[default]
|
||||
Ordinary,
|
||||
/// Non-exhaustive enumeration of known functions (e.g. `builtins.reveal_type`, ...) that might
|
||||
/// have special behavior.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum KnownFunction {
|
||||
/// `builtins.reveal_type`, `typing.reveal_type` or `typing_extensions.reveal_type`
|
||||
RevealType,
|
||||
}
|
||||
@@ -1220,9 +1400,18 @@ pub struct ClassType<'db> {
|
||||
definition: Definition<'db>,
|
||||
|
||||
body_scope: ScopeId<'db>,
|
||||
|
||||
known: Option<KnownClass>,
|
||||
}
|
||||
|
||||
impl<'db> ClassType<'db> {
|
||||
pub fn is_known(self, db: &'db dyn Db, known_class: KnownClass) -> bool {
|
||||
match self.known(db) {
|
||||
Some(known) => known == known_class,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this class is a standard library type with given module name and name.
|
||||
pub(crate) fn is_stdlib_symbol(self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
|
||||
name == self.name(db)
|
||||
@@ -1349,8 +1538,8 @@ pub struct TupleType<'db> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
builtins_symbol_ty, BytesLiteralType, StringLiteralType, Truthiness, TupleType, Type,
|
||||
UnionType,
|
||||
builtins_symbol_ty, types_symbol_ty, BytesLiteralType, StringLiteralType, Truthiness,
|
||||
TupleType, Type, UnionType,
|
||||
};
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
@@ -1385,6 +1574,7 @@ mod tests {
|
||||
enum Ty {
|
||||
Never,
|
||||
Unknown,
|
||||
None,
|
||||
Any,
|
||||
IntLiteral(i64),
|
||||
BoolLiteral(bool),
|
||||
@@ -1392,6 +1582,7 @@ mod tests {
|
||||
LiteralString,
|
||||
BytesLiteral(&'static str),
|
||||
BuiltinInstance(&'static str),
|
||||
TypesModuleInstance(&'static str),
|
||||
Union(Vec<Ty>),
|
||||
Tuple(Vec<Ty>),
|
||||
}
|
||||
@@ -1401,6 +1592,7 @@ mod tests {
|
||||
match self {
|
||||
Ty::Never => Type::Never,
|
||||
Ty::Unknown => Type::Unknown,
|
||||
Ty::None => Type::None,
|
||||
Ty::Any => Type::Any,
|
||||
Ty::IntLiteral(n) => Type::IntLiteral(n),
|
||||
Ty::StringLiteral(s) => {
|
||||
@@ -1412,6 +1604,7 @@ mod tests {
|
||||
Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes().into()))
|
||||
}
|
||||
Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db),
|
||||
Ty::TypesModuleInstance(s) => types_symbol_ty(db, s).to_instance(db),
|
||||
Ty::Union(tys) => {
|
||||
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
|
||||
}
|
||||
@@ -1489,6 +1682,29 @@ mod tests {
|
||||
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
|
||||
}
|
||||
|
||||
#[test_case(Ty::None)]
|
||||
#[test_case(Ty::BoolLiteral(true))]
|
||||
#[test_case(Ty::BoolLiteral(false))]
|
||||
#[test_case(Ty::Tuple(vec![]))]
|
||||
#[test_case(Ty::TypesModuleInstance("EllipsisType"))]
|
||||
fn is_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_singleton(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::Never)]
|
||||
#[test_case(Ty::IntLiteral(345))]
|
||||
#[test_case(Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None]))]
|
||||
#[test_case(Ty::Tuple(vec![Ty::None, Ty::BoolLiteral(true)]))]
|
||||
fn is_not_singleton(from: Ty) {
|
||||
let db = setup_db();
|
||||
|
||||
assert!(!from.into_type(&db).is_singleton(&db));
|
||||
}
|
||||
|
||||
#[test_case(Ty::IntLiteral(1); "is_int_literal_truthy")]
|
||||
#[test_case(Ty::IntLiteral(-1))]
|
||||
#[test_case(Ty::StringLiteral("foo"))]
|
||||
|
||||
@@ -25,10 +25,12 @@
|
||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
||||
//! eliminate the supertype from the intersection).
|
||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use super::KnownClass;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: Vec<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
@@ -64,7 +66,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
let mut to_remove = SmallVec::<[usize; 2]>::new();
|
||||
for (index, element) in self.elements.iter().enumerate() {
|
||||
if Some(*element) == bool_pair {
|
||||
to_add = builtins_symbol_ty(self.db, "bool");
|
||||
to_add = KnownClass::Bool.to_instance(self.db);
|
||||
to_remove.push(index);
|
||||
// The type we are adding is a BooleanLiteral, which doesn't have any
|
||||
// subtypes. And we just found that the union already contained our
|
||||
@@ -300,7 +302,7 @@ mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::{builtins_symbol_ty, UnionBuilder};
|
||||
use crate::types::{KnownClass, UnionBuilder};
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
@@ -360,7 +362,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_bool() {
|
||||
let db = setup_db();
|
||||
let bool_ty = builtins_symbol_ty(&db, "bool");
|
||||
let bool_instance_ty = KnownClass::Bool.to_instance(&db);
|
||||
|
||||
let t0 = Type::BooleanLiteral(true);
|
||||
let t1 = Type::BooleanLiteral(true);
|
||||
@@ -371,7 +373,7 @@ mod tests {
|
||||
assert_eq!(union.elements(&db), &[t0, t3]);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
|
||||
assert_eq!(union.elements(&db), &[bool_ty, t3]);
|
||||
assert_eq!(union.elements(&db), &[bool_instance_ty, t3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -389,7 +391,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_simplify_subtype() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::builtin_str_instance(&db);
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::LiteralString;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
@@ -401,7 +403,7 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_no_simplify_unknown() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::builtin_str_instance(&db);
|
||||
let t0 = KnownClass::Str.to_instance(&db);
|
||||
let t1 = Type::Unknown;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u1 = UnionType::from_elements(&db, [t1, t0]);
|
||||
@@ -413,9 +415,9 @@ mod tests {
|
||||
#[test]
|
||||
fn build_union_subsume_multiple() {
|
||||
let db = setup_db();
|
||||
let str_ty = Type::builtin_str_instance(&db);
|
||||
let int_ty = Type::builtin_int_instance(&db);
|
||||
let object_ty = builtins_symbol_ty(&db, "object").to_instance(&db);
|
||||
let str_ty = KnownClass::Str.to_instance(&db);
|
||||
let int_ty = KnownClass::Int.to_instance(&db);
|
||||
let object_ty = KnownClass::Object.to_instance(&db);
|
||||
let unknown_ty = Type::Unknown;
|
||||
|
||||
let u0 = UnionType::from_elements(&db, [str_ty, unknown_ty, int_ty, object_ty]);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -155,13 +155,24 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
|
||||
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
|
||||
if matches!(op, ast::CmpOp::IsNot) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
};
|
||||
// TODO other comparison types
|
||||
match op {
|
||||
ast::CmpOp::IsNot => {
|
||||
if comp_ty.is_singleton(self.db) {
|
||||
let ty = IntersectionBuilder::new(self.db)
|
||||
.add_negative(comp_ty)
|
||||
.build();
|
||||
self.constraints.insert(symbol, ty);
|
||||
} else {
|
||||
// Non-singletons cannot be safely narrowed using `is not`
|
||||
}
|
||||
}
|
||||
ast::CmpOp::Is => {
|
||||
self.constraints.insert(symbol, comp_ty);
|
||||
}
|
||||
_ => {
|
||||
// TODO other comparison types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
14
crates/red_knot_python_semantic/tests/mdtest.rs
Normal file
14
crates/red_knot_python_semantic/tests/mdtest.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use red_knot_test::run;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// See `crates/red_knot_test/README.md` for documentation on these tests.
|
||||
#[rstest::rstest]
|
||||
fn mdtest(#[files("resources/mdtest/**/*.md")] path: PathBuf) {
|
||||
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("resources")
|
||||
.join("mdtest")
|
||||
.canonicalize()
|
||||
.unwrap();
|
||||
let title = path.strip_prefix(crate_dir).unwrap();
|
||||
run(&path, title.as_os_str().to_str().unwrap());
|
||||
}
|
||||
33
crates/red_knot_test/Cargo.toml
Normal file
33
crates/red_knot_test/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "red_knot_test"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
red_knot_python_semantic = { workspace = true }
|
||||
red_knot_vendored = { workspace = true }
|
||||
ruff_db = { workspace = true }
|
||||
ruff_index = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
470
crates/red_knot_test/README.md
Normal file
470
crates/red_knot_test/README.md
Normal file
@@ -0,0 +1,470 @@
|
||||
# Writing type-checking / type-inference tests
|
||||
|
||||
Any Markdown file can be a test suite.
|
||||
|
||||
In order for it to be run as one, `red_knot_test::run` must be called with its path; see
|
||||
`crates/red_knot_python_semantic/tests/mdtest.rs` for an example that treats all Markdown files
|
||||
under a certain directory as test suites.
|
||||
|
||||
A Markdown test suite can contain any number of tests. A test consists of one or more embedded
|
||||
"files", each defined by a triple-backticks fenced code block. The code block must have a tag string
|
||||
specifying its language; currently only `py` (Python files) and `pyi` (type stub files) are
|
||||
supported.
|
||||
|
||||
The simplest possible test suite consists of just a single test, with a single embedded file:
|
||||
|
||||
````markdown
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
````
|
||||
|
||||
When running this test, the mdtest framework will write a file with these contents to the default
|
||||
file path (`/src/test.py`) in its in-memory file system, run a type check on that file, and then
|
||||
match the resulting diagnostics with the assertions in the test. Assertions are in the form of
|
||||
Python comments. If all diagnostics and all assertions are matched, the test passes; otherwise, it
|
||||
fails.
|
||||
|
||||
<!---
|
||||
(If you are reading this document in raw Markdown source rather than rendered Markdown, note that
|
||||
the quadruple-backtick-fenced "markdown" language code block above is NOT itself part of the mdtest
|
||||
syntax, it's just how this README embeds an example mdtest Markdown document.)
|
||||
--->
|
||||
|
||||
See actual example mdtest suites in
|
||||
[`crates/red_knot_python_semantic/resources/mdtest`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/resources/mdtest).
|
||||
|
||||
> ℹ️ Note: If you use `rstest` to generate a separate test for all Markdown files in a certain directory,
|
||||
> as with the example in `crates/red_knot_python_semantic/tests/mdtest.rs`,
|
||||
> you will likely want to also make sure that the crate the tests are in is rebuilt every time a
|
||||
> Markdown file is added or removed from the directory. See
|
||||
> [`crates/red_knot_python_semantic/build.rs`](https://github.com/astral-sh/ruff/tree/main/crates/red_knot_python_semantic/build.rs)
|
||||
> for an example of how to do this.
|
||||
>
|
||||
> This is because `rstest` generates its tests at build time rather than at runtime.
|
||||
> Without the `build.rs` file to force a rebuild when a Markdown file is added or removed,
|
||||
> a new Markdown test suite might not be run unless some other change in the crate caused a rebuild
|
||||
> following the addition of the new test file.
|
||||
|
||||
## Assertions
|
||||
|
||||
Two kinds of assertions are supported: `# revealed:` (shown above) and `# error:`.
|
||||
|
||||
### Assertion kinds
|
||||
|
||||
#### revealed
|
||||
|
||||
A `# revealed:` assertion should always be paired with a call to the `reveal_type` utility, which
|
||||
reveals (via a diagnostic) the inferred type of its argument (which can be any expression). The text
|
||||
after `# revealed:` must match exactly with the displayed form of the revealed type of that
|
||||
expression.
|
||||
|
||||
The `reveal_type` function can be imported from the `typing` standard library module (or, for older
|
||||
Python versions, from the `typing_extensions` pseudo-standard-library module[^extensions]):
|
||||
|
||||
```py
|
||||
from typing import reveal_type
|
||||
|
||||
reveal_type("foo") # revealed: Literal["foo"]
|
||||
```
|
||||
|
||||
For convenience, type checkers also pretend that `reveal_type` is a built-in, so that this import is
|
||||
not required. Using `reveal_type` without importing it issues a diagnostic warning that it was used
|
||||
without importing it, in addition to the diagnostic revealing the type of the expression.
|
||||
|
||||
The `# revealed:` assertion must always match a revealed-type diagnostic, and will also match the
|
||||
undefined-reveal diagnostic, if present, so it's safe to use `reveal_type` in tests either with or
|
||||
without importing it. (Style preference is to not import it in tests, unless specifically testing
|
||||
something about the behavior of importing it.)
|
||||
|
||||
#### error
|
||||
|
||||
A comment beginning with `# error:` is an assertion that a type checker diagnostic will be emitted,
|
||||
with text span starting on that line. The matching can be narrowed in three ways:
|
||||
|
||||
- `# error: [invalid-assignment]` requires that the matched diagnostic have the rule code
|
||||
`invalid-assignment`. (The square brackets are required.)
|
||||
- `# error: "Some text"` requires that the diagnostic's full message contain the text `Some text`.
|
||||
(The double quotes are required in the assertion comment; they are not part of the matched text.)
|
||||
- `# error: 8 [rule-code]` or `# error: 8 "Some text"` additionally requires that the matched
|
||||
diagnostic's text span begins on column 8 (one-indexed) of this line.
|
||||
|
||||
Assertions must contain either a rule code or a contains-text, or both, and may optionally also
|
||||
include a column number. They must come in order: first column, if present; then rule code, if
|
||||
present; then contains-text, if present. For example, an assertion using all three would look like
|
||||
`# error: 8 [invalid-assignment] "Some text"`.
|
||||
|
||||
Error assertions in tests intended to test type checker semantics should primarily use rule-code
|
||||
assertions, with occasional contains-text assertions where needed to disambiguate or validate some
|
||||
details of the diagnostic message.
|
||||
|
||||
### Assertion locations
|
||||
|
||||
An assertion comment may be a line-trailing comment, in which case it applies to the line it is on:
|
||||
|
||||
```py
|
||||
x: str = 1 # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
Or it may be a comment on its own line, in which case it applies to the next line that does not
|
||||
contain an assertion comment:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment]
|
||||
x: str = 1
|
||||
```
|
||||
|
||||
Multiple assertions applying to the same line may be stacked:
|
||||
|
||||
```py
|
||||
# error: [invalid-assignment]
|
||||
# revealed: Literal[1]
|
||||
x: str = reveal_type(1)
|
||||
```
|
||||
|
||||
Intervening empty lines or non-assertion comments are not allowed; an assertion stack must be one
|
||||
assertion per line, immediately following each other, with the line immediately following the last
|
||||
assertion as the line of source code on which the matched diagnostics are emitted.
|
||||
|
||||
## Multi-file tests
|
||||
|
||||
Some tests require multiple files, with imports from one file into another. Multiple fenced code
|
||||
blocks represent multiple embedded files. Since files must have unique names, at most one file can
|
||||
use the default name of `/src/test.py`. Other files must explicitly specify their file name:
|
||||
|
||||
````markdown
|
||||
```py
|
||||
from b import C
|
||||
reveal_type(C) # revealed: Literal[C]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
class C: pass
|
||||
```
|
||||
````
|
||||
|
||||
Relative file names are always relative to the "workspace root", which is also an import root (that
|
||||
is, the equivalent of a runtime entry on `sys.path`).
|
||||
|
||||
The default workspace root is `/src/`. Currently it is not possible to customize this in a test, but
|
||||
this is a feature we will want to add in the future.
|
||||
|
||||
So the above test creates two files, `/src/test.py` and `/src/b.py`, and sets the workspace root to
|
||||
`/src/`, allowing `test.py` to import from `b.py` using the module name `b`.
|
||||
|
||||
## Multi-test suites
|
||||
|
||||
A single test suite (Markdown file) can contain multiple tests, by demarcating them using Markdown
|
||||
header lines:
|
||||
|
||||
````markdown
|
||||
# Same-file invalid assignment
|
||||
|
||||
```py
|
||||
x: int = "foo" # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
# Cross-file invalid assignment
|
||||
|
||||
```py
|
||||
from b import y
|
||||
x: int = y # error: [invalid-assignment]
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
y = "foo"
|
||||
```
|
||||
````
|
||||
|
||||
This test suite contains two tests, one named "Same-file invalid assignment" and the other named
|
||||
"Cross-file invalid assignment". The first test involves only a single embedded file, and the second
|
||||
test involves two embedded files.
|
||||
|
||||
The tests are run independently, in independent in-memory file systems and with new red-knot
|
||||
[Salsa](https://github.com/salsa-rs/salsa) databases. This means that each is a from-scratch run of
|
||||
the type checker, with no data persisting from any previous test.
|
||||
|
||||
Due to `cargo test` limitations, an entire test suite (Markdown file) is run as a single Rust test,
|
||||
so it's not possible to select individual tests within it to run.
|
||||
|
||||
## Structured test suites
|
||||
|
||||
Markdown headers can also be used to group related tests within a suite:
|
||||
|
||||
````markdown
|
||||
# Literals
|
||||
|
||||
## Numbers
|
||||
|
||||
### Integer
|
||||
|
||||
```py
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
### Float
|
||||
|
||||
```py
|
||||
reveal_type(1.0) # revealed: float
|
||||
```
|
||||
|
||||
## Strings
|
||||
|
||||
```py
|
||||
reveal_type("foo") # revealed: Literal["foo"]
|
||||
```
|
||||
````
|
||||
|
||||
This test suite contains three tests, named "Literals - Numbers - Integer", "Literals - Numbers -
|
||||
Float", and "Literals - Strings".
|
||||
|
||||
A header-demarcated section must either be a test or a grouping header; it cannot be both. That is,
|
||||
a header section can either contain embedded files (making it a test), or it can contain more
|
||||
deeply-nested headers (headers with more `#`), but it cannot contain both.
|
||||
|
||||
## Documentation of tests
|
||||
|
||||
Arbitrary Markdown syntax (including of course normal prose paragraphs) is permitted (and ignored by
|
||||
the test framework) between fenced code blocks. This permits natural documentation of
|
||||
why a test exists, and what it intends to assert:
|
||||
|
||||
````markdown
|
||||
Assigning a string to a variable annotated as `int` is not permitted:
|
||||
|
||||
```py
|
||||
x: int = "foo" # error: [invalid-assignment]
|
||||
```
|
||||
````
|
||||
|
||||
## Planned features
|
||||
|
||||
There are some designed features that we intend for the test framework to have, but have not yet
|
||||
implemented:
|
||||
|
||||
### Multi-line diagnostic assertions
|
||||
|
||||
We may want to be able to assert that a diagnostic spans multiple lines, and to assert the columns it
|
||||
begins and/or ends on. The planned syntax for this will use `<<<` and `>>>` to mark the start and end lines for
|
||||
an assertion:
|
||||
|
||||
```py
|
||||
(3 # error: 2 [unsupported-operands] <<<
|
||||
+
|
||||
"foo") # error: 6 >>>
|
||||
```
|
||||
|
||||
The column assertion `6` on the ending line should be optional.
|
||||
|
||||
In cases of overlapping such assertions, resolve ambiguity using more angle brackets: `<<<<` begins
|
||||
an assertion ended by `>>>>`, etc.
|
||||
|
||||
### Non-Python files
|
||||
|
||||
Some tests may need to specify non-Python embedded files: typeshed `stdlib/VERSIONS`, `pth` files,
|
||||
`py.typed` files, `pyvenv.cfg` files...
|
||||
|
||||
We will allow specifying any of these using the `text` language in the code block tag string:
|
||||
|
||||
````markdown
|
||||
```text path=/third-party/foo/py.typed
|
||||
partial
|
||||
```
|
||||
````
|
||||
|
||||
We may want to also support testing Jupyter notebooks as embedded files; exact syntax for this is
|
||||
yet to be determined.
|
||||
|
||||
Of course, red-knot is only run directly on `py` and `pyi` files, and assertion comments are only
|
||||
possible in these files.
|
||||
|
||||
A fenced code block with no language will always be an error.
|
||||
|
||||
### Configuration
|
||||
|
||||
We will add the ability to specify non-default red-knot configurations to use in tests, by including
|
||||
a TOML code block:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot]
|
||||
warn-on-any = true
|
||||
```
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def f(x: Any): # error: [use-of-any]
|
||||
pass
|
||||
```
|
||||
````
|
||||
|
||||
It should be possible to include a TOML code block in a single test (as shown), or in a grouping
|
||||
section, in which case it applies to all nested tests within that grouping section. Configurations
|
||||
at multiple level are allowed and merged, with the most-nested (closest to the test) taking
|
||||
precedence.
|
||||
|
||||
### Running just a single test from a suite
|
||||
|
||||
Having each test in a suite always run as a distinct Rust test would require writing our own test
|
||||
runner or code-generating tests in a build script; neither of these is planned.
|
||||
|
||||
We could still allow running just a single test from a suite, for debugging purposes, either via
|
||||
some "focus" syntax that could be easily temporarily added to a test, or via an environment
|
||||
variable.
|
||||
|
||||
### Configuring search paths and kinds
|
||||
|
||||
The red-knot TOML configuration format hasn't been designed yet, and we may want to implement
|
||||
support in the test framework for configuring search paths before it is designed. If so, we can
|
||||
define some configuration options for now under the `[tool.knot.tests]` namespace. In the future,
|
||||
perhaps some of these can be replaced by real red-knot configuration options; some or all may also
|
||||
be kept long-term as test-specific options.
|
||||
|
||||
Some configuration options we will want to provide:
|
||||
|
||||
- We should be able to configure the default workspace root to something other than `/src/` using a
|
||||
`workspace-root` configuration option.
|
||||
|
||||
- We should be able to add a third-party root using the `third-party-root` configuration option.
|
||||
|
||||
- We may want to add additional configuration options for setting additional search path kinds.
|
||||
|
||||
Paths for `workspace-root` and `third-party-root` must be absolute.
|
||||
|
||||
Relative embedded-file paths are relative to the workspace root, even if it is explicitly set to a
|
||||
non-default value using the `workspace-root` config.
|
||||
|
||||
### Specifying a custom typeshed
|
||||
|
||||
Some tests will need to override the default typeshed with custom files. The `[tool.knot.tests]`
|
||||
configuration option `typeshed-root` should be usable for this:
|
||||
|
||||
````markdown
|
||||
```toml
|
||||
[tool.knot.tests]
|
||||
typeshed-root = "/typeshed"
|
||||
```
|
||||
|
||||
This file is importable as part of our custom typeshed, because it is within `/typeshed`, which we
|
||||
configured above as our custom typeshed root:
|
||||
|
||||
```py path=/typeshed/stdlib/builtins.pyi
|
||||
I_AM_THE_ONLY_BUILTIN = 1
|
||||
```
|
||||
|
||||
This file is written to `/src/test.py`, because the default workspace root is `/src/ and the default
|
||||
file path is `test.py`:
|
||||
|
||||
```py
|
||||
reveal_type(I_AM_THE_ONLY_BUILTIN) # revealed: Literal[1]
|
||||
```
|
||||
|
||||
````
|
||||
|
||||
A fenced code block with language `text` can be used to provide a `stdlib/VERSIONS` file in the
|
||||
custom typeshed root. If no such file is created explicitly, one should be created implicitly
|
||||
including entries enabling all specified `<typeshed-root>/stdlib` files for all supported Python
|
||||
versions.
|
||||
|
||||
### I/O errors
|
||||
|
||||
We could use an `error=` configuration option in the tag string to make an embedded file cause an
|
||||
I/O error on read.
|
||||
|
||||
### Asserting on full diagnostic output
|
||||
|
||||
The inline comment diagnostic assertions are useful for making quick, readable assertions about
|
||||
diagnostics in a particular location. But sometimes we will want to assert on the full diagnostic
|
||||
output of checking an embedded Python file. Or sometimes (see “incremental tests” below) we will
|
||||
want to assert on diagnostics in a file, without impacting the contents of that file by changing a
|
||||
comment in it. In these cases, a Python code block in a test could be followed by a fenced code
|
||||
block with language `output`; this would contain the full diagnostic output for the preceding test
|
||||
file:
|
||||
|
||||
````markdown
|
||||
# full output
|
||||
|
||||
```py
|
||||
x = 1
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
This is just an example, not a proposal that red-knot would ever actually output diagnostics in
|
||||
precisely this format:
|
||||
|
||||
```output
|
||||
test.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
````
|
||||
|
||||
We will want to build tooling to automatically capture and update these “full diagnostic output”
|
||||
blocks, when tests are run in an update-output mode (probably specified by an environment variable.)
|
||||
|
||||
By default, an `output` block will specify diagnostic output for the file `<workspace-root>/test.py`.
|
||||
An `output` block can have a `path=` option, to explicitly specify the Python file for which it
|
||||
asserts diagnostic output, and a `stage=` option, to specify which stage of an incremental test it
|
||||
specifies diagnostic output at. (See “incremental tests” below.)
|
||||
|
||||
It is an error for an `output` block to exist, if there is no `py` or `python` block in the same
|
||||
test for the same file path.
|
||||
|
||||
### Incremental tests
|
||||
|
||||
Some tests should validate incremental checking, by initially creating some files, checking them,
|
||||
and then modifying/adding/deleting files and checking again.
|
||||
|
||||
We should add the capability to create an incremental test by using the `stage=` option on some
|
||||
fenced code blocks in the test:
|
||||
|
||||
````markdown
|
||||
# Incremental
|
||||
|
||||
## modify a file
|
||||
|
||||
Initial version of `test.py` and `b.py`:
|
||||
|
||||
```py
|
||||
from b import x
|
||||
reveal_type(x)
|
||||
```
|
||||
|
||||
```py path=b.py
|
||||
x = 1
|
||||
```
|
||||
|
||||
Initial expected output for `test.py`:
|
||||
|
||||
```output
|
||||
/src/test.py, line 1, col 1: revealed type is 'Literal[1]'
|
||||
```
|
||||
|
||||
Now in our first incremental stage, modify the contents of `b.py`:
|
||||
|
||||
```py path=b.py stage=1
|
||||
# b.py
|
||||
x = 2
|
||||
```
|
||||
|
||||
And this is our updated expected output for `test.py` at stage 1:
|
||||
|
||||
```output stage=1
|
||||
/src/test.py, line 1, col 1: revealed type is 'Literal[2]'
|
||||
```
|
||||
|
||||
(One reason to use full-diagnostic-output blocks in this test is that updating
|
||||
inline-comment diagnostic assertions for `test.py` would require specifying new
|
||||
contents for `test.py` in stage 1, which we don't want to do in this test.)
|
||||
````
|
||||
|
||||
It will be possible to provide any number of stages in an incremental test. If a stage re-specifies
|
||||
a filename that was specified in a previous stage (or the initial stage), that file is modified. A
|
||||
new filename appearing for the first time in a new stage will create a new file. To delete a
|
||||
previously created file, specify that file with the tag `delete` in its tag string (in this case, it
|
||||
is an error to provide non-empty contents). Any previously-created files that are not re-specified
|
||||
in a later stage continue to exist with their previously-specified contents, and are not "touched".
|
||||
|
||||
All stages should be run in order, incrementally, and then the final state should also be re-checked
|
||||
cold, to validate equivalence of cold and incremental check results.
|
||||
|
||||
[^extensions]: `typing-extensions` is a third-party module, but typeshed, and thus type checkers
|
||||
also, treat it as part of the standard library.
|
||||
621
crates/red_knot_test/src/assertion.rs
Normal file
621
crates/red_knot_test/src/assertion.rs
Normal file
@@ -0,0 +1,621 @@
|
||||
//! Parse type and type-error assertions in Python comment form.
|
||||
//!
|
||||
//! Parses comments of the form `# revealed: SomeType` and `# error: 8 [rule-code] "message text"`.
|
||||
//! In the latter case, the `8` is a column number, and `"message text"` asserts that the full
|
||||
//! diagnostic message contains the text `"message text"`; all three are optional (`# error:` will
|
||||
//! match any error.)
|
||||
//!
|
||||
//! Assertion comments may be placed at end-of-line:
|
||||
//!
|
||||
//! ```py
|
||||
//! x: int = "foo" # error: [invalid-assignment]
|
||||
//! ```
|
||||
//!
|
||||
//! Or as a full-line comment on the preceding line:
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [invalid-assignment]
|
||||
//! x: int = "foo"
|
||||
//! ```
|
||||
//!
|
||||
//! Multiple assertion comments may apply to the same line; in this case all (or all but the last)
|
||||
//! must be full-line comments:
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [unbound-name]
|
||||
//! reveal_type(x) # revealed: Unbound
|
||||
//! ```
|
||||
//!
|
||||
//! or
|
||||
//!
|
||||
//! ```py
|
||||
//! # error: [unbound-name]
|
||||
//! # revealed: Unbound
|
||||
//! reveal_type(x)
|
||||
//! ```
|
||||
|
||||
use crate::db::Db;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::{LineIndex, Locator, OneIndexed};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use smallvec::SmallVec;
|
||||
use std::ops::Deref;
|
||||
|
||||
/// Diagnostic assertion comments in a single embedded file.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct InlineFileAssertions {
|
||||
comment_ranges: CommentRanges,
|
||||
source: SourceText,
|
||||
lines: LineIndex,
|
||||
}
|
||||
|
||||
impl InlineFileAssertions {
|
||||
pub(crate) fn from_file(db: &Db, file: File) -> Self {
|
||||
let source = source_text(db, file);
|
||||
let lines = line_index(db, file);
|
||||
let parsed = parsed_module(db, file);
|
||||
let comment_ranges = CommentRanges::from(parsed.tokens());
|
||||
Self {
|
||||
comment_ranges,
|
||||
source,
|
||||
lines,
|
||||
}
|
||||
}
|
||||
|
||||
fn locator(&self) -> Locator {
|
||||
Locator::with_index(&self.source, self.lines.clone())
|
||||
}
|
||||
|
||||
fn line_number(&self, range: &impl Ranged) -> OneIndexed {
|
||||
self.lines.line_index(range.start())
|
||||
}
|
||||
|
||||
fn is_own_line_comment(&self, ranged_assertion: &AssertionWithRange) -> bool {
|
||||
CommentRanges::is_own_line(ranged_assertion.start(), &self.locator())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a InlineFileAssertions {
|
||||
type Item = LineAssertions<'a>;
|
||||
type IntoIter = LineAssertionsIterator<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
Self::IntoIter {
|
||||
file_assertions: self,
|
||||
inner: AssertionWithRangeIterator {
|
||||
file_assertions: self,
|
||||
inner: self.comment_ranges.into_iter(),
|
||||
}
|
||||
.peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Assertion`] with the [`TextRange`] of its original inline comment.
|
||||
#[derive(Debug)]
|
||||
struct AssertionWithRange<'a>(Assertion<'a>, TextRange);
|
||||
|
||||
impl<'a> Deref for AssertionWithRange<'a> {
|
||||
type Target = Assertion<'a>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for AssertionWithRange<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<AssertionWithRange<'a>> for Assertion<'a> {
|
||||
fn from(value: AssertionWithRange<'a>) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator that yields all assertions within a single embedded Python file.
|
||||
#[derive(Debug)]
|
||||
struct AssertionWithRangeIterator<'a> {
|
||||
file_assertions: &'a InlineFileAssertions,
|
||||
inner: std::iter::Copied<std::slice::Iter<'a, TextRange>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AssertionWithRangeIterator<'a> {
|
||||
type Item = AssertionWithRange<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let locator = self.file_assertions.locator();
|
||||
loop {
|
||||
let inner_next = self.inner.next()?;
|
||||
let comment = locator.slice(inner_next);
|
||||
if let Some(assertion) = Assertion::from_comment(comment) {
|
||||
return Some(AssertionWithRange(assertion, inner_next));
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for AssertionWithRangeIterator<'_> {}
|
||||
|
||||
/// A vector of [`Assertion`]s belonging to a single line.
|
||||
///
|
||||
/// Most lines will have zero or one assertion, so we use a [`SmallVec`] optimized for a single
|
||||
/// element to avoid most heap vector allocations.
|
||||
type AssertionVec<'a> = SmallVec<[Assertion<'a>; 1]>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineAssertionsIterator<'a> {
|
||||
file_assertions: &'a InlineFileAssertions,
|
||||
inner: std::iter::Peekable<AssertionWithRangeIterator<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for LineAssertionsIterator<'a> {
|
||||
type Item = LineAssertions<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let file = self.file_assertions;
|
||||
let ranged_assertion = self.inner.next()?;
|
||||
let mut collector = AssertionVec::new();
|
||||
let mut line_number = file.line_number(&ranged_assertion);
|
||||
// Collect all own-line comments on consecutive lines; these all apply to the same line of
|
||||
// code. For example:
|
||||
//
|
||||
// ```py
|
||||
// # error: [unbound-name]
|
||||
// # revealed: Unbound
|
||||
// reveal_type(x)
|
||||
// ```
|
||||
//
|
||||
if file.is_own_line_comment(&ranged_assertion) {
|
||||
collector.push(ranged_assertion.into());
|
||||
let mut only_own_line = true;
|
||||
while let Some(ranged_assertion) = self.inner.peek() {
|
||||
let next_line_number = line_number.saturating_add(1);
|
||||
if file.line_number(ranged_assertion) == next_line_number {
|
||||
if !file.is_own_line_comment(ranged_assertion) {
|
||||
only_own_line = false;
|
||||
}
|
||||
line_number = next_line_number;
|
||||
collector.push(self.inner.next().unwrap().into());
|
||||
// If we see an end-of-line comment, it has to be the end of the stack,
|
||||
// otherwise we'd botch this case, attributing all three errors to the `bar`
|
||||
// line:
|
||||
//
|
||||
// ```py
|
||||
// # error:
|
||||
// foo # error:
|
||||
// bar # error:
|
||||
// ```
|
||||
//
|
||||
if !only_own_line {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if only_own_line {
|
||||
// The collected comments apply to the _next_ line in the code.
|
||||
line_number = line_number.saturating_add(1);
|
||||
}
|
||||
} else {
|
||||
// We have a line-trailing comment; it applies to its own line, and is not grouped.
|
||||
collector.push(ranged_assertion.into());
|
||||
}
|
||||
Some(LineAssertions {
|
||||
line_number,
|
||||
assertions: collector,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::FusedIterator for LineAssertionsIterator<'_> {}
|
||||
|
||||
/// One or more assertions referring to the same line of code.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineAssertions<'a> {
|
||||
/// The line these assertions refer to.
|
||||
///
|
||||
/// Not necessarily the same line the assertion comment is located on; for an own-line comment,
|
||||
/// it's the next non-assertion line.
|
||||
pub(crate) line_number: OneIndexed,
|
||||
|
||||
/// The assertions referring to this line.
|
||||
pub(crate) assertions: AssertionVec<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Deref for LineAssertions<'a> {
|
||||
type Target = [Assertion<'a>];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.assertions
|
||||
}
|
||||
}
|
||||
|
||||
static TYPE_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^#\s*revealed:\s*(?<ty_display>.+?)\s*$").unwrap());
|
||||
|
||||
static ERROR_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r#"^#\s*error:(\s*(?<column>\d+))?(\s*\[(?<rule>.+?)\])?(\s*"(?<message>.+?)")?\s*$"#,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// A single diagnostic assertion comment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Assertion<'a> {
|
||||
/// A `revealed: ` assertion.
|
||||
Revealed(&'a str),
|
||||
|
||||
/// An `error: ` assertion.
|
||||
Error(ErrorAssertion<'a>),
|
||||
}
|
||||
|
||||
impl<'a> Assertion<'a> {
|
||||
fn from_comment(comment: &'a str) -> Option<Self> {
|
||||
if let Some(caps) = TYPE_RE.captures(comment) {
|
||||
Some(Self::Revealed(caps.name("ty_display").unwrap().as_str()))
|
||||
} else {
|
||||
ERROR_RE.captures(comment).map(|caps| {
|
||||
Self::Error(ErrorAssertion {
|
||||
rule: caps.name("rule").map(|m| m.as_str()),
|
||||
column: caps.name("column").and_then(|m| m.as_str().parse().ok()),
|
||||
message_contains: caps.name("message").map(|m| m.as_str()),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Assertion<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Revealed(expected_type) => write!(f, "revealed: {expected_type}"),
|
||||
Self::Error(assertion) => assertion.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An `error: ` assertion comment.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ErrorAssertion<'a> {
|
||||
/// The diagnostic rule code we expect.
|
||||
pub(crate) rule: Option<&'a str>,
|
||||
|
||||
/// The column we expect the diagnostic range to start at.
|
||||
pub(crate) column: Option<OneIndexed>,
|
||||
|
||||
/// A string we expect to be contained in the diagnostic message.
|
||||
pub(crate) message_contains: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ErrorAssertion<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("error:")?;
|
||||
if let Some(column) = self.column {
|
||||
write!(f, " {column}")?;
|
||||
}
|
||||
if let Some(rule) = self.rule {
|
||||
write!(f, " [{rule}]")?;
|
||||
}
|
||||
if let Some(message) = self.message_contains {
|
||||
write!(f, r#" "{message}""#)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{Assertion, InlineFileAssertions, LineAssertions};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
fn get_assertions(source: &str) -> InlineFileAssertions {
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
InlineFileAssertions::from_file(&db, file)
|
||||
}
|
||||
|
||||
fn as_vec(assertions: &InlineFileAssertions) -> Vec<LineAssertions> {
|
||||
assertions.into_iter().collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ty_display() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
reveal_type(1) # revealed: Literal[1]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "revealed: Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error:
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error:");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prior_line() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Literal[1]
|
||||
reveal_type(1)
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "revealed: Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_prior_line() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Unbound
|
||||
# error: [unbound-name]
|
||||
reveal_type(x)
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [assert1, assert2] = &line.assertions[..] else {
|
||||
panic!("expected two assertions");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert1}"), "revealed: Unbound");
|
||||
assert_eq!(format!("{assert2}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacked_mixed() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
# revealed: Unbound
|
||||
reveal_type(x) # error: [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert1, assert2] = &line.assertions[..] else {
|
||||
panic!("expected two assertions");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert1}"), "revealed: Unbound");
|
||||
assert_eq!(format!("{assert2}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lines() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [invalid-assignment]
|
||||
x: int = "foo"
|
||||
y # error: [unbound-name]
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line1, line2] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(2));
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [Assertion::Error(error1)] = &line1.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error1.rule, Some("invalid-assignment"));
|
||||
|
||||
let [Assertion::Error(error2)] = &line2.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error2.rule, Some("unbound-name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_lines_mixed_stack() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [invalid-assignment]
|
||||
x: int = reveal_type("foo") # revealed: str
|
||||
y # error: [unbound-name]
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line1, line2] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(2));
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(3));
|
||||
|
||||
let [Assertion::Error(error1), Assertion::Revealed(expected_ty)] = &line1.assertions[..]
|
||||
else {
|
||||
panic!("expected one error assertion and one Revealed assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error1.rule, Some("invalid-assignment"));
|
||||
assert_eq!(*expected_ty, "str");
|
||||
|
||||
let [Assertion::Error(error2)] = &line2.assertions[..] else {
|
||||
panic!("expected one error assertion");
|
||||
};
|
||||
|
||||
assert_eq!(error2.rule, Some("unbound-name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error: [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error: [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
"
|
||||
x # error: 1 [unbound-name]
|
||||
",
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(1));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), "error: 1 [unbound-name]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_message() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: [unbound-name] "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format!("{assert}"),
|
||||
r#"error: [unbound-name] "`x` is unbound""#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_message_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: 1 "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(format!("{assert}"), r#"error: 1 "`x` is unbound""#);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_with_rule_and_message_and_column() {
|
||||
let assertions = get_assertions(&dedent(
|
||||
r#"
|
||||
# error: 1 [unbound-name] "`x` is unbound"
|
||||
x
|
||||
"#,
|
||||
));
|
||||
|
||||
let [line] = &as_vec(&assertions)[..] else {
|
||||
panic!("expected one line");
|
||||
};
|
||||
|
||||
assert_eq!(line.line_number, OneIndexed::from_zero_indexed(2));
|
||||
|
||||
let [assert] = &line.assertions[..] else {
|
||||
panic!("expected one assertion");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
format!("{assert}"),
|
||||
r#"error: 1 [unbound-name] "`x` is unbound""#
|
||||
);
|
||||
}
|
||||
}
|
||||
88
crates/red_knot_test/src/db.rs
Normal file
88
crates/red_knot_test/src/db.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use red_knot_python_semantic::{
|
||||
Db as SemanticDb, Program, ProgramSettings, PythonVersion, SearchPathSettings,
|
||||
};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
#[salsa::db]
|
||||
pub(crate) struct Db {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
pub(crate) fn setup(workspace_root: SystemPathBuf) -> Self {
|
||||
let db = Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: red_knot_vendored::file_system().clone(),
|
||||
files: Files::default(),
|
||||
};
|
||||
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&workspace_root)
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(workspace_root),
|
||||
},
|
||||
)
|
||||
.expect("Invalid search path settings");
|
||||
|
||||
db
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for Db {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SourceDb for Db {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl Upcast<dyn SourceDb> for Db {
|
||||
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
fn upcast_mut(&mut self) -> &mut (dyn SourceDb + 'static) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl SemanticDb for Db {
|
||||
fn is_file_open(&self, file: File) -> bool {
|
||||
!file.path(self).is_vendored_path()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::db]
|
||||
impl salsa::Database for Db {
|
||||
fn salsa_event(&self, _event: &dyn Fn() -> salsa::Event) {}
|
||||
}
|
||||
173
crates/red_knot_test/src/diagnostic.rs
Normal file
173
crates/red_knot_test/src/diagnostic.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
//! Sort and group diagnostics by line number, so they can be correlated with assertions.
|
||||
//!
|
||||
//! We don't assume that we will get the diagnostics in source order.
|
||||
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::ops::{Deref, Range};
|
||||
|
||||
/// All diagnostics for one embedded Python file, sorted and grouped by start line number.
|
||||
///
|
||||
/// The diagnostics are kept in a flat vector, sorted by line number. A separate vector of
|
||||
/// [`LineDiagnosticRange`] has one entry for each contiguous slice of the diagnostics vector
|
||||
/// containing diagnostics which all start on the same line.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SortedDiagnostics<T> {
|
||||
diagnostics: Vec<T>,
|
||||
line_ranges: Vec<LineDiagnosticRange>,
|
||||
}
|
||||
|
||||
impl<T> SortedDiagnostics<T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
{
|
||||
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
|
||||
let mut diagnostics: Vec<_> = diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| DiagnosticWithLine {
|
||||
line_number: line_index.line_index(diagnostic.start()),
|
||||
diagnostic,
|
||||
})
|
||||
.collect();
|
||||
diagnostics.sort_unstable_by_key(|diagnostic_with_line| diagnostic_with_line.line_number);
|
||||
|
||||
let mut diags = Self {
|
||||
diagnostics: Vec::with_capacity(diagnostics.len()),
|
||||
line_ranges: vec![],
|
||||
};
|
||||
|
||||
let mut current_line_number = None;
|
||||
let mut start = 0;
|
||||
for DiagnosticWithLine {
|
||||
line_number,
|
||||
diagnostic,
|
||||
} in diagnostics
|
||||
{
|
||||
match current_line_number {
|
||||
None => {
|
||||
current_line_number = Some(line_number);
|
||||
}
|
||||
Some(current) => {
|
||||
if line_number != current {
|
||||
let end = diags.diagnostics.len();
|
||||
diags.line_ranges.push(LineDiagnosticRange {
|
||||
line_number: current,
|
||||
diagnostic_index_range: start..end,
|
||||
});
|
||||
start = end;
|
||||
current_line_number = Some(line_number);
|
||||
}
|
||||
}
|
||||
}
|
||||
diags.diagnostics.push(diagnostic);
|
||||
}
|
||||
if let Some(line_number) = current_line_number {
|
||||
diags.line_ranges.push(LineDiagnosticRange {
|
||||
line_number,
|
||||
diagnostic_index_range: start..diags.diagnostics.len(),
|
||||
});
|
||||
}
|
||||
|
||||
diags
|
||||
}
|
||||
|
||||
pub(crate) fn iter_lines(&self) -> LineDiagnosticsIterator<T> {
|
||||
LineDiagnosticsIterator {
|
||||
diagnostics: self.diagnostics.as_slice(),
|
||||
inner: self.line_ranges.iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Range delineating diagnostics in [`SortedDiagnostics`] that begin on a single line.
|
||||
#[derive(Debug)]
|
||||
struct LineDiagnosticRange {
|
||||
line_number: OneIndexed,
|
||||
diagnostic_index_range: Range<usize>,
|
||||
}
|
||||
|
||||
/// Iterator to group sorted diagnostics by line.
|
||||
pub(crate) struct LineDiagnosticsIterator<'a, T> {
|
||||
diagnostics: &'a [T],
|
||||
inner: std::slice::Iter<'a, LineDiagnosticRange>,
|
||||
}
|
||||
|
||||
impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
{
|
||||
type Item = LineDiagnostics<'a, T>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let LineDiagnosticRange {
|
||||
line_number,
|
||||
diagnostic_index_range,
|
||||
} = self.inner.next()?;
|
||||
Some(LineDiagnostics {
|
||||
line_number: *line_number,
|
||||
diagnostics: &self.diagnostics[diagnostic_index_range.clone()],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Clone + Ranged {}
|
||||
|
||||
/// All diagnostics that start on a single line of source code in one embedded Python file.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LineDiagnostics<'a, T> {
|
||||
/// Line number on which these diagnostics start.
|
||||
pub(crate) line_number: OneIndexed,
|
||||
|
||||
/// Diagnostics starting on this line.
|
||||
pub(crate) diagnostics: &'a [T],
|
||||
}
|
||||
|
||||
impl<T> Deref for LineDiagnostics<'_, T> {
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DiagnosticWithLine<T> {
|
||||
line_number: OneIndexed,
|
||||
diagnostic: T,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
#[test]
|
||||
fn sort_and_group() {
|
||||
let mut db = Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", "one\ntwo\n").unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
let lines = line_index(&db, file);
|
||||
|
||||
let ranges = vec![
|
||||
TextRange::new(TextSize::new(0), TextSize::new(1)),
|
||||
TextRange::new(TextSize::new(5), TextSize::new(10)),
|
||||
TextRange::new(TextSize::new(1), TextSize::new(7)),
|
||||
];
|
||||
|
||||
let sorted = super::SortedDiagnostics::new(&ranges, &lines);
|
||||
let grouped = sorted.iter_lines().collect::<Vec<_>>();
|
||||
|
||||
let [line1, line2] = &grouped[..] else {
|
||||
panic!("expected two lines");
|
||||
};
|
||||
|
||||
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(0));
|
||||
assert_eq!(line1.diagnostics.len(), 2);
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
|
||||
assert_eq!(line2.diagnostics.len(), 1);
|
||||
}
|
||||
}
|
||||
95
crates/red_knot_test/src/lib.rs
Normal file
95
crates/red_knot_test/src/lib.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
type Failures = BTreeMap<SystemPathBuf, matcher::FailuresByLine>;
|
||||
|
||||
mod assertion;
|
||||
mod db;
|
||||
mod diagnostic;
|
||||
mod matcher;
|
||||
mod parser;
|
||||
|
||||
/// Run `path` as a markdown test suite with given `title`.
|
||||
///
|
||||
/// Panic on test failure, and print failure details.
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub fn run(path: &PathBuf, title: &str) {
|
||||
let source = std::fs::read_to_string(path).unwrap();
|
||||
let suite = match test_parser::parse(title, &source) {
|
||||
Ok(suite) => suite,
|
||||
Err(err) => {
|
||||
panic!("Error parsing `{}`: {err}", path.to_str().unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
let mut any_failures = false;
|
||||
for test in suite.tests() {
|
||||
if let Err(failures) = run_test(&test) {
|
||||
any_failures = true;
|
||||
println!("\n{}\n", test.name().bold().underline());
|
||||
|
||||
for (path, by_line) in failures {
|
||||
println!("{}", path.as_str().bold());
|
||||
for (line_number, failures) in by_line.iter() {
|
||||
for failure in failures {
|
||||
let line_info = format!("line {line_number}:").cyan();
|
||||
println!(" {line_info} {failure}");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("{}\n", "-".repeat(50));
|
||||
|
||||
assert!(!any_failures, "Some tests failed.");
|
||||
}
|
||||
|
||||
fn run_test(test: &parser::MarkdownTest) -> Result<(), Failures> {
|
||||
let workspace_root = SystemPathBuf::from("/src");
|
||||
let mut db = db::Db::setup(workspace_root.clone());
|
||||
|
||||
let mut system_paths = vec![];
|
||||
|
||||
for file in test.files() {
|
||||
assert!(
|
||||
matches!(file.lang, "py" | "pyi"),
|
||||
"Non-Python files not supported yet."
|
||||
);
|
||||
let full_path = workspace_root.join(file.path);
|
||||
db.write_file(&full_path, file.code).unwrap();
|
||||
system_paths.push(full_path);
|
||||
}
|
||||
|
||||
let mut failures = BTreeMap::default();
|
||||
|
||||
for path in system_paths {
|
||||
let file = system_path_to_file(&db, path.clone()).unwrap();
|
||||
let parsed = parsed_module(&db, file);
|
||||
|
||||
// TODO allow testing against code with syntax errors
|
||||
assert!(
|
||||
parsed.errors().is_empty(),
|
||||
"Python syntax errors in {}, {:?}: {:?}",
|
||||
test.name(),
|
||||
path,
|
||||
parsed.errors()
|
||||
);
|
||||
|
||||
matcher::match_file(&db, file, check_types(&db, file)).unwrap_or_else(|line_failures| {
|
||||
failures.insert(path, line_failures);
|
||||
});
|
||||
}
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
929
crates/red_knot_test/src/matcher.rs
Normal file
929
crates/red_knot_test/src/matcher.rs
Normal file
@@ -0,0 +1,929 @@
|
||||
//! Match [`TypeCheckDiagnostic`]s against [`Assertion`]s and produce test failure messages for any
|
||||
//! mismatches.
|
||||
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::SortedDiagnostics;
|
||||
use colored::Colorize;
|
||||
use red_knot_python_semantic::types::TypeCheckDiagnostic;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct FailuresByLine {
|
||||
failures: Vec<String>,
|
||||
lines: Vec<LineFailures>,
|
||||
}
|
||||
|
||||
impl FailuresByLine {
|
||||
pub(super) fn iter(&self) -> impl Iterator<Item = (OneIndexed, &[String])> {
|
||||
self.lines.iter().map(|line_failures| {
|
||||
(
|
||||
line_failures.line_number,
|
||||
&self.failures[line_failures.range.clone()],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn push(&mut self, line_number: OneIndexed, messages: Vec<String>) {
|
||||
let start = self.failures.len();
|
||||
self.failures.extend(messages);
|
||||
self.lines.push(LineFailures {
|
||||
line_number,
|
||||
range: start..self.failures.len(),
|
||||
});
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.lines.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LineFailures {
|
||||
line_number: OneIndexed,
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
pub(super) fn match_file<T>(
|
||||
db: &Db,
|
||||
file: File,
|
||||
diagnostics: impl IntoIterator<Item = T>,
|
||||
) -> Result<(), FailuresByLine>
|
||||
where
|
||||
T: Diagnostic + Clone,
|
||||
{
|
||||
// Parse assertions from comments in the file, and get diagnostics from the file; both
|
||||
// ordered by line number.
|
||||
let assertions = InlineFileAssertions::from_file(db, file);
|
||||
let diagnostics = SortedDiagnostics::new(diagnostics, &line_index(db, file));
|
||||
|
||||
// Get iterators over assertions and diagnostics grouped by line, in ascending line order.
|
||||
let mut line_assertions = assertions.into_iter();
|
||||
let mut line_diagnostics = diagnostics.iter_lines();
|
||||
|
||||
let mut current_assertions = line_assertions.next();
|
||||
let mut current_diagnostics = line_diagnostics.next();
|
||||
|
||||
let matcher = Matcher::from_file(db, file);
|
||||
let mut failures = FailuresByLine::default();
|
||||
|
||||
loop {
|
||||
match (¤t_assertions, ¤t_diagnostics) {
|
||||
(Some(assertions), Some(diagnostics)) => {
|
||||
match assertions.line_number.cmp(&diagnostics.line_number) {
|
||||
Ordering::Equal => {
|
||||
// We have assertions and diagnostics on the same line; check for
|
||||
// matches and error on any that don't match, then advance both
|
||||
// iterators.
|
||||
matcher
|
||||
.match_line(diagnostics, assertions)
|
||||
.unwrap_or_else(|messages| {
|
||||
failures.push(assertions.line_number, messages);
|
||||
});
|
||||
current_assertions = line_assertions.next();
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
Ordering::Less => {
|
||||
// We have assertions on an earlier line than diagnostics; report these
|
||||
// assertions as all unmatched, and advance the assertions iterator.
|
||||
failures.push(assertions.line_number, unmatched(assertions));
|
||||
current_assertions = line_assertions.next();
|
||||
}
|
||||
Ordering::Greater => {
|
||||
// We have diagnostics on an earlier line than assertions; report these
|
||||
// diagnostics as all unmatched, and advance the diagnostics iterator.
|
||||
failures.push(diagnostics.line_number, unmatched(diagnostics));
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
(Some(assertions), None) => {
|
||||
// We've exhausted diagnostics but still have assertions; report these assertions
|
||||
// as unmatched and advance the assertions iterator.
|
||||
failures.push(assertions.line_number, unmatched(assertions));
|
||||
current_assertions = line_assertions.next();
|
||||
}
|
||||
(None, Some(diagnostics)) => {
|
||||
// We've exhausted assertions but still have diagnostics; report these
|
||||
// diagnostics as unmatched and advance the diagnostics iterator.
|
||||
failures.push(diagnostics.line_number, unmatched(diagnostics));
|
||||
current_diagnostics = line_diagnostics.next();
|
||||
}
|
||||
// When we've exhausted both diagnostics and assertions, break.
|
||||
(None, None) => break,
|
||||
}
|
||||
}
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait Diagnostic: Ranged {
|
||||
fn rule(&self) -> &str;
|
||||
|
||||
fn message(&self) -> &str;
|
||||
}
|
||||
|
||||
impl Diagnostic for Arc<TypeCheckDiagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
self.as_ref().rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.as_ref().message()
|
||||
}
|
||||
}
|
||||
|
||||
trait Unmatched {
|
||||
fn unmatched(&self) -> String;
|
||||
}
|
||||
|
||||
fn unmatched<'a, T: Unmatched + 'a>(unmatched: &'a [T]) -> Vec<String> {
|
||||
unmatched.iter().map(Unmatched::unmatched).collect()
|
||||
}
|
||||
|
||||
trait UnmatchedWithColumn {
|
||||
fn unmatched_with_column(&self, column: OneIndexed) -> String;
|
||||
}
|
||||
|
||||
impl Unmatched for Assertion<'_> {
|
||||
fn unmatched(&self) -> String {
|
||||
format!("{} {self}", "unmatched assertion:".red())
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_add_undefined_reveal_clarification<T: Diagnostic>(
|
||||
diagnostic: &T,
|
||||
original: std::fmt::Arguments,
|
||||
) -> String {
|
||||
if diagnostic.rule() == "undefined-reveal" {
|
||||
format!(
|
||||
"{} add a `# revealed` assertion on this line (original diagnostic: {original})",
|
||||
"used built-in `reveal_type`:".yellow()
|
||||
)
|
||||
} else {
|
||||
format!("{} {original}", "unexpected error:".red())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Unmatched for T
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn unmatched(&self) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"[{}] "{}""#, self.rule(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> UnmatchedWithColumn for T
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn unmatched_with_column(&self, column: OneIndexed) -> String {
|
||||
maybe_add_undefined_reveal_clarification(
|
||||
self,
|
||||
format_args!(r#"{column} [{}] "{}""#, self.rule(), self.message()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct Matcher {
|
||||
line_index: LineIndex,
|
||||
source: SourceText,
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
fn from_file(db: &Db, file: File) -> Self {
|
||||
Self {
|
||||
line_index: line_index(db, file),
|
||||
source: source_text(db, file),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check a slice of [`Diagnostic`]s against a slice of [`Assertion`]s.
|
||||
///
|
||||
/// Return vector of [`Unmatched`] for any unmatched diagnostics or assertions.
|
||||
fn match_line<'a, 'b, T: Diagnostic + 'a>(
|
||||
&self,
|
||||
diagnostics: &'a [T],
|
||||
assertions: &'a [Assertion<'b>],
|
||||
) -> Result<(), Vec<String>>
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
let mut failures = vec![];
|
||||
let mut unmatched: Vec<_> = diagnostics.iter().collect();
|
||||
for assertion in assertions {
|
||||
if matches!(
|
||||
assertion,
|
||||
Assertion::Error(ErrorAssertion {
|
||||
rule: None,
|
||||
message_contains: None,
|
||||
..
|
||||
})
|
||||
) {
|
||||
failures.push(format!(
|
||||
"{} no rule or message text",
|
||||
"invalid assertion:".red()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if !self.matches(assertion, &mut unmatched) {
|
||||
failures.push(assertion.unmatched());
|
||||
}
|
||||
}
|
||||
for diagnostic in unmatched {
|
||||
failures.push(diagnostic.unmatched_with_column(self.column(diagnostic)));
|
||||
}
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(failures)
|
||||
}
|
||||
}
|
||||
|
||||
fn column<T: Ranged>(&self, ranged: &T) -> OneIndexed {
|
||||
self.line_index
|
||||
.source_location(ranged.start(), &self.source)
|
||||
.column
|
||||
}
|
||||
|
||||
/// Check if `assertion` matches any [`Diagnostic`]s in `unmatched`.
|
||||
///
|
||||
/// If so, return `true` and remove the matched diagnostics from `unmatched`. Otherwise, return
|
||||
/// `false`.
|
||||
///
|
||||
/// An `Error` assertion can only match one diagnostic; even if it could match more than one,
|
||||
/// we short-circuit after the first match.
|
||||
///
|
||||
/// A `Revealed` assertion must match a revealed-type diagnostic, and may also match an
|
||||
/// undefined-reveal diagnostic, if present.
|
||||
fn matches<T: Diagnostic>(&self, assertion: &Assertion, unmatched: &mut Vec<&T>) -> bool {
|
||||
match assertion {
|
||||
Assertion::Error(error) => {
|
||||
let position = unmatched.iter().position(|diagnostic| {
|
||||
!error.rule.is_some_and(|rule| rule != diagnostic.rule())
|
||||
&& !error
|
||||
.column
|
||||
.is_some_and(|col| col != self.column(*diagnostic))
|
||||
&& !error
|
||||
.message_contains
|
||||
.is_some_and(|needle| !diagnostic.message().contains(needle))
|
||||
});
|
||||
if let Some(position) = position {
|
||||
unmatched.swap_remove(position);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Assertion::Revealed(expected_type) => {
|
||||
let mut matched_revealed_type = None;
|
||||
let mut matched_undefined_reveal = None;
|
||||
let expected_reveal_type_message = format!("Revealed type is `{expected_type}`");
|
||||
for (index, diagnostic) in unmatched.iter().enumerate() {
|
||||
if matched_revealed_type.is_none()
|
||||
&& diagnostic.rule() == "revealed-type"
|
||||
&& diagnostic.message() == expected_reveal_type_message
|
||||
{
|
||||
matched_revealed_type = Some(index);
|
||||
} else if matched_undefined_reveal.is_none()
|
||||
&& diagnostic.rule() == "undefined-reveal"
|
||||
{
|
||||
matched_undefined_reveal = Some(index);
|
||||
}
|
||||
if matched_revealed_type.is_some() && matched_undefined_reveal.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let mut idx = 0;
|
||||
unmatched.retain(|_| {
|
||||
let retain =
|
||||
Some(idx) != matched_revealed_type && Some(idx) != matched_undefined_reveal;
|
||||
idx += 1;
|
||||
retain
|
||||
});
|
||||
matched_revealed_type.is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TestDiagnostic {
|
||||
rule: &'static str,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl TestDiagnostic {
|
||||
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self {
|
||||
let offset: u32 = offset.try_into().unwrap();
|
||||
Self {
|
||||
rule,
|
||||
message,
|
||||
range: TextRange::new(offset.into(), (offset + 1).into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Diagnostic for TestDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
self.rule
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TestDiagnostic {
|
||||
fn range(&self) -> ruff_text_size::TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
fn get_result(source: &str, diagnostics: Vec<TestDiagnostic>) -> Result<(), FailuresByLine> {
|
||||
colored::control::set_override(false);
|
||||
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
|
||||
super::match_file(&db, file, diagnostics)
|
||||
}
|
||||
|
||||
fn assert_fail(result: Result<(), FailuresByLine>, messages: &[(usize, &[&str])]) {
|
||||
let Err(failures) = result else {
|
||||
panic!("expected a failure");
|
||||
};
|
||||
|
||||
let expected: Vec<(OneIndexed, Vec<String>)> = messages
|
||||
.iter()
|
||||
.map(|(idx, msgs)| {
|
||||
(
|
||||
OneIndexed::from_zero_indexed(*idx),
|
||||
msgs.iter().map(ToString::to_string).collect(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let failures: Vec<(OneIndexed, Vec<String>)> = failures
|
||||
.iter()
|
||||
.map(|(idx, msgs)| (idx, msgs.to_vec()))
|
||||
.collect();
|
||||
|
||||
assert_eq!(failures, expected);
|
||||
}
|
||||
|
||||
fn assert_ok(result: &Result<(), FailuresByLine>) {
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
"revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
"not-revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [not-revealed-type] "Revealed type is `Foo`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_wrong_message() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("revealed-type", "Something else", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [revealed-type] "Something else""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_unmatched() {
|
||||
let result = get_result("x # revealed: Foo", vec![]);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match_with_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_match_with_only_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0)],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revealed_mismatch_with_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: revealed: Foo",
|
||||
r#"unexpected error: 1 [revealed-type] "Revealed type is `Bar`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_reveal_type_unmatched() {
|
||||
let result = get_result(
|
||||
"reveal_type(1)",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: [undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undefined_reveal_type_mismatched() {
|
||||
let result = get_result(
|
||||
"reveal_type(1) # error: [something-else]",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: [something-else]",
|
||||
"used built-in `reveal_type`: add a `# revealed` assertion on this line (\
|
||||
original diagnostic: 1 [undefined-reveal] \"undefined reveal message\")",
|
||||
r#"unexpected error: 13 [revealed-type] "Revealed type is `Literal[1]`""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_unmatched() {
|
||||
let result = get_result("x # error: [rule]", vec![]);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: error: [rule]"])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: [some-rule]",
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: "contains this""#,
|
||||
r#"unexpected error: 1 [anything] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_column_and_rule() {
|
||||
let result = get_result(
|
||||
"x # error: 1 [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_wrong_column() {
|
||||
let result = get_result(
|
||||
"x # error: 2 [rule]",
|
||||
vec![TestDiagnostic::new("rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"unmatched assertion: error: 2 [rule]",
|
||||
r#"unexpected error: 1 [rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_column_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_rule_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_column() {
|
||||
let result = get_result(
|
||||
r#"x # error: 2 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "message contains this", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 2 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_rule() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new(
|
||||
"other-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [other-rule] "message contains this""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn error_match_all_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
r#"unmatched assertion: error: 1 [some-rule] "contains this""#,
|
||||
r#"unexpected error: 1 [some-rule] "Any message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interspersed_matches_and_mismatches() {
|
||||
let source = dedent(
|
||||
r#"
|
||||
1 # error: [line-one]
|
||||
2
|
||||
3 # error: [line-three]
|
||||
4 # error: [line-four]
|
||||
5
|
||||
6: # error: [line-six]
|
||||
"#,
|
||||
);
|
||||
let two = source.find('2').unwrap();
|
||||
let three = source.find('3').unwrap();
|
||||
let five = source.find('5').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
TestDiagnostic::new("line-three", "msg", three),
|
||||
TestDiagnostic::new("line-five", "msg", five),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[
|
||||
(1, &["unmatched assertion: error: [line-one]"]),
|
||||
(2, &[r#"unexpected error: [line-two] "msg""#]),
|
||||
(4, &["unmatched assertion: error: [line-four]"]),
|
||||
(5, &[r#"unexpected error: [line-five] "msg""#]),
|
||||
(6, &["unmatched assertion: error: [line-six]"]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_diagnostics_than_assertions() {
|
||||
let source = dedent(
|
||||
r#"
|
||||
1 # error: [line-one]
|
||||
2
|
||||
"#,
|
||||
);
|
||||
let one = source.find('1').unwrap();
|
||||
let two = source.find('2').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-one", "msg", one),
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(2, &[r#"unexpected error: [line-two] "msg""#])]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [other-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line_all_same() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [one-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_assertions_and_diagnostics_same_line_mismatch() {
|
||||
let source = dedent(
|
||||
"
|
||||
# error: [one-rule]
|
||||
# error: [other-rule]
|
||||
x
|
||||
",
|
||||
);
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
TestDiagnostic::new("third-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(3, &[r#"unexpected error: 1 [third-rule] "msg""#])],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parenthesized_expression() {
|
||||
let source = dedent(
|
||||
"
|
||||
a = b + (
|
||||
error: [undefined-reveal]
|
||||
reveal_type(5) # revealed: Literal[5]
|
||||
)
|
||||
",
|
||||
);
|
||||
let reveal = source.find("reveal_type").unwrap();
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bare_error_assertion_not_allowed() {
|
||||
let source = "x # error:";
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn column_only_error_assertion_not_allowed() {
|
||||
let source = "x # error: 1";
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
result,
|
||||
&[(
|
||||
0,
|
||||
&[
|
||||
"invalid assertion: no rule or message text",
|
||||
r#"unexpected error: 1 [some-rule] "some message""#,
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
}
|
||||
576
crates/red_knot_test/src/parser.rs
Normal file
576
crates/red_knot_test/src/parser.rs
Normal file
@@ -0,0 +1,576 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::{Captures, Regex};
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
/// Parse the Markdown `source` as a test suite with given `title`.
|
||||
pub(crate) fn parse<'s>(title: &'s str, source: &'s str) -> anyhow::Result<MarkdownTestSuite<'s>> {
|
||||
let parser = Parser::new(title, source);
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
/// A parsed markdown file containing tests.
|
||||
///
|
||||
/// Borrows from the source string and filepath it was created from.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTestSuite<'s> {
|
||||
/// Header sections.
|
||||
sections: IndexVec<SectionId, Section<'s>>,
|
||||
|
||||
/// Test files embedded within the Markdown file.
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
}
|
||||
|
||||
impl<'s> MarkdownTestSuite<'s> {
|
||||
pub(crate) fn tests(&self) -> MarkdownTestIterator<'_, 's> {
|
||||
MarkdownTestIterator {
|
||||
suite: self,
|
||||
current_file_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single test inside a [`MarkdownTestSuite`].
|
||||
///
|
||||
/// A test is a single header section (or the implicit root section, if there are no Markdown
|
||||
/// headers in the file), containing one or more embedded Python files as fenced code blocks, and
|
||||
/// containing no nested header subsections.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTest<'m, 's> {
|
||||
suite: &'m MarkdownTestSuite<'s>,
|
||||
section: &'m Section<'s>,
|
||||
files: &'m [EmbeddedFile<'s>],
|
||||
}
|
||||
|
||||
impl<'m, 's> MarkdownTest<'m, 's> {
|
||||
pub(crate) fn name(&self) -> String {
|
||||
let mut name = String::new();
|
||||
let mut parent_id = self.section.parent_id;
|
||||
while let Some(next_id) = parent_id {
|
||||
let parent = &self.suite.sections[next_id];
|
||||
parent_id = parent.parent_id;
|
||||
if !name.is_empty() {
|
||||
name.insert_str(0, " - ");
|
||||
}
|
||||
name.insert_str(0, parent.title);
|
||||
}
|
||||
if !name.is_empty() {
|
||||
name.push_str(" - ");
|
||||
}
|
||||
name.push_str(self.section.title);
|
||||
name
|
||||
}
|
||||
|
||||
pub(crate) fn files(&self) -> impl Iterator<Item = &'m EmbeddedFile<'s>> {
|
||||
self.files.iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator yielding all [`MarkdownTest`]s in a [`MarkdownTestSuite`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct MarkdownTestIterator<'m, 's> {
|
||||
suite: &'m MarkdownTestSuite<'s>,
|
||||
current_file_index: usize,
|
||||
}
|
||||
|
||||
impl<'m, 's> Iterator for MarkdownTestIterator<'m, 's> {
|
||||
type Item = MarkdownTest<'m, 's>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut current_file_index = self.current_file_index;
|
||||
let mut file = self.suite.files.get(current_file_index.into());
|
||||
let section_id = file?.section;
|
||||
while file.is_some_and(|file| file.section == section_id) {
|
||||
current_file_index += 1;
|
||||
file = self.suite.files.get(current_file_index.into());
|
||||
}
|
||||
let files = &self.suite.files[EmbeddedFileId::from_usize(self.current_file_index)
|
||||
..EmbeddedFileId::from_usize(current_file_index)];
|
||||
self.current_file_index = current_file_index;
|
||||
Some(MarkdownTest {
|
||||
suite: self.suite,
|
||||
section: &self.suite.sections[section_id],
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
struct SectionId;
|
||||
|
||||
/// A single header section of a [`MarkdownTestSuite`], or the implicit root "section".
|
||||
///
|
||||
/// A header section is the part of a Markdown file beginning with a `#`-prefixed header line, and
|
||||
/// extending until the next header line at the same or higher outline level (that is, with the
|
||||
/// same number or fewer `#` characters).
|
||||
///
|
||||
/// A header section may either contain one or more embedded Python files (making it a
|
||||
/// [`MarkdownTest`]), or it may contain nested sections (headers with more `#` characters), but
|
||||
/// not both.
|
||||
#[derive(Debug)]
|
||||
struct Section<'s> {
|
||||
title: &'s str,
|
||||
level: u8,
|
||||
parent_id: Option<SectionId>,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
struct EmbeddedFileId;
|
||||
|
||||
/// A single file embedded in a [`Section`] as a fenced code block.
|
||||
///
|
||||
/// Currently must be a Python file (`py` language) or type stub (`pyi`). In the future we plan
|
||||
/// support other kinds of files as well (TOML configuration, typeshed VERSIONS, `pth` files...).
|
||||
///
|
||||
/// A Python embedded file makes its containing [`Section`] into a [`MarkdownTest`], and will be
|
||||
/// type-checked and searched for inline-comment assertions to match against the diagnostics from
|
||||
/// type checking.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct EmbeddedFile<'s> {
|
||||
section: SectionId,
|
||||
pub(crate) path: &'s str,
|
||||
pub(crate) lang: &'s str,
|
||||
pub(crate) code: &'s str,
|
||||
}
|
||||
|
||||
/// Matches an arbitrary amount of whitespace (including newlines), followed by a sequence of `#`
|
||||
/// characters, followed by a title heading, followed by a newline.
|
||||
static HEADER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(\s*\n)*(?<level>#+)\s+(?<title>.+)\s*\n").unwrap());
|
||||
|
||||
/// Matches a code block fenced by triple backticks, possibly with language and `key=val`
|
||||
/// configuration items following the opening backticks (in the "tag string" of the code block).
|
||||
static CODE_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"^```(?<lang>\w+)(?<config>( +\S+)*)\s*\n(?<code>(.|\n)*?)\n```\s*\n").unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SectionStack(Vec<SectionId>);
|
||||
|
||||
impl SectionStack {
|
||||
fn new(root_section_id: SectionId) -> Self {
|
||||
Self(vec![root_section_id])
|
||||
}
|
||||
|
||||
fn push(&mut self, section_id: SectionId) {
|
||||
self.0.push(section_id);
|
||||
}
|
||||
|
||||
fn pop(&mut self) -> Option<SectionId> {
|
||||
let popped = self.0.pop();
|
||||
debug_assert_ne!(popped, None, "Should never pop the implicit root section");
|
||||
debug_assert!(
|
||||
!self.0.is_empty(),
|
||||
"Should never pop the implicit root section"
|
||||
);
|
||||
popped
|
||||
}
|
||||
|
||||
fn parent(&mut self) -> SectionId {
|
||||
*self
|
||||
.0
|
||||
.last()
|
||||
.expect("Should never pop the implicit root section")
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the source of a Markdown file into a [`MarkdownTestSuite`].
|
||||
#[derive(Debug)]
|
||||
struct Parser<'s> {
|
||||
/// [`Section`]s of the final [`MarkdownTestSuite`].
|
||||
sections: IndexVec<SectionId, Section<'s>>,
|
||||
|
||||
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
|
||||
/// The unparsed remainder of the Markdown source.
|
||||
unparsed: &'s str,
|
||||
|
||||
/// Stack of ancestor sections.
|
||||
stack: SectionStack,
|
||||
|
||||
/// Names of embedded files in current active section.
|
||||
current_section_files: Option<FxHashSet<&'s str>>,
|
||||
}
|
||||
|
||||
impl<'s> Parser<'s> {
|
||||
fn new(title: &'s str, source: &'s str) -> Self {
|
||||
let mut sections = IndexVec::default();
|
||||
let root_section_id = sections.push(Section {
|
||||
title,
|
||||
level: 0,
|
||||
parent_id: None,
|
||||
});
|
||||
Self {
|
||||
sections,
|
||||
files: IndexVec::default(),
|
||||
unparsed: source,
|
||||
stack: SectionStack::new(root_section_id),
|
||||
current_section_files: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(mut self) -> anyhow::Result<MarkdownTestSuite<'s>> {
|
||||
self.parse_impl()?;
|
||||
Ok(self.finish())
|
||||
}
|
||||
|
||||
fn finish(mut self) -> MarkdownTestSuite<'s> {
|
||||
self.sections.shrink_to_fit();
|
||||
self.files.shrink_to_fit();
|
||||
|
||||
MarkdownTestSuite {
|
||||
sections: self.sections,
|
||||
files: self.files,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_impl(&mut self) -> anyhow::Result<()> {
|
||||
while !self.unparsed.is_empty() {
|
||||
if let Some(captures) = self.scan(&HEADER_RE) {
|
||||
self.parse_header(&captures)?;
|
||||
} else if let Some(captures) = self.scan(&CODE_RE) {
|
||||
self.parse_code_block(&captures)?;
|
||||
} else {
|
||||
// ignore other Markdown syntax (paragraphs, etc) used as comments in the test
|
||||
if let Some(next_newline) = self.unparsed.find('\n') {
|
||||
(_, self.unparsed) = self.unparsed.split_at(next_newline + 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_header(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
|
||||
let header_level = captures["level"].len();
|
||||
self.pop_sections_to_level(header_level);
|
||||
|
||||
let parent = self.stack.parent();
|
||||
|
||||
let section = Section {
|
||||
// HEADER_RE can't match without a match for group 'title'.
|
||||
title: captures.name("title").unwrap().into(),
|
||||
level: header_level.try_into()?,
|
||||
parent_id: Some(parent),
|
||||
};
|
||||
|
||||
if self.current_section_files.is_some() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Header '{}' not valid inside a test case; parent '{}' has code files.",
|
||||
section.title,
|
||||
self.sections[parent].title,
|
||||
));
|
||||
}
|
||||
|
||||
let section_id = self.sections.push(section);
|
||||
self.stack.push(section_id);
|
||||
|
||||
self.current_section_files = None;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_code_block(&mut self, captures: &Captures<'s>) -> anyhow::Result<()> {
|
||||
// We never pop the implicit root section.
|
||||
let parent = self.stack.parent();
|
||||
|
||||
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
|
||||
|
||||
if let Some(config_match) = captures.name("config") {
|
||||
for item in config_match.as_str().split_whitespace() {
|
||||
let mut parts = item.split('=');
|
||||
let key = parts.next().unwrap();
|
||||
let Some(val) = parts.next() else {
|
||||
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
|
||||
};
|
||||
if parts.next().is_some() {
|
||||
return Err(anyhow::anyhow!("Invalid config item `{}`.", item));
|
||||
}
|
||||
if config.insert(key, val).is_some() {
|
||||
return Err(anyhow::anyhow!("Duplicate config item `{}`.", item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let path = config.get("path").copied().unwrap_or("test.py");
|
||||
|
||||
self.files.push(EmbeddedFile {
|
||||
path,
|
||||
section: parent,
|
||||
// CODE_RE can't match without matches for 'lang' and 'code'.
|
||||
lang: captures.name("lang").unwrap().into(),
|
||||
code: captures.name("code").unwrap().into(),
|
||||
});
|
||||
|
||||
if let Some(current_files) = &mut self.current_section_files {
|
||||
if !current_files.insert(path) {
|
||||
if path == "test.py" {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`. \
|
||||
(This is the default filename; \
|
||||
consider giving some files an explicit name with `path=...`.)",
|
||||
self.sections[parent].title
|
||||
));
|
||||
}
|
||||
return Err(anyhow::anyhow!(
|
||||
"Test `{}` has duplicate files named `{path}`.",
|
||||
self.sections[parent].title
|
||||
));
|
||||
};
|
||||
} else {
|
||||
self.current_section_files = Some(FxHashSet::from_iter([path]));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pop_sections_to_level(&mut self, level: usize) {
|
||||
while level <= self.sections[self.stack.parent()].level.into() {
|
||||
self.stack.pop();
|
||||
// We would have errored before pushing a child section if there were files, so we know
|
||||
// no parent section can have files.
|
||||
self.current_section_files = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get capture groups and advance cursor past match if unparsed text matches `pattern`.
|
||||
fn scan(&mut self, pattern: &Regex) -> Option<Captures<'s>> {
|
||||
if let Some(captures) = pattern.captures(self.unparsed) {
|
||||
let (_, unparsed) = self.unparsed.split_at(captures.get(0).unwrap().end());
|
||||
self.unparsed = unparsed;
|
||||
Some(captures)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let mf = super::parse("file.md", "").unwrap();
|
||||
|
||||
assert!(mf.tests().next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_file_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
|
||||
assert_eq!(test.name(), "file.md");
|
||||
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_tests() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
# Two
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test1, test2] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected two tests");
|
||||
};
|
||||
|
||||
assert_eq!(test1.name(), "file.md - One");
|
||||
assert_eq!(test2.name(), "file.md - Two");
|
||||
|
||||
let [file] = test1.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
|
||||
let [file] = test2.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "test.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "y = 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_file_path() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py path=foo.py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "foo.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "x = 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_line_file() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let mf = super::parse("file.md", &source).unwrap();
|
||||
|
||||
let [test] = &mf.tests().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one test");
|
||||
};
|
||||
let [file] = test.files().collect::<Vec<_>>()[..] else {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.code, "x = 1\ny = 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_header_inside_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
# One
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
## Two
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Header 'Two' not valid inside a test case; parent 'One' has code files."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_no_equals() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Invalid config item `foo`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_too_many_equals() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar=baz
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Invalid config item `foo=bar=baz`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_config_item_duplicate() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py foo=bar foo=baz
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Duplicate config item `foo=baz`.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_name_files_in_test() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Test `file.md` has duplicate files named `test.py`. \
|
||||
(This is the default filename; consider giving some files an explicit name \
|
||||
with `path=...`.)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_duplicate_name_files_in_test_non_default() {
|
||||
let source = dedent(
|
||||
"
|
||||
```py path=foo.py
|
||||
x = 1
|
||||
```
|
||||
|
||||
```py path=foo.py
|
||||
y = 2
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Test `file.md` has duplicate files named `foo.py`."
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,6 @@ ruff_python_ast = { workspace = true }
|
||||
ruff_python_codegen = { workspace = true }
|
||||
ruff_python_formatter = { workspace = true }
|
||||
ruff_python_parser = { workspace = true }
|
||||
ruff_python_stdlib = { workspace = true }
|
||||
ruff_python_trivia = { workspace = true }
|
||||
ruff_workspace = { workspace = true, features = ["schemars"] }
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_codegen::round_trip;
|
||||
use ruff_python_stdlib::path::is_jupyter_notebook;
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
@@ -18,7 +18,7 @@ pub(crate) struct Args {
|
||||
|
||||
pub(crate) fn main(args: &Args) -> Result<()> {
|
||||
let path = args.file.as_path();
|
||||
if is_jupyter_notebook(path) {
|
||||
if PySourceType::from(path).is_ipynb() {
|
||||
println!("{}", ruff_notebook::round_trip(path)?);
|
||||
} else {
|
||||
let contents = fs::read_to_string(&args.file)?;
|
||||
|
||||
@@ -36,6 +36,11 @@ async def func():
|
||||
...
|
||||
|
||||
|
||||
async def main():
|
||||
async with asyncio.timeout(7):
|
||||
print({i async for i in long_running_range()})
|
||||
|
||||
|
||||
async def func():
|
||||
with anyio.move_on_after(delay=0.2):
|
||||
...
|
||||
|
||||
@@ -87,3 +87,7 @@ field24: typing.Union[Literal[1], typing.Union[Literal[2], typing.Union[Literal[
|
||||
|
||||
# Should use the first literal subscript attribute when fixing
|
||||
field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
|
||||
|
||||
from typing import IO, Literal
|
||||
|
||||
InlineOption = Literal["a"] | Literal["b"] | IO[str]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# TDO006 - accepted
|
||||
# TODO (evanrittenhouse): this is a valid TODO
|
||||
# Todoism is a word which starts with todo, but is not a todo
|
||||
# TDO006 - error
|
||||
# ToDo (evanrittenhouse): invalid capitalization
|
||||
# todo (evanrittenhouse): another invalid capitalization
|
||||
|
||||
@@ -46,3 +46,12 @@ open(p, opener=opener)
|
||||
open(p, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
open(p, 'r', - 1, None, None, None, True, None)
|
||||
open(p, 'r', - 1, None, None, None, False, opener)
|
||||
|
||||
# Cannot be upgraded `pathlib.Open` does not support fds
|
||||
# See https://github.com/astral-sh/ruff/issues/12871
|
||||
open(1)
|
||||
open(1, "w")
|
||||
x = 2
|
||||
open(x)
|
||||
def foo(y: int):
|
||||
open(y)
|
||||
|
||||
@@ -104,3 +104,41 @@ x = [
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
# Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
x:A = "foo"[::-1],
|
||||
y:B = [[["foo", "bar"]]],
|
||||
z:object = "fooo",
|
||||
):
|
||||
pass
|
||||
|
||||
class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
self,
|
||||
x:A = "foo"[::-1],
|
||||
y:B = [[["foo", "bar"]]],
|
||||
z:object = "fooo",
|
||||
):
|
||||
pass
|
||||
|
||||
class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
pass
|
||||
|
||||
# Should be no E231 errors on any of these:
|
||||
def pep_696_good[A: object="foo"[::-1], B: object =[[["foo", "bar"]]], C: object= bytes](
|
||||
x: A = "foo"[::-1],
|
||||
y: B = [[["foo", "bar"]]],
|
||||
z: object = "fooo",
|
||||
):
|
||||
pass
|
||||
|
||||
class PEP696Good[A: object="foo"[::-1], B: object =[[["foo", "bar"]]], C: object= bytes]:
|
||||
pass
|
||||
|
||||
class PEP696GoodWithEmptyBases[A: object="foo"[::-1], B: object =[[["foo", "bar"]]], C: object= bytes]():
|
||||
pass
|
||||
|
||||
class PEP696GoodWithNonEmptyBases[A: object="foo"[::-1], B: object =[[["foo", "bar"]]], C: object= bytes](object, something_dynamic[x::-1]):
|
||||
pass
|
||||
|
||||
@@ -59,3 +59,19 @@ f"normal {f"{a=}"} normal"
|
||||
print(f"{foo = }")
|
||||
# ...but then it creates false negatives for now
|
||||
print(f"{foo(a = 1)}")
|
||||
|
||||
# There should be at least one E251 diagnostic for each type parameter here:
|
||||
def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
pass
|
||||
|
||||
class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
pass
|
||||
|
||||
# The last of these should cause us to emit E231,
|
||||
# but E231 isn't tested by this fixture:
|
||||
def pep_696_good[A = int, B: object = str, C:object = memoryview]():
|
||||
pass
|
||||
|
||||
class PEP696Good[A = int, B: object = str, C:object = memoryview]:
|
||||
def pep_696_good_method[A = int, B: object = str, C:object = memoryview](self):
|
||||
pass
|
||||
|
||||
53
crates/ruff_linter/resources/test/fixtures/refurb/FURB156.py
vendored
Normal file
53
crates/ruff_linter/resources/test/fixtures/refurb/FURB156.py
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
# Errors
|
||||
|
||||
_ = "0123456789"
|
||||
_ = "01234567"
|
||||
_ = "0123456789abcdefABCDEF"
|
||||
_ = "abcdefghijklmnopqrstuvwxyz"
|
||||
_ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
_ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
_ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
_ = " \t\n\r\v\f"
|
||||
|
||||
_ = "" in "1234567890"
|
||||
_ = "" in "12345670"
|
||||
_ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
_ = (
|
||||
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'
|
||||
"'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
|
||||
)
|
||||
_ = id("0123"
|
||||
"4567"
|
||||
"89")
|
||||
_ = "" in ("123"
|
||||
"456"
|
||||
"789"
|
||||
"0")
|
||||
|
||||
_ = "" in ( # comment
|
||||
"123"
|
||||
"456"
|
||||
"789"
|
||||
"0")
|
||||
|
||||
|
||||
_ = "" in (
|
||||
"123"
|
||||
"456" # inline comment
|
||||
"789"
|
||||
"0")
|
||||
|
||||
_ = (
|
||||
"0123456789"
|
||||
).capitalize()
|
||||
|
||||
_ = (
|
||||
"0123456789"
|
||||
# with comment
|
||||
).capitalize()
|
||||
|
||||
# Ok
|
||||
|
||||
_ = "1234567890"
|
||||
_ = "1234"
|
||||
_ = "" in "1234"
|
||||
@@ -169,4 +169,32 @@ def ignore_step():
|
||||
text = "!x!y!z"
|
||||
if text.startswith("!"):
|
||||
text = text[1::2]
|
||||
print(text)
|
||||
print(text)
|
||||
|
||||
def handle_unicode():
|
||||
# should be skipped!
|
||||
text = "řetězec"
|
||||
if text.startswith("ř"):
|
||||
text = text[2:]
|
||||
|
||||
# should be linted
|
||||
# with fix `text = text.removeprefix("ř")`
|
||||
text = "řetězec"
|
||||
if text.startswith("ř"):
|
||||
text = text[1:]
|
||||
|
||||
|
||||
def handle_surrogates():
|
||||
# should be linted
|
||||
text = "\ud800\udc00heythere"
|
||||
if text.startswith("\ud800\udc00"):
|
||||
text = text[2:]
|
||||
text = "\U00010000heythere"
|
||||
if text.startswith("\U00010000"):
|
||||
text = text[1:]
|
||||
|
||||
# should not be linted
|
||||
text = "\ud800\udc00heythere"
|
||||
if text.startswith("\ud800\udc00"):
|
||||
text = text[1:]
|
||||
|
||||
@@ -185,3 +185,9 @@ def f():
|
||||
global task
|
||||
loop = asyncio.get_event_loop()
|
||||
task = loop.create_task(main()) # Error
|
||||
|
||||
# OK
|
||||
# The task is returned by the lambda
|
||||
f = lambda *args: asyncio.create_task(foo())
|
||||
f = lambda *args: lambda *args: asyncio.create_task(foo())
|
||||
f = lambda *args: [asyncio.create_task(foo()) for x in args]
|
||||
|
||||
@@ -1355,6 +1355,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::SingleItemMembershipTest) {
|
||||
refurb::rules::single_item_membership_test(checker, expr, left, ops, comparators);
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedStringCharset) {
|
||||
refurb::rules::hardcoded_string_charset_comparison(checker, compare);
|
||||
}
|
||||
}
|
||||
Expr::NumberLiteral(number_literal @ ast::ExprNumberLiteral { .. }) => {
|
||||
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
|
||||
@@ -1364,7 +1367,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
refurb::rules::math_constant(checker, number_literal);
|
||||
}
|
||||
}
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, range: _ }) => {
|
||||
Expr::StringLiteral(string_like @ ast::ExprStringLiteral { value, range: _ }) => {
|
||||
if checker.enabled(Rule::UnicodeKindPrefix) {
|
||||
for string_part in value {
|
||||
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
|
||||
@@ -1375,6 +1378,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
ruff::rules::missing_fstring_syntax(checker, string_literal);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::HardcodedStringCharset) {
|
||||
refurb::rules::hardcoded_string_charset_literal(checker, string_like);
|
||||
}
|
||||
}
|
||||
Expr::If(
|
||||
if_exp @ ast::ExprIf {
|
||||
|
||||
@@ -1055,6 +1055,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "148") => (RuleGroup::Preview, rules::refurb::rules::UnnecessaryEnumerate),
|
||||
(Refurb, "152") => (RuleGroup::Preview, rules::refurb::rules::MathConstant),
|
||||
(Refurb, "154") => (RuleGroup::Preview, rules::refurb::rules::RepeatedGlobal),
|
||||
(Refurb, "156") => (RuleGroup::Preview, rules::refurb::rules::HardcodedStringCharset),
|
||||
(Refurb, "157") => (RuleGroup::Preview, rules::refurb::rules::VerboseDecimalConstructor),
|
||||
(Refurb, "161") => (RuleGroup::Stable, rules::refurb::rules::BitCount),
|
||||
(Refurb, "163") => (RuleGroup::Stable, rules::refurb::rules::RedundantLogBase),
|
||||
|
||||
@@ -287,19 +287,23 @@ impl<'a> TodoDirective<'a> {
|
||||
pub(crate) fn from_comment(comment: &'a str, comment_range: TextRange) -> Option<Self> {
|
||||
// The directive's offset from the start of the comment.
|
||||
let mut relative_offset = TextSize::new(0);
|
||||
let mut subset_opt = Some(comment);
|
||||
let mut subset = comment;
|
||||
|
||||
// Loop over `#`-delimited sections of the comment to check for directives. This will
|
||||
// correctly handle cases like `# foo # TODO`.
|
||||
while let Some(subset) = subset_opt {
|
||||
loop {
|
||||
let trimmed = subset.trim_start_matches('#').trim_start();
|
||||
|
||||
let offset = subset.text_len() - trimmed.text_len();
|
||||
relative_offset += offset;
|
||||
|
||||
// Find the first word. Don't use split by whitespace because that would include the `:` character
|
||||
// in `TODO:`
|
||||
let first_word = trimmed.split(|c: char| !c.is_alphanumeric()).next()?;
|
||||
|
||||
// If we detect a TodoDirectiveKind variant substring in the comment, construct and
|
||||
// return the appropriate TodoDirective
|
||||
if let Ok(directive_kind) = trimmed.parse::<TodoDirectiveKind>() {
|
||||
if let Ok(directive_kind) = first_word.parse::<TodoDirectiveKind>() {
|
||||
let len = directive_kind.len();
|
||||
|
||||
return Some(Self {
|
||||
@@ -310,11 +314,11 @@ impl<'a> TodoDirective<'a> {
|
||||
}
|
||||
|
||||
// Shrink the subset to check for the next phrase starting with "#".
|
||||
subset_opt = if let Some(new_offset) = trimmed.find('#') {
|
||||
if let Some(new_offset) = trimmed.find('#') {
|
||||
relative_offset += TextSize::try_from(new_offset).unwrap();
|
||||
subset.get(relative_offset.to_usize()..)
|
||||
subset = &subset[relative_offset.to_usize()..];
|
||||
} else {
|
||||
None
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -334,30 +338,13 @@ impl FromStr for TodoDirectiveKind {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// The lengths of the respective variant strings: TODO, FIXME, HACK, XXX
|
||||
for length in [3, 4, 5] {
|
||||
let Some(substr) = s.get(..length) else {
|
||||
break;
|
||||
};
|
||||
|
||||
match substr.to_lowercase().as_str() {
|
||||
"fixme" => {
|
||||
return Ok(TodoDirectiveKind::Fixme);
|
||||
}
|
||||
"hack" => {
|
||||
return Ok(TodoDirectiveKind::Hack);
|
||||
}
|
||||
"todo" => {
|
||||
return Ok(TodoDirectiveKind::Todo);
|
||||
}
|
||||
"xxx" => {
|
||||
return Ok(TodoDirectiveKind::Xxx);
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
match s.to_lowercase().as_str() {
|
||||
"fixme" => Ok(TodoDirectiveKind::Fixme),
|
||||
"hack" => Ok(TodoDirectiveKind::Hack),
|
||||
"todo" => Ok(TodoDirectiveKind::Todo),
|
||||
"xxx" => Ok(TodoDirectiveKind::Xxx),
|
||||
_ => Err(()),
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,28 +19,19 @@ ASYNC100.py:18:5: ASYNC100 A `with trio.move_on_after(...):` context does not co
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:40:5: ASYNC100 A `with anyio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
39 | async def func():
|
||||
40 | with anyio.move_on_after(delay=0.2):
|
||||
| _____^
|
||||
41 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:45:5: ASYNC100 A `with anyio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:45:5: ASYNC100 A `with anyio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
44 | async def func():
|
||||
45 | with anyio.fail_after():
|
||||
45 | with anyio.move_on_after(delay=0.2):
|
||||
| _____^
|
||||
46 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:50:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:50:5: ASYNC100 A `with anyio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
49 | async def func():
|
||||
50 | with anyio.CancelScope():
|
||||
50 | with anyio.fail_after():
|
||||
| _____^
|
||||
51 | | ...
|
||||
| |___________^ ASYNC100
|
||||
@@ -49,7 +40,7 @@ ASYNC100.py:50:5: ASYNC100 A `with anyio.CancelScope(...):` context does not con
|
||||
ASYNC100.py:55:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
54 | async def func():
|
||||
55 | with anyio.CancelScope(), nullcontext():
|
||||
55 | with anyio.CancelScope():
|
||||
| _____^
|
||||
56 | | ...
|
||||
| |___________^ ASYNC100
|
||||
@@ -58,44 +49,53 @@ ASYNC100.py:55:5: ASYNC100 A `with anyio.CancelScope(...):` context does not con
|
||||
ASYNC100.py:60:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
59 | async def func():
|
||||
60 | with nullcontext(), anyio.CancelScope():
|
||||
60 | with anyio.CancelScope(), nullcontext():
|
||||
| _____^
|
||||
61 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:65:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:65:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
64 | async def func():
|
||||
65 | async with asyncio.timeout(delay=0.2):
|
||||
65 | with nullcontext(), anyio.CancelScope():
|
||||
| _____^
|
||||
66 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:70:5: ASYNC100 A `with asyncio.timeout_at(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:70:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
69 | async def func():
|
||||
70 | async with asyncio.timeout_at(when=0.2):
|
||||
70 | async with asyncio.timeout(delay=0.2):
|
||||
| _____^
|
||||
71 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:80:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:75:5: ASYNC100 A `with asyncio.timeout_at(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
79 | async def func():
|
||||
80 | async with asyncio.timeout(delay=0.2), asyncio.TaskGroup(), asyncio.timeout(delay=0.2):
|
||||
74 | async def func():
|
||||
75 | async with asyncio.timeout_at(when=0.2):
|
||||
| _____^
|
||||
81 | | ...
|
||||
76 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:90:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:85:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
89 | async def func():
|
||||
90 | async with asyncio.timeout(delay=0.2), asyncio.timeout(delay=0.2):
|
||||
84 | async def func():
|
||||
85 | async with asyncio.timeout(delay=0.2), asyncio.TaskGroup(), asyncio.timeout(delay=0.2):
|
||||
| _____^
|
||||
91 | | ...
|
||||
86 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:95:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
94 | async def func():
|
||||
95 | async with asyncio.timeout(delay=0.2), asyncio.timeout(delay=0.2):
|
||||
| _____^
|
||||
96 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
@@ -191,22 +191,28 @@ impl Violation for StartProcessWithAShell {
|
||||
/// Checks for functions that start a process without a shell.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `subprocess` module provides more powerful facilities for spawning new
|
||||
/// processes and retrieving their results; using that module is preferable to
|
||||
/// using these functions.
|
||||
/// Invoking any kind of external executable via a function call can pose
|
||||
/// security risks if arbitrary variables are passed to the executable, or if
|
||||
/// the input is otherwise unsanitised or unvalidated.
|
||||
///
|
||||
/// This rule specifically flags functions in the `os` module that spawn
|
||||
/// subprocesses *without* the use of a shell. Note that these typically pose a
|
||||
/// much smaller security risk than subprocesses that are started *with* a
|
||||
/// shell, which are flagged by [`start-process-with-a-shell`] (`S605`). This
|
||||
/// gives you the option of enabling one rule while disabling the other if you
|
||||
/// decide that the security risk from these functions is acceptable for your
|
||||
/// use case.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
|
||||
/// import os
|
||||
///
|
||||
///
|
||||
/// def insecure_function(arbitrary_user_input: str):
|
||||
/// os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", arbitrary_user_input)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// subprocess.Popen(["/bin/mycmd", "myarg"])
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Replacing the `os.spawn` family](https://docs.python.org/3/library/subprocess.html#replacing-the-os-spawn-family)
|
||||
/// [start-process-with-a-shell]: https://docs.astral.sh/ruff/rules/start-process-with-a-shell/#start-process-with-a-shell-s605
|
||||
#[violation]
|
||||
pub struct StartProcessWithNoShell;
|
||||
|
||||
@@ -480,7 +486,7 @@ impl From<&Expr> for Safety {
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import subprocess
|
||||
/// import os
|
||||
///
|
||||
/// os.system("/bin/ls")
|
||||
/// os.system("./bin/ls")
|
||||
|
||||
@@ -16,9 +16,9 @@ use crate::fix::edits::add_argument;
|
||||
/// resulting iterator will be silently truncated to the length of the shortest
|
||||
/// iterable. This can lead to subtle bugs.
|
||||
///
|
||||
/// Use the `strict` parameter to raise a `ValueError` if the iterables are of
|
||||
/// non-uniform length. If the iterables are intentionally different lengths, the
|
||||
/// parameter should be explicitly set to `False`.
|
||||
/// Pass `strict=True` to raise a `ValueError` if the iterables are of
|
||||
/// non-uniform length. Alternatively, if the iterables are deliberately
|
||||
/// different lengths, pass `strict=False` to make the intention explicit.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
@@ -47,7 +47,7 @@ impl AlwaysFixableViolation for ZipWithoutExplicitStrict {
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Add explicit `strict=False`".to_string()
|
||||
"Add explicit value for parameter `strict=`".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ B905.py:4:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
5 | zip(range(3))
|
||||
6 | zip("a", "b")
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | from itertools import count, cycle, repeat
|
||||
@@ -30,7 +30,7 @@ B905.py:5:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
6 | zip("a", "b")
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
@@ -51,7 +51,7 @@ B905.py:6:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
7 | zip("a", "b", *zip("c"))
|
||||
8 | zip(zip("a"), strict=False)
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | # Errors
|
||||
@@ -72,7 +72,7 @@ B905.py:7:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
8 | zip(zip("a"), strict=False)
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | zip()
|
||||
@@ -93,7 +93,7 @@ B905.py:7:16: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
8 | zip(zip("a"), strict=False)
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | zip()
|
||||
@@ -113,7 +113,7 @@ B905.py:8:5: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
| ^^^^^^^^ B905
|
||||
9 | zip(zip("a", strict=True))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | zip(range(3))
|
||||
@@ -134,7 +134,7 @@ B905.py:9:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
10 |
|
||||
11 | # OK
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | zip("a", "b")
|
||||
@@ -153,7 +153,7 @@ B905.py:24:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
25 | zip([1, 2, 3], repeat(1, times=4))
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
21 21 | zip([1, 2, 3], repeat(1, times=None))
|
||||
@@ -174,7 +174,7 @@ B905.py:25:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
26 |
|
||||
27 | import builtins
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 |
|
||||
@@ -193,7 +193,7 @@ B905.py:29:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
||||
29 | builtins.zip([1, 2, 3])
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ B905
|
||||
|
|
||||
= help: Add explicit `strict=False`
|
||||
= help: Add explicit value for parameter `strict=`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 |
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_stdlib::path::is_module_file;
|
||||
use ruff_python_stdlib::sys::is_known_standard_library;
|
||||
use ruff_text_size::TextRange;
|
||||
@@ -42,10 +43,7 @@ pub(crate) fn builtin_module_shadowing(
|
||||
allowed_modules: &[String],
|
||||
target_version: PythonVersion,
|
||||
) -> Option<Diagnostic> {
|
||||
if !path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext == "py" || ext == "pyi")
|
||||
{
|
||||
if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file_or_stub) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,8 @@ use crate::rules::flake8_comprehensions::fixes;
|
||||
/// Checks for unnecessary `dict`, `list`, and `set` comprehension.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// It's unnecessary to use a `dict`/`list`/`set` comprehension to build a
|
||||
/// data structure if the elements are unchanged. Wrap the iterable with
|
||||
/// `dict()`, `list()`, or `set()` instead.
|
||||
/// It's unnecessary to use a `dict`/`list`/`set` comprehension to build a data structure if the
|
||||
/// elements are unchanged. Wrap the iterable with `dict()`, `list()`, or `set()` instead.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
@@ -30,10 +29,33 @@ use crate::rules::flake8_comprehensions::fixes;
|
||||
/// set(iterable)
|
||||
/// ```
|
||||
///
|
||||
/// ## Known problems
|
||||
///
|
||||
/// This rule may produce false positives for dictionary comprehensions that iterate over a mapping.
|
||||
/// The `dict` constructor behaves differently depending on if it receives a sequence (e.g., a
|
||||
/// `list`) or a mapping (e.g., a `dict`). When a comprehension iterates over the keys of a mapping,
|
||||
/// replacing it with a `dict` constructor call will give a different result.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```pycon
|
||||
/// >>> d1 = {(1, 2): 3, (4, 5): 6}
|
||||
/// >>> {x: y for x, y in d1} # Iterates over the keys of a mapping
|
||||
/// {1: 2, 4: 5}
|
||||
/// >>> dict(d1) # Ruff's incorrect suggested fix
|
||||
/// (1, 2): 3, (4, 5): 6}
|
||||
/// >>> dict(d1.keys()) # Correct fix
|
||||
/// {1: 2, 4: 5}
|
||||
/// ```
|
||||
///
|
||||
/// When the comprehension iterates over a sequence, Ruff's suggested fix is correct. However, Ruff
|
||||
/// cannot consistently infer if the iterable type is a sequence or a mapping and cannot suggest
|
||||
/// the correct fix for mappings.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as unsafe, as it may occasionally drop comments
|
||||
/// when rewriting the comprehension. In most cases, though, comments will be
|
||||
/// preserved.
|
||||
/// Due to the known problem with dictionary comprehensions, this fix is marked as unsafe.
|
||||
///
|
||||
/// Additionally, this fix may drop comments when rewriting the comprehension.
|
||||
#[violation]
|
||||
pub struct UnnecessaryComprehension {
|
||||
obj_type: String,
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::{Path, PathBuf};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
@@ -51,7 +52,7 @@ pub(crate) fn implicit_namespace_package(
|
||||
) -> Option<Diagnostic> {
|
||||
if package.is_none()
|
||||
// Ignore non-`.py` files, which don't require an `__init__.py`.
|
||||
&& path.extension().is_some_and( |ext| ext == "py")
|
||||
&& PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file)
|
||||
// Ignore any files that are direct children of the project root.
|
||||
&& !path
|
||||
.parent()
|
||||
|
||||
@@ -80,6 +80,8 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
|
||||
} else {
|
||||
literal_exprs.push(slice);
|
||||
}
|
||||
} else {
|
||||
other_exprs.push(expr);
|
||||
}
|
||||
} else {
|
||||
other_exprs.push(expr);
|
||||
|
||||
@@ -517,6 +517,8 @@ PYI030.py:89:10: PYI030 [*] Multiple literal members in a union. Use a single li
|
||||
88 | # Should use the first literal subscript attribute when fixing
|
||||
89 | field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
|
||||
90 |
|
||||
91 | from typing import IO, Literal
|
||||
|
|
||||
= help: Replace with a single `Literal`
|
||||
|
||||
@@ -526,5 +528,22 @@ PYI030.py:89:10: PYI030 [*] Multiple literal members in a union. Use a single li
|
||||
88 88 | # Should use the first literal subscript attribute when fixing
|
||||
89 |-field25: typing.Union[typing_extensions.Literal[1], typing.Union[Literal[2], typing.Union[Literal[3], Literal[4]]], str] # Error
|
||||
89 |+field25: typing.Union[typing_extensions.Literal[1, 2, 3, 4], str] # Error
|
||||
90 90 |
|
||||
91 91 | from typing import IO, Literal
|
||||
92 92 |
|
||||
|
||||
PYI030.py:93:16: PYI030 [*] Multiple literal members in a union. Use a single literal, e.g. `Literal["a", "b"]`
|
||||
|
|
||||
91 | from typing import IO, Literal
|
||||
92 |
|
||||
93 | InlineOption = Literal["a"] | Literal["b"] | IO[str]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI030
|
||||
|
|
||||
= help: Replace with a single `Literal`
|
||||
|
||||
ℹ Safe fix
|
||||
90 90 |
|
||||
91 91 | from typing import IO, Literal
|
||||
92 92 |
|
||||
93 |-InlineOption = Literal["a"] | Literal["b"] | IO[str]
|
||||
93 |+InlineOption = Literal["a", "b"] | IO[str]
|
||||
|
||||
@@ -1,58 +1,56 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_todos/mod.rs
|
||||
---
|
||||
TD006.py:4:3: TD006 [*] Invalid TODO capitalization: `ToDo` should be `TODO`
|
||||
TD006.py:5:3: TD006 [*] Invalid TODO capitalization: `ToDo` should be `TODO`
|
||||
|
|
||||
2 | # TODO (evanrittenhouse): this is a valid TODO
|
||||
3 | # TDO006 - error
|
||||
4 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
3 | # Todoism is a word which starts with todo, but is not a todo
|
||||
4 | # TDO006 - error
|
||||
5 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
| ^^^^ TD006
|
||||
5 | # todo (evanrittenhouse): another invalid capitalization
|
||||
6 | # foo # todo: invalid capitalization
|
||||
6 | # todo (evanrittenhouse): another invalid capitalization
|
||||
7 | # foo # todo: invalid capitalization
|
||||
|
|
||||
= help: Replace `ToDo` with `TODO`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # TDO006 - accepted
|
||||
2 2 | # TODO (evanrittenhouse): this is a valid TODO
|
||||
3 3 | # TDO006 - error
|
||||
4 |-# ToDo (evanrittenhouse): invalid capitalization
|
||||
4 |+# TODO (evanrittenhouse): invalid capitalization
|
||||
5 5 | # todo (evanrittenhouse): another invalid capitalization
|
||||
6 6 | # foo # todo: invalid capitalization
|
||||
3 3 | # Todoism is a word which starts with todo, but is not a todo
|
||||
4 4 | # TDO006 - error
|
||||
5 |-# ToDo (evanrittenhouse): invalid capitalization
|
||||
5 |+# TODO (evanrittenhouse): invalid capitalization
|
||||
6 6 | # todo (evanrittenhouse): another invalid capitalization
|
||||
7 7 | # foo # todo: invalid capitalization
|
||||
|
||||
TD006.py:5:3: TD006 [*] Invalid TODO capitalization: `todo` should be `TODO`
|
||||
TD006.py:6:3: TD006 [*] Invalid TODO capitalization: `todo` should be `TODO`
|
||||
|
|
||||
3 | # TDO006 - error
|
||||
4 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
5 | # todo (evanrittenhouse): another invalid capitalization
|
||||
4 | # TDO006 - error
|
||||
5 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
6 | # todo (evanrittenhouse): another invalid capitalization
|
||||
| ^^^^ TD006
|
||||
6 | # foo # todo: invalid capitalization
|
||||
7 | # foo # todo: invalid capitalization
|
||||
|
|
||||
= help: Replace `todo` with `TODO`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | # TODO (evanrittenhouse): this is a valid TODO
|
||||
3 3 | # TDO006 - error
|
||||
4 4 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
5 |-# todo (evanrittenhouse): another invalid capitalization
|
||||
5 |+# TODO (evanrittenhouse): another invalid capitalization
|
||||
6 6 | # foo # todo: invalid capitalization
|
||||
3 3 | # Todoism is a word which starts with todo, but is not a todo
|
||||
4 4 | # TDO006 - error
|
||||
5 5 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
6 |-# todo (evanrittenhouse): another invalid capitalization
|
||||
6 |+# TODO (evanrittenhouse): another invalid capitalization
|
||||
7 7 | # foo # todo: invalid capitalization
|
||||
|
||||
TD006.py:6:9: TD006 [*] Invalid TODO capitalization: `todo` should be `TODO`
|
||||
TD006.py:7:9: TD006 [*] Invalid TODO capitalization: `todo` should be `TODO`
|
||||
|
|
||||
4 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
5 | # todo (evanrittenhouse): another invalid capitalization
|
||||
6 | # foo # todo: invalid capitalization
|
||||
5 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
6 | # todo (evanrittenhouse): another invalid capitalization
|
||||
7 | # foo # todo: invalid capitalization
|
||||
| ^^^^ TD006
|
||||
|
|
||||
= help: Replace `todo` with `TODO`
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | # TDO006 - error
|
||||
4 4 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
5 5 | # todo (evanrittenhouse): another invalid capitalization
|
||||
6 |-# foo # todo: invalid capitalization
|
||||
6 |+# foo # TODO: invalid capitalization
|
||||
|
||||
|
||||
4 4 | # TDO006 - error
|
||||
5 5 | # ToDo (evanrittenhouse): invalid capitalization
|
||||
6 6 | # todo (evanrittenhouse): another invalid capitalization
|
||||
7 |-# foo # todo: invalid capitalization
|
||||
7 |+# foo # TODO: invalid capitalization
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
|
||||
use ruff_python_ast::{Expr, ExprBooleanLiteral, ExprCall};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprBooleanLiteral, ExprCall};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -124,6 +126,10 @@ pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, call: &ExprCall) {
|
||||
.arguments
|
||||
.find_argument("opener", 7)
|
||||
.is_some_and(|expr| !expr.is_none_literal_expr())
|
||||
|| call
|
||||
.arguments
|
||||
.find_positional(0)
|
||||
.is_some_and(|expr| is_file_descriptor(expr, checker.semantic()))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -159,3 +165,26 @@ pub(crate) fn replaceable_by_pathlib(checker: &mut Checker, call: &ExprCall) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
|
||||
fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
if matches!(
|
||||
expr,
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Int(_),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Some(name) = expr.as_name_expr() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
typing::is_int(binding, semantic)
|
||||
}
|
||||
|
||||
@@ -317,5 +317,3 @@ full_name.py:47:1: PTH123 `open()` should be replaced by `Path.open()`
|
||||
| ^^^^ PTH123
|
||||
48 | open(p, 'r', - 1, None, None, None, False, opener)
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::path::Path;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_python_stdlib::identifiers::{is_migration_name, is_module_name};
|
||||
use ruff_python_stdlib::path::is_module_file;
|
||||
use ruff_text_size::TextRange;
|
||||
@@ -53,10 +54,7 @@ pub(crate) fn invalid_module_name(
|
||||
package: Option<&Path>,
|
||||
ignore_names: &IgnoreNames,
|
||||
) -> Option<Diagnostic> {
|
||||
if !path
|
||||
.extension()
|
||||
.is_some_and(|ext| ext == "py" || ext == "pyi")
|
||||
{
|
||||
if !PySourceType::try_from_path(path).is_some_and(PySourceType::is_py_file_or_stub) {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::logical_lines::LogicalLinesContext;
|
||||
|
||||
use super::LogicalLine;
|
||||
use super::{DefinitionState, LogicalLine};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for missing whitespace after `,`, `;`, and `:`.
|
||||
@@ -28,22 +28,10 @@ pub struct MissingWhitespace {
|
||||
token: TokenKind,
|
||||
}
|
||||
|
||||
impl MissingWhitespace {
|
||||
fn token_text(&self) -> char {
|
||||
match self.token {
|
||||
TokenKind::Colon => ':',
|
||||
TokenKind::Semi => ';',
|
||||
TokenKind::Comma => ',',
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for MissingWhitespace {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let token = self.token_text();
|
||||
format!("Missing whitespace after '{token}'")
|
||||
format!("Missing whitespace after {}", self.token)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
@@ -54,11 +42,13 @@ impl AlwaysFixableViolation for MissingWhitespace {
|
||||
/// E231
|
||||
pub(crate) fn missing_whitespace(line: &LogicalLine, context: &mut LogicalLinesContext) {
|
||||
let mut fstrings = 0u32;
|
||||
let mut definition_state = DefinitionState::from_tokens(line.tokens());
|
||||
let mut brackets = Vec::new();
|
||||
let mut iter = line.tokens().iter().peekable();
|
||||
|
||||
while let Some(token) = iter.next() {
|
||||
let kind = token.kind();
|
||||
definition_state.visit_token_kind(kind);
|
||||
match kind {
|
||||
TokenKind::FStringStart => fstrings += 1,
|
||||
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||
@@ -97,7 +87,9 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, context: &mut LogicalLinesC
|
||||
if let Some(next_token) = iter.peek() {
|
||||
match (kind, next_token.kind()) {
|
||||
(TokenKind::Colon, _)
|
||||
if matches!(brackets.last(), Some(TokenKind::Lsqb)) =>
|
||||
if matches!(brackets.last(), Some(TokenKind::Lsqb))
|
||||
&& !(definition_state.in_type_params()
|
||||
&& brackets.len() == 1) =>
|
||||
{
|
||||
continue; // Slice syntax, no space required
|
||||
}
|
||||
@@ -111,13 +103,10 @@ pub(crate) fn missing_whitespace(line: &LogicalLine, context: &mut LogicalLinesC
|
||||
}
|
||||
}
|
||||
|
||||
let mut diagnostic =
|
||||
let diagnostic =
|
||||
Diagnostic::new(MissingWhitespace { token: kind }, token.range());
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
|
||||
" ".to_string(),
|
||||
token.end(),
|
||||
)));
|
||||
context.push_diagnostic(diagnostic);
|
||||
let fix = Fix::safe_edit(Edit::insertion(" ".to_string(), token.end()));
|
||||
context.push_diagnostic(diagnostic.with_fix(fix));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -470,6 +470,112 @@ struct Line {
|
||||
tokens_end: u32,
|
||||
}
|
||||
|
||||
/// Keeps track of whether we are currently visiting a class or function definition in a
|
||||
/// [`LogicalLine`]. If we are visiting a class or function, the enum also keeps track
|
||||
/// of the [type parameters] of the class/function.
|
||||
///
|
||||
/// Call [`DefinitionState::visit_token_kind`] on the [`TokenKind`] of each
|
||||
/// successive [`LogicalLineToken`] to ensure the state remains up to date.
|
||||
///
|
||||
/// [type parameters]: https://docs.python.org/3/reference/compound_stmts.html#type-params
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum DefinitionState {
|
||||
InClass(TypeParamsState),
|
||||
InFunction(TypeParamsState),
|
||||
NotInClassOrFunction,
|
||||
}
|
||||
|
||||
impl DefinitionState {
|
||||
fn from_tokens<'a>(tokens: impl IntoIterator<Item = &'a LogicalLineToken>) -> Self {
|
||||
let mut token_kinds = tokens.into_iter().map(LogicalLineToken::kind);
|
||||
while let Some(token_kind) = token_kinds.next() {
|
||||
let state = match token_kind {
|
||||
TokenKind::Indent | TokenKind::Dedent => continue,
|
||||
TokenKind::Class => Self::InClass(TypeParamsState::default()),
|
||||
TokenKind::Def => Self::InFunction(TypeParamsState::default()),
|
||||
TokenKind::Async if matches!(token_kinds.next(), Some(TokenKind::Def)) => {
|
||||
Self::InFunction(TypeParamsState::default())
|
||||
}
|
||||
_ => Self::NotInClassOrFunction,
|
||||
};
|
||||
return state;
|
||||
}
|
||||
Self::NotInClassOrFunction
|
||||
}
|
||||
|
||||
const fn in_function_definition(self) -> bool {
|
||||
matches!(self, Self::InFunction(_))
|
||||
}
|
||||
|
||||
const fn type_params_state(self) -> Option<TypeParamsState> {
|
||||
match self {
|
||||
Self::InClass(state) | Self::InFunction(state) => Some(state),
|
||||
Self::NotInClassOrFunction => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn in_type_params(self) -> bool {
|
||||
matches!(
|
||||
self.type_params_state(),
|
||||
Some(TypeParamsState::InTypeParams { .. })
|
||||
)
|
||||
}
|
||||
|
||||
fn visit_token_kind(&mut self, token_kind: TokenKind) {
|
||||
let type_params_state_mut = match self {
|
||||
Self::InClass(type_params_state) | Self::InFunction(type_params_state) => {
|
||||
type_params_state
|
||||
}
|
||||
Self::NotInClassOrFunction => return,
|
||||
};
|
||||
match token_kind {
|
||||
TokenKind::Lpar if type_params_state_mut.before_type_params() => {
|
||||
*type_params_state_mut = TypeParamsState::TypeParamsEnded;
|
||||
}
|
||||
TokenKind::Lsqb => match type_params_state_mut {
|
||||
TypeParamsState::TypeParamsEnded => {}
|
||||
TypeParamsState::BeforeTypeParams => {
|
||||
*type_params_state_mut = TypeParamsState::InTypeParams {
|
||||
inner_square_brackets: 0,
|
||||
};
|
||||
}
|
||||
TypeParamsState::InTypeParams {
|
||||
inner_square_brackets,
|
||||
} => *inner_square_brackets += 1,
|
||||
},
|
||||
TokenKind::Rsqb => {
|
||||
if let TypeParamsState::InTypeParams {
|
||||
inner_square_brackets,
|
||||
} = type_params_state_mut
|
||||
{
|
||||
if *inner_square_brackets == 0 {
|
||||
*type_params_state_mut = TypeParamsState::TypeParamsEnded;
|
||||
} else {
|
||||
*inner_square_brackets -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
enum TypeParamsState {
|
||||
#[default]
|
||||
BeforeTypeParams,
|
||||
InTypeParams {
|
||||
inner_square_brackets: u32,
|
||||
},
|
||||
TypeParamsEnded,
|
||||
}
|
||||
|
||||
impl TypeParamsState {
|
||||
const fn before_type_params(self) -> bool {
|
||||
matches!(self, Self::BeforeTypeParams)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_python_parser::parse_module;
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_python_parser::TokenKind;
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::logical_lines::LogicalLinesContext;
|
||||
use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineToken};
|
||||
use crate::rules::pycodestyle::rules::logical_lines::{DefinitionState, LogicalLine};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for missing whitespace around the equals sign in an unannotated
|
||||
@@ -84,18 +84,6 @@ impl AlwaysFixableViolation for MissingWhitespaceAroundParameterEquals {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_in_def(tokens: &[LogicalLineToken]) -> bool {
|
||||
for token in tokens {
|
||||
match token.kind() {
|
||||
TokenKind::Async | TokenKind::Indent | TokenKind::Dedent => continue,
|
||||
TokenKind::Def => return true,
|
||||
_ => return false,
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// E251, E252
|
||||
pub(crate) fn whitespace_around_named_parameter_equals(
|
||||
line: &LogicalLine,
|
||||
@@ -106,17 +94,14 @@ pub(crate) fn whitespace_around_named_parameter_equals(
|
||||
let mut annotated_func_arg = false;
|
||||
let mut prev_end = TextSize::default();
|
||||
|
||||
let in_def = is_in_def(line.tokens());
|
||||
let mut definition_state = DefinitionState::from_tokens(line.tokens());
|
||||
let mut iter = line.tokens().iter().peekable();
|
||||
|
||||
while let Some(token) = iter.next() {
|
||||
let kind = token.kind();
|
||||
|
||||
if kind == TokenKind::NonLogicalNewline {
|
||||
continue;
|
||||
}
|
||||
|
||||
definition_state.visit_token_kind(kind);
|
||||
match kind {
|
||||
TokenKind::NonLogicalNewline => continue,
|
||||
TokenKind::FStringStart => fstrings += 1,
|
||||
TokenKind::FStringEnd => fstrings = fstrings.saturating_sub(1),
|
||||
TokenKind::Lpar | TokenKind::Lsqb => {
|
||||
@@ -128,15 +113,16 @@ pub(crate) fn whitespace_around_named_parameter_equals(
|
||||
annotated_func_arg = false;
|
||||
}
|
||||
}
|
||||
|
||||
TokenKind::Colon if parens == 1 && in_def => {
|
||||
TokenKind::Colon if parens == 1 && definition_state.in_function_definition() => {
|
||||
annotated_func_arg = true;
|
||||
}
|
||||
TokenKind::Comma if parens == 1 => {
|
||||
annotated_func_arg = false;
|
||||
}
|
||||
TokenKind::Equal if parens > 0 && fstrings == 0 => {
|
||||
if annotated_func_arg && parens == 1 {
|
||||
TokenKind::Equal
|
||||
if definition_state.in_type_params() || (parens > 0 && fstrings == 0) =>
|
||||
{
|
||||
if definition_state.in_type_params() || (annotated_func_arg && parens == 1) {
|
||||
let start = token.start();
|
||||
if start == prev_end && prev_end != TextSize::new(0) {
|
||||
let mut diagnostic =
|
||||
|
||||
@@ -476,3 +476,432 @@ E23.py:102:24: E231 [*] Missing whitespace after ','
|
||||
103 103 | },
|
||||
104 104 | ]
|
||||
105 105 | }
|
||||
|
||||
E23.py:109:18: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
| ^ E231
|
||||
110 | x:A = "foo"[::-1],
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
106 106 | ]
|
||||
107 107 |
|
||||
108 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 |-def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
109 |+def pep_696_bad[A: object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
110 110 | x:A = "foo"[::-1],
|
||||
111 111 | y:B = [[["foo", "bar"]]],
|
||||
112 112 | z:object = "fooo",
|
||||
|
||||
E23.py:109:40: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
| ^ E231
|
||||
110 | x:A = "foo"[::-1],
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
106 106 | ]
|
||||
107 107 |
|
||||
108 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 |-def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
109 |+def pep_696_bad[A:object="foo"[::-1], B: object =[[["foo", "bar"]]], C:object= bytes](
|
||||
110 110 | x:A = "foo"[::-1],
|
||||
111 111 | y:B = [[["foo", "bar"]]],
|
||||
112 112 | z:object = "fooo",
|
||||
|
||||
E23.py:109:70: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
| ^ E231
|
||||
110 | x:A = "foo"[::-1],
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
106 106 | ]
|
||||
107 107 |
|
||||
108 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 |-def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
109 |+def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C: object= bytes](
|
||||
110 110 | x:A = "foo"[::-1],
|
||||
111 111 | y:B = [[["foo", "bar"]]],
|
||||
112 112 | z:object = "fooo",
|
||||
|
||||
E23.py:110:6: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
110 | x:A = "foo"[::-1],
|
||||
| ^ E231
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
112 | z:object = "fooo",
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
107 107 |
|
||||
108 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
110 |- x:A = "foo"[::-1],
|
||||
110 |+ x: A = "foo"[::-1],
|
||||
111 111 | y:B = [[["foo", "bar"]]],
|
||||
112 112 | z:object = "fooo",
|
||||
113 113 | ):
|
||||
|
||||
E23.py:111:6: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
110 | x:A = "foo"[::-1],
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
| ^ E231
|
||||
112 | z:object = "fooo",
|
||||
113 | ):
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
108 108 | # Should be E231 errors on all of these type parameters and function parameters, but not on their (strange) defaults
|
||||
109 109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
110 110 | x:A = "foo"[::-1],
|
||||
111 |- y:B = [[["foo", "bar"]]],
|
||||
111 |+ y: B = [[["foo", "bar"]]],
|
||||
112 112 | z:object = "fooo",
|
||||
113 113 | ):
|
||||
114 114 | pass
|
||||
|
||||
E23.py:112:6: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
110 | x:A = "foo"[::-1],
|
||||
111 | y:B = [[["foo", "bar"]]],
|
||||
112 | z:object = "fooo",
|
||||
| ^ E231
|
||||
113 | ):
|
||||
114 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
109 109 | def pep_696_bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
110 110 | x:A = "foo"[::-1],
|
||||
111 111 | y:B = [[["foo", "bar"]]],
|
||||
112 |- z:object = "fooo",
|
||||
112 |+ z: object = "fooo",
|
||||
113 113 | ):
|
||||
114 114 | pass
|
||||
115 115 |
|
||||
|
||||
E23.py:116:18: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
114 | pass
|
||||
115 |
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
| ^ E231
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 | self,
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
113 113 | ):
|
||||
114 114 | pass
|
||||
115 115 |
|
||||
116 |-class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
116 |+class PEP696Bad[A: object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
|
||||
E23.py:116:40: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
114 | pass
|
||||
115 |
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
| ^ E231
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 | self,
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
113 113 | ):
|
||||
114 114 | pass
|
||||
115 115 |
|
||||
116 |-class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
116 |+class PEP696Bad[A:object="foo"[::-1], B: object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
|
||||
E23.py:116:70: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
114 | pass
|
||||
115 |
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
| ^ E231
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 | self,
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
113 113 | ):
|
||||
114 114 | pass
|
||||
115 115 |
|
||||
116 |-class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
116 |+class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C: object= bytes]:
|
||||
117 117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
|
||||
E23.py:117:29: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
| ^ E231
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
114 114 | pass
|
||||
115 115 |
|
||||
116 116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 |- def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
117 |+ def pep_696_bad_method[A: object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
120 120 | y:B = [[["foo", "bar"]]],
|
||||
|
||||
E23.py:117:51: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
| ^ E231
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
114 114 | pass
|
||||
115 115 |
|
||||
116 116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 |- def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
117 |+ def pep_696_bad_method[A:object="foo"[::-1], B: object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
120 120 | y:B = [[["foo", "bar"]]],
|
||||
|
||||
E23.py:117:81: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
| ^ E231
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
114 114 | pass
|
||||
115 115 |
|
||||
116 116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 |- def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
117 |+ def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C: object= bytes](
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
120 120 | y:B = [[["foo", "bar"]]],
|
||||
|
||||
E23.py:119:10: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
| ^ E231
|
||||
120 | y:B = [[["foo", "bar"]]],
|
||||
121 | z:object = "fooo",
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
116 116 | class PEP696Bad[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]:
|
||||
117 117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 118 | self,
|
||||
119 |- x:A = "foo"[::-1],
|
||||
119 |+ x: A = "foo"[::-1],
|
||||
120 120 | y:B = [[["foo", "bar"]]],
|
||||
121 121 | z:object = "fooo",
|
||||
122 122 | ):
|
||||
|
||||
E23.py:120:10: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
118 | self,
|
||||
119 | x:A = "foo"[::-1],
|
||||
120 | y:B = [[["foo", "bar"]]],
|
||||
| ^ E231
|
||||
121 | z:object = "fooo",
|
||||
122 | ):
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
117 117 | def pep_696_bad_method[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
120 |- y:B = [[["foo", "bar"]]],
|
||||
120 |+ y: B = [[["foo", "bar"]]],
|
||||
121 121 | z:object = "fooo",
|
||||
122 122 | ):
|
||||
123 123 | pass
|
||||
|
||||
E23.py:121:10: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
119 | x:A = "foo"[::-1],
|
||||
120 | y:B = [[["foo", "bar"]]],
|
||||
121 | z:object = "fooo",
|
||||
| ^ E231
|
||||
122 | ):
|
||||
123 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
118 118 | self,
|
||||
119 119 | x:A = "foo"[::-1],
|
||||
120 120 | y:B = [[["foo", "bar"]]],
|
||||
121 |- z:object = "fooo",
|
||||
121 |+ z: object = "fooo",
|
||||
122 122 | ):
|
||||
123 123 | pass
|
||||
124 124 |
|
||||
|
||||
E23.py:125:32: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
123 | pass
|
||||
124 |
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
| ^ E231
|
||||
126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
122 122 | ):
|
||||
123 123 | pass
|
||||
124 124 |
|
||||
125 |-class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
125 |+class PEP696BadWithEmptyBases[A: object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 127 | pass
|
||||
128 128 |
|
||||
|
||||
E23.py:125:54: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
123 | pass
|
||||
124 |
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
| ^ E231
|
||||
126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
122 122 | ):
|
||||
123 123 | pass
|
||||
124 124 |
|
||||
125 |-class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
125 |+class PEP696BadWithEmptyBases[A:object="foo"[::-1], B: object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 127 | pass
|
||||
128 128 |
|
||||
|
||||
E23.py:125:84: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
123 | pass
|
||||
124 |
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
| ^ E231
|
||||
126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
122 122 | ):
|
||||
123 123 | pass
|
||||
124 124 |
|
||||
125 |-class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
125 |+class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C: object= bytes]():
|
||||
126 126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 127 | pass
|
||||
128 128 |
|
||||
|
||||
E23.py:126:47: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
| ^ E231
|
||||
127 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
123 123 | pass
|
||||
124 124 |
|
||||
125 125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 |- class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
126 |+ class IndentedPEP696BadWithNonEmptyBases[A: object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 127 | pass
|
||||
128 128 |
|
||||
129 129 | # Should be no E231 errors on any of these:
|
||||
|
||||
E23.py:126:69: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
| ^ E231
|
||||
127 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
123 123 | pass
|
||||
124 124 |
|
||||
125 125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 |- class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
126 |+ class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B: object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
127 127 | pass
|
||||
128 128 |
|
||||
129 129 | # Should be no E231 errors on any of these:
|
||||
|
||||
E23.py:126:99: E231 [*] Missing whitespace after ':'
|
||||
|
|
||||
125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 | class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
| ^ E231
|
||||
127 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
123 123 | pass
|
||||
124 124 |
|
||||
125 125 | class PEP696BadWithEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes]():
|
||||
126 |- class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C:object= bytes](object, something_dynamic[x::-1]):
|
||||
126 |+ class IndentedPEP696BadWithNonEmptyBases[A:object="foo"[::-1], B:object =[[["foo", "bar"]]], C: object= bytes](object, something_dynamic[x::-1]):
|
||||
127 127 | pass
|
||||
128 128 |
|
||||
129 129 | # Should be no E231 errors on any of these:
|
||||
|
||||
@@ -85,4 +85,392 @@ E25.py:46:36: E252 [*] Missing whitespace around parameter equals
|
||||
48 48 | #: Okay
|
||||
49 49 | def add(a: int = _default(name='f')):
|
||||
|
||||
E25.py:64:18: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A =int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:18: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A= int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:26: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B = str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:33: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B =str, C = bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:49: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B =str, C= bool, D:object =int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:49: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B =str, C= bool, D:object= int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:64: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object =str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:64: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object= str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:80: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object = bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:64:96: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
| ^ E252
|
||||
65 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | print(f"{foo(a = 1)}")
|
||||
62 62 |
|
||||
63 63 | # There should be at least one E251 diagnostic for each type parameter here:
|
||||
64 |-def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
64 |+def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object = bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
|
||||
E25.py:67:18: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A =int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:18: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A= int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:26: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B = str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:33: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B =str, C = bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:49: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B =str, C= bool, D:object =int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:49: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B =str, C= bool, D:object= int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:64: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object =str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:64: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object= str, F: object =bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:80: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object = bool, G: object= bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
E25.py:67:96: E252 [*] Missing whitespace around parameter equals
|
||||
|
|
||||
65 | pass
|
||||
66 |
|
||||
67 | class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
| ^ E252
|
||||
68 | pass
|
||||
|
|
||||
= help: Add missing whitespace
|
||||
|
||||
ℹ Safe fix
|
||||
64 64 | def pep_696_bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]():
|
||||
65 65 | pass
|
||||
66 66 |
|
||||
67 |-class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object= bytes]:
|
||||
67 |+class PEP696Bad[A=int, B =str, C= bool, D:object=int, E: object=str, F: object =bool, G: object = bytes]:
|
||||
68 68 | pass
|
||||
69 69 |
|
||||
70 70 | # The last of these should cause us to emit E231,
|
||||
|
||||
@@ -33,7 +33,22 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// for city, population in data.items():
|
||||
/// print(f"{city} has population {population}.")
|
||||
///
|
||||
/// ## Known problems
|
||||
/// If the dictionary key is a tuple, e.g.:
|
||||
///
|
||||
/// ```python
|
||||
/// d = {(1, 2): 3, (3, 4): 5}
|
||||
/// for x, y in d:
|
||||
/// print(x, y)
|
||||
/// ```
|
||||
///
|
||||
/// The tuple key is unpacked into `x` and `y` instead of the key and values. This means that
|
||||
/// the suggested fix of using `d.items()` would result in different runtime behavior. Ruff
|
||||
/// cannot consistently infer the type of a dictionary's keys.
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// Due to the known problem with tuple keys, this fix is unsafe.
|
||||
#[violation]
|
||||
pub struct DictIterMissingItems;
|
||||
|
||||
@@ -48,6 +63,7 @@ impl AlwaysFixableViolation for DictIterMissingItems {
|
||||
}
|
||||
}
|
||||
|
||||
/// PLE1141
|
||||
pub(crate) fn dict_iter_missing_items(checker: &mut Checker, target: &Expr, iter: &Expr) {
|
||||
let Expr::Tuple(tuple) = target else {
|
||||
return;
|
||||
@@ -78,7 +94,7 @@ pub(crate) fn dict_iter_missing_items(checker: &mut Checker, target: &Expr, iter
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(DictIterMissingItems, iter.range());
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
|
||||
format!("{}.items()", name.id),
|
||||
iter.range(),
|
||||
)));
|
||||
|
||||
@@ -10,7 +10,7 @@ dict_iter_missing_items.py:13:13: PLE1141 [*] Unpacking a dictionary in iteratio
|
||||
|
|
||||
= help: Add a call to `.items()`
|
||||
|
||||
ℹ Safe fix
|
||||
ℹ Unsafe fix
|
||||
10 10 | s2 = {1, 2, 3}
|
||||
11 11 |
|
||||
12 12 | # Errors
|
||||
@@ -30,7 +30,7 @@ dict_iter_missing_items.py:16:13: PLE1141 [*] Unpacking a dictionary in iteratio
|
||||
|
|
||||
= help: Add a call to `.items()`
|
||||
|
||||
ℹ Safe fix
|
||||
ℹ Unsafe fix
|
||||
13 13 | for k, v in d:
|
||||
14 14 | pass
|
||||
15 15 |
|
||||
@@ -38,6 +38,4 @@ dict_iter_missing_items.py:16:13: PLE1141 [*] Unpacking a dictionary in iteratio
|
||||
16 |+for k, v in d_tuple_incorrect_tuple.items():
|
||||
17 17 | pass
|
||||
18 18 |
|
||||
19 19 |
|
||||
|
||||
|
||||
19 19 |
|
||||
|
||||
@@ -29,6 +29,7 @@ mod tests {
|
||||
#[test_case(Rule::UnnecessaryEnumerate, Path::new("FURB148.py"))]
|
||||
#[test_case(Rule::MathConstant, Path::new("FURB152.py"))]
|
||||
#[test_case(Rule::RepeatedGlobal, Path::new("FURB154.py"))]
|
||||
#[test_case(Rule::HardcodedStringCharset, Path::new("FURB156.py"))]
|
||||
#[test_case(Rule::VerboseDecimalConstructor, Path::new("FURB157.py"))]
|
||||
#[test_case(Rule::UnnecessaryFromFloat, Path::new("FURB164.py"))]
|
||||
#[test_case(Rule::PrintEmptyString, Path::new("FURB105.py"))]
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{CmpOp, Expr, ExprCompare, ExprStringLiteral};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of hardcoded charsets, which are defined in Python string module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Usage of named charsets from the standard library is more readable and less error-prone.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// x = "0123456789"
|
||||
/// y in "abcdefghijklmnopqrstuvwxyz"
|
||||
/// ```
|
||||
///
|
||||
/// Use instead
|
||||
/// ```python
|
||||
/// import string
|
||||
///
|
||||
/// x = string.digits
|
||||
/// y in string.ascii_lowercase
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: String constants](https://docs.python.org/3/library/string.html#string-constants)
|
||||
#[violation]
|
||||
pub struct HardcodedStringCharset {
|
||||
name: &'static str,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for HardcodedStringCharset {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Use of hardcoded string charset")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let HardcodedStringCharset { name } = self;
|
||||
format!("Replace hardcoded charset with `string.{name}`")
|
||||
}
|
||||
}
|
||||
|
||||
struct NamedCharset {
|
||||
name: &'static str,
|
||||
bytes: &'static [u8],
|
||||
ascii_char_set: AsciiCharSet,
|
||||
}
|
||||
|
||||
/// Represents the set of ascii characters in form of a bitset.
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
struct AsciiCharSet(u128);
|
||||
|
||||
impl AsciiCharSet {
|
||||
/// Creates the set of ascii characters from `bytes`.
|
||||
/// Returns None if there is non-ascii byte.
|
||||
const fn from_bytes(bytes: &[u8]) -> Option<Self> {
|
||||
// TODO: simplify implementation, when const-traits are supported
|
||||
// https://github.com/rust-lang/rust-project-goals/issues/106
|
||||
let mut bitset = 0;
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
if !bytes[i].is_ascii() {
|
||||
return None;
|
||||
}
|
||||
bitset |= 1 << bytes[i];
|
||||
i += 1;
|
||||
}
|
||||
Some(Self(bitset))
|
||||
}
|
||||
}
|
||||
|
||||
impl NamedCharset {
|
||||
const fn new(name: &'static str, bytes: &'static [u8]) -> Self {
|
||||
Self {
|
||||
name,
|
||||
bytes,
|
||||
// SAFETY: The named charset is guaranteed to have only ascii bytes.
|
||||
// TODO: replace with `.unwrap()`, when `Option::unwrap` will be stable in `const fn`
|
||||
// https://github.com/rust-lang/rust/issues/67441
|
||||
ascii_char_set: match AsciiCharSet::from_bytes(bytes) {
|
||||
Some(ascii_char_set) => ascii_char_set,
|
||||
None => unreachable!(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KNOWN_NAMED_CHARSETS: [NamedCharset; 9] = [
|
||||
NamedCharset::new(
|
||||
"ascii_letters",
|
||||
b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
),
|
||||
NamedCharset::new("ascii_lowercase", b"abcdefghijklmnopqrstuvwxyz"),
|
||||
NamedCharset::new("ascii_uppercase", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
|
||||
NamedCharset::new("digits", b"0123456789"),
|
||||
NamedCharset::new("hexdigits", b"0123456789abcdefABCDEF"),
|
||||
NamedCharset::new("octdigits", b"01234567"),
|
||||
NamedCharset::new("punctuation", b"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"),
|
||||
NamedCharset::new(
|
||||
"printable",
|
||||
b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"\
|
||||
#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c",
|
||||
),
|
||||
NamedCharset::new("whitespace", b" \t\n\r\x0b\x0c"),
|
||||
];
|
||||
|
||||
fn check_charset_as_set(bytes: &[u8]) -> Option<&NamedCharset> {
|
||||
let ascii_char_set = AsciiCharSet::from_bytes(bytes)?;
|
||||
|
||||
KNOWN_NAMED_CHARSETS
|
||||
.iter()
|
||||
.find(|&charset| charset.ascii_char_set == ascii_char_set)
|
||||
}
|
||||
|
||||
fn check_charset_exact(bytes: &[u8]) -> Option<&NamedCharset> {
|
||||
KNOWN_NAMED_CHARSETS
|
||||
.iter()
|
||||
.find(|&charset| charset.bytes == bytes)
|
||||
}
|
||||
|
||||
fn push_diagnostic(checker: &mut Checker, range: TextRange, charset: &NamedCharset) {
|
||||
let name = charset.name;
|
||||
let mut diagnostic = Diagnostic::new(HardcodedStringCharset { name }, range);
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import("string", name),
|
||||
range.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
Ok(Fix::safe_edits(
|
||||
Edit::range_replacement(binding, range),
|
||||
[edit],
|
||||
))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
/// FURB156
|
||||
pub(crate) fn hardcoded_string_charset_comparison(checker: &mut Checker, compare: &ExprCompare) {
|
||||
let (
|
||||
[CmpOp::In | CmpOp::NotIn],
|
||||
[Expr::StringLiteral(string_literal @ ExprStringLiteral { value, .. })],
|
||||
) = (compare.ops.as_ref(), compare.comparators.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let bytes = value.to_str().as_bytes();
|
||||
|
||||
let Some(charset) = check_charset_as_set(bytes) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// In this case the diagnostic will be emitted via string_literal check.
|
||||
if charset.bytes == bytes {
|
||||
return;
|
||||
}
|
||||
|
||||
push_diagnostic(checker, string_literal.range, charset);
|
||||
}
|
||||
|
||||
/// FURB156
|
||||
pub(crate) fn hardcoded_string_charset_literal(checker: &mut Checker, expr: &ExprStringLiteral) {
|
||||
if let Some(charset) = check_charset_exact(expr.value.to_str().as_bytes()) {
|
||||
push_diagnostic(checker, expr.range, charset);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ 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 hardcoded_string_charset::*;
|
||||
pub(crate) use hashlib_digest_hex::*;
|
||||
pub(crate) use if_exp_instead_of_or_operator::*;
|
||||
pub(crate) use if_expr_min_max::*;
|
||||
@@ -36,6 +37,7 @@ mod check_and_remove_from_set;
|
||||
mod delete_full_slice;
|
||||
mod for_loop_set_mutations;
|
||||
mod fstring_number_format;
|
||||
mod hardcoded_string_charset;
|
||||
mod hashlib_digest_hex;
|
||||
mod if_exp_instead_of_or_operator;
|
||||
mod if_expr_min_max;
|
||||
|
||||
@@ -4,7 +4,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the removal of a prefix or suffix from a string by assigning
|
||||
@@ -334,8 +334,9 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) -
|
||||
}),
|
||||
) => num
|
||||
.as_int()
|
||||
.and_then(ast::Int::as_u32) // Only support prefix removal for size at most `u32::MAX`
|
||||
.is_some_and(|x| x == string_val.to_str().text_len().to_u32()),
|
||||
// Only support prefix removal for size at most `usize::MAX`
|
||||
.and_then(ast::Int::as_usize)
|
||||
.is_some_and(|x| x == string_val.chars().count()),
|
||||
(
|
||||
AffixKind::StartsWith,
|
||||
ast::Expr::Call(ast::ExprCall {
|
||||
@@ -369,8 +370,8 @@ fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) -
|
||||
// Only support prefix removal for size at most `u32::MAX`
|
||||
value
|
||||
.as_int()
|
||||
.and_then(ast::Int::as_u32)
|
||||
.is_some_and(|x| x == string_val.to_str().text_len().to_u32())
|
||||
.and_then(ast::Int::as_usize)
|
||||
.is_some_and(|x| x == string_val.chars().count())
|
||||
},
|
||||
),
|
||||
(
|
||||
|
||||
@@ -0,0 +1,486 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB156.py:3:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
1 | # Errors
|
||||
2 |
|
||||
3 | _ = "0123456789"
|
||||
| ^^^^^^^^^^^^ FURB156
|
||||
4 | _ = "01234567"
|
||||
5 | _ = "0123456789abcdefABCDEF"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 |-_ = "0123456789"
|
||||
4 |+_ = string.digits
|
||||
4 5 | _ = "01234567"
|
||||
5 6 | _ = "0123456789abcdefABCDEF"
|
||||
6 7 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
||||
FURB156.py:4:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
3 | _ = "0123456789"
|
||||
4 | _ = "01234567"
|
||||
| ^^^^^^^^^^ FURB156
|
||||
5 | _ = "0123456789abcdefABCDEF"
|
||||
6 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.octdigits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 |-_ = "01234567"
|
||||
5 |+_ = string.octdigits
|
||||
5 6 | _ = "0123456789abcdefABCDEF"
|
||||
6 7 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
FURB156.py:5:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
3 | _ = "0123456789"
|
||||
4 | _ = "01234567"
|
||||
5 | _ = "0123456789abcdefABCDEF"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB156
|
||||
6 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.hexdigits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
5 |-_ = "0123456789abcdefABCDEF"
|
||||
6 |+_ = string.hexdigits
|
||||
6 7 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 9 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
FURB156.py:6:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
4 | _ = "01234567"
|
||||
5 | _ = "0123456789abcdefABCDEF"
|
||||
6 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB156
|
||||
7 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.ascii_lowercase`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
5 6 | _ = "0123456789abcdefABCDEF"
|
||||
6 |-_ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 |+_ = string.ascii_lowercase
|
||||
7 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 9 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 10 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
|
||||
FURB156.py:7:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
5 | _ = "0123456789abcdefABCDEF"
|
||||
6 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB156
|
||||
8 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.ascii_uppercase`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
5 6 | _ = "0123456789abcdefABCDEF"
|
||||
6 7 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 |-_ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 |+_ = string.ascii_uppercase
|
||||
8 9 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 10 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
10 11 | _ = " \t\n\r\v\f"
|
||||
|
||||
FURB156.py:8:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
6 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB156
|
||||
9 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
10 | _ = " \t\n\r\v\f"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.ascii_letters`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
5 6 | _ = "0123456789abcdefABCDEF"
|
||||
6 7 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 |-_ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 |+_ = string.ascii_letters
|
||||
9 10 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
10 11 | _ = " \t\n\r\v\f"
|
||||
11 12 |
|
||||
|
||||
FURB156.py:9:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
7 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB156
|
||||
10 | _ = " \t\n\r\v\f"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.punctuation`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
6 7 | _ = "abcdefghijklmnopqrstuvwxyz"
|
||||
7 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 9 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 |-_ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
10 |+_ = string.punctuation
|
||||
10 11 | _ = " \t\n\r\v\f"
|
||||
11 12 |
|
||||
12 13 | _ = "" in "1234567890"
|
||||
|
||||
FURB156.py:10:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
8 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
10 | _ = " \t\n\r\v\f"
|
||||
| ^^^^^^^^^^^^^ FURB156
|
||||
11 |
|
||||
12 | _ = "" in "1234567890"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.whitespace`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
7 8 | _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
8 9 | _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
9 10 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
10 |-_ = " \t\n\r\v\f"
|
||||
11 |+_ = string.whitespace
|
||||
11 12 |
|
||||
12 13 | _ = "" in "1234567890"
|
||||
13 14 | _ = "" in "12345670"
|
||||
|
||||
FURB156.py:12:11: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
10 | _ = " \t\n\r\v\f"
|
||||
11 |
|
||||
12 | _ = "" in "1234567890"
|
||||
| ^^^^^^^^^^^^ FURB156
|
||||
13 | _ = "" in "12345670"
|
||||
14 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
9 10 | _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~"""
|
||||
10 11 | _ = " \t\n\r\v\f"
|
||||
11 12 |
|
||||
12 |-_ = "" in "1234567890"
|
||||
13 |+_ = "" in string.digits
|
||||
13 14 | _ = "" in "12345670"
|
||||
14 15 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
15 16 | _ = (
|
||||
|
||||
FURB156.py:13:11: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
12 | _ = "" in "1234567890"
|
||||
13 | _ = "" in "12345670"
|
||||
| ^^^^^^^^^^ FURB156
|
||||
14 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
15 | _ = (
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.octdigits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
10 11 | _ = " \t\n\r\v\f"
|
||||
11 12 |
|
||||
12 13 | _ = "" in "1234567890"
|
||||
13 |-_ = "" in "12345670"
|
||||
14 |+_ = "" in string.octdigits
|
||||
14 15 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
15 16 | _ = (
|
||||
16 17 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'
|
||||
|
||||
FURB156.py:14:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
12 | _ = "" in "1234567890"
|
||||
13 | _ = "" in "12345670"
|
||||
14 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB156
|
||||
15 | _ = (
|
||||
16 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.printable`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
11 12 |
|
||||
12 13 | _ = "" in "1234567890"
|
||||
13 14 | _ = "" in "12345670"
|
||||
14 |-_ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
15 |+_ = string.printable
|
||||
15 16 | _ = (
|
||||
16 17 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'
|
||||
17 18 | "'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
|
||||
|
||||
FURB156.py:16:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
14 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
15 | _ = (
|
||||
16 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'
|
||||
| _____^
|
||||
17 | | "'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
|
||||
| |________________________________________________^ FURB156
|
||||
18 | )
|
||||
19 | _ = id("0123"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.printable`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
13 14 | _ = "" in "12345670"
|
||||
14 15 | _ = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
|
||||
15 16 | _ = (
|
||||
16 |- '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'
|
||||
17 |- "'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
|
||||
17 |+ string.printable
|
||||
18 18 | )
|
||||
19 19 | _ = id("0123"
|
||||
20 20 | "4567"
|
||||
|
||||
FURB156.py:19:8: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
17 | "'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
|
||||
18 | )
|
||||
19 | _ = id("0123"
|
||||
| ________^
|
||||
20 | | "4567"
|
||||
21 | | "89")
|
||||
| |___________^ FURB156
|
||||
22 | _ = "" in ("123"
|
||||
23 | "456"
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
16 17 | '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&'
|
||||
17 18 | "'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c"
|
||||
18 19 | )
|
||||
19 |-_ = id("0123"
|
||||
20 |- "4567"
|
||||
21 |- "89")
|
||||
20 |+_ = id(string.digits)
|
||||
22 21 | _ = "" in ("123"
|
||||
23 22 | "456"
|
||||
24 23 | "789"
|
||||
|
||||
FURB156.py:22:12: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
20 | "4567"
|
||||
21 | "89")
|
||||
22 | _ = "" in ("123"
|
||||
| ____________^
|
||||
23 | | "456"
|
||||
24 | | "789"
|
||||
25 | | "0")
|
||||
| |______________^ FURB156
|
||||
26 |
|
||||
27 | _ = "" in ( # comment
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
19 20 | _ = id("0123"
|
||||
20 21 | "4567"
|
||||
21 22 | "89")
|
||||
22 |-_ = "" in ("123"
|
||||
23 |- "456"
|
||||
24 |- "789"
|
||||
25 |- "0")
|
||||
23 |+_ = "" in (string.digits)
|
||||
26 24 |
|
||||
27 25 | _ = "" in ( # comment
|
||||
28 26 | "123"
|
||||
|
||||
FURB156.py:28:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
27 | _ = "" in ( # comment
|
||||
28 | "123"
|
||||
| _____^
|
||||
29 | | "456"
|
||||
30 | | "789"
|
||||
31 | | "0")
|
||||
| |_______^ FURB156
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
25 26 | "0")
|
||||
26 27 |
|
||||
27 28 | _ = "" in ( # comment
|
||||
28 |- "123"
|
||||
29 |- "456"
|
||||
30 |- "789"
|
||||
31 |- "0")
|
||||
29 |+ string.digits)
|
||||
32 30 |
|
||||
33 31 |
|
||||
34 32 | _ = "" in (
|
||||
|
||||
FURB156.py:35:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
34 | _ = "" in (
|
||||
35 | "123"
|
||||
| _____^
|
||||
36 | | "456" # inline comment
|
||||
37 | | "789"
|
||||
38 | | "0")
|
||||
| |_______^ FURB156
|
||||
39 |
|
||||
40 | _ = (
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
32 33 |
|
||||
33 34 |
|
||||
34 35 | _ = "" in (
|
||||
35 |- "123"
|
||||
36 |- "456" # inline comment
|
||||
37 |- "789"
|
||||
38 |- "0")
|
||||
36 |+ string.digits)
|
||||
39 37 |
|
||||
40 38 | _ = (
|
||||
41 39 | "0123456789"
|
||||
|
||||
FURB156.py:41:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
40 | _ = (
|
||||
41 | "0123456789"
|
||||
| ^^^^^^^^^^^^ FURB156
|
||||
42 | ).capitalize()
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
38 39 | "0")
|
||||
39 40 |
|
||||
40 41 | _ = (
|
||||
41 |- "0123456789"
|
||||
42 |+ string.digits
|
||||
42 43 | ).capitalize()
|
||||
43 44 |
|
||||
44 45 | _ = (
|
||||
|
||||
FURB156.py:45:5: FURB156 [*] Use of hardcoded string charset
|
||||
|
|
||||
44 | _ = (
|
||||
45 | "0123456789"
|
||||
| ^^^^^^^^^^^^ FURB156
|
||||
46 | # with comment
|
||||
47 | ).capitalize()
|
||||
|
|
||||
= help: Replace hardcoded charset with `string.digits`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Errors
|
||||
2 |+import string
|
||||
2 3 |
|
||||
3 4 | _ = "0123456789"
|
||||
4 5 | _ = "01234567"
|
||||
--------------------------------------------------------------------------------
|
||||
42 43 | ).capitalize()
|
||||
43 44 |
|
||||
44 45 | _ = (
|
||||
45 |- "0123456789"
|
||||
46 |+ string.digits
|
||||
46 47 | # with comment
|
||||
47 48 | ).capitalize()
|
||||
48 49 |
|
||||
@@ -250,4 +250,74 @@ FURB188.py:162:5: FURB188 [*] Prefer `removeprefix` over conditionally replacing
|
||||
162 |+ text = text.removeprefix("!")
|
||||
164 163 | print(text)
|
||||
165 164 |
|
||||
166 165 |
|
||||
166 165 |
|
||||
|
||||
FURB188.py:183:5: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
||||
|
|
||||
181 | # with fix `text = text.removeprefix("ř")`
|
||||
182 | text = "řetězec"
|
||||
183 | if text.startswith("ř"):
|
||||
| _____^
|
||||
184 | | text = text[1:]
|
||||
| |_______________________^ FURB188
|
||||
|
|
||||
= help: Use removeprefix instead of assignment conditional upon startswith.
|
||||
|
||||
ℹ Safe fix
|
||||
180 180 | # should be linted
|
||||
181 181 | # with fix `text = text.removeprefix("ř")`
|
||||
182 182 | text = "řetězec"
|
||||
183 |- if text.startswith("ř"):
|
||||
184 |- text = text[1:]
|
||||
183 |+ text = text.removeprefix("ř")
|
||||
185 184 |
|
||||
186 185 |
|
||||
187 186 | def handle_surrogates():
|
||||
|
||||
FURB188.py:190:5: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
||||
|
|
||||
188 | # should be linted
|
||||
189 | text = "\ud800\udc00heythere"
|
||||
190 | if text.startswith("\ud800\udc00"):
|
||||
| _____^
|
||||
191 | | text = text[2:]
|
||||
| |_______________________^ FURB188
|
||||
192 | text = "\U00010000heythere"
|
||||
193 | if text.startswith("\U00010000"):
|
||||
|
|
||||
= help: Use removeprefix instead of assignment conditional upon startswith.
|
||||
|
||||
ℹ Safe fix
|
||||
187 187 | def handle_surrogates():
|
||||
188 188 | # should be linted
|
||||
189 189 | text = "\ud800\udc00heythere"
|
||||
190 |- if text.startswith("\ud800\udc00"):
|
||||
191 |- text = text[2:]
|
||||
190 |+ text = text.removeprefix("\ud800\udc00")
|
||||
192 191 | text = "\U00010000heythere"
|
||||
193 192 | if text.startswith("\U00010000"):
|
||||
194 193 | text = text[1:]
|
||||
|
||||
FURB188.py:193:5: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
||||
|
|
||||
191 | text = text[2:]
|
||||
192 | text = "\U00010000heythere"
|
||||
193 | if text.startswith("\U00010000"):
|
||||
| _____^
|
||||
194 | | text = text[1:]
|
||||
| |_______________________^ FURB188
|
||||
195 |
|
||||
196 | # should not be linted
|
||||
|
|
||||
= help: Use removeprefix instead of assignment conditional upon startswith.
|
||||
|
||||
ℹ Safe fix
|
||||
190 190 | if text.startswith("\ud800\udc00"):
|
||||
191 191 | text = text[2:]
|
||||
192 192 | text = "\U00010000heythere"
|
||||
193 |- if text.startswith("\U00010000"):
|
||||
194 |- text = text[1:]
|
||||
193 |+ text = text.removeprefix("\U00010000")
|
||||
195 194 |
|
||||
196 195 | # should not be linted
|
||||
197 196 | text = "\ud800\udc00heythere"
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::rules::fastapi::rules::is_fastapi_route_call;
|
||||
/// 5. The string references variables that are not in scope, or it doesn't capture variables at all.
|
||||
/// 6. Any format specifiers in the potential f-string are invalid.
|
||||
/// 7. The string is part of a function call that is known to expect a template string rather than an
|
||||
/// evaluated f-string: for example, a [`logging`] call, a [`gettext`] call, or a [`fastAPI` path].
|
||||
/// evaluated f-string: for example, a [`logging`] call, a [`gettext`] call, or a [fastAPI path].
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
@@ -52,9 +52,9 @@ use crate::rules::fastapi::rules::is_fastapi_route_call;
|
||||
/// print(f"Hello {name}! It is {day_of_week} today!")
|
||||
/// ```
|
||||
///
|
||||
/// [`logging`]: https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application
|
||||
/// [`gettext`]: https://docs.python.org/3/library/gettext.html
|
||||
/// [`fastAPI` path]: https://fastapi.tiangolo.com/tutorial/path-params/
|
||||
/// [logging]: https://docs.python.org/3/howto/logging-cookbook.html#using-particular-formatting-styles-throughout-your-application
|
||||
/// [gettext]: https://docs.python.org/3/library/gettext.html
|
||||
/// [fastAPI path]: https://fastapi.tiangolo.com/tutorial/path-params/
|
||||
#[violation]
|
||||
pub struct MissingFStringSyntax;
|
||||
|
||||
|
||||
@@ -238,9 +238,19 @@ impl Cell {
|
||||
//
|
||||
// This is to avoid false positives when these variables are referenced
|
||||
// elsewhere in the notebook.
|
||||
//
|
||||
// Refer https://github.com/astral-sh/ruff/issues/13718 for `ipytest`.
|
||||
!matches!(
|
||||
command,
|
||||
"capture" | "debug" | "prun" | "pypy" | "python" | "python3" | "time" | "timeit"
|
||||
"capture"
|
||||
| "debug"
|
||||
| "ipytest"
|
||||
| "prun"
|
||||
| "pypy"
|
||||
| "python"
|
||||
| "python3"
|
||||
| "time"
|
||||
| "timeit"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1004,6 +1004,14 @@ impl Visitor<'_> for AwaitVisitor {
|
||||
crate::visitor::walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_comprehension(&mut self, comprehension: &'_ crate::Comprehension) {
|
||||
if comprehension.is_async {
|
||||
self.seen_await = true;
|
||||
} else {
|
||||
crate::visitor::walk_comprehension(self, comprehension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if a `Stmt` is a docstring.
|
||||
|
||||
@@ -96,6 +96,14 @@ impl Int {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an u64, if it can be represented as that data type.
|
||||
pub fn as_usize(&self) -> Option<usize> {
|
||||
match &self.0 {
|
||||
Number::Small(small) => usize::try_from(*small).ok(),
|
||||
Number::Big(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the [`Int`] as an i8, if it can be represented as that data type.
|
||||
pub fn as_i8(&self) -> Option<i8> {
|
||||
match &self.0 {
|
||||
|
||||
@@ -68,7 +68,7 @@ pub enum TomlSourceType {
|
||||
Unrecognized,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, is_macro::Is)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum PySourceType {
|
||||
/// The source is a Python file (`.py`).
|
||||
@@ -99,13 +99,33 @@ impl PySourceType {
|
||||
|
||||
Some(ty)
|
||||
}
|
||||
|
||||
pub fn try_from_path(path: impl AsRef<Path>) -> Option<Self> {
|
||||
path.as_ref()
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.and_then(Self::try_from_extension)
|
||||
}
|
||||
|
||||
pub const fn is_py_file(self) -> bool {
|
||||
matches!(self, Self::Python)
|
||||
}
|
||||
|
||||
pub const fn is_stub(self) -> bool {
|
||||
matches!(self, Self::Stub)
|
||||
}
|
||||
|
||||
pub const fn is_py_file_or_stub(self) -> bool {
|
||||
matches!(self, Self::Python | Self::Stub)
|
||||
}
|
||||
|
||||
pub const fn is_ipynb(self) -> bool {
|
||||
matches!(self, Self::Ipynb)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> From<P> for PySourceType {
|
||||
fn from(path: P) -> Self {
|
||||
path.as_ref()
|
||||
.extension()
|
||||
.and_then(OsStr::to_str)
|
||||
.map_or(Self::Python, Self::from_extension)
|
||||
Self::try_from_path(path).unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,15 @@ node_lines = (
|
||||
nodes = []
|
||||
for node_line in node_lines:
|
||||
node = node_line.split("(")[1].split(")")[0].split("::")[-1].split("<")[0]
|
||||
# `FString` and `StringLiteral` has a custom implementation while the formatting for
|
||||
# `FStringLiteralElement` and `FStringExpressionElement` are handled by the `FString`
|
||||
# `FString` has a custom implementation while the formatting for
|
||||
# `FStringLiteralElement`, `FStringFormatSpec` and `FStringExpressionElement` are handled by the `FString`
|
||||
# implementation.
|
||||
if node in (
|
||||
"FString",
|
||||
"StringLiteral",
|
||||
"FStringLiteralElement",
|
||||
"FStringExpressionElement",
|
||||
"FStringFormatSpec",
|
||||
"Identifier",
|
||||
):
|
||||
continue
|
||||
nodes.append(node)
|
||||
|
||||
@@ -307,3 +307,11 @@ hello {
|
||||
]
|
||||
} --------
|
||||
"""
|
||||
|
||||
|
||||
# Implicit concatenated f-string containing quotes
|
||||
_ = (
|
||||
'This string should change its quotes to double quotes'
|
||||
f'This string uses double quotes in an expression {"woah"}'
|
||||
f'This f-string does not use any quotes.'
|
||||
)
|
||||
|
||||
@@ -4,37 +4,17 @@ use ruff_python_ast::{AnyNodeRef, ExprStringLiteral};
|
||||
use crate::expression::parentheses::{
|
||||
in_parentheses_only_group, NeedsParentheses, OptionalParentheses,
|
||||
};
|
||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
use crate::prelude::*;
|
||||
use crate::string::{AnyString, FormatImplicitConcatenatedString};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct FormatExprStringLiteral {
|
||||
kind: ExprStringLiteralKind,
|
||||
}
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug)]
|
||||
pub enum ExprStringLiteralKind {
|
||||
#[default]
|
||||
String,
|
||||
Docstring,
|
||||
}
|
||||
|
||||
impl ExprStringLiteralKind {
|
||||
const fn string_literal_kind(self) -> StringLiteralKind {
|
||||
match self {
|
||||
ExprStringLiteralKind::String => StringLiteralKind::String,
|
||||
ExprStringLiteralKind::Docstring => StringLiteralKind::Docstring,
|
||||
}
|
||||
}
|
||||
|
||||
const fn is_docstring(self) -> bool {
|
||||
matches!(self, ExprStringLiteralKind::Docstring)
|
||||
}
|
||||
kind: StringLiteralKind,
|
||||
}
|
||||
|
||||
impl FormatRuleWithOptions<ExprStringLiteral, PyFormatContext<'_>> for FormatExprStringLiteral {
|
||||
type Options = ExprStringLiteralKind;
|
||||
type Options = StringLiteralKind;
|
||||
|
||||
fn with_options(mut self, options: Self::Options) -> Self {
|
||||
self.kind = options;
|
||||
@@ -47,9 +27,7 @@ impl FormatNodeRule<ExprStringLiteral> for FormatExprStringLiteral {
|
||||
let ExprStringLiteral { value, .. } = item;
|
||||
|
||||
match value.as_slice() {
|
||||
[string_literal] => {
|
||||
FormatStringLiteral::new(string_literal, self.kind.string_literal_kind()).fmt(f)
|
||||
}
|
||||
[string_literal] => string_literal.format().with_options(self.kind).fmt(f),
|
||||
_ => {
|
||||
// This is just a sanity check because [`DocstringStmt::try_from_statement`]
|
||||
// ensures that the docstring is a *single* string literal.
|
||||
|
||||
@@ -2935,6 +2935,42 @@ impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::TypeParamParamSpec {
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatRule<ast::StringLiteral, PyFormatContext<'_>>
|
||||
for crate::other::string_literal::FormatStringLiteral
|
||||
{
|
||||
#[inline]
|
||||
fn fmt(&self, node: &ast::StringLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
FormatNodeRule::<ast::StringLiteral>::fmt(self, node, f)
|
||||
}
|
||||
}
|
||||
impl<'ast> AsFormat<PyFormatContext<'ast>> for ast::StringLiteral {
|
||||
type Format<'a> = FormatRefWithRule<
|
||||
'a,
|
||||
ast::StringLiteral,
|
||||
crate::other::string_literal::FormatStringLiteral,
|
||||
PyFormatContext<'ast>,
|
||||
>;
|
||||
fn format(&self) -> Self::Format<'_> {
|
||||
FormatRefWithRule::new(
|
||||
self,
|
||||
crate::other::string_literal::FormatStringLiteral::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl<'ast> IntoFormat<PyFormatContext<'ast>> for ast::StringLiteral {
|
||||
type Format = FormatOwnedWithRule<
|
||||
ast::StringLiteral,
|
||||
crate::other::string_literal::FormatStringLiteral,
|
||||
PyFormatContext<'ast>,
|
||||
>;
|
||||
fn into_format(self) -> Self::Format {
|
||||
FormatOwnedWithRule::new(
|
||||
self,
|
||||
crate::other::string_literal::FormatStringLiteral::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatRule<ast::BytesLiteral, PyFormatContext<'_>>
|
||||
for crate::other::bytes_literal::FormatBytesLiteral
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@ pub struct FormatBytesLiteral;
|
||||
impl FormatNodeRule<BytesLiteral> for FormatBytesLiteral {
|
||||
fn fmt_fields(&self, item: &BytesLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
StringNormalizer::from_context(f.context())
|
||||
.with_preferred_quote_style(f.options().quote_style())
|
||||
.normalize(item.into())
|
||||
.fmt(f)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use crate::prelude::*;
|
||||
use crate::preview::{
|
||||
is_f_string_formatting_enabled, is_f_string_implicit_concatenated_string_literal_quotes_enabled,
|
||||
};
|
||||
use crate::string::{Quoting, StringNormalizer, StringQuotes};
|
||||
use ruff_formatter::write;
|
||||
use ruff_python_ast::{AnyStringFlags, FString, StringFlags};
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_formatting_enabled;
|
||||
use crate::string::{Quoting, StringNormalizer, StringQuotes};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use super::f_string_element::FormatFStringElement;
|
||||
|
||||
@@ -29,9 +31,16 @@ impl Format<PyFormatContext<'_>> for FormatFString<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let locator = f.context().locator();
|
||||
|
||||
let normalizer = StringNormalizer::from_context(f.context())
|
||||
.with_quoting(self.quoting)
|
||||
.with_preferred_quote_style(f.options().quote_style());
|
||||
// If the preview style is enabled, make the decision on what quotes to use locally for each
|
||||
// f-string instead of globally for the entire f-string expression.
|
||||
let quoting =
|
||||
if is_f_string_implicit_concatenated_string_literal_quotes_enabled(f.context()) {
|
||||
f_string_quoting(self.value, &locator)
|
||||
} else {
|
||||
self.quoting
|
||||
};
|
||||
|
||||
let normalizer = StringNormalizer::from_context(f.context()).with_quoting(quoting);
|
||||
|
||||
// If f-string formatting is disabled (not in preview), then we will
|
||||
// fall back to the previous behavior of normalizing the f-string.
|
||||
@@ -140,3 +149,20 @@ impl FStringLayout {
|
||||
matches!(self, FStringLayout::Multiline)
|
||||
}
|
||||
}
|
||||
|
||||
fn f_string_quoting(f_string: &FString, locator: &Locator) -> Quoting {
|
||||
let triple_quoted = f_string.flags.is_triple_quoted();
|
||||
|
||||
if f_string.elements.expressions().any(|expression| {
|
||||
let string_content = locator.slice(expression.range());
|
||||
if triple_quoted {
|
||||
string_content.contains(r#"""""#) || string_content.contains("'''")
|
||||
} else {
|
||||
string_content.contains(['"', '\''])
|
||||
}
|
||||
}) {
|
||||
Quoting::Preserve
|
||||
} else {
|
||||
Quoting::CanChange
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_python_ast::FStringPart;
|
||||
|
||||
use crate::other::f_string::FormatFString;
|
||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
use crate::prelude::*;
|
||||
use crate::string::Quoting;
|
||||
|
||||
@@ -25,14 +25,13 @@ impl<'a> FormatFStringPart<'a> {
|
||||
impl Format<PyFormatContext<'_>> for FormatFStringPart<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
match self.part {
|
||||
FStringPart::Literal(string_literal) => FormatStringLiteral::new(
|
||||
string_literal,
|
||||
// If an f-string part is a string literal, the f-string is always
|
||||
// implicitly concatenated e.g., `"foo" f"bar {x}"`. A standalone
|
||||
// string literal would be a string expression, not an f-string.
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(self.quoting),
|
||||
)
|
||||
.fmt(f),
|
||||
#[allow(deprecated)]
|
||||
FStringPart::Literal(string_literal) => string_literal
|
||||
.format()
|
||||
.with_options(StringLiteralKind::InImplicitlyConcatenatedFString(
|
||||
self.quoting,
|
||||
))
|
||||
.fmt(f),
|
||||
FStringPart::FString(f_string) => FormatFString::new(f_string, self.quoting).fmt(f),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,28 @@
|
||||
use ruff_formatter::FormatRuleWithOptions;
|
||||
use ruff_python_ast::StringLiteral;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::preview::is_f_string_implicit_concatenated_string_literal_quotes_enabled;
|
||||
use crate::string::{docstring, Quoting, StringNormalizer};
|
||||
use crate::QuoteStyle;
|
||||
|
||||
pub(crate) struct FormatStringLiteral<'a> {
|
||||
value: &'a StringLiteral,
|
||||
#[derive(Default)]
|
||||
pub struct FormatStringLiteral {
|
||||
layout: StringLiteralKind,
|
||||
}
|
||||
|
||||
impl<'a> FormatStringLiteral<'a> {
|
||||
pub(crate) fn new(value: &'a StringLiteral, layout: StringLiteralKind) -> Self {
|
||||
Self { value, layout }
|
||||
impl FormatRuleWithOptions<StringLiteral, PyFormatContext<'_>> for FormatStringLiteral {
|
||||
type Options = StringLiteralKind;
|
||||
|
||||
fn with_options(mut self, layout: StringLiteralKind) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of a string literal.
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
pub(crate) enum StringLiteralKind {
|
||||
pub enum StringLiteralKind {
|
||||
/// A normal string literal e.g., `"foo"`.
|
||||
#[default]
|
||||
String,
|
||||
@@ -26,6 +31,8 @@ pub(crate) enum StringLiteralKind {
|
||||
/// A string literal that is implicitly concatenated with an f-string. This
|
||||
/// makes the overall expression an f-string whose quoting detection comes
|
||||
/// from the parent node (f-string expression).
|
||||
#[deprecated]
|
||||
#[allow(private_interfaces)]
|
||||
InImplicitlyConcatenatedFString(Quoting),
|
||||
}
|
||||
|
||||
@@ -36,16 +43,28 @@ impl StringLiteralKind {
|
||||
}
|
||||
|
||||
/// Returns the quoting to be used for this string literal.
|
||||
fn quoting(self) -> Quoting {
|
||||
fn quoting(self, context: &PyFormatContext) -> Quoting {
|
||||
match self {
|
||||
StringLiteralKind::String | StringLiteralKind::Docstring => Quoting::CanChange,
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting) => quoting,
|
||||
#[allow(deprecated)]
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting) => {
|
||||
// Allow string literals to pick the "optimal" quote character
|
||||
// even if any other fstring in the implicit concatenation uses an expression
|
||||
// containing a quote character.
|
||||
// TODO: Remove StringLiteralKind::InImplicitlyConcatenatedFString when promoting
|
||||
// this style to stable and remove the layout from `AnyStringPart::String`.
|
||||
if is_f_string_implicit_concatenated_string_literal_quotes_enabled(context) {
|
||||
Quoting::CanChange
|
||||
} else {
|
||||
quoting
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for FormatStringLiteral<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
impl FormatNodeRule<StringLiteral> for FormatStringLiteral {
|
||||
fn fmt_fields(&self, item: &StringLiteral, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
let quote_style = f.options().quote_style();
|
||||
let quote_style = if self.layout.is_docstring() && !quote_style.is_preserve() {
|
||||
// Per PEP 8 and PEP 257, always prefer double quotes for docstrings,
|
||||
@@ -56,9 +75,9 @@ impl Format<PyFormatContext<'_>> for FormatStringLiteral<'_> {
|
||||
};
|
||||
|
||||
let normalized = StringNormalizer::from_context(f.context())
|
||||
.with_quoting(self.layout.quoting())
|
||||
.with_quoting(self.layout.quoting(f.context()))
|
||||
.with_preferred_quote_style(quote_style)
|
||||
.normalize(self.value.into());
|
||||
.normalize(item.into());
|
||||
|
||||
if self.layout.is_docstring() {
|
||||
docstring::format(&normalized, f)
|
||||
|
||||
@@ -19,6 +19,13 @@ pub(crate) fn is_f_string_formatting_enabled(context: &PyFormatContext) -> bool
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
/// See [#13539](https://github.com/astral-sh/ruff/pull/13539)
|
||||
pub(crate) fn is_f_string_implicit_concatenated_string_literal_quotes_enabled(
|
||||
context: &PyFormatContext,
|
||||
) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
pub(crate) fn is_with_single_item_pre_39_enabled(context: &PyFormatContext) -> bool {
|
||||
context.is_preview()
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::comments::{
|
||||
leading_comments, trailing_comments, Comments, LeadingDanglingTrailingComments,
|
||||
};
|
||||
use crate::context::{NodeLevel, TopLevelStatementPosition, WithIndentLevel, WithNodeLevel};
|
||||
use crate::expression::expr_string_literal::ExprStringLiteralKind;
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
use crate::prelude::*;
|
||||
use crate::statement::stmt_expr::FormatStmtExpr;
|
||||
use crate::verbatim::{
|
||||
@@ -850,7 +850,7 @@ impl Format<PyFormatContext<'_>> for DocstringStmt<'_> {
|
||||
.then_some(source_position(self.docstring.start())),
|
||||
string_literal
|
||||
.format()
|
||||
.with_options(ExprStringLiteralKind::Docstring),
|
||||
.with_options(StringLiteralKind::Docstring),
|
||||
f.options()
|
||||
.source_map_generation()
|
||||
.is_enabled()
|
||||
|
||||
@@ -10,9 +10,6 @@ use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::expression::expr_f_string::f_string_quoting;
|
||||
use crate::other::f_string::FormatFString;
|
||||
use crate::other::string_literal::{FormatStringLiteral, StringLiteralKind};
|
||||
use crate::prelude::*;
|
||||
use crate::string::Quoting;
|
||||
|
||||
/// Represents any kind of string expression. This could be either a string,
|
||||
@@ -46,6 +43,10 @@ impl<'a> AnyString<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn is_fstring(self) -> bool {
|
||||
matches!(self, Self::FString(_))
|
||||
}
|
||||
|
||||
/// Returns the quoting to be used for this string.
|
||||
pub(super) fn quoting(self, locator: &Locator<'_>) -> Quoting {
|
||||
match self {
|
||||
@@ -54,23 +55,21 @@ impl<'a> AnyString<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a vector of all the [`AnyStringPart`] of this string.
|
||||
pub(super) fn parts(self, quoting: Quoting) -> AnyStringPartsIter<'a> {
|
||||
/// Returns an iterator over the [`AnyStringPart`]s of this string.
|
||||
pub(super) fn parts(self) -> AnyStringPartsIter<'a> {
|
||||
match self {
|
||||
Self::String(ExprStringLiteral { value, .. }) => {
|
||||
AnyStringPartsIter::String(value.iter())
|
||||
}
|
||||
Self::Bytes(ExprBytesLiteral { value, .. }) => AnyStringPartsIter::Bytes(value.iter()),
|
||||
Self::FString(ExprFString { value, .. }) => {
|
||||
AnyStringPartsIter::FString(value.iter(), quoting)
|
||||
}
|
||||
Self::FString(ExprFString { value, .. }) => AnyStringPartsIter::FString(value.iter()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_multiline(self, source: &str) -> bool {
|
||||
match self {
|
||||
AnyString::String(_) | AnyString::Bytes(_) => {
|
||||
self.parts(Quoting::default())
|
||||
self.parts()
|
||||
.next()
|
||||
.is_some_and(|part| part.flags().is_triple_quoted())
|
||||
&& memchr2(b'\n', b'\r', source[self.range()].as_bytes()).is_some()
|
||||
@@ -139,7 +138,7 @@ impl<'a> From<&'a ExprFString> for AnyString<'a> {
|
||||
pub(super) enum AnyStringPartsIter<'a> {
|
||||
String(std::slice::Iter<'a, StringLiteral>),
|
||||
Bytes(std::slice::Iter<'a, ast::BytesLiteral>),
|
||||
FString(std::slice::Iter<'a, ast::FStringPart>, Quoting),
|
||||
FString(std::slice::Iter<'a, ast::FStringPart>),
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AnyStringPartsIter<'a> {
|
||||
@@ -147,27 +146,12 @@ impl<'a> Iterator for AnyStringPartsIter<'a> {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let part = match self {
|
||||
Self::String(inner) => {
|
||||
let part = inner.next()?;
|
||||
AnyStringPart::String {
|
||||
part,
|
||||
layout: StringLiteralKind::String,
|
||||
}
|
||||
}
|
||||
Self::String(inner) => AnyStringPart::String(inner.next()?),
|
||||
Self::Bytes(inner) => AnyStringPart::Bytes(inner.next()?),
|
||||
Self::FString(inner, quoting) => {
|
||||
let part = inner.next()?;
|
||||
match part {
|
||||
ast::FStringPart::Literal(string_literal) => AnyStringPart::String {
|
||||
part: string_literal,
|
||||
layout: StringLiteralKind::InImplicitlyConcatenatedFString(*quoting),
|
||||
},
|
||||
ast::FStringPart::FString(f_string) => AnyStringPart::FString {
|
||||
part: f_string,
|
||||
quoting: *quoting,
|
||||
},
|
||||
}
|
||||
}
|
||||
Self::FString(inner) => match inner.next()? {
|
||||
ast::FStringPart::Literal(string_literal) => AnyStringPart::String(string_literal),
|
||||
ast::FStringPart::FString(f_string) => AnyStringPart::FString(f_string),
|
||||
},
|
||||
};
|
||||
|
||||
Some(part)
|
||||
@@ -182,23 +166,17 @@ impl FusedIterator for AnyStringPartsIter<'_> {}
|
||||
/// This is constructed from the [`AnyString::parts`] method on [`AnyString`].
|
||||
#[derive(Clone, Debug)]
|
||||
pub(super) enum AnyStringPart<'a> {
|
||||
String {
|
||||
part: &'a ast::StringLiteral,
|
||||
layout: StringLiteralKind,
|
||||
},
|
||||
String(&'a ast::StringLiteral),
|
||||
Bytes(&'a ast::BytesLiteral),
|
||||
FString {
|
||||
part: &'a ast::FString,
|
||||
quoting: Quoting,
|
||||
},
|
||||
FString(&'a ast::FString),
|
||||
}
|
||||
|
||||
impl AnyStringPart<'_> {
|
||||
fn flags(&self) -> AnyStringFlags {
|
||||
match self {
|
||||
Self::String { part, .. } => part.flags.into(),
|
||||
Self::String(part) => part.flags.into(),
|
||||
Self::Bytes(bytes_literal) => bytes_literal.flags.into(),
|
||||
Self::FString { part, .. } => part.flags.into(),
|
||||
Self::FString(part) => part.flags.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -206,9 +184,9 @@ impl AnyStringPart<'_> {
|
||||
impl<'a> From<&AnyStringPart<'a>> for AnyNodeRef<'a> {
|
||||
fn from(value: &AnyStringPart<'a>) -> Self {
|
||||
match value {
|
||||
AnyStringPart::String { part, .. } => AnyNodeRef::StringLiteral(part),
|
||||
AnyStringPart::String(part) => AnyNodeRef::StringLiteral(part),
|
||||
AnyStringPart::Bytes(part) => AnyNodeRef::BytesLiteral(part),
|
||||
AnyStringPart::FString { part, .. } => AnyNodeRef::FString(part),
|
||||
AnyStringPart::FString(part) => AnyNodeRef::FString(part),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,21 +194,9 @@ impl<'a> From<&AnyStringPart<'a>> for AnyNodeRef<'a> {
|
||||
impl Ranged for AnyStringPart<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::String { part, .. } => part.range(),
|
||||
Self::String(part) => part.range(),
|
||||
Self::Bytes(part) => part.range(),
|
||||
Self::FString { part, .. } => part.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Format<PyFormatContext<'_>> for AnyStringPart<'_> {
|
||||
fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> {
|
||||
match self {
|
||||
AnyStringPart::String { part, layout } => {
|
||||
FormatStringLiteral::new(part, *layout).fmt(f)
|
||||
}
|
||||
AnyStringPart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
|
||||
AnyStringPart::FString { part, quoting } => FormatFString::new(part, *quoting).fmt(f),
|
||||
Self::FString(part) => part.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::comments::{leading_comments, trailing_comments};
|
||||
use crate::expression::parentheses::in_parentheses_only_soft_line_break_or_space;
|
||||
use crate::other::f_string::FormatFString;
|
||||
use crate::other::string_literal::StringLiteralKind;
|
||||
use crate::prelude::*;
|
||||
use crate::string::any::AnyStringPart;
|
||||
use crate::QuoteStyle;
|
||||
|
||||
mod any;
|
||||
@@ -46,12 +49,28 @@ impl Format<PyFormatContext<'_>> for FormatImplicitConcatenatedString<'_> {
|
||||
|
||||
let mut joiner = f.join_with(in_parentheses_only_soft_line_break_or_space());
|
||||
|
||||
for part in self.string.parts(quoting) {
|
||||
for part in self.string.parts() {
|
||||
let part_comments = comments.leading_dangling_trailing(&part);
|
||||
|
||||
let format_part = format_with(|f: &mut PyFormatter| match part {
|
||||
AnyStringPart::String(part) => {
|
||||
let kind = if self.string.is_fstring() {
|
||||
#[allow(deprecated)]
|
||||
StringLiteralKind::InImplicitlyConcatenatedFString(quoting)
|
||||
} else {
|
||||
StringLiteralKind::String
|
||||
};
|
||||
|
||||
part.format().with_options(kind).fmt(f)
|
||||
}
|
||||
AnyStringPart::Bytes(bytes_literal) => bytes_literal.format().fmt(f),
|
||||
AnyStringPart::FString(part) => FormatFString::new(part, quoting).fmt(f),
|
||||
});
|
||||
|
||||
joiner.entry(&format_args![
|
||||
line_suffix_boundary(),
|
||||
leading_comments(part_comments.leading),
|
||||
part,
|
||||
format_part,
|
||||
trailing_comments(part_comments.trailing)
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ impl<'a, 'src> StringNormalizer<'a, 'src> {
|
||||
pub(crate) fn from_context(context: &'a PyFormatContext<'src>) -> Self {
|
||||
Self {
|
||||
quoting: Quoting::default(),
|
||||
preferred_quote_style: QuoteStyle::default(),
|
||||
preferred_quote_style: context.options().quote_style(),
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -313,6 +313,14 @@ hello {
|
||||
]
|
||||
} --------
|
||||
"""
|
||||
|
||||
|
||||
# Implicit concatenated f-string containing quotes
|
||||
_ = (
|
||||
'This string should change its quotes to double quotes'
|
||||
f'This string uses double quotes in an expression {"woah"}'
|
||||
f'This f-string does not use any quotes.'
|
||||
)
|
||||
```
|
||||
|
||||
## Outputs
|
||||
@@ -649,6 +657,14 @@ hello {
|
||||
]
|
||||
} --------
|
||||
"""
|
||||
|
||||
|
||||
# Implicit concatenated f-string containing quotes
|
||||
_ = (
|
||||
"This string should change its quotes to double quotes"
|
||||
f'This string uses double quotes in an expression {"woah"}'
|
||||
f"This f-string does not use any quotes."
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -973,6 +989,14 @@ hello {
|
||||
]
|
||||
} --------
|
||||
"""
|
||||
|
||||
|
||||
# Implicit concatenated f-string containing quotes
|
||||
_ = (
|
||||
'This string should change its quotes to double quotes'
|
||||
f'This string uses double quotes in an expression {"woah"}'
|
||||
f'This f-string does not use any quotes.'
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -1279,7 +1303,7 @@ hello {
|
||||
# comment 27
|
||||
# comment 28
|
||||
} woah {x}"
|
||||
@@ -287,19 +299,19 @@
|
||||
@@ -287,27 +299,27 @@
|
||||
if indent2:
|
||||
foo = f"""hello world
|
||||
hello {
|
||||
@@ -1314,4 +1338,14 @@ hello {
|
||||
+ ]
|
||||
+ } --------
|
||||
"""
|
||||
|
||||
|
||||
# Implicit concatenated f-string containing quotes
|
||||
_ = (
|
||||
- 'This string should change its quotes to double quotes'
|
||||
+ "This string should change its quotes to double quotes"
|
||||
f'This string uses double quotes in an expression {"woah"}'
|
||||
- f'This f-string does not use any quotes.'
|
||||
+ f"This f-string does not use any quotes."
|
||||
)
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ use ruff_python_stdlib::typing::{
|
||||
};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::analyze::type_inference::{PythonType, ResolvedPythonType};
|
||||
use crate::analyze::type_inference::{NumberLike, PythonType, ResolvedPythonType};
|
||||
use crate::model::SemanticModel;
|
||||
use crate::{Binding, BindingKind, Modules};
|
||||
|
||||
@@ -576,7 +576,7 @@ trait BuiltinTypeChecker {
|
||||
/// Builtin type name.
|
||||
const BUILTIN_TYPE_NAME: &'static str;
|
||||
/// Type name as found in the `Typing` module.
|
||||
const TYPING_NAME: &'static str;
|
||||
const TYPING_NAME: Option<&'static str>;
|
||||
/// [`PythonType`] associated with the intended type.
|
||||
const EXPR_TYPE: PythonType;
|
||||
|
||||
@@ -584,7 +584,7 @@ trait BuiltinTypeChecker {
|
||||
fn match_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
|
||||
let value = map_subscript(annotation);
|
||||
semantic.match_builtin_expr(value, Self::BUILTIN_TYPE_NAME)
|
||||
|| semantic.match_typing_expr(value, Self::TYPING_NAME)
|
||||
|| Self::TYPING_NAME.is_some_and(|name| semantic.match_typing_expr(value, name))
|
||||
}
|
||||
|
||||
/// Check initializer expression to match the intended type.
|
||||
@@ -624,7 +624,7 @@ struct ListChecker;
|
||||
|
||||
impl BuiltinTypeChecker for ListChecker {
|
||||
const BUILTIN_TYPE_NAME: &'static str = "list";
|
||||
const TYPING_NAME: &'static str = "List";
|
||||
const TYPING_NAME: Option<&'static str> = Some("List");
|
||||
const EXPR_TYPE: PythonType = PythonType::List;
|
||||
}
|
||||
|
||||
@@ -632,7 +632,7 @@ struct DictChecker;
|
||||
|
||||
impl BuiltinTypeChecker for DictChecker {
|
||||
const BUILTIN_TYPE_NAME: &'static str = "dict";
|
||||
const TYPING_NAME: &'static str = "Dict";
|
||||
const TYPING_NAME: Option<&'static str> = Some("Dict");
|
||||
const EXPR_TYPE: PythonType = PythonType::Dict;
|
||||
}
|
||||
|
||||
@@ -640,7 +640,7 @@ struct SetChecker;
|
||||
|
||||
impl BuiltinTypeChecker for SetChecker {
|
||||
const BUILTIN_TYPE_NAME: &'static str = "set";
|
||||
const TYPING_NAME: &'static str = "Set";
|
||||
const TYPING_NAME: Option<&'static str> = Some("Set");
|
||||
const EXPR_TYPE: PythonType = PythonType::Set;
|
||||
}
|
||||
|
||||
@@ -648,10 +648,18 @@ struct TupleChecker;
|
||||
|
||||
impl BuiltinTypeChecker for TupleChecker {
|
||||
const BUILTIN_TYPE_NAME: &'static str = "tuple";
|
||||
const TYPING_NAME: &'static str = "Tuple";
|
||||
const TYPING_NAME: Option<&'static str> = Some("Tuple");
|
||||
const EXPR_TYPE: PythonType = PythonType::Tuple;
|
||||
}
|
||||
|
||||
struct IntChecker;
|
||||
|
||||
impl BuiltinTypeChecker for IntChecker {
|
||||
const BUILTIN_TYPE_NAME: &'static str = "int";
|
||||
const TYPING_NAME: Option<&'static str> = None;
|
||||
const EXPR_TYPE: PythonType = PythonType::Number(NumberLike::Integer);
|
||||
}
|
||||
|
||||
pub struct IoBaseChecker;
|
||||
|
||||
impl TypeChecker for IoBaseChecker {
|
||||
@@ -761,6 +769,11 @@ pub fn is_dict(binding: &Binding, semantic: &SemanticModel) -> bool {
|
||||
check_type::<DictChecker>(binding, semantic)
|
||||
}
|
||||
|
||||
/// Test whether the given binding can be considered an integer.
|
||||
pub fn is_int(binding: &Binding, semantic: &SemanticModel) -> bool {
|
||||
check_type::<IntChecker>(binding, semantic)
|
||||
}
|
||||
|
||||
/// Test whether the given binding can be considered a set.
|
||||
///
|
||||
/// For this, we check what value might be associated with it through it's initialization and
|
||||
|
||||
@@ -5,8 +5,7 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_python_ast::helpers::from_relative_import;
|
||||
use ruff_python_ast::name::{QualifiedName, UnqualifiedName};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, Stmt};
|
||||
use ruff_python_stdlib::path::is_python_stub_file;
|
||||
use ruff_python_ast::{self as ast, Expr, ExprContext, Operator, PySourceType, Stmt};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::binding::{
|
||||
@@ -2246,7 +2245,7 @@ bitflags! {
|
||||
|
||||
impl SemanticModelFlags {
|
||||
pub fn new(path: &Path) -> Self {
|
||||
if is_python_stub_file(path) {
|
||||
if PySourceType::from(path).is_stub() {
|
||||
Self::STUB_FILE
|
||||
} else {
|
||||
Self::default()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
|
||||
/// Return `true` if the [`Path`] is named `pyproject.toml`.
|
||||
@@ -6,38 +7,10 @@ pub fn is_pyproject_toml(path: &Path) -> bool {
|
||||
.is_some_and(|name| name == "pyproject.toml")
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Path`] appears to be that of a Python interface definition file (`.pyi`).
|
||||
pub fn is_python_stub_file(path: &Path) -> bool {
|
||||
path.extension().is_some_and(|ext| ext == "pyi")
|
||||
}
|
||||
|
||||
/// Return `true` if the [`Path`] appears to be that of a Jupyter notebook (`.ipynb`).
|
||||
pub fn is_jupyter_notebook(path: &Path) -> bool {
|
||||
path.extension().is_some_and(|ext| ext == "ipynb")
|
||||
}
|
||||
|
||||
/// Return `true` if a [`Path`] should use the name of its parent directory as its module name.
|
||||
pub fn is_module_file(path: &Path) -> bool {
|
||||
path.file_name().is_some_and(|file_name| {
|
||||
file_name == "__init__.py"
|
||||
|| file_name == "__init__.pyi"
|
||||
|| file_name == "__main__.py"
|
||||
|| file_name == "__main__.pyi"
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
||||
use crate::path::is_jupyter_notebook;
|
||||
|
||||
#[test]
|
||||
fn test_is_jupyter_notebook() {
|
||||
let path = Path::new("foo/bar/baz.ipynb");
|
||||
assert!(is_jupyter_notebook(path));
|
||||
|
||||
let path = Path::new("foo/bar/baz.py");
|
||||
assert!(!is_jupyter_notebook(path));
|
||||
}
|
||||
matches!(
|
||||
path.file_name().and_then(OsStr::to_str),
|
||||
Some("__init__.py" | "__init__.pyi" | "__main__.py" | "__main__.pyi")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ impl CommentRanges {
|
||||
}
|
||||
|
||||
/// Returns `true` if a comment is an own-line comment (as opposed to an end-of-line comment).
|
||||
fn is_own_line(offset: TextSize, locator: &Locator) -> bool {
|
||||
pub fn is_own_line(offset: TextSize, locator: &Locator) -> bool {
|
||||
let range = TextRange::new(locator.line_start(offset), offset);
|
||||
locator.slice(range).chars().all(is_python_whitespace)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::sync::Arc;
|
||||
use {crate::TextRange, crate::TextSize, std::convert::TryInto};
|
||||
|
||||
use priv_in_pub::Sealed;
|
||||
@@ -66,3 +67,12 @@ where
|
||||
T::range(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Ranged for Arc<T>
|
||||
where
|
||||
T: Ranged,
|
||||
{
|
||||
fn range(&self) -> TextRange {
|
||||
T::range(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,7 +796,7 @@ pub struct LintCommonOptions {
|
||||
)]
|
||||
pub typing_modules: Option<Vec<String>>,
|
||||
|
||||
/// A list of modules which is allowed even thought they are not used
|
||||
/// A list of modules which is allowed even though they are not used
|
||||
/// in the code.
|
||||
///
|
||||
/// This is useful when a module has a side effect when imported.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PyYAML==6.0.2
|
||||
ruff==0.6.8
|
||||
ruff==0.6.9
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@38c0b8187325c3bab386b666daf3518ac036f2f4
|
||||
mkdocs-redirects==1.2.1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PyYAML==6.0.2
|
||||
ruff==0.6.8
|
||||
ruff==0.6.9
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.1.18
|
||||
mkdocs-redirects==1.2.1
|
||||
|
||||
55
playground/api/package-lock.json
generated
55
playground/api/package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"@cloudflare/workers-types": "^4.20230801.0",
|
||||
"miniflare": "^3.20230801.1",
|
||||
"typescript": "^5.1.6",
|
||||
"wrangler": "3.78.12"
|
||||
"wrangler": "3.80.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/kv-asset-handler": {
|
||||
@@ -132,9 +132,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/workers-types": {
|
||||
"version": "4.20240925.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240925.0.tgz",
|
||||
"integrity": "sha512-KpqyRWvanEuXgBTKYFzRp4NsWOEcswxjsPRSre1zYQcODmc8PUrraVHQUmgvkJgv3FzB+vI9xm7J6oE4MmZHCA==",
|
||||
"version": "4.20241004.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20241004.0.tgz",
|
||||
"integrity": "sha512-3LrPvtecs4umknOF1bTPNLHUG/ZjeSE6PYBQ/tbO7lwaVhjZTaTugiaCny2byrZupBlVNuubQVktcAgMfw0C1A==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0"
|
||||
},
|
||||
@@ -559,20 +559,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@miniflare/kv": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@miniflare/kv/-/kv-2.14.2.tgz",
|
||||
"integrity": "sha512-3rs4cJOGACT/U7NH7j4KD29ugXRYUiM0aGkvOEdFQtChXLsYClJljXpezTfJJxBwZjdS4F2UFTixtFcHp74UfA==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@miniflare/kv/-/kv-2.14.4.tgz",
|
||||
"integrity": "sha512-QlERH0Z+klwLg0xw+/gm2yC34Nnr/I0GcQ+ASYqXeIXBwjqOtMBa3YVQnocaD+BPy/6TUtSpOAShHsEj76R2uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@miniflare/shared": "2.14.2"
|
||||
"@miniflare/shared": "2.14.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@miniflare/kv/node_modules/@miniflare/shared": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@miniflare/shared/-/shared-2.14.2.tgz",
|
||||
"integrity": "sha512-dDnYIztz10zDQjaFJ8Gy9UaaBWZkw3NyhFdpX6tAeyPA/2lGvkftc42MYmNi8s5ljqkZAtKgWAJnSf2K75NCJw==",
|
||||
"node_modules/@miniflare/shared": {
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@miniflare/shared/-/shared-2.14.4.tgz",
|
||||
"integrity": "sha512-upl4RSB3hyCnITOFmRZjJj4A72GmkVrtfZTilkdq5Qe5TTlzsjVeDJp7AuNUM9bM8vswRo+N5jOiot6O4PVwwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"kleur": "^4.1.4",
|
||||
@@ -584,25 +586,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@miniflare/storage-memory": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@miniflare/storage-memory/-/storage-memory-2.14.2.tgz",
|
||||
"integrity": "sha512-9Wtz9mayHIY0LDsfpMGx+/sfKCq3eAQJzYY+ju1tTEaKR0sVAuO51wu0wbyldsjj9OcBcd2X32iPbIa7KcSZQQ==",
|
||||
"version": "2.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@miniflare/storage-memory/-/storage-memory-2.14.4.tgz",
|
||||
"integrity": "sha512-9jB5BqNkMZ3SFjbPFeiVkLi1BuSahMhc/W1Y9H0W89qFDrrD+z7EgRgDtHTG1ZRyi9gIlNtt9qhkO1B6W2qb2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@miniflare/shared": "2.14.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@miniflare/storage-memory/node_modules/@miniflare/shared": {
|
||||
"version": "2.14.2",
|
||||
"resolved": "https://registry.npmjs.org/@miniflare/shared/-/shared-2.14.2.tgz",
|
||||
"integrity": "sha512-dDnYIztz10zDQjaFJ8Gy9UaaBWZkw3NyhFdpX6tAeyPA/2lGvkftc42MYmNi8s5ljqkZAtKgWAJnSf2K75NCJw==",
|
||||
"dependencies": {
|
||||
"@types/better-sqlite3": "^7.6.0",
|
||||
"kleur": "^4.1.4",
|
||||
"npx-import": "^1.1.4",
|
||||
"picomatch": "^2.3.1"
|
||||
"@miniflare/shared": "2.14.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
@@ -1585,9 +1574,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wrangler": {
|
||||
"version": "3.78.12",
|
||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.78.12.tgz",
|
||||
"integrity": "sha512-a/xk/N04IvOGk9J+BLkiFg42GDyPS+0BiJimbrHsbX+CDr8Iqq3HNMEyQld+6zbmq01u/gmc8S7GKVR9vDx4+g==",
|
||||
"version": "3.80.0",
|
||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.80.0.tgz",
|
||||
"integrity": "sha512-c9aN7Buf7XgTPpQkw1d0VjNRI4qg3m35PTg70Tg4mOeHwHGjsd74PAsP1G8BjpdqDjfWtsua7tj1g4M3/5dYNQ==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"@cloudflare/workers-types": "^4.20230801.0",
|
||||
"miniflare": "^3.20230801.1",
|
||||
"typescript": "^5.1.6",
|
||||
"wrangler": "3.78.12"
|
||||
"wrangler": "3.80.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
113
playground/package-lock.json
generated
113
playground/package-lock.json
generated
@@ -1129,9 +1129,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.10.tgz",
|
||||
"integrity": "sha512-02sAAlBnP39JgXwkAq3PeU9DVaaGpZyF3MGcC0MKgQVkZor5IiiDAipVaxQHtDJAmO4GIy/rVBy/LzVj76Cyqg==",
|
||||
"version": "18.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz",
|
||||
"integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1149,17 +1149,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.7.0.tgz",
|
||||
"integrity": "sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.0.tgz",
|
||||
"integrity": "sha512-wORFWjU30B2WJ/aXBfOm1LX9v9nyt9D3jsSOxC3cCaTQGCW5k4jNpmjFv3U7p/7s4yvdjHzwtv2Sd2dOyhjS0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/type-utils": "8.7.0",
|
||||
"@typescript-eslint/utils": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.8.0",
|
||||
"@typescript-eslint/type-utils": "8.8.0",
|
||||
"@typescript-eslint/utils": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1183,16 +1183,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.7.0.tgz",
|
||||
"integrity": "sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.0.tgz",
|
||||
"integrity": "sha512-uEFUsgR+tl8GmzmLjRqz+VrDv4eoaMqMXW7ruXfgThaAShO9JTciKpEsB+TvnfFfbg5IpujgMXVV36gOJRLtZg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.8.0",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/typescript-estree": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1212,14 +1212,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.7.0.tgz",
|
||||
"integrity": "sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.0.tgz",
|
||||
"integrity": "sha512-EL8eaGC6gx3jDd8GwEFEV091210U97J0jeEHrAYvIYosmEGet4wJ+g0SYmLu+oRiAwbSA5AVrt6DxLHfdd+bUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0"
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1230,14 +1230,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.7.0.tgz",
|
||||
"integrity": "sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.0.tgz",
|
||||
"integrity": "sha512-IKwJSS7bCqyCeG4NVGxnOP6lLT9Okc3Zj8hLO96bpMkJab+10HIfJbMouLrlpyOr3yrQ1cA413YPFiGd1mW9/Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.7.0",
|
||||
"@typescript-eslint/utils": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.8.0",
|
||||
"@typescript-eslint/utils": "8.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1255,9 +1255,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.7.0.tgz",
|
||||
"integrity": "sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.0.tgz",
|
||||
"integrity": "sha512-QJwc50hRCgBd/k12sTykOJbESe1RrzmX6COk8Y525C9l7oweZ+1lw9JiU56im7Amm8swlz00DRIlxMYLizr2Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1269,14 +1269,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.7.0.tgz",
|
||||
"integrity": "sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.0.tgz",
|
||||
"integrity": "sha512-ZaMJwc/0ckLz5DaAZ+pNLmHv8AMVGtfWxZe/x2JVEkD5LnmhWiQMMcYT7IY7gkdJuzJ9P14fRy28lUrlDSWYdw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/visitor-keys": "8.7.0",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/visitor-keys": "8.8.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1324,16 +1324,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.7.0.tgz",
|
||||
"integrity": "sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.0.tgz",
|
||||
"integrity": "sha512-QE2MgfOTem00qrlPgyByaCHay9yb1+9BjnMFnSFkUKQfu7adBXDTnCAivURnuPPAG/qiB+kzKkZKmKfaMT0zVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.7.0",
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/typescript-estree": "8.7.0"
|
||||
"@typescript-eslint/scope-manager": "8.8.0",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"@typescript-eslint/typescript-estree": "8.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1347,13 +1347,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.7.0.tgz",
|
||||
"integrity": "sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==",
|
||||
"version": "8.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.0.tgz",
|
||||
"integrity": "sha512-8mq51Lx6Hpmd7HnA2fcHQo3YgfX1qbccxQOgZcb4tvasu//zXRaA1j5ZRFeCw/VRAdFi4mRM9DnZw0Nu0Q2d1g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.7.0",
|
||||
"@typescript-eslint/types": "8.8.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2398,9 +2398,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz",
|
||||
"integrity": "sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==",
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz",
|
||||
"integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2425,9 +2425,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz",
|
||||
"integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==",
|
||||
"version": "2.31.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2439,7 +2439,7 @@
|
||||
"debug": "^3.2.7",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"eslint-module-utils": "^2.9.0",
|
||||
"eslint-module-utils": "^2.12.0",
|
||||
"hasown": "^2.0.2",
|
||||
"is-core-module": "^2.15.1",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -2448,13 +2448,14 @@
|
||||
"object.groupby": "^1.0.3",
|
||||
"object.values": "^1.2.0",
|
||||
"semver": "^6.3.1",
|
||||
"string.prototype.trimend": "^1.0.8",
|
||||
"tsconfig-paths": "^3.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
|
||||
"eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import/node_modules/debug": {
|
||||
@@ -2491,9 +2492,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.37.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.0.tgz",
|
||||
"integrity": "sha512-IHBePmfWH5lKhJnJ7WB1V+v/GolbB0rjS8XYVCSQCZKaQCAUhMoVoOEn1Ef8Z8Wf0a7l8KTJvuZg5/e4qrZ6nA==",
|
||||
"version": "7.37.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz",
|
||||
"integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
||||
name = "ruff-ecosystem"
|
||||
version = "0.0.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["unidiff==0.7.5", "tomli_w==1.0.0", "tomli==2.0.1"]
|
||||
dependencies = ["unidiff==0.7.5", "tomli_w==1.0.0", "tomli==2.0.2"]
|
||||
|
||||
[project.scripts]
|
||||
ruff-ecosystem = "ruff_ecosystem.cli:entrypoint"
|
||||
|
||||
5
ruff.schema.json
generated
5
ruff.schema.json
generated
@@ -17,7 +17,7 @@
|
||||
}
|
||||
},
|
||||
"allowed-unused-imports": {
|
||||
"description": "A list of modules which is allowed even thought they are not used in the code.\n\nThis is useful when a module has a side effect when imported.",
|
||||
"description": "A list of modules which is allowed even though they are not used in the code.\n\nThis is useful when a module has a side effect when imported.",
|
||||
"deprecated": true,
|
||||
"type": [
|
||||
"array",
|
||||
@@ -1899,7 +1899,7 @@
|
||||
}
|
||||
},
|
||||
"allowed-unused-imports": {
|
||||
"description": "A list of modules which is allowed even thought they are not used in the code.\n\nThis is useful when a module has a side effect when imported.",
|
||||
"description": "A list of modules which is allowed even though they are not used in the code.\n\nThis is useful when a module has a side effect when imported.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
@@ -3279,6 +3279,7 @@
|
||||
"FURB15",
|
||||
"FURB152",
|
||||
"FURB154",
|
||||
"FURB156",
|
||||
"FURB157",
|
||||
"FURB16",
|
||||
"FURB161",
|
||||
|
||||
Reference in New Issue
Block a user