diff --git a/.cargo/config.toml b/.cargo/config.toml index f88d75d01d..d6ef99b66f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,6 @@ [alias] dev = "run --package ruff_dev --bin ruff_dev" -benchmark = "bench -p ruff_benchmark --" +benchmark = "bench -p ruff_benchmark --bench linter --bench formatter --" [target.'cfg(all())'] rustflags = [ diff --git a/Cargo.lock b/Cargo.lock index 3df63e6e02..861a87f509 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -188,9 +194,9 @@ checksum = "6776fc96284a0bb647b615056fc496d1fe1644a7ab01829818a6d91cae888b84" [[package]] name = "bstr" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" dependencies = [ "memchr", "once_cell", @@ -200,9 +206,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.2" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "cachedir" @@ -242,13 +248,13 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "time", "wasm-bindgen", @@ -284,21 +290,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.25" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" -dependencies = [ - "bitflags 1.3.2", - "clap_lex 0.2.4", - "indexmap", - "textwrap", -] - -[[package]] -name = "clap" -version = "4.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d21f9bf1b425d2968943631ec91202fe5e837264063503708b83013f8fc938" +checksum = "b4ed2379f8603fa2b7509891660e802b88c70a79a6427a70abb5968054de2c28" dependencies = [ "clap_builder", "clap_derive", @@ -307,24 +301,24 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.2.7" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "914c8c79fb560f238ef6429439a30023c862f7a28e688c58f7203f12b29970bd" +checksum = "72394f3339a76daf211e57d4bcb374410f3965dcc606dd0e03738c7888766980" dependencies = [ "anstream", "anstyle", "bitflags 1.3.2", - "clap_lex 0.4.1", + "clap_lex", "strsim", ] [[package]] name = "clap_complete" -version = "4.2.3" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1594fe2312ec4abf402076e407628f5c313e54c32ade058521df4ee34ecac8a8" +checksum = "7f6b5c519bab3ea61843a7923d074b04245624bb84a64a8c150f5deb014e388b" dependencies = [ - "clap 4.2.7", + "clap", ] [[package]] @@ -333,7 +327,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "183495371ea78d4c9ff638bfc6497d46fed2396e4f9c50aebc1278a4a9919a3d" dependencies = [ - "clap 4.2.7", + "clap", "clap_complete", "clap_complete_fig", "clap_complete_nushell", @@ -341,50 +335,41 @@ dependencies = [ [[package]] name = "clap_complete_fig" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3af28956330989baa428ed4d3471b853715d445c62de21b67292e22cf8a41fa" +checksum = "99fee1d30a51305a6c2ed3fc5709be3c8af626c9c958e04dd9ae94e27bcbce9f" dependencies = [ - "clap 4.2.7", + "clap", "clap_complete", ] [[package]] name = "clap_complete_nushell" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fa41f5e6aa83bd151b70fd0ceaee703d68cd669522795dc812df9edad1252c" +checksum = "5d02bc8b1a18ee47c4d2eec3fb5ac034dc68ebea6125b1509e9ccdffcddce66e" dependencies = [ - "clap 4.2.7", + "clap", "clap_complete", ] [[package]] name = "clap_derive" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +checksum = "59e9ef9a08ee1c0e1f2e162121665ac45ac3783b0f897db7244ae75ad9a8f65b" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "clap_lex" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" [[package]] name = "clearscreen" @@ -424,14 +409,14 @@ checksum = "5458d9d1a587efaf5091602c59d299696a3877a439c8f6d461a2d3cce11df87a" [[package]] name = "console" -version = "0.15.5" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" dependencies = [ "encode_unicode", "lazy_static", "libc", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -477,19 +462,19 @@ dependencies = [ [[package]] name = "criterion" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" dependencies = [ "anes", - "atty", "cast", "ciborium", - "clap 3.2.25", + "clap", "criterion-plot", + "is-terminal", "itertools", - "lazy_static", "num-traits", + "once_cell", "oorandom", "plotters", "rayon", @@ -709,7 +694,7 @@ name = "flake8-to-ruff" version = "0.0.270" dependencies = [ "anyhow", - "clap 4.2.7", + "clap", "colored", "configparser", "once_cell", @@ -950,9 +935,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", @@ -1001,9 +986,9 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "js-sys" -version = "0.3.62" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" +checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" dependencies = [ "wasm-bindgen", ] @@ -1118,18 +1103,15 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "log" -version = "0.4.17" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de" [[package]] name = "matches" @@ -1178,14 +1160,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1230,9 +1212,9 @@ dependencies = [ [[package]] name = "notify" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ea850aa68a06e48fdb069c0ec44d0d64c8dbffa49bf3b6f7f0a901fdea1ba9" +checksum = "729f63e1ca555a43fe3efa4f3efdf4801c479da85b432242a7b726f353c88486" dependencies = [ "bitflags 1.3.2", "crossbeam-channel", @@ -1243,7 +1225,7 @@ dependencies = [ "libc", "mio", "walkdir", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] @@ -1288,9 +1270,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" [[package]] name = "oorandom" @@ -1563,9 +1545,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] @@ -1608,9 +1590,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -1683,9 +1665,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390" dependencies = [ "aho-corasick 1.0.1", "memchr", @@ -1700,9 +1682,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "result-like" @@ -1749,7 +1731,7 @@ dependencies = [ "anyhow", "bitflags 2.3.1", "chrono", - "clap 4.2.7", + "clap", "colored", "dirs 5.0.1", "fern", @@ -1814,6 +1796,7 @@ dependencies = [ "once_cell", "ruff", "ruff_python_ast", + "ruff_python_formatter", "rustpython-parser", "serde", "serde_json", @@ -1846,7 +1829,7 @@ dependencies = [ "bitflags 2.3.1", "cachedir", "chrono", - "clap 4.2.7", + "clap", "clap_complete_command", "clearscreen", "colored", @@ -1885,7 +1868,7 @@ name = "ruff_dev" version = "0.0.0" dependencies = [ "anyhow", - "clap 4.2.7", + "clap", "itertools", "libcst", "once_cell", @@ -1944,7 +1927,7 @@ dependencies = [ "proc-macro2", "quote", "ruff_textwrap", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -1984,7 +1967,7 @@ name = "ruff_python_formatter" version = "0.0.0" dependencies = [ "anyhow", - "clap 4.2.7", + "clap", "countme", "insta", "is-macro", @@ -2041,7 +2024,7 @@ dependencies = [ "glob", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2290,7 +2273,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2318,9 +2301,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" dependencies = [ "serde", ] @@ -2405,9 +2388,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" dependencies = [ "proc-macro2", "quote", @@ -2490,12 +2473,6 @@ dependencies = [ "test-case-core", ] -[[package]] -name = "textwrap" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" - [[package]] name = "thiserror" version = "1.0.40" @@ -2513,7 +2490,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2593,9 +2570,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "toml" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" dependencies = [ "serde", "serde_spanned", @@ -2605,18 +2582,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" dependencies = [ "indexmap", "serde", @@ -2646,7 +2623,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", ] [[package]] @@ -2736,9 +2713,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -2805,9 +2782,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +checksum = "345444e32442451b267fc254ae85a209c64be56d2890e601a0c37ff0c3c5ecd2" [[package]] name = "version_check" @@ -2848,9 +2825,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" +checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2858,24 +2835,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" +checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.35" +version = "0.4.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" +checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" dependencies = [ "cfg-if", "js-sys", @@ -2885,9 +2862,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" +checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2895,28 +2872,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" +checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.18", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.85" +version = "0.2.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" +checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" [[package]] name = "wasm-bindgen-test" -version = "0.3.35" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b27e15b4a3030b9944370ba1d8cec6f21f66a1ad4fd14725c5685600460713ec" +checksum = "c9e636f3a428ff62b3742ebc3c70e254dfe12b8c2b469d688ea59cdd4abcf502" dependencies = [ "console_error_panic_hook", "js-sys", @@ -2928,9 +2905,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.35" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dbaa9b9a574eac00c4f3a9c4941ac051f07632ecd0484a8588abd95af6b99d2" +checksum = "f18c1fad2f7c4958e7bcce014fa212f59a65d5e3721d0f77e6c0b27ede936ba3" dependencies = [ "proc-macro2", "quote", @@ -2938,9 +2915,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.62" +version = "0.3.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" +checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" dependencies = [ "js-sys", "wasm-bindgen", @@ -3025,21 +3002,6 @@ dependencies = [ "windows-targets 0.48.0", ] -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index 89f15ffe0b..8e477ffd6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crates/*"] [workspace.package] edition = "2021" -rust-version = "1.69" +rust-version = "1.70" homepage = "https://beta.ruff.rs/docs/" documentation = "https://beta.ruff.rs/docs/" repository = "https://github.com/charliermarsh/ruff" diff --git a/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py b/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py index eacf248284..581668e932 100644 --- a/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py +++ b/crates/ruff/resources/test/fixtures/flake8_boolean_trap/FBT.py @@ -57,12 +57,16 @@ dict.fromkeys(("world",), True) {}.deploy(True, False) getattr(someobj, attrname, False) mylist.index(True) +bool(False) int(True) str(int(False)) cfg.get("hello", True) cfg.getint("hello", True) cfg.getfloat("hello", True) cfg.getboolean("hello", True) +os.set_blocking(0, False) +g_action.set_enabled(True) +settings.set_enable_developer_extras(True) class Registry: diff --git a/crates/ruff/resources/test/fixtures/pylint/duplicate_value.py b/crates/ruff/resources/test/fixtures/flake8_bugbear/B033.py similarity index 100% rename from crates/ruff/resources/test/fixtures/pylint/duplicate_value.py rename to crates/ruff/resources/test/fixtures/flake8_bugbear/B033.py diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.py b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.py new file mode 100644 index 0000000000..20cc7d6ae5 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.py @@ -0,0 +1,57 @@ +import builtins +from abc import abstractmethod + + +def __repr__(self) -> str: + ... + + +def __str__(self) -> builtins.str: + ... + + +def __repr__(self, /, foo) -> str: + ... + + +def __repr__(self, *, foo) -> str: + ... + + +class ShouldRemoveSingle: + def __str__(self) -> builtins.str: + ... + + +class ShouldRemove: + def __repr__(self) -> str: + ... + + def __str__(self) -> builtins.str: + ... + + +class NoReturnSpecified: + def __str__(self): + ... + + def __repr__(self): + ... + + +class NonMatchingArgs: + def __str__(self, *, extra) -> builtins.str: + ... + + def __repr__(self, /, extra) -> str: + ... + + +class MatchingArgsButAbstract: + @abstractmethod + def __str__(self) -> builtins.str: + ... + + @abstractmethod + def __repr__(self) -> str: + ... diff --git a/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.pyi b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.pyi new file mode 100644 index 0000000000..a082a733a2 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/flake8_pyi/PYI029.pyi @@ -0,0 +1,28 @@ +import builtins +from abc import abstractmethod + +def __repr__(self) -> str: ... +def __str__(self) -> builtins.str: ... +def __repr__(self, /, foo) -> str: ... +def __repr__(self, *, foo) -> str: ... + +class ShouldRemoveSingle: + def __str__(self) -> builtins.str: ... # Error: PYI029 + +class ShouldRemove: + def __repr__(self) -> str: ... # Error: PYI029 + def __str__(self) -> builtins.str: ... # Error: PYI029 + +class NoReturnSpecified: + def __str__(self): ... + def __repr__(self): ... + +class NonMatchingArgs: + def __str__(self, *, extra) -> builtins.str: ... + def __repr__(self, /, extra) -> str: ... + +class MatchingArgsButAbstract: + @abstractmethod + def __str__(self) -> builtins.str: ... + @abstractmethod + def __repr__(self) -> str: ... diff --git a/crates/ruff/resources/test/fixtures/flake8_return/RET504.py b/crates/ruff/resources/test/fixtures/flake8_return/RET504.py index 96bc755687..80b92b3193 100644 --- a/crates/ruff/resources/test/fixtures/flake8_return/RET504.py +++ b/crates/ruff/resources/test/fixtures/flake8_return/RET504.py @@ -272,3 +272,34 @@ def str_to_bool(val): if isinstance(val, bool): return some_obj return val + + +# Mixed assignments +def function_assignment(x): + def f(): ... + + return f + + +def class_assignment(x): + class Foo: ... + + return Foo + + +def mixed_function_assignment(x): + if x: + def f(): ... + else: + f = 42 + + return f + + +def mixed_class_assignment(x): + if x: + class Foo: ... + else: + Foo = 42 + + return Foo diff --git a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py index 7b082da74e..82d6d2f10b 100644 --- a/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py +++ b/crates/ruff/resources/test/fixtures/flake8_type_checking/TCH002.py @@ -150,3 +150,17 @@ def f(): def f(): import pandas as pd + + +def f(): + from pandas import DataFrame # noqa: TCH002 + + x: DataFrame = 2 + + +def f(): + from pandas import ( # noqa: TCH002 + DataFrame, + ) + + x: DataFrame = 2 diff --git a/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py b/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py index 409ca96ff8..4882fefe9a 100644 --- a/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py +++ b/crates/ruff/resources/test/fixtures/flake8_type_checking/strict.py @@ -2,7 +2,7 @@ from __future__ import annotations def f(): - # Even in strict mode, this shouldn't rase an error, since `pkg` is used at runtime, + # Even in strict mode, this shouldn't raise an error, since `pkg` is used at runtime, # and implicitly imports `pkg.bar`. import pkg import pkg.bar @@ -12,7 +12,7 @@ def f(): def f(): - # Even in strict mode, this shouldn't rase an error, since `pkg.bar` is used at + # Even in strict mode, this shouldn't raise an error, since `pkg.bar` is used at # runtime, and implicitly imports `pkg`. import pkg import pkg.bar @@ -22,7 +22,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. import pkg from pkg import A @@ -31,7 +31,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. from pkg import A, B def test(value: A): @@ -39,7 +39,7 @@ def f(): def f(): - # Even in strict mode, this shouldn't rase an error, since `pkg.baz` is used at + # Even in strict mode, this shouldn't raise an error, since `pkg.baz` is used at # runtime, and implicitly imports `pkg.bar`. import pkg.bar import pkg.baz @@ -49,7 +49,7 @@ def f(): def f(): - # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. + # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime import pkg from pkg.bar import A @@ -58,7 +58,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. import pkg import pkg.bar as B @@ -67,7 +67,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. import pkg.foo as F import pkg.foo.bar as B @@ -76,7 +76,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. import pkg import pkg.foo.bar as B @@ -85,7 +85,7 @@ def f(): def f(): - # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. + # In un-strict mode, this _should_ raise an error, since `pkg` isn't used at runtime. # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is # testing the implementation. import pkg @@ -96,7 +96,7 @@ def f(): def f(): - # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. + # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. import pkg.bar as B import pkg.foo as F diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F401_16.py b/crates/ruff/resources/test/fixtures/pyflakes/F401_16.py new file mode 100644 index 0000000000..dd815bb900 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pyflakes/F401_16.py @@ -0,0 +1,15 @@ +"""Test that `__all__` exports are respected even with multiple declarations.""" + +import random + + +def some_dependency_check(): + return random.uniform(0.0, 1.0) > 0.49999 + + +if some_dependency_check(): + import math + + __all__ = ["math"] +else: + __all__ = [] diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F523.py b/crates/ruff/resources/test/fixtures/pyflakes/F523.py index 2055dd3a31..d3dd1b68db 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F523.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F523.py @@ -17,3 +17,17 @@ "{0}{1}".format(1, *args) # No issues "{0}{1}".format(1, 2, *args) # No issues "{0}{1}".format(1, 2, 3, *args) # F523 + +# With nested quotes +"''1{0}".format(1, 2, 3) # F523 +"\"\"{1}{0}".format(1, 2, 3) # F523 +'""{1}{0}'.format(1, 2, 3) # F523 + +# With modified indexes +"{1}{2}".format(1, 2, 3) # F523, # F524 +"{1}{3}".format(1, 2, 3, 4) # F523, # F524 +"{1} {8}".format(0, 1) # F523, # F524 + +# Not fixable +('' +.format(2)) diff --git a/crates/ruff/resources/test/fixtures/pyflakes/F524.py b/crates/ruff/resources/test/fixtures/pyflakes/F524.py index 30cc80ea0e..feffa38af3 100644 --- a/crates/ruff/resources/test/fixtures/pyflakes/F524.py +++ b/crates/ruff/resources/test/fixtures/pyflakes/F524.py @@ -4,3 +4,4 @@ "{0} {bar}".format(1) # F524 "{0} {bar}".format() # F524 "{bar} {0}".format() # F524 +"{1} {8}".format(0, 1) diff --git a/crates/ruff/resources/test/fixtures/pylint/invalid_return_type_str.py b/crates/ruff/resources/test/fixtures/pylint/invalid_return_type_str.py new file mode 100644 index 0000000000..a47ed1b306 --- /dev/null +++ b/crates/ruff/resources/test/fixtures/pylint/invalid_return_type_str.py @@ -0,0 +1,28 @@ +class Str: + def __str__(self): + return 1 + +class Float: + def __str__(self): + return 3.05 + +class Int: + def __str__(self): + return 0 + +class Bool: + def __str__(self): + return False + +class Str2: + def __str__(self): + x = "ruff" + return x + +# TODO fixme once Ruff has better type checking +def return_int(): + return 3 + +class ComplexReturn: + def __str__(self): + return return_int() \ No newline at end of file diff --git a/crates/ruff/resources/test/fixtures/ruff/RUF010.py b/crates/ruff/resources/test/fixtures/ruff/RUF010.py index 2d2604f9dc..f5850115de 100644 --- a/crates/ruff/resources/test/fixtures/ruff/RUF010.py +++ b/crates/ruff/resources/test/fixtures/ruff/RUF010.py @@ -12,6 +12,8 @@ f"{str(d['a'])}, {repr(d['b'])}, {ascii(d['c'])}" # RUF010 f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + f"{foo(bla)}" # OK f"{str(bla, 'ascii')}, {str(bla, encoding='cp1255')}" # OK diff --git a/crates/ruff/src/autofix/codemods.rs b/crates/ruff/src/autofix/codemods.rs index 389107f787..b73b2d5041 100644 --- a/crates/ruff/src/autofix/codemods.rs +++ b/crates/ruff/src/autofix/codemods.rs @@ -11,6 +11,23 @@ use ruff_python_ast::source_code::{Locator, Stylist}; use crate::cst::helpers::compose_module_path; use crate::cst::matchers::match_statement; +/// Glue code to make libcst codegen work with ruff's Stylist +pub(crate) trait CodegenStylist<'a>: Codegen<'a> { + fn codegen_stylist(&self, stylist: &'a Stylist) -> String; +} + +impl<'a, T: Codegen<'a>> CodegenStylist<'a> for T { + fn codegen_stylist(&self, stylist: &'a Stylist) -> String { + let mut state = CodegenState { + default_newline: stylist.line_ending().as_str(), + default_indent: stylist.indentation(), + ..Default::default() + }; + self.codegen(&mut state); + state.to_string() + } +} + /// Given an import statement, remove any imports that are specified in the `imports` iterator. /// /// Returns `Ok(None)` if the statement is empty after removing the imports. @@ -40,11 +57,11 @@ pub(crate) fn remove_imports<'a>( // entire statement. let mut found_star = false; for import in imports { - let full_name = match import_body.module.as_ref() { + let qualified_name = match import_body.module.as_ref() { Some(module_name) => format!("{}.*", compose_module_path(module_name)), None => "*".to_string(), }; - if import == full_name { + if import == qualified_name { found_star = true; } else { bail!("Expected \"*\" for unused import (got: \"{}\")", import); @@ -66,26 +83,26 @@ pub(crate) fn remove_imports<'a>( for import in imports { let alias_index = aliases.iter().position(|alias| { - let full_name = match import_module { + let qualified_name = match import_module { Some((relative, module)) => { let module = module.map(compose_module_path); let member = compose_module_path(&alias.name); - let mut full_name = String::with_capacity( + let mut qualified_name = String::with_capacity( relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1, ); for _ in 0..relative.len() { - full_name.push('.'); + qualified_name.push('.'); } if let Some(module) = module { - full_name.push_str(&module); - full_name.push('.'); + qualified_name.push_str(&module); + qualified_name.push('.'); } - full_name.push_str(&member); - full_name + qualified_name.push_str(&member); + qualified_name } None => compose_module_path(&alias.name), }; - full_name == import + qualified_name == import }); if let Some(index) = alias_index { @@ -114,14 +131,7 @@ pub(crate) fn remove_imports<'a>( return Ok(None); } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Some(state.to_string())) + Ok(Some(tree.codegen_stylist(stylist))) } /// Given an import statement, remove any imports that are not specified in the `imports` slice. @@ -160,26 +170,26 @@ pub(crate) fn retain_imports( aliases.retain(|alias| { imports.iter().any(|import| { - let full_name = match import_module { + let qualified_name = match import_module { Some((relative, module)) => { let module = module.map(compose_module_path); let member = compose_module_path(&alias.name); - let mut full_name = String::with_capacity( + let mut qualified_name = String::with_capacity( relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1, ); for _ in 0..relative.len() { - full_name.push('.'); + qualified_name.push('.'); } if let Some(module) = module { - full_name.push_str(&module); - full_name.push('.'); + qualified_name.push_str(&module); + qualified_name.push('.'); } - full_name.push_str(&member); - full_name + qualified_name.push_str(&member); + qualified_name } None => compose_module_path(&alias.name), }; - full_name == *import + qualified_name == *import }) }); @@ -200,11 +210,5 @@ pub(crate) fn retain_imports( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - Ok(state.to_string()) + Ok(tree.codegen_stylist(stylist)) } diff --git a/crates/ruff/src/checkers/ast/mod.rs b/crates/ruff/src/checkers/ast/mod.rs index ddfe02b275..3e1e87359a 100644 --- a/crates/ruff/src/checkers/ast/mod.rs +++ b/crates/ruff/src/checkers/ast/mod.rs @@ -257,21 +257,14 @@ where Stmt::Global(ast::StmtGlobal { names, range: _ }) => { let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); if !self.semantic_model.scope_id.is_global() { - // Add the binding to the current scope. - let context = self.semantic_model.execution_context(); - let exceptions = self.semantic_model.exceptions(); - let scope = &mut self.semantic_model.scopes[self.semantic_model.scope_id]; for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding_id = self.semantic_model.bindings.push(Binding { - kind: BindingKind::Global, - range: *range, - references: Vec::new(), - source: self.semantic_model.stmt_id, - context, - exceptions, - flags: BindingFlags::empty(), - }); + let binding_id = self.semantic_model.push_binding( + *range, + BindingKind::Global, + BindingFlags::empty(), + ); + let scope = self.semantic_model.scope_mut(); scope.add(name, binding_id); } } @@ -286,20 +279,14 @@ where Stmt::Nonlocal(ast::StmtNonlocal { names, range: _ }) => { let ranges: Vec = helpers::find_names(stmt, self.locator).collect(); if !self.semantic_model.scope_id.is_global() { - let context = self.semantic_model.execution_context(); - let exceptions = self.semantic_model.exceptions(); - let scope = &mut self.semantic_model.scopes[self.semantic_model.scope_id]; for (name, range) in names.iter().zip(ranges.iter()) { // Add a binding to the current scope. - let binding_id = self.semantic_model.bindings.push(Binding { - kind: BindingKind::Nonlocal, - range: *range, - references: Vec::new(), - source: self.semantic_model.stmt_id, - context, - exceptions, - flags: BindingFlags::empty(), - }); + let binding_id = self.semantic_model.push_binding( + *range, + BindingKind::Nonlocal, + BindingFlags::empty(), + ); + let scope = self.semantic_model.scope_mut(); scope.add(name, binding_id); } @@ -395,6 +382,10 @@ where } } + if self.enabled(Rule::InvalidStrReturnType) { + pylint::rules::invalid_str_return(self, name, body); + } + if self.enabled(Rule::InvalidFunctionName) { if let Some(diagnostic) = pep8_naming::rules::invalid_function_name( stmt, @@ -460,6 +451,9 @@ where stmt.is_async_function_def_stmt(), ); } + if self.enabled(Rule::StrOrReprDefinedInStub) { + flake8_pyi::rules::str_or_repr_defined_in_stub(self, stmt); + } if self.enabled(Rule::NoReturnArgumentAnnotationInStub) { flake8_pyi::rules::no_return_argument_annotation(self, args); } @@ -844,18 +838,11 @@ where for alias in names { if &alias.name == "__future__" { let name = alias.asname.as_ref().unwrap_or(&alias.name); - self.add_binding( name, - Binding { - kind: BindingKind::FutureImportation, - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + alias.range(), + BindingKind::FutureImportation, + BindingFlags::empty(), ); if self.enabled(Rule::LateFutureImport) { @@ -867,45 +854,33 @@ where } } } else if alias.name.contains('.') && alias.asname.is_none() { - // Given `import foo.bar`, `name` would be "foo", and `full_name` would be + // Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be // "foo.bar". let name = alias.name.split('.').next().unwrap(); - let full_name = &alias.name; + let qualified_name = &alias.name; self.add_binding( name, - Binding { - kind: BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name, - }), - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + alias.range(), + BindingKind::SubmoduleImportation(SubmoduleImportation { + qualified_name, + }), + BindingFlags::empty(), ); } else { let name = alias.asname.as_ref().unwrap_or(&alias.name); - let full_name = &alias.name; + let qualified_name = &alias.name; self.add_binding( name, - Binding { - kind: BindingKind::Importation(Importation { full_name }), - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + alias.range(), + BindingKind::Importation(Importation { qualified_name }), + if alias + .asname + .as_ref() + .map_or(false, |asname| asname == &alias.name) + { + BindingFlags::EXPLICIT_EXPORT + } else { + BindingFlags::empty() }, ); @@ -1130,15 +1105,9 @@ where self.add_binding( name, - Binding { - kind: BindingKind::FutureImportation, - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + alias.range(), + BindingKind::FutureImportation, + BindingFlags::empty(), ); if self.enabled(Rule::FutureFeatureNotDefined) { @@ -1189,30 +1158,24 @@ where } } - // Given `from foo import bar`, `name` would be "bar" and `full_name` would + // Given `from foo import bar`, `name` would be "bar" and `qualified_name` would // be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz" - // and `full_name` would be "foo.bar". + // and `qualified_name` would be "foo.bar". let name = alias.asname.as_ref().unwrap_or(&alias.name); - let full_name = + let qualified_name = helpers::format_import_from_member(level, module, &alias.name); self.add_binding( name, - Binding { - kind: BindingKind::FromImportation(FromImportation { full_name }), - range: alias.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: if alias - .asname - .as_ref() - .map_or(false, |asname| asname == &alias.name) - { - BindingFlags::EXPLICIT_EXPORT - } else { - BindingFlags::empty() - }, + alias.range(), + BindingKind::FromImportation(FromImportation { qualified_name }), + if alias + .asname + .as_ref() + .map_or(false, |asname| asname == &alias.name) + { + BindingFlags::EXPLICIT_EXPORT + } else { + BindingFlags::empty() }, ); } @@ -1240,12 +1203,12 @@ where } if self.enabled(Rule::UnconventionalImportAlias) { - let full_name = + let qualified_name = helpers::format_import_from_member(level, module, &alias.name); if let Some(diagnostic) = flake8_import_conventions::rules::conventional_import_alias( stmt, - &full_name, + &qualified_name, alias.asname.as_deref(), &self.settings.flake8_import_conventions.aliases, ) @@ -1256,12 +1219,12 @@ where if self.enabled(Rule::BannedImportAlias) { if let Some(asname) = &alias.asname { - let full_name = + let qualified_name = helpers::format_import_from_member(level, module, &alias.name); if let Some(diagnostic) = flake8_import_conventions::rules::banned_import_alias( stmt, - &full_name, + &qualified_name, asname, &self.settings.flake8_import_conventions.banned_aliases, ) @@ -1930,15 +1893,9 @@ where self.add_binding( name, - Binding { - kind: BindingKind::FunctionDefinition, - range: stmt.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + stmt.range(), + BindingKind::FunctionDefinition, + BindingFlags::empty(), ); let definition = docstrings::extraction::extract_definition( @@ -2166,15 +2123,9 @@ where self.semantic_model.pop_definition(); self.add_binding( name, - Binding { - kind: BindingKind::ClassDefinition, - range: stmt.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + stmt.range(), + BindingKind::ClassDefinition, + BindingFlags::empty(), ); } _ => {} @@ -3112,7 +3063,7 @@ where } Expr::Set(ast::ExprSet { elts, range: _ }) => { if self.enabled(Rule::DuplicateValue) { - pylint::rules::duplicate_value(self, elts); + flake8_bugbear::rules::duplicate_value(self, elts); } } Expr::Yield(_) => { @@ -4203,15 +4154,9 @@ where // upstream. self.add_binding( &arg.arg, - Binding { - kind: BindingKind::Argument, - range: arg.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + arg.range(), + BindingKind::Argument, + BindingFlags::empty(), ); if self.enabled(Rule::AmbiguousVariableName) { @@ -4251,15 +4196,9 @@ where { self.add_binding( name, - Binding { - kind: BindingKind::Assignment, - range: pattern.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + pattern.range(), + BindingKind::Assignment, + BindingFlags::empty(), ); } @@ -4383,9 +4322,33 @@ impl<'a> Checker<'a> { } /// Add a [`Binding`] to the current scope, bound to the given name. - fn add_binding(&mut self, name: &'a str, binding: Binding<'a>) -> BindingId { - let binding_id = self.semantic_model.bindings.next_id(); - if let Some((stack_index, existing_binding_id)) = self + fn add_binding( + &mut self, + name: &'a str, + range: TextRange, + kind: BindingKind<'a>, + flags: BindingFlags, + ) -> BindingId { + // Determine the scope to which the binding belongs. + // Per [PEP 572](https://peps.python.org/pep-0572/#scope-of-the-target), named + // expressions in generators and comprehensions bind to the scope that contains the + // outermost comprehension. + let scope_id = if kind.is_named_expr_assignment() { + self.semantic_model + .scopes + .ancestor_ids(self.semantic_model.scope_id) + .find_or_last(|scope_id| !self.semantic_model.scopes[*scope_id].kind.is_generator()) + .unwrap_or(self.semantic_model.scope_id) + } else { + self.semantic_model.scope_id + }; + + // Create the `Binding`. + let binding_id = self.semantic_model.push_binding(range, kind, flags); + let binding = &self.semantic_model.bindings[binding_id]; + + // Determine whether the binding shadows any existing bindings. + if let Some((stack_index, shadowed_id)) = self .semantic_model .scopes .ancestors(self.semantic_model.scope_id) @@ -4394,26 +4357,26 @@ impl<'a> Checker<'a> { scope.get(name).map(|binding_id| (stack_index, binding_id)) }) { - let existing = &self.semantic_model.bindings[existing_binding_id]; + let shadowed = &self.semantic_model.bindings[shadowed_id]; let in_current_scope = stack_index == 0; - if !existing.kind.is_builtin() - && existing.source.map_or(true, |left| { + if !shadowed.kind.is_builtin() + && shadowed.source.map_or(true, |left| { binding.source.map_or(true, |right| { !branch_detection::different_forks(left, right, &self.semantic_model.stmts) }) }) { - let existing_is_import = matches!( - existing.kind, + let shadows_import = matches!( + shadowed.kind, BindingKind::Importation(..) | BindingKind::FromImportation(..) | BindingKind::SubmoduleImportation(..) | BindingKind::FutureImportation ); - if binding.kind.is_loop_var() && existing_is_import { + if binding.kind.is_loop_var() && shadows_import { if self.enabled(Rule::ImportShadowedByLoopVar) { #[allow(deprecated)] - let line = self.locator.compute_line_index(existing.range.start()); + let line = self.locator.compute_line_index(shadowed.range.start()); self.diagnostics.push(Diagnostic::new( pyflakes::rules::ImportShadowedByLoopVar { @@ -4424,21 +4387,21 @@ impl<'a> Checker<'a> { )); } } else if in_current_scope { - if !existing.is_used() - && binding.redefines(existing) - && (!self.settings.dummy_variable_rgx.is_match(name) || existing_is_import) - && !(existing.kind.is_function_definition() + if !shadowed.is_used() + && binding.redefines(shadowed) + && (!self.settings.dummy_variable_rgx.is_match(name) || shadows_import) + && !(shadowed.kind.is_function_definition() && analyze::visibility::is_overload( &self.semantic_model, cast::decorator_list( - self.semantic_model.stmts[existing.source.unwrap()], + self.semantic_model.stmts[shadowed.source.unwrap()], ), )) { if self.enabled(Rule::RedefinedWhileUnused) { #[allow(deprecated)] let line = self.locator.compute_line_index( - existing + shadowed .trimmed_range(&self.semantic_model, self.locator) .start(), ); @@ -4450,81 +4413,60 @@ impl<'a> Checker<'a> { }, binding.trimmed_range(&self.semantic_model, self.locator), ); - if let Some(parent) = binding.source { - let parent = self.semantic_model.stmts[parent]; - if matches!(parent, Stmt::ImportFrom(_)) - && parent.range().contains_range(binding.range) - { - diagnostic.set_parent(parent.start()); - } + if let Some(range) = binding.parent_range(&self.semantic_model) { + diagnostic.set_parent(range.start()); } self.diagnostics.push(diagnostic); } } - } else if existing_is_import && binding.redefines(existing) { + } else if shadows_import && binding.redefines(shadowed) { self.semantic_model .shadowed_bindings - .entry(existing_binding_id) - .or_insert_with(Vec::new) - .push(binding_id); + .insert(binding_id, shadowed_id); } } } - // Per [PEP 572](https://peps.python.org/pep-0572/#scope-of-the-target), named - // expressions in generators and comprehensions bind to the scope that contains the - // outermost comprehension. - let scope_id = if binding.kind.is_named_expr_assignment() { - self.semantic_model - .scopes - .ancestor_ids(self.semantic_model.scope_id) - .find_or_last(|scope_id| !self.semantic_model.scopes[*scope_id].kind.is_generator()) - .unwrap_or(self.semantic_model.scope_id) - } else { - self.semantic_model.scope_id - }; - let scope = &mut self.semantic_model.scopes[scope_id]; - - let binding = if let Some(binding_id) = scope.get(name) { - let existing = &self.semantic_model.bindings[binding_id]; - match &existing.kind { + // If there's an existing binding in this scope, copy its references. + if let Some(shadowed) = self.semantic_model.scopes[scope_id] + .get(name) + .map(|binding_id| &self.semantic_model.bindings[binding_id]) + { + match &shadowed.kind { BindingKind::Builtin => { // Avoid overriding builtins. - binding } kind @ (BindingKind::Global | BindingKind::Nonlocal) => { - // If the original binding was a global or nonlocal, and the new binding conflicts within - // the current scope, then the new binding is also as the same. - Binding { - references: existing.references.clone(), - kind: kind.clone(), - ..binding - } + // If the original binding was a global or nonlocal, then the new binding is + // too. + let references = shadowed.references.clone(); + self.semantic_model.bindings[binding_id].kind = kind.clone(); + self.semantic_model.bindings[binding_id].references = references; + } + _ => { + let references = shadowed.references.clone(); + self.semantic_model.bindings[binding_id].references = references; } - _ => Binding { - references: existing.references.clone(), - ..binding - }, } - } else { - binding - }; - // Don't treat annotations as assignments if there is an existing value - // in scope. - if binding.kind.is_annotation() && scope.defines(name) { - return self.semantic_model.bindings.push(binding); + // If this is an annotation, and we already have an existing value in the same scope, + // don't treat it as an assignment (i.e., avoid adding it to the scope). + if self.semantic_model.bindings[binding_id] + .kind + .is_annotation() + { + return binding_id; + } } // Add the binding to the scope. + let scope = &mut self.semantic_model.scopes[scope_id]; scope.add(name, binding_id); - // Add the binding to the arena. - self.semantic_model.bindings.push(binding) + binding_id } fn bind_builtins(&mut self) { - let scope = &mut self.semantic_model.scopes[self.semantic_model.scope_id]; for builtin in BUILTINS .iter() .chain(MAGIC_GLOBALS.iter()) @@ -4532,15 +4474,8 @@ impl<'a> Checker<'a> { .chain(self.settings.builtins.iter().map(String::as_str)) { // Add the builtin to the scope. - let binding_id = self.semantic_model.bindings.push(Binding { - kind: BindingKind::Builtin, - range: TextRange::default(), - source: None, - references: Vec::new(), - context: ExecutionContext::Runtime, - exceptions: Exceptions::empty(), - flags: BindingFlags::empty(), - }); + let binding_id = self.semantic_model.push_builtin(); + let scope = self.semantic_model.scope_mut(); scope.add(builtin, binding_id); } } @@ -4650,15 +4585,9 @@ impl<'a> Checker<'a> { ) { self.add_binding( id, - Binding { - kind: BindingKind::Annotation, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::Annotation, + BindingFlags::empty(), ); return; } @@ -4666,15 +4595,9 @@ impl<'a> Checker<'a> { if matches!(parent, Stmt::For(_) | Stmt::AsyncFor(_)) { self.add_binding( id, - Binding { - kind: BindingKind::LoopVar, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::LoopVar, + BindingFlags::empty(), ); return; } @@ -4682,29 +4605,17 @@ impl<'a> Checker<'a> { if helpers::is_unpacking_assignment(parent, expr) { self.add_binding( id, - Binding { - kind: BindingKind::Binding, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::Binding, + BindingFlags::empty(), ); return; } let scope = self.semantic_model.scope(); - if id == "__all__" - && scope.kind.is_module() - && matches!( - parent, - Stmt::Assign(_) | Stmt::AugAssign(_) | Stmt::AnnAssign(_) - ) - { - if match parent { + if scope.kind.is_module() + && match parent { Stmt::Assign(ast::StmtAssign { targets, .. }) => { if let Some(Expr::Name(ast::ExprName { id, .. })) = targets.first() { id == "__all__" @@ -4727,53 +4638,32 @@ impl<'a> Checker<'a> { } } _ => false, - } { - let (all_names, all_names_flags) = { - let (mut names, flags) = - extract_all_names(parent, |name| self.semantic_model.is_builtin(name)); - - // Grab the existing bound __all__ values. - if let Stmt::AugAssign(_) = parent { - if let Some(binding_id) = scope.get("__all__") { - if let BindingKind::Export(Export { names: existing }) = - &self.semantic_model.bindings[binding_id].kind - { - names.extend_from_slice(existing); - } - } - } - - (names, flags) - }; - - if self.enabled(Rule::InvalidAllFormat) { - if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) { - self.diagnostics - .push(pylint::rules::invalid_all_format(expr)); - } - } - - if self.enabled(Rule::InvalidAllObject) { - if matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT) { - self.diagnostics - .push(pylint::rules::invalid_all_object(expr)); - } - } - - self.add_binding( - id, - Binding { - kind: BindingKind::Export(Export { names: all_names }), - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, - ); - return; } + { + let (names, flags) = + extract_all_names(parent, |name| self.semantic_model.is_builtin(name)); + + if self.enabled(Rule::InvalidAllFormat) { + if matches!(flags, AllNamesFlags::INVALID_FORMAT) { + self.diagnostics + .push(pylint::rules::invalid_all_format(expr)); + } + } + + if self.enabled(Rule::InvalidAllObject) { + if matches!(flags, AllNamesFlags::INVALID_OBJECT) { + self.diagnostics + .push(pylint::rules::invalid_all_object(expr)); + } + } + + self.add_binding( + id, + expr.range(), + BindingKind::Export(Export { names }), + BindingFlags::empty(), + ); + return; } if self @@ -4783,30 +4673,18 @@ impl<'a> Checker<'a> { { self.add_binding( id, - Binding { - kind: BindingKind::NamedExprAssignment, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::NamedExprAssignment, + BindingFlags::empty(), ); return; } self.add_binding( id, - Binding { - kind: BindingKind::Assignment, - range: expr.range(), - references: Vec::new(), - source: self.semantic_model.stmt_id, - context: self.semantic_model.execution_context(), - exceptions: self.semantic_model.exceptions(), - flags: BindingFlags::empty(), - }, + expr.range(), + BindingKind::Assignment, + BindingFlags::empty(), ); } @@ -5019,50 +4897,31 @@ impl<'a> Checker<'a> { } // Mark anything referenced in `__all__` as used. - let all_bindings: Option<(Vec, TextRange)> = { + let exports: Vec<(&str, TextRange)> = { let global_scope = self.semantic_model.global_scope(); - let all_names: Option<(&[&str], TextRange)> = global_scope - .get("__all__") + global_scope + .bindings_for_name("__all__") .map(|binding_id| &self.semantic_model.bindings[binding_id]) - .and_then(|binding| match &binding.kind { + .filter_map(|binding| match &binding.kind { BindingKind::Export(Export { names }) => { - Some((names.as_slice(), binding.range)) + Some(names.iter().map(|name| (*name, binding.range))) } _ => None, - }); - - all_names.map(|(names, range)| { - ( - names - .iter() - .filter_map(|name| global_scope.get(name)) - .collect(), - range, - ) - }) + }) + .flatten() + .collect() }; - if let Some((bindings, range)) = all_bindings { - for binding_id in bindings { + for (name, range) in &exports { + if let Some(binding_id) = self.semantic_model.global_scope().get(name) { self.semantic_model.add_global_reference( binding_id, - range, + *range, ExecutionContext::Runtime, ); } } - // Extract `__all__` names from the global scope. - let all_names: Option<(&[&str], TextRange)> = self - .semantic_model - .global_scope() - .get("__all__") - .map(|binding_id| &self.semantic_model.bindings[binding_id]) - .and_then(|binding| match &binding.kind { - BindingKind::Export(Export { names }) => Some((names.as_slice(), binding.range)), - _ => None, - }); - // Identify any valid runtime imports. If a module is imported at runtime, and // used at runtime, then by default, we avoid flagging any other // imports from that model as typing-only. @@ -5099,35 +4958,33 @@ impl<'a> Checker<'a> { // F822 if self.enabled(Rule::UndefinedExport) { if !self.path.ends_with("__init__.py") { - if let Some((names, range)) = all_names { + for (name, range) in &exports { diagnostics - .extend(pyflakes::rules::undefined_export(names, range, scope)); + .extend(pyflakes::rules::undefined_export(name, *range, scope)); } } } // F405 if self.enabled(Rule::UndefinedLocalWithImportStarUsage) { - if let Some((names, range)) = &all_names { - let sources: Vec = scope - .star_imports() - .map(|StarImportation { level, module }| { - helpers::format_import_from(*level, *module) - }) - .sorted() - .dedup() - .collect(); - if !sources.is_empty() { - for name in names.iter() { - if !scope.defines(name) { - diagnostics.push(Diagnostic::new( - pyflakes::rules::UndefinedLocalWithImportStarUsage { - name: (*name).to_string(), - sources: sources.clone(), - }, - *range, - )); - } + let sources: Vec = scope + .star_imports() + .map(|StarImportation { level, module }| { + helpers::format_import_from(*level, *module) + }) + .sorted() + .dedup() + .collect(); + if !sources.is_empty() { + for (name, range) in &exports { + if !scope.defines(name) { + diagnostics.push(Diagnostic::new( + pyflakes::rules::UndefinedLocalWithImportStarUsage { + name: (*name).to_string(), + sources: sources.clone(), + }, + *range, + )); } } } @@ -5160,52 +5017,35 @@ impl<'a> Checker<'a> { } // Look for any bindings that were redefined in another scope, and remain - // unused. Note that we only store references in `redefinitions` if + // unused. Note that we only store references in `shadowed_bindings` if // the bindings are in different scopes. if self.enabled(Rule::RedefinedWhileUnused) { for (name, binding_id) in scope.bindings() { - let binding = &self.semantic_model.bindings[binding_id]; - - if matches!( - binding.kind, - BindingKind::Importation(..) - | BindingKind::FromImportation(..) - | BindingKind::SubmoduleImportation(..) - ) { - if binding.is_used() { + if let Some(shadowed) = self.semantic_model.shadowed_binding(binding_id) { + if shadowed.is_used() { continue; } - if let Some(shadowed_ids) = - self.semantic_model.shadowed_bindings.get(&binding_id) - { - for binding_id in shadowed_ids.iter().copied() { - let rebound = &self.semantic_model.bindings[binding_id]; - #[allow(deprecated)] - let line = self.locator.compute_line_index( - binding - .trimmed_range(&self.semantic_model, self.locator) - .start(), - ); + let binding = &self.semantic_model.bindings[binding_id]; - let mut diagnostic = Diagnostic::new( - pyflakes::rules::RedefinedWhileUnused { - name: (*name).to_string(), - line, - }, - rebound.trimmed_range(&self.semantic_model, self.locator), - ); - if let Some(source) = rebound.source { - let parent = &self.semantic_model.stmts[source]; - if matches!(parent, Stmt::ImportFrom(_)) - && parent.range().contains_range(rebound.range) - { - diagnostic.set_parent(parent.start()); - } - }; - diagnostics.push(diagnostic); - } + #[allow(deprecated)] + let line = self.locator.compute_line_index( + shadowed + .trimmed_range(&self.semantic_model, self.locator) + .start(), + ); + + let mut diagnostic = Diagnostic::new( + pyflakes::rules::RedefinedWhileUnused { + name: (*name).to_string(), + line, + }, + binding.trimmed_range(&self.semantic_model, self.locator), + ); + if let Some(range) = binding.parent_range(&self.semantic_model) { + diagnostic.set_parent(range.start()); } + diagnostics.push(diagnostic); } } } diff --git a/crates/ruff/src/checkers/noqa.rs b/crates/ruff/src/checkers/noqa.rs index f54b937d32..52d47182b5 100644 --- a/crates/ruff/src/checkers/noqa.rs +++ b/crates/ruff/src/checkers/noqa.rs @@ -18,10 +18,9 @@ pub(crate) fn check_noqa( locator: &Locator, comment_ranges: &[TextRange], noqa_line_for: &NoqaMapping, + analyze_directives: bool, settings: &Settings, ) -> Vec { - let enforce_noqa = settings.rules.enabled(Rule::UnusedNOQA); - // Identify any codes that are globally exempted (within the current file). let exemption = noqa::file_exemption(locator.contents(), comment_ranges); @@ -93,7 +92,7 @@ pub(crate) fn check_noqa( } // Enforce that the noqa directive was actually used (RUF100). - if enforce_noqa { + if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) { for line in noqa_directives.lines() { match &line.directive { Directive::All(leading_spaces, noqa_range, trailing_spaces) => { diff --git a/crates/ruff/src/codes.rs b/crates/ruff/src/codes.rs index 917eeb30d7..641755699e 100644 --- a/crates/ruff/src/codes.rs +++ b/crates/ruff/src/codes.rs @@ -167,6 +167,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "E0118") => (RuleGroup::Unspecified, rules::pylint::rules::LoadBeforeGlobalDeclaration), (Pylint, "E0241") => (RuleGroup::Unspecified, rules::pylint::rules::DuplicateBases), (Pylint, "E0302") => (RuleGroup::Unspecified, rules::pylint::rules::UnexpectedSpecialMethodSignature), + (Pylint, "E0307") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidStrReturnType), (Pylint, "E0604") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidAllObject), (Pylint, "E0605") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidAllFormat), (Pylint, "E1142") => (RuleGroup::Unspecified, rules::pylint::rules::AwaitOutsideAsync), @@ -196,7 +197,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf), (Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop), (Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral), - (Pylint, "W0130") => (RuleGroup::Unspecified, rules::pylint::rules::DuplicateValue), (Pylint, "W0131") => (RuleGroup::Unspecified, rules::pylint::rules::NamedExprWithoutContext), (Pylint, "W0406") => (RuleGroup::Unspecified, rules::pylint::rules::ImportSelf), (Pylint, "W0602") => (RuleGroup::Unspecified, rules::pylint::rules::GlobalVariableNotAssigned), @@ -248,6 +248,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Bugbear, "030") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ExceptWithNonExceptionClasses), (Flake8Bugbear, "031") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ReuseOfGroupbyGenerator), (Flake8Bugbear, "032") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::UnintentionalTypeAnnotation), + (Flake8Bugbear, "033") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::DuplicateValue), (Flake8Bugbear, "904") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::RaiseWithoutFromInsideExcept), (Flake8Bugbear, "905") => (RuleGroup::Unspecified, rules::flake8_bugbear::rules::ZipWithoutExplicitStrict), @@ -604,6 +605,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> { (Flake8Pyi, "021") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DocstringInStub), (Flake8Pyi, "024") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CollectionsNamedTuple), (Flake8Pyi, "025") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnaliasedCollectionsAbcSetImport), + (Flake8Pyi, "029") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StrOrReprDefinedInStub), (Flake8Pyi, "032") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::AnyEqNeAnnotation), (Flake8Pyi, "033") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::TypeCommentInStub), (Flake8Pyi, "034") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NonSelfReturnType), diff --git a/crates/ruff/src/cst/matchers.rs b/crates/ruff/src/cst/matchers.rs index 0df0590fcc..23b6382d14 100644 --- a/crates/ruff/src/cst/matchers.rs +++ b/crates/ruff/src/cst/matchers.rs @@ -3,7 +3,7 @@ use libcst_native::{ Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FormattedString, FormattedStringContent, FormattedStringExpression, FunctionDef, GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda, ListComp, Module, Name, - SimpleString, SmallStatement, Statement, Suite, Tuple, With, + SmallStatement, Statement, Suite, Tuple, With, }; pub(crate) fn match_module(module_text: &str) -> Result { @@ -109,16 +109,6 @@ pub(crate) fn match_attribute<'a, 'b>( } } -pub(crate) fn match_simple_string<'a, 'b>( - expression: &'a mut Expression<'b>, -) -> Result<&'a mut SimpleString<'b>> { - if let Expression::SimpleString(simple_string) = expression { - Ok(simple_string) - } else { - bail!("Expected Expression::SimpleString") - } -} - pub(crate) fn match_formatted_string<'a, 'b>( expression: &'a mut Expression<'b>, ) -> Result<&'a mut FormattedString<'b>> { diff --git a/crates/ruff/src/importer/mod.rs b/crates/ruff/src/importer/mod.rs index ca05d6b958..ba923aed3f 100644 --- a/crates/ruff/src/importer/mod.rs +++ b/crates/ruff/src/importer/mod.rs @@ -3,11 +3,12 @@ use std::error::Error; use anyhow::Result; -use libcst_native::{Codegen, CodegenState, ImportAlias, Name, NameOrAttribute}; +use libcst_native::{ImportAlias, Name, NameOrAttribute}; use ruff_text_size::TextSize; use rustpython_parser::ast::{self, Ranged, Stmt, Suite}; use crate::autofix; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::imports::{AnyImport, Import, ImportFrom}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -87,7 +88,7 @@ impl<'a> Importer<'a> { ) -> Result { // Generate the modified import statement. let content = autofix::codemods::retain_imports( - &[import.full_name], + &[import.qualified_name], import.stmt, self.locator, self.stylist, @@ -119,7 +120,7 @@ impl<'a> Importer<'a> { ) -> Result { // Generate the modified import statement. let content = autofix::codemods::retain_imports( - &[import.full_name], + &[import.qualified_name], import.stmt, self.locator, self.stylist, @@ -324,13 +325,10 @@ impl<'a> Importer<'a> { asname: None, comma: aliases.last().and_then(|alias| alias.comma.clone()), }); - let mut state = CodegenState { - default_newline: &self.stylist.line_ending(), - default_indent: self.stylist.indentation(), - ..CodegenState::default() - }; - statement.codegen(&mut state); - Ok(Edit::range_replacement(state.to_string(), stmt.range())) + Ok(Edit::range_replacement( + statement.codegen_stylist(self.stylist), + stmt.range(), + )) } /// Add a `TYPE_CHECKING` block to the given module. @@ -449,7 +447,7 @@ pub(crate) struct StmtImport<'a> { /// The import statement. pub(crate) stmt: &'a Stmt, /// The "full name" of the imported module or member. - pub(crate) full_name: &'a str, + pub(crate) qualified_name: &'a str, } /// The result of an [`Importer::get_or_import_symbol`] call. diff --git a/crates/ruff/src/linter.rs b/crates/ruff/src/linter.rs index 79ed8fba70..bd8f713d87 100644 --- a/crates/ruff/src/linter.rs +++ b/crates/ruff/src/linter.rs @@ -214,6 +214,7 @@ pub fn check_path( locator, indexer.comment_ranges(), &directives.noqa_line_for, + error.is_none(), settings, ); if noqa.into() { diff --git a/crates/ruff/src/rule_redirects.rs b/crates/ruff/src/rule_redirects.rs index 4749b64cca..ebf63dddd0 100644 --- a/crates/ruff/src/rule_redirects.rs +++ b/crates/ruff/src/rule_redirects.rs @@ -93,5 +93,6 @@ static REDIRECTS: Lazy> = Lazy::new(|| { // TODO(charlie): Remove by 2023-06-01. ("RUF004", "B026"), ("PIE802", "C419"), + ("PLW0130", "B033"), ]) }); diff --git a/crates/ruff/src/rule_selector.rs b/crates/ruff/src/rule_selector.rs index bc5a3ee523..6985c1be3c 100644 --- a/crates/ruff/src/rule_selector.rs +++ b/crates/ruff/src/rule_selector.rs @@ -12,8 +12,10 @@ use crate::rule_redirects::get_redirect; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum RuleSelector { - /// Select all rules. + /// Select all stable rules. All, + /// Select all nursery rules. + Nursery, /// Legacy category to select both the `mccabe` and `flake8-comprehensions` linters /// via a single selector. C, @@ -39,30 +41,30 @@ impl FromStr for RuleSelector { type Err = ParseError; fn from_str(s: &str) -> Result { - if s == "ALL" { - Ok(Self::All) - } else if s == "C" { - Ok(Self::C) - } else if s == "T" { - Ok(Self::T) - } else { - let (s, redirected_from) = match get_redirect(s) { - Some((from, target)) => (target, Some(from)), - None => (s, None), - }; + match s { + "ALL" => Ok(Self::All), + "NURSERY" => Ok(Self::Nursery), + "C" => Ok(Self::C), + "T" => Ok(Self::T), + _ => { + let (s, redirected_from) = match get_redirect(s) { + Some((from, target)) => (target, Some(from)), + None => (s, None), + }; - let (linter, code) = - Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?; + let (linter, code) = + Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?; - if code.is_empty() { - return Ok(Self::Linter(linter)); + if code.is_empty() { + return Ok(Self::Linter(linter)); + } + + Ok(Self::Prefix { + prefix: RuleCodePrefix::parse(&linter, code) + .map_err(|_| ParseError::Unknown(s.to_string()))?, + redirected_from, + }) } - - Ok(Self::Prefix { - prefix: RuleCodePrefix::parse(&linter, code) - .map_err(|_| ParseError::Unknown(s.to_string()))?, - redirected_from, - }) } } } @@ -79,6 +81,7 @@ impl RuleSelector { pub fn prefix_and_code(&self) -> (&'static str, &'static str) { match self { RuleSelector::All => ("", "ALL"), + RuleSelector::Nursery => ("", "NURSERY"), RuleSelector::C => ("", "C"), RuleSelector::T => ("", "T"), RuleSelector::Prefix { prefix, .. } => { @@ -141,13 +144,6 @@ impl From for RuleSelector { } } -/// Returns `true` if the given rule should be selected by the `RuleSelector::All` selector. -fn select_all(rule: Rule) -> bool { - // Nursery rules have to be explicitly selected, so we ignore them when looking at - // prefixes. - !rule.is_nursery() -} - impl IntoIterator for &RuleSelector { type Item = Rule; type IntoIter = RuleSelectorIter; @@ -155,7 +151,10 @@ impl IntoIterator for &RuleSelector { fn into_iter(self) -> Self::IntoIter { match self { RuleSelector::All => { - RuleSelectorIter::All(Rule::iter().filter(|rule| select_all(*rule))) + RuleSelectorIter::All(Rule::iter().filter(|rule| !rule.is_nursery())) + } + RuleSelector::Nursery => { + RuleSelectorIter::Nursery(Rule::iter().filter(Rule::is_nursery)) } RuleSelector::C => RuleSelectorIter::Chain( Linter::Flake8Comprehensions @@ -175,6 +174,7 @@ impl IntoIterator for &RuleSelector { pub enum RuleSelectorIter { All(std::iter::Filter bool>), + Nursery(std::iter::Filter bool>), Chain(std::iter::Chain, std::vec::IntoIter>), Vec(std::vec::IntoIter), } @@ -185,6 +185,7 @@ impl Iterator for RuleSelectorIter { fn next(&mut self) -> Option { match self { RuleSelectorIter::All(iter) => iter.next(), + RuleSelectorIter::Nursery(iter) => iter.next(), RuleSelectorIter::Chain(iter) => iter.next(), RuleSelectorIter::Vec(iter) => iter.next(), } @@ -262,6 +263,7 @@ impl RuleSelector { pub(crate) fn specificity(&self) -> Specificity { match self { RuleSelector::All => Specificity::All, + RuleSelector::Nursery => Specificity::All, RuleSelector::T => Specificity::LinterGroup, RuleSelector::C => Specificity::LinterGroup, RuleSelector::Linter(..) => Specificity::Linter, diff --git a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs index 11bfd64781..492be79613 100644 --- a/crates/ruff/src/rules/flake8_annotations/rules/definition.rs +++ b/crates/ruff/src/rules/flake8_annotations/rules/definition.rs @@ -418,12 +418,14 @@ impl Violation for AnyType { fn is_none_returning(body: &[Stmt]) -> bool { let mut visitor = ReturnStatementVisitor::default(); visitor.visit_body(body); - for expr in visitor.returns.into_iter().flatten() { - if !matches!( - expr, - Expr::Constant(ref constant) if constant.value.is_none() - ) { - return false; + for stmt in visitor.returns { + if let Some(value) = stmt.value.as_deref() { + if !matches!( + value, + Expr::Constant(constant) if constant.value.is_none() + ) { + return false; + } } } true diff --git a/crates/ruff/src/rules/flake8_bandit/settings.rs b/crates/ruff/src/rules/flake8_bandit/settings.rs index 76f363edc4..d43bd48700 100644 --- a/crates/ruff/src/rules/flake8_bandit/settings.rs +++ b/crates/ruff/src/rules/flake8_bandit/settings.rs @@ -6,7 +6,7 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions}; fn default_tmp_dirs() -> Vec { ["/tmp", "/var/tmp", "/dev/shm"] - .map(std::string::ToString::to_string) + .map(ToString::to_string) .to_vec() } diff --git a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs index 963fd62b89..2c397470b7 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs +++ b/crates/ruff/src/rules/flake8_boolean_trap/helpers.rs @@ -10,6 +10,7 @@ pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[ "assertEquals", "assertNotEqual", "assertNotEquals", + "bool", "bytes", "count", "failIfEqual", @@ -27,6 +28,8 @@ pub(super) const FUNC_CALL_NAME_ALLOWLIST: &[&str] = &[ "param", "pop", "remove", + "set_blocking", + "set_enabled", "setattr", "__setattr__", "setdefault", diff --git a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap index c940e93364..4f54d13871 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap +++ b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT001_FBT.py.snap @@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean positional arg in function definition 23 | kwonly_nonvalued_nohint, | -FBT.py:81:19: FBT001 Boolean positional arg in function definition +FBT.py:85:19: FBT001 Boolean positional arg in function definition | -81 | # FBT001: Boolean positional arg in function definition -82 | def foo(self, value: bool) -> None: +85 | # FBT001: Boolean positional arg in function definition +86 | def foo(self, value: bool) -> None: | ^^^^^^^^^^^ FBT001 -83 | pass +87 | pass | diff --git a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap index 43058488d3..1aeb008182 100644 --- a/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap +++ b/crates/ruff/src/rules/flake8_boolean_trap/snapshots/ruff__rules__flake8_boolean_trap__tests__FBT003_FBT.py.snap @@ -28,4 +28,12 @@ FBT.py:57:17: FBT003 Boolean positional value in function call 61 | mylist.index(True) | +FBT.py:69:38: FBT003 Boolean positional value in function call + | +69 | os.set_blocking(0, False) +70 | g_action.set_enabled(True) +71 | settings.set_enable_developer_extras(True) + | ^^^^ FBT003 + | + diff --git a/crates/ruff/src/rules/flake8_bugbear/mod.rs b/crates/ruff/src/rules/flake8_bugbear/mod.rs index 896b977adf..2bf5676306 100644 --- a/crates/ruff/src/rules/flake8_bugbear/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/mod.rs @@ -14,39 +14,40 @@ mod tests { use crate::settings::Settings; use crate::test::test_path; - #[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"))] - #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] - #[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))] - #[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))] - #[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))] - #[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))] - #[test_case(Rule::FunctionCallInDefaultArgument, Path::new("B006_B008.py"))] - #[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"))] - #[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))] - #[test_case(Rule::AssertFalse, Path::new("B011.py"))] - #[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"))] - #[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"))] - #[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"))] - #[test_case(Rule::UselessComparison, Path::new("B015.py"))] - #[test_case(Rule::CannotRaiseLiteral, Path::new("B016.py"))] - #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] - #[test_case(Rule::UselessExpression, Path::new("B018.py"))] - #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] - #[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))] - #[test_case(Rule::FStringDocstring, Path::new("B021.py"))] - #[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))] - #[test_case(Rule::FunctionUsesLoopVariable, Path::new("B023.py"))] #[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"))] + #[test_case(Rule::AssertFalse, Path::new("B011.py"))] + #[test_case(Rule::AssertRaisesException, Path::new("B017.py"))] + #[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"))] + #[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"))] + #[test_case(Rule::CannotRaiseLiteral, Path::new("B016.py"))] + #[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"))] #[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"))] - #[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))] + #[test_case(Rule::DuplicateValue, Path::new("B033.py"))] #[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"))] #[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.pyi"))] - #[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))] #[test_case(Rule::ExceptWithEmptyTuple, Path::new("B029.py"))] #[test_case(Rule::ExceptWithNonExceptionClasses, Path::new("B030.py"))] - #[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))] - #[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))] + #[test_case(Rule::FStringDocstring, Path::new("B021.py"))] + #[test_case(Rule::FunctionCallInDefaultArgument, Path::new("B006_B008.py"))] + #[test_case(Rule::FunctionUsesLoopVariable, Path::new("B023.py"))] + #[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"))] + #[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"))] + #[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"))] + #[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"))] + #[test_case(Rule::NoExplicitStacklevel, Path::new("B028.py"))] #[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"))] + #[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"))] + #[test_case(Rule::ReuseOfGroupbyGenerator, Path::new("B031.py"))] + #[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"))] + #[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"))] + #[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"))] + #[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"))] + #[test_case(Rule::UnintentionalTypeAnnotation, Path::new("B032.py"))] + #[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"))] + #[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"))] + #[test_case(Rule::UselessComparison, Path::new("B015.py"))] + #[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))] + #[test_case(Rule::UselessExpression, Path::new("B018.py"))] #[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"))] fn rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy()); diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_value.rs b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_value.rs new file mode 100644 index 0000000000..e66da4623c --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/rules/duplicate_value.rs @@ -0,0 +1,57 @@ +use rustc_hash::FxHashSet; +use rustpython_parser::ast::{self, Expr, Ranged}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::comparable::ComparableExpr; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for set literals that contain duplicate items. +/// +/// ## Why is this bad? +/// In Python, sets are unordered collections of unique elements. Including a +/// duplicate item in a set literal is redundant, as the duplicate item will be +/// replaced with a single item at runtime. +/// +/// ## Example +/// ```python +/// {1, 2, 3, 1} +/// ``` +/// +/// Use instead: +/// ```python +/// {1, 2, 3} +/// ``` +#[violation] +pub struct DuplicateValue { + value: String, +} + +impl Violation for DuplicateValue { + #[derive_message_formats] + fn message(&self) -> String { + let DuplicateValue { value } = self; + format!("Sets should not contain duplicate item `{value}`") + } +} + +/// B033 +pub(crate) fn duplicate_value(checker: &mut Checker, elts: &Vec) { + let mut seen_values: FxHashSet = FxHashSet::default(); + for elt in elts { + if let Expr::Constant(ast::ExprConstant { value, .. }) = elt { + let comparable_value: ComparableExpr = elt.into(); + + if !seen_values.insert(comparable_value) { + checker.diagnostics.push(Diagnostic::new( + DuplicateValue { + value: checker.generator().constant(value), + }, + elt.range(), + )); + } + }; + } +} diff --git a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs index 77714559fb..f350160a57 100644 --- a/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_bugbear/rules/mod.rs @@ -10,6 +10,7 @@ pub(crate) use cannot_raise_literal::{cannot_raise_literal, CannotRaiseLiteral}; pub(crate) use duplicate_exceptions::{ duplicate_exceptions, DuplicateHandlerException, DuplicateTryBlockException, }; +pub(crate) use duplicate_value::{duplicate_value, DuplicateValue}; pub(crate) use except_with_empty_tuple::{except_with_empty_tuple, ExceptWithEmptyTuple}; pub(crate) use except_with_non_exception_classes::{ except_with_non_exception_classes, ExceptWithNonExceptionClasses, @@ -66,6 +67,7 @@ mod assignment_to_os_environ; mod cached_instance_method; mod cannot_raise_literal; mod duplicate_exceptions; +mod duplicate_value; mod except_with_empty_tuple; mod except_with_non_exception_classes; mod f_string_docstring; diff --git a/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B033_B033.py.snap b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B033_B033.py.snap new file mode 100644 index 0000000000..a71540f42f --- /dev/null +++ b/crates/ruff/src/rules/flake8_bugbear/snapshots/ruff__rules__flake8_bugbear__tests__B033_B033.py.snap @@ -0,0 +1,23 @@ +--- +source: crates/ruff/src/rules/flake8_bugbear/mod.rs +--- +B033.py:4:35: B033 Sets should not contain duplicate item `"value1"` + | +4 | # Errors. +5 | ### +6 | incorrect_set = {"value1", 23, 5, "value1"} + | ^^^^^^^^ B033 +7 | incorrect_set = {1, 1} + | + +B033.py:5:21: B033 Sets should not contain duplicate item `1` + | +5 | ### +6 | incorrect_set = {"value1", 23, 5, "value1"} +7 | incorrect_set = {1, 1} + | ^ B033 +8 | +9 | ### + | + + diff --git a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs index b9054be763..b10156a39a 100644 --- a/crates/ruff/src/rules/flake8_comprehensions/fixes.rs +++ b/crates/ruff/src/rules/flake8_comprehensions/fixes.rs @@ -1,14 +1,15 @@ use anyhow::{bail, Result}; use itertools::Itertools; use libcst_native::{ - Arg, AssignEqual, AssignTargetExpression, Call, Codegen, CodegenState, Comment, CompFor, Dict, - DictComp, DictElement, Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, - LeftSquareBracket, List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedWhitespace, - RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace, + Arg, AssignEqual, AssignTargetExpression, Call, Comment, CompFor, Dict, DictComp, DictElement, + Element, EmptyLine, Expression, GeneratorExp, LeftCurlyBrace, LeftParen, LeftSquareBracket, + List, ListComp, Name, ParenthesizableWhitespace, ParenthesizedWhitespace, RightCurlyBrace, + RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace, TrailingWhitespace, Tuple, }; use rustpython_parser::ast::Ranged; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{Edit, Fix}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -44,14 +45,10 @@ pub(crate) fn fix_unnecessary_generator_list( rpar: generator_exp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C401) Convert `set(x for x in y)` to `{x for x in y}`. @@ -82,14 +79,7 @@ pub(crate) fn fix_unnecessary_generator_set( rpar: generator_exp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. @@ -136,14 +126,7 @@ pub(crate) fn fix_unnecessary_generator_dict( whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. @@ -182,14 +165,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_set( rpar: list_comp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C404) Convert `dict([(i, i) for i in range(3)])` to `{i: i for i in @@ -229,14 +208,10 @@ pub(crate) fn fix_unnecessary_list_comprehension_dict( rpar: list_comp.rpar.clone(), })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// Drop a trailing comma from a list of tuple elements. @@ -291,7 +266,7 @@ pub(crate) fn fix_unnecessary_literal_set( // Expr(Call(List|Tuple)))) -> Expr(Set))) let module_text = locator.slice(expr.range()); let mut tree = match_expression(module_text)?; - let mut call = match_call_mut(&mut tree)?; + let call = match_call_mut(&mut tree)?; let arg = match_arg(call)?; let (elements, whitespace_after, whitespace_before) = match &arg.value { @@ -318,14 +293,10 @@ pub(crate) fn fix_unnecessary_literal_set( })); } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C406) Convert `dict([(1, 2)])` to `{1: 2}`. @@ -386,14 +357,10 @@ pub(crate) fn fix_unnecessary_literal_dict( rpar: vec![], })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C408) @@ -495,14 +462,10 @@ pub(crate) fn fix_unnecessary_collection_call( } }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C409) Convert `tuple([1, 2])` to `tuple(1, 2)` @@ -549,14 +512,10 @@ pub(crate) fn fix_unnecessary_literal_within_tuple_call( }], })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C410) Convert `list([1, 2])` to `[1, 2]` @@ -605,14 +564,10 @@ pub(crate) fn fix_unnecessary_literal_within_list_call( rpar: vec![], })); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C411) Convert `list([i * i for i in x])` to `[i * i for i in x]`. @@ -629,14 +584,10 @@ pub(crate) fn fix_unnecessary_list_call( tree = arg.value.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C413) Convert `list(sorted([2, 3, 1]))` to `sorted([2, 3, 1])`. @@ -747,14 +698,10 @@ pub(crate) fn fix_unnecessary_call_around_sorted( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C414) Convert `sorted(list(foo))` to `sorted(foo)` @@ -765,7 +712,7 @@ pub(crate) fn fix_unnecessary_double_cast_or_process( ) -> Result { let module_text = locator.slice(expr.range()); let mut tree = match_expression(module_text)?; - let mut outer_call = match_call_mut(&mut tree)?; + let outer_call = match_call_mut(&mut tree)?; outer_call.args = match outer_call.args.split_first() { Some((first, rest)) => { @@ -781,14 +728,10 @@ pub(crate) fn fix_unnecessary_double_cast_or_process( None => bail!("Expected at least one argument in outer function call"), }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C416) Convert `[i for i in x]` to `list(x)`. @@ -872,14 +815,10 @@ pub(crate) fn fix_unnecessary_comprehension( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C417) Convert `map(lambda x: x * 2, bar)` to `(x * 2 for x in bar)`. @@ -1018,14 +957,7 @@ pub(crate) fn fix_unnecessary_map( } } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); // If the expression is embedded in an f-string, surround it with spaces to avoid // syntax errors. @@ -1054,14 +986,10 @@ pub(crate) fn fix_unnecessary_literal_within_dict_call( tree = arg.value.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), expr.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + expr.range(), + )) } /// (C419) Convert `[i for i in a]` into `i for i in a` @@ -1231,15 +1159,8 @@ pub(crate) fn fix_unnecessary_comprehension_any_all( _ => whitespace_after_arg, }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - Ok(Fix::suggested(Edit::range_replacement( - state.to_string(), + tree.codegen_stylist(stylist), expr.range(), ))) } diff --git a/crates/ruff/src/rules/flake8_pyi/mod.rs b/crates/ruff/src/rules/flake8_pyi/mod.rs index 84e601b4c9..783018fba8 100644 --- a/crates/ruff/src/rules/flake8_pyi/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/mod.rs @@ -48,6 +48,8 @@ mod tests { #[test_case(Rule::SnakeCaseTypeAlias, Path::new("PYI042.pyi"))] #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.py"))] #[test_case(Rule::UnassignedSpecialVariableInStub, Path::new("PYI035.pyi"))] + #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.py"))] + #[test_case(Rule::StrOrReprDefinedInStub, Path::new("PYI029.pyi"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.py"))] #[test_case(Rule::StubBodyMultipleStatements, Path::new("PYI048.pyi"))] #[test_case(Rule::TSuffixedTypeAlias, Path::new("PYI043.py"))] diff --git a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs index 0beb7b51ed..a754a518d2 100644 --- a/crates/ruff/src/rules/flake8_pyi/rules/mod.rs +++ b/crates/ruff/src/rules/flake8_pyi/rules/mod.rs @@ -27,6 +27,7 @@ pub(crate) use simple_defaults::{ unassigned_special_variable_in_stub, ArgumentDefaultInStub, AssignmentDefaultInStub, TypedArgumentDefaultInStub, UnannotatedAssignmentInStub, UnassignedSpecialVariableInStub, }; +pub(crate) use str_or_repr_defined_in_stub::{str_or_repr_defined_in_stub, StrOrReprDefinedInStub}; pub(crate) use string_or_bytes_too_long::{string_or_bytes_too_long, StringOrBytesTooLong}; pub(crate) use stub_body_multiple_statements::{ stub_body_multiple_statements, StubBodyMultipleStatements, @@ -58,6 +59,7 @@ mod pass_statement_stub_body; mod prefix_type_params; mod quoted_annotation_in_stub; mod simple_defaults; +mod str_or_repr_defined_in_stub; mod string_or_bytes_too_long; mod stub_body_multiple_statements; mod type_alias_naming; diff --git a/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs new file mode 100644 index 0000000000..88f204fcc3 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/rules/str_or_repr_defined_in_stub.rs @@ -0,0 +1,110 @@ +use rustpython_parser::ast; +use rustpython_parser::ast::Stmt; + +use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::helpers::identifier_range; +use ruff_python_semantic::analyze::visibility::is_abstract; + +use crate::autofix::edits::delete_stmt; +use crate::checkers::ast::Checker; +use crate::registry::AsRule; + +/// ## What it does +/// Checks for redundant definitions of `__str__` or `__repr__` in stubs. +/// +/// ## Why is this bad? +/// Defining `__str__` or `__repr__` in a stub is almost always redundant, +/// as the signatures are almost always identical to those of the default +/// equivalent, `object.__str__` and `object.__repr__`, respectively. +/// +/// ## Example +/// ```python +/// class Foo: +/// def __repr__(self) -> str: +/// ... +/// ``` +#[violation] +pub struct StrOrReprDefinedInStub { + name: String, +} + +impl AlwaysAutofixableViolation for StrOrReprDefinedInStub { + #[derive_message_formats] + fn message(&self) -> String { + let StrOrReprDefinedInStub { name } = self; + format!("Defining `{name}` in a stub is almost always redundant") + } + + fn autofix_title(&self) -> String { + let StrOrReprDefinedInStub { name } = self; + format!("Remove definition of `{name}`") + } +} + +/// PYI029 +pub(crate) fn str_or_repr_defined_in_stub(checker: &mut Checker, stmt: &Stmt) { + let Stmt::FunctionDef(ast::StmtFunctionDef { + name, + decorator_list, + returns, + args, + .. + }) = stmt else { + return + }; + + let Some(returns) = returns else { + return; + }; + + if !matches!(name.as_str(), "__str__" | "__repr__") { + return; + } + + if !checker.semantic_model().scope().kind.is_class() { + return; + } + + // It is a violation only if the method signature matches that of `object.__str__` + // or `object.__repr__` exactly and the method is not decorated as abstract. + if !args.kwonlyargs.is_empty() || (args.args.len() + args.posonlyargs.len()) > 1 { + return; + } + + if is_abstract(checker.semantic_model(), decorator_list) { + return; + } + + if checker + .semantic_model() + .resolve_call_path(returns) + .map_or(true, |call_path| { + !matches!(call_path.as_slice(), ["" | "builtins", "str"]) + }) + { + return; + } + + let mut diagnostic = Diagnostic::new( + StrOrReprDefinedInStub { + name: name.to_string(), + }, + identifier_range(stmt, checker.locator), + ); + if checker.patch(diagnostic.kind.rule()) { + let stmt = checker.semantic_model().stmt(); + let parent = checker.semantic_model().stmt_parent(); + let edit = delete_stmt( + stmt, + parent, + checker.locator, + checker.indexer, + checker.stylist, + ); + diagnostic.set_fix( + Fix::automatic(edit).isolate(checker.isolation(checker.semantic_model().stmt_parent())), + ); + } + checker.diagnostics.push(diagnostic); +} diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.py.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.py.snap new file mode 100644 index 0000000000..d1aa2e9116 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- + diff --git a/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.pyi.snap b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.pyi.snap new file mode 100644 index 0000000000..b1c616ccb4 --- /dev/null +++ b/crates/ruff/src/rules/flake8_pyi/snapshots/ruff__rules__flake8_pyi__tests__PYI029_PYI029.pyi.snap @@ -0,0 +1,62 @@ +--- +source: crates/ruff/src/rules/flake8_pyi/mod.rs +--- +PYI029.pyi:10:9: PYI029 [*] Defining `__str__` in a stub is almost always redundant + | +10 | class ShouldRemoveSingle: +11 | def __str__(self) -> builtins.str: ... # Error: PYI029 + | ^^^^^^^ PYI029 +12 | +13 | class ShouldRemove: + | + = help: Remove definition of `str` + +ℹ Fix +7 7 | def __repr__(self, *, foo) -> str: ... +8 8 | +9 9 | class ShouldRemoveSingle: +10 |- def __str__(self) -> builtins.str: ... # Error: PYI029 + 10 |+ pass # Error: PYI029 +11 11 | +12 12 | class ShouldRemove: +13 13 | def __repr__(self) -> str: ... # Error: PYI029 + +PYI029.pyi:13:9: PYI029 [*] Defining `__repr__` in a stub is almost always redundant + | +13 | class ShouldRemove: +14 | def __repr__(self) -> str: ... # Error: PYI029 + | ^^^^^^^^ PYI029 +15 | def __str__(self) -> builtins.str: ... # Error: PYI029 + | + = help: Remove definition of `repr` + +ℹ Fix +10 10 | def __str__(self) -> builtins.str: ... # Error: PYI029 +11 11 | +12 12 | class ShouldRemove: +13 |- def __repr__(self) -> str: ... # Error: PYI029 +14 13 | def __str__(self) -> builtins.str: ... # Error: PYI029 +15 14 | +16 15 | class NoReturnSpecified: + +PYI029.pyi:14:9: PYI029 [*] Defining `__str__` in a stub is almost always redundant + | +14 | class ShouldRemove: +15 | def __repr__(self) -> str: ... # Error: PYI029 +16 | def __str__(self) -> builtins.str: ... # Error: PYI029 + | ^^^^^^^ PYI029 +17 | +18 | class NoReturnSpecified: + | + = help: Remove definition of `str` + +ℹ Fix +11 11 | +12 12 | class ShouldRemove: +13 13 | def __repr__(self) -> str: ... # Error: PYI029 +14 |- def __str__(self) -> builtins.str: ... # Error: PYI029 +15 14 | +16 15 | class NoReturnSpecified: +17 16 | def __str__(self): ... + + diff --git a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs index aa59cf845d..ee2f4f7c10 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/rules/assertion.rs @@ -3,12 +3,13 @@ use std::borrow::Cow; use anyhow::bail; use anyhow::Result; use libcst_native::{ - Assert, BooleanOp, Codegen, CodegenState, CompoundStatement, Expression, - ParenthesizableWhitespace, ParenthesizedNode, SimpleStatementLine, SimpleWhitespace, - SmallStatement, Statement, TrailingWhitespace, UnaryOp, UnaryOperation, + Assert, BooleanOp, CompoundStatement, Expression, ParenthesizableWhitespace, ParenthesizedNode, + SimpleStatementLine, SimpleWhitespace, SmallStatement, Statement, TrailingWhitespace, UnaryOp, + UnaryOperation, }; use rustpython_parser::ast::{self, Boolop, Excepthandler, Expr, Keyword, Ranged, Stmt, Unaryop}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::helpers::{has_comments_in, Truthiness}; @@ -410,15 +411,8 @@ fn fix_composite_condition(stmt: &Stmt, locator: &Locator, stylist: &Stylist) -> })); } - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - // Reconstruct and reformat the code. - let module_text = state.to_string(); + let module_text = tree.codegen_stylist(stylist); let contents = if outer_indent.is_empty() { module_text } else { diff --git a/crates/ruff/src/rules/flake8_pytest_style/settings.rs b/crates/ruff/src/rules/flake8_pytest_style/settings.rs index e70545c6d4..d00a3d8ebc 100644 --- a/crates/ruff/src/rules/flake8_pytest_style/settings.rs +++ b/crates/ruff/src/rules/flake8_pytest_style/settings.rs @@ -16,7 +16,7 @@ fn default_broad_exceptions() -> Vec { "EnvironmentError", "socket.error", ] - .map(std::string::ToString::to_string) + .map(ToString::to_string) .to_vec() } diff --git a/crates/ruff/src/rules/flake8_return/rules/function.rs b/crates/ruff/src/rules/flake8_return/rules/function.rs index cc84657be0..9bfb85ade3 100644 --- a/crates/ruff/src/rules/flake8_return/rules/function.rs +++ b/crates/ruff/src/rules/flake8_return/rules/function.rs @@ -506,34 +506,36 @@ fn implicit_return(checker: &mut Checker, stmt: &Stmt) { } } -/// Return `true` if the `id` has multiple assignments within the function. -fn has_multiple_assigns(id: &str, stack: &Stack) -> bool { - if let Some(assigns) = stack.assignments.get(&id) { - if assigns.len() > 1 { - return true; - } - } - false +/// Return `true` if the `id` has multiple declarations within the function. +fn has_multiple_declarations(id: &str, stack: &Stack) -> bool { + stack + .declarations + .get(&id) + .map_or(false, |declarations| declarations.len() > 1) } /// Return `true` if the `id` has a (read) reference between the `return_location` and its -/// preceding assignment. -fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) -> bool { - let mut assignment_before_return: Option = None; - let mut assignment_after_return: Option = None; - if let Some(assignments) = stack.assignments.get(&id) { +/// preceding declaration. +fn has_references_before_next_declaration( + id: &str, + return_range: TextRange, + stack: &Stack, +) -> bool { + let mut declaration_before_return: Option = None; + let mut declaration_after_return: Option = None; + if let Some(assignments) = stack.declarations.get(&id) { for location in assignments.iter().sorted() { if *location > return_range.start() { - assignment_after_return = Some(*location); + declaration_after_return = Some(*location); break; } - assignment_before_return = Some(*location); + declaration_before_return = Some(*location); } } - // If there is no assignment before the return, then the variable must be defined in + // If there is no declaration before the return, then the variable must be declared in // some other way (e.g., a function argument). No need to check for references. - let Some(assignment_before_return) = assignment_before_return else { + let Some(declaration_before_return) = declaration_before_return else { return true; }; @@ -543,9 +545,9 @@ fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) continue; } - if assignment_before_return < *location { - if let Some(assignment_after_return) = assignment_after_return { - if *location <= assignment_after_return { + if declaration_before_return < *location { + if let Some(declaration_after_return) = declaration_after_return { + if *location <= declaration_after_return { return true; } } else { @@ -559,7 +561,7 @@ fn has_refs_before_next_assign(id: &str, return_range: TextRange, stack: &Stack) } /// Return `true` if the `id` has a read or write reference within a `try` or loop body. -fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool { +fn has_references_or_declarations_within_try_or_loop(id: &str, stack: &Stack) -> bool { if let Some(references) = stack.references.get(&id) { for location in references { for try_range in &stack.tries { @@ -574,7 +576,7 @@ fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool { } } } - if let Some(references) = stack.assignments.get(&id) { + if let Some(references) = stack.declarations.get(&id) { for location in references { for try_range in &stack.tries { if try_range.contains(*location) { @@ -594,7 +596,7 @@ fn has_refs_or_assigns_within_try_or_loop(id: &str, stack: &Stack) -> bool { /// RET504 fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) { if let Expr::Name(ast::ExprName { id, .. }) = expr { - if !stack.assignments.contains_key(id.as_str()) { + if !stack.assigned_names.contains(id.as_str()) { return; } @@ -605,9 +607,9 @@ fn unnecessary_assign(checker: &mut Checker, stack: &Stack, expr: &Expr) { return; } - if has_multiple_assigns(id, stack) - || has_refs_before_next_assign(id, expr.range(), stack) - || has_refs_or_assigns_within_try_or_loop(id, stack) + if has_multiple_declarations(id, stack) + || has_references_before_next_declaration(id, expr.range(), stack) + || has_references_or_declarations_within_try_or_loop(id, stack) { return; } diff --git a/crates/ruff/src/rules/flake8_return/visitor.rs b/crates/ruff/src/rules/flake8_return/visitor.rs index c7c3657138..f0d419c7bc 100644 --- a/crates/ruff/src/rules/flake8_return/visitor.rs +++ b/crates/ruff/src/rules/flake8_return/visitor.rs @@ -11,9 +11,14 @@ pub(crate) struct Stack<'a> { pub(crate) yields: Vec<&'a Expr>, pub(crate) elses: Vec<&'a Stmt>, pub(crate) elifs: Vec<&'a Stmt>, + /// The names that are assigned to in the current scope (e.g., anything on the left-hand side of + /// an assignment). + pub(crate) assigned_names: FxHashSet<&'a str>, + /// The names that are declared in the current scope, and the ranges of those declarations + /// (e.g., assignments, but also function and class definitions). + pub(crate) declarations: FxHashMap<&'a str, Vec>, pub(crate) references: FxHashMap<&'a str, Vec>, pub(crate) non_locals: FxHashSet<&'a str>, - pub(crate) assignments: FxHashMap<&'a str, Vec>, pub(crate) loops: Vec, pub(crate) tries: Vec, } @@ -34,8 +39,9 @@ impl<'a> ReturnVisitor<'a> { return; } Expr::Name(ast::ExprName { id, .. }) => { + self.stack.assigned_names.insert(id.as_str()); self.stack - .assignments + .declarations .entry(id) .or_insert_with(Vec::new) .push(expr.start()); @@ -45,7 +51,7 @@ impl<'a> ReturnVisitor<'a> { // Attribute assignments are often side-effects (e.g., `self.property = value`), // so we conservatively treat them as references to every known // variable. - for name in self.stack.assignments.keys() { + for name in self.stack.declarations.keys() { self.stack .references .entry(name) @@ -68,18 +74,44 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { .non_locals .extend(names.iter().map(Identifier::as_str)); } - Stmt::FunctionDef(ast::StmtFunctionDef { + Stmt::ClassDef(ast::StmtClassDef { decorator_list, + name, + .. + }) => { + // Mark a declaration. + self.stack + .declarations + .entry(name.as_str()) + .or_insert_with(Vec::new) + .push(stmt.start()); + + // Don't recurse into the body, but visit the decorators, etc. + for expr in decorator_list { + visitor::walk_expr(self, expr); + } + } + Stmt::FunctionDef(ast::StmtFunctionDef { + name, args, + decorator_list, returns, .. }) | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { - decorator_list, + name, args, + decorator_list, returns, .. }) => { + // Mark a declaration. + self.stack + .declarations + .entry(name.as_str()) + .or_insert_with(Vec::new) + .push(stmt.start()); + // Don't recurse into the body, but visit the decorators, etc. for expr in decorator_list { visitor::walk_expr(self, expr); @@ -138,7 +170,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { if let Some(target) = targets.first() { // Skip unpacking assignments, like `x, y = my_object`. - if matches!(target, Expr::Tuple(_)) && !value.is_tuple_expr() { + if target.is_tuple_expr() && !value.is_tuple_expr() { return; } @@ -172,7 +204,7 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> { Expr::Call(_) => { // Arbitrary function calls can have side effects, so we conservatively treat // every function call as a reference to every known variable. - for name in self.stack.assignments.keys() { + for name in self.stack.declarations.keys() { self.stack .references .entry(name) diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs index 4a58149ff8..a36364dedf 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_if.rs @@ -2,12 +2,12 @@ use std::borrow::Cow; use anyhow::{bail, Result}; use libcst_native::{ - BooleanOp, BooleanOperation, Codegen, CodegenState, CompoundStatement, Expression, If, - LeftParen, ParenthesizableWhitespace, ParenthesizedNode, RightParen, SimpleWhitespace, - Statement, Suite, + BooleanOp, BooleanOperation, CompoundStatement, Expression, If, LeftParen, + ParenthesizableWhitespace, ParenthesizedNode, RightParen, SimpleWhitespace, Statement, Suite, }; use rustpython_parser::ast::Ranged; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::whitespace; @@ -111,15 +111,8 @@ pub(crate) fn fix_nested_if_statements( })); outer_if.body = inner_if.body.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..Default::default() - }; - tree.codegen(&mut state); - // Reconstruct and reformat the code. - let module_text = state.to_string(); + let module_text = tree.codegen_stylist(stylist); let module_text = if outer_indent.is_empty() { &module_text } else { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs index eaae0f8575..b3636cabbc 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/fix_with.rs @@ -1,7 +1,8 @@ use anyhow::{bail, Result}; -use libcst_native::{Codegen, CodegenState, CompoundStatement, Statement, Suite, With}; +use libcst_native::{CompoundStatement, Statement, Suite, With}; use rustpython_parser::ast::Ranged; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::whitespace; @@ -70,15 +71,8 @@ pub(crate) fn fix_multiple_with_statements( } outer_with.body = inner_with.body.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - // Reconstruct and reformat the code. - let module_text = state.to_string(); + let module_text = tree.codegen_stylist(stylist); let contents = if outer_indent.is_empty() { module_text } else { diff --git a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs index e63827a352..8463476e96 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/key_in_dict.rs @@ -1,9 +1,10 @@ use anyhow::Result; -use libcst_native::{Codegen, CodegenState}; + use log::error; use ruff_text_size::TextRange; use rustpython_parser::ast::{self, Cmpop, Expr, Ranged}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -42,14 +43,7 @@ fn get_value_content_for_key_in_dict( let call = match_call_mut(&mut expression)?; let attribute = match_attribute(&mut call.func)?; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - attribute.value.codegen(&mut state); - - Ok(state.to_string()) + Ok(attribute.value.codegen_stylist(stylist)) } /// SIM118 diff --git a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs index c52b6d12dc..b790019576 100644 --- a/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs +++ b/crates/ruff/src/rules/flake8_simplify/rules/yoda_conditions.rs @@ -1,7 +1,8 @@ use anyhow::Result; -use libcst_native::{Codegen, CodegenState, CompOp}; +use libcst_native::CompOp; use rustpython_parser::ast::{self, Cmpop, Expr, Ranged, Unaryop}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -59,7 +60,7 @@ fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Resu let contents = locator.slice(range); let mut expression = match_expression(contents)?; - let mut comparison = match_comparison(&mut expression)?; + let comparison = match_comparison(&mut expression)?; let left = (*comparison.left).clone(); @@ -117,13 +118,7 @@ fn reverse_comparison(expr: &Expr, locator: &Locator, stylist: &Stylist) -> Resu _ => panic!("Expected comparison operator"), }; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - expression.codegen(&mut state); - Ok(state.to_string()) + Ok(expression.codegen_stylist(stylist)) } /// SIM300 diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs index 61586fdf75..a2da8d1c98 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/runtime_import_in_type_checking_block.rs @@ -1,8 +1,6 @@ use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{ - Binding, BindingKind, FromImportation, Importation, SubmoduleImportation, -}; +use ruff_python_semantic::binding::Binding; use crate::autofix; use crate::checkers::ast::Checker; @@ -41,7 +39,7 @@ use crate::registry::AsRule; /// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct RuntimeImportInTypeCheckingBlock { - full_name: String, + qualified_name: String, } impl Violation for RuntimeImportInTypeCheckingBlock { @@ -49,9 +47,9 @@ impl Violation for RuntimeImportInTypeCheckingBlock { #[derive_message_formats] fn message(&self) -> String { - let RuntimeImportInTypeCheckingBlock { full_name } = self; + let RuntimeImportInTypeCheckingBlock { qualified_name } = self; format!( - "Move import `{full_name}` out of type-checking block. Import is used for more than type hinting." + "Move import `{qualified_name}` out of type-checking block. Import is used for more than type hinting." ) } @@ -66,11 +64,8 @@ pub(crate) fn runtime_import_in_type_checking_block( binding: &Binding, diagnostics: &mut Vec, ) { - let full_name = match &binding.kind { - BindingKind::Importation(Importation { full_name }) => full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => full_name, - _ => return, + let Some(qualified_name) = binding.qualified_name() else { + return; }; let Some(reference_id) = binding.references.first() else { @@ -89,10 +84,13 @@ pub(crate) fn runtime_import_in_type_checking_block( { let mut diagnostic = Diagnostic::new( RuntimeImportInTypeCheckingBlock { - full_name: full_name.to_string(), + qualified_name: qualified_name.to_string(), }, - binding.range, + binding.trimmed_range(checker.semantic_model(), checker.locator), ); + if let Some(range) = binding.parent_range(checker.semantic_model()) { + diagnostic.set_parent(range.start()); + } if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { @@ -102,7 +100,7 @@ pub(crate) fn runtime_import_in_type_checking_block( let stmt = checker.semantic_model().stmts[source]; let parent = checker.semantic_model().stmts.parent(stmt); let remove_import_edit = autofix::edits::remove_unused_imports( - std::iter::once(full_name), + std::iter::once(qualified_name), stmt, parent, checker.locator, @@ -113,7 +111,10 @@ pub(crate) fn runtime_import_in_type_checking_block( // Step 2) Add the import to the top-level. let reference = checker.semantic_model().references.resolve(*reference_id); let add_import_edit = checker.importer.runtime_import_edit( - &StmtImport { stmt, full_name }, + &StmtImport { + stmt, + qualified_name, + }, reference.range().start(), )?; diff --git a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs index 8abcb73943..085c565271 100644 --- a/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs +++ b/crates/ruff/src/rules/flake8_type_checking/rules/typing_only_runtime_import.rs @@ -1,8 +1,6 @@ -use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation}; +use ruff_diagnostics::{AutofixKind, Diagnostic, DiagnosticKind, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{ - Binding, BindingKind, FromImportation, Importation, SubmoduleImportation, -}; +use ruff_python_semantic::binding::Binding; use crate::autofix; use crate::checkers::ast::Checker; @@ -47,7 +45,7 @@ use crate::rules::isort::{categorize, ImportSection, ImportType}; /// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyFirstPartyImport { - full_name: String, + qualified_name: String, } impl Violation for TypingOnlyFirstPartyImport { @@ -57,7 +55,7 @@ impl Violation for TypingOnlyFirstPartyImport { fn message(&self) -> String { format!( "Move application import `{}` into a type-checking block", - self.full_name + self.qualified_name ) } @@ -103,7 +101,7 @@ impl Violation for TypingOnlyFirstPartyImport { /// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyThirdPartyImport { - full_name: String, + qualified_name: String, } impl Violation for TypingOnlyThirdPartyImport { @@ -113,7 +111,7 @@ impl Violation for TypingOnlyThirdPartyImport { fn message(&self) -> String { format!( "Move third-party import `{}` into a type-checking block", - self.full_name + self.qualified_name ) } @@ -159,7 +157,7 @@ impl Violation for TypingOnlyThirdPartyImport { /// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking) #[violation] pub struct TypingOnlyStandardLibraryImport { - full_name: String, + qualified_name: String, } impl Violation for TypingOnlyStandardLibraryImport { @@ -169,7 +167,7 @@ impl Violation for TypingOnlyStandardLibraryImport { fn message(&self) -> String { format!( "Move standard library import `{}` into a type-checking block", - self.full_name + self.qualified_name ) } @@ -180,65 +178,13 @@ impl Violation for TypingOnlyStandardLibraryImport { /// Return `true` if `this` is implicitly loaded via importing `that`. fn is_implicit_import(this: &Binding, that: &Binding) -> bool { - match &this.kind { - BindingKind::Importation(Importation { - full_name: this_name, - }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: this_name, - }) => match &that.kind { - BindingKind::FromImportation(FromImportation { - full_name: that_name, - }) => { - // Ex) `pkg.A` vs. `pkg` - let this_name = this_name.split('.').next().unwrap_or(this_name); - that_name - .rfind('.') - .map_or(false, |i| that_name[..i] == *this_name) - } - BindingKind::Importation(Importation { - full_name: that_name, - }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: that_name, - }) => { - // Submodule importation with an alias (`import pkg.A as B`) - // are represented as `Importation`. - let this_name = this_name.split('.').next().unwrap_or(this_name); - let that_name = that_name.split('.').next().unwrap_or(that_name); - this_name == that_name - } - _ => false, - }, - BindingKind::FromImportation(FromImportation { - full_name: this_name, - }) => match &that.kind { - BindingKind::Importation(Importation { - full_name: that_name, - }) - | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: that_name, - }) => { - // Ex) `pkg.A` vs. `pkg` - let that_name = that_name.split('.').next().unwrap_or(that_name); - this_name - .rfind('.') - .map_or(false, |i| &this_name[..i] == that_name) - } - BindingKind::FromImportation(FromImportation { - full_name: that_name, - }) => { - // Ex) `pkg.A` vs. `pkg.B` - this_name.rfind('.').map_or(false, |i| { - that_name - .rfind('.') - .map_or(false, |j| this_name[..i] == that_name[..j]) - }) - } - _ => false, - }, - _ => false, - } + let Some(this_module) = this.module_name() else { + return false; + }; + let Some(that_module) = that.module_name() else { + return false; + }; + this_module == that_module } /// Return `true` if `name` is exempt from typing-only enforcement. @@ -274,15 +220,12 @@ pub(crate) fn typing_only_runtime_import( return; } - let full_name = match &binding.kind { - BindingKind::Importation(Importation { full_name }) => full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => full_name, - _ => return, + let Some(qualified_name) = binding.qualified_name() else { + return; }; if is_exempt( - full_name, + qualified_name, &checker .settings .flake8_type_checking @@ -312,7 +255,7 @@ pub(crate) fn typing_only_runtime_import( // Extract the module base and level from the full name. // Ex) `foo.bar.baz` -> `foo`, `0` // Ex) `.foo.bar.baz` -> `foo`, `1` - let level = full_name + let level = qualified_name .chars() .take_while(|c| *c == '.') .count() @@ -320,8 +263,8 @@ pub(crate) fn typing_only_runtime_import( .unwrap(); // Categorize the import. - let mut diagnostic = match categorize( - full_name, + let kind: DiagnosticKind = match categorize( + qualified_name, Some(level), &checker.settings.src, checker.package(), @@ -329,32 +272,35 @@ pub(crate) fn typing_only_runtime_import( checker.settings.target_version, ) { ImportSection::Known(ImportType::LocalFolder | ImportType::FirstParty) => { - Diagnostic::new( - TypingOnlyFirstPartyImport { - full_name: full_name.to_string(), - }, - binding.range, - ) + TypingOnlyFirstPartyImport { + qualified_name: qualified_name.to_string(), + } + .into() } ImportSection::Known(ImportType::ThirdParty) | ImportSection::UserDefined(_) => { - Diagnostic::new( - TypingOnlyThirdPartyImport { - full_name: full_name.to_string(), - }, - binding.range, - ) + TypingOnlyThirdPartyImport { + qualified_name: qualified_name.to_string(), + } + .into() } - ImportSection::Known(ImportType::StandardLibrary) => Diagnostic::new( - TypingOnlyStandardLibraryImport { - full_name: full_name.to_string(), - }, - binding.range, - ), + ImportSection::Known(ImportType::StandardLibrary) => TypingOnlyStandardLibraryImport { + qualified_name: qualified_name.to_string(), + } + .into(), + ImportSection::Known(ImportType::Future) => { unreachable!("`__future__` imports should be marked as used") } }; + let mut diagnostic = Diagnostic::new( + kind, + binding.trimmed_range(checker.semantic_model(), checker.locator), + ); + if let Some(range) = binding.parent_range(checker.semantic_model()) { + diagnostic.set_parent(range.start()); + } + if checker.patch(diagnostic.kind.rule()) { diagnostic.try_set_fix(|| { // Step 1) Remove the import. @@ -363,7 +309,7 @@ pub(crate) fn typing_only_runtime_import( let stmt = checker.semantic_model().stmts[source]; let parent = checker.semantic_model().stmts.parent(stmt); let remove_import_edit = autofix::edits::remove_unused_imports( - std::iter::once(full_name), + std::iter::once(qualified_name), stmt, parent, checker.locator, @@ -374,7 +320,10 @@ pub(crate) fn typing_only_runtime_import( // Step 2) Add the import to a `TYPE_CHECKING` block. let reference = checker.semantic_model().references.resolve(*reference_id); let add_import_edit = checker.importer.typing_import_edit( - &StmtImport { stmt, full_name }, + &StmtImport { + stmt, + qualified_name, + }, reference.range().start(), checker.semantic_model(), )?; diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap index bee4fa7c52..1e6a497223 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__strict.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- strict.py:27:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking block | -27 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +27 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 28 | import pkg 29 | from pkg import A | ^ TCH002 @@ -23,7 +23,7 @@ strict.py:27:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking 4 8 | def f(): -------------------------------------------------------------------------------- 24 28 | def f(): -25 29 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +25 29 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 26 30 | import pkg 27 |- from pkg import A 28 31 | @@ -33,7 +33,7 @@ strict.py:27:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking strict.py:35:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking block | 35 | def f(): -36 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +36 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 37 | from pkg import A, B | ^ TCH002 38 | @@ -53,7 +53,7 @@ strict.py:35:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking -------------------------------------------------------------------------------- 32 36 | 33 37 | def f(): -34 38 | # In un-strict mode, this shouldn't rase an error, since `pkg` is used at runtime. +34 38 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 35 |- from pkg import A, B 39 |+ from pkg import B 36 40 | @@ -62,7 +62,7 @@ strict.py:35:21: TCH002 [*] Move third-party import `pkg.A` into a type-checking strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-checking block | -54 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +54 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 55 | import pkg 56 | from pkg.bar import A | ^ TCH002 @@ -82,7 +82,7 @@ strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-chec 4 8 | def f(): -------------------------------------------------------------------------------- 51 55 | def f(): -52 56 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +52 56 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 53 57 | import pkg 54 |- from pkg.bar import A 55 58 | @@ -92,7 +92,7 @@ strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-chec strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 62 | def f(): -63 | # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. +63 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. 64 | import pkg | ^^^ TCH002 65 | import pkg.bar as B @@ -111,7 +111,7 @@ strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking b -------------------------------------------------------------------------------- 59 63 | 60 64 | def f(): -61 65 | # In un-strict mode, this shouldn't rase an error, since `pkg.bar` is used at runtime. +61 65 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. 62 |- import pkg 63 66 | import pkg.bar as B 64 67 | @@ -120,7 +120,7 @@ strict.py:62:12: TCH002 [*] Move third-party import `pkg` into a type-checking b strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | 71 | def f(): -72 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +72 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 73 | import pkg.foo as F | ^^^^^^^^^^^^ TCH002 74 | import pkg.foo.bar as B @@ -139,7 +139,7 @@ strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checki -------------------------------------------------------------------------------- 68 72 | 69 73 | def f(): -70 74 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +70 74 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 71 |- import pkg.foo as F 72 75 | import pkg.foo.bar as B 73 76 | @@ -148,7 +148,7 @@ strict.py:71:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checki strict.py:80:12: TCH002 [*] Move third-party import `pkg` into a type-checking block | 80 | def f(): -81 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +81 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 82 | import pkg | ^^^ TCH002 83 | import pkg.foo.bar as B @@ -167,7 +167,7 @@ strict.py:80:12: TCH002 [*] Move third-party import `pkg` into a type-checking b -------------------------------------------------------------------------------- 77 81 | 78 82 | def f(): -79 83 | # In un-strict mode, this shouldn't rase an error, since `pkg.foo.bar` is used at runtime. +79 83 | # In un-strict mode, this shouldn't raise an error, since `pkg.foo.bar` is used at runtime. 80 |- import pkg 81 84 | import pkg.foo.bar as B 82 85 | @@ -193,7 +193,7 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 3 7 | 4 8 | def f(): -------------------------------------------------------------------------------- -88 92 | # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. +88 92 | # In un-strict mode, this _should_ raise an error, since `pkg` isn't used at runtime. 89 93 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is 90 94 | # testing the implementation. 91 |- import pkg @@ -203,7 +203,7 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-checking block | -101 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. +101 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 102 | import pkg.bar as B 103 | import pkg.foo as F | ^^^^^^^^^^^^ TCH002 @@ -223,7 +223,7 @@ strict.py:101:12: TCH002 [*] Move third-party import `pkg.foo` into a type-check 4 8 | def f(): -------------------------------------------------------------------------------- 98 102 | def f(): -99 103 | # In un-strict mode, this shouldn't raise an error, since `pkg.bar` is used at runtime. +99 103 | # In un-strict mode, this shouldn't raise an error, since `pkg` is used at runtime. 100 104 | import pkg.bar as B 101 |- import pkg.foo as F 102 105 | diff --git a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap index 9d6efbff99..00cc9830d0 100644 --- a/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap +++ b/crates/ruff/src/rules/flake8_type_checking/snapshots/ruff__rules__flake8_type_checking__tests__typing-only-third-party-import_strict.py.snap @@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_type_checking/mod.rs --- strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-checking block | -54 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +54 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 55 | import pkg 56 | from pkg.bar import A | ^ TCH002 @@ -23,7 +23,7 @@ strict.py:54:25: TCH002 [*] Move third-party import `pkg.bar.A` into a type-chec 4 8 | def f(): -------------------------------------------------------------------------------- 51 55 | def f(): -52 56 | # In un-strict mode, this _should_ rase an error, since `pkg` is used at runtime. +52 56 | # In un-strict mode, this _should_ raise an error, since `pkg.bar` isn't used at runtime 53 57 | import pkg 54 |- from pkg.bar import A 55 58 | @@ -50,7 +50,7 @@ strict.py:91:12: TCH002 [*] Move third-party import `pkg` into a type-checking b 3 7 | 4 8 | def f(): -------------------------------------------------------------------------------- -88 92 | # In un-strict mode, this _should_ rase an error, since `pkgfoo.bar` is used at runtime. +88 92 | # In un-strict mode, this _should_ raise an error, since `pkg` isn't used at runtime. 89 93 | # Note that `pkg` is a prefix of `pkgfoo` which are both different modules. This is 90 94 | # testing the implementation. 91 |- import pkg diff --git a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs index cd05bace7c..2faab59911 100644 --- a/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs +++ b/crates/ruff/src/rules/flynt/rules/static_join_to_fstring.rs @@ -1,5 +1,6 @@ +use itertools::Itertools; use ruff_text_size::TextRange; -use rustpython_parser::ast::{self, Expr, Ranged}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; @@ -27,15 +28,48 @@ impl AlwaysAutofixableViolation for StaticJoinToFString { } fn is_static_length(elts: &[Expr]) -> bool { - elts.iter().all(|e| !matches!(e, Expr::Starred(_))) + elts.iter().all(|e| !e.is_starred_expr()) } fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option { + // If all elements are string constants, join them into a single string. + if joinees.iter().all(|expr| { + matches!( + expr, + Expr::Constant(ast::ExprConstant { + value: Constant::Str(_), + .. + }) + ) + }) { + let node = ast::ExprConstant { + value: Constant::Str( + joinees + .iter() + .filter_map(|expr| { + if let Expr::Constant(ast::ExprConstant { + value: Constant::Str(string), + .. + }) = expr + { + Some(string.as_str()) + } else { + None + } + }) + .join(joiner), + ), + range: TextRange::default(), + kind: None, + }; + return Some(node.into()); + } + let mut fstring_elems = Vec::with_capacity(joinees.len() * 2); let mut first = true; for expr in joinees { - if matches!(expr, Expr::JoinedStr(_)) { + if expr.is_joined_str_expr() { // Oops, already an f-string. We don't know how to handle those // gracefully right now. return None; @@ -58,7 +92,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner: args, keywords, .. - })= expr else { + }) = expr else { return; }; diff --git a/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap b/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap index d268b251ba..e987f86206 100644 --- a/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap +++ b/crates/ruff/src/rules/flynt/snapshots/ruff__rules__flynt__tests__FLY002_FLY002.py.snap @@ -42,7 +42,7 @@ FLY002.py:6:7: FLY002 [*] Consider `f"Finally, {a} World"` instead of string joi 8 8 | ok4 = "y".join([1, 2, 3]) # Technically OK, though would've been an error originally 9 9 | ok5 = "a".join([random(), random()]) # OK (simple calls) -FLY002.py:7:7: FLY002 [*] Consider `f"1x2x3"` instead of string join +FLY002.py:7:7: FLY002 [*] Consider `"1x2x3"` instead of string join | 7 | ok1 = " ".join([a, " World"]) # OK 8 | ok2 = "".join(["Finally, ", a, " World"]) # OK @@ -51,14 +51,14 @@ FLY002.py:7:7: FLY002 [*] Consider `f"1x2x3"` instead of string join 10 | ok4 = "y".join([1, 2, 3]) # Technically OK, though would've been an error originally 11 | ok5 = "a".join([random(), random()]) # OK (simple calls) | - = help: Replace with `f"1x2x3"` + = help: Replace with `"1x2x3"` ℹ Suggested fix 4 4 | a = "Hello" 5 5 | ok1 = " ".join([a, " World"]) # OK 6 6 | ok2 = "".join(["Finally, ", a, " World"]) # OK 7 |-ok3 = "x".join(("1", "2", "3")) # OK - 7 |+ok3 = f"1x2x3" # OK + 7 |+ok3 = "1x2x3" # OK 8 8 | ok4 = "y".join([1, 2, 3]) # Technically OK, though would've been an error originally 9 9 | ok5 = "a".join([random(), random()]) # OK (simple calls) 10 10 | ok6 = "a".join([secrets.token_urlsafe(), secrets.token_hex()]) # OK (attr calls) diff --git a/crates/ruff/src/rules/pandas_vet/helpers.rs b/crates/ruff/src/rules/pandas_vet/helpers.rs index 1ec93a6d80..9622571434 100644 --- a/crates/ruff/src/rules/pandas_vet/helpers.rs +++ b/crates/ruff/src/rules/pandas_vet/helpers.rs @@ -40,11 +40,9 @@ pub(crate) fn test_expression(expr: &Expr, model: &SemanticModel) -> Resolution | BindingKind::LoopVar | BindingKind::Global | BindingKind::Nonlocal => Resolution::RelevantLocal, - BindingKind::Importation(Importation { full_name: module }) - if module == "pandas" => - { - Resolution::PandasModule - } + BindingKind::Importation(Importation { + qualified_name: module, + }) if module == "pandas" => Resolution::PandasModule, _ => Resolution::IrrelevantBinding, } }) diff --git a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs index 9295bd6b62..ebb56710ce 100644 --- a/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs +++ b/crates/ruff/src/rules/pandas_vet/rules/inplace_argument.rs @@ -71,7 +71,7 @@ pub(crate) fn inplace_argument( matches!( binding.kind, BindingKind::Importation(Importation { - full_name: "pandas" + qualified_name: "pandas" }) ) }); diff --git a/crates/ruff/src/rules/pyflakes/fixes.rs b/crates/ruff/src/rules/pyflakes/fixes.rs index 6bc0e2aae3..56d2347cc4 100644 --- a/crates/ruff/src/rules/pyflakes/fixes.rs +++ b/crates/ruff/src/rules/pyflakes/fixes.rs @@ -1,9 +1,6 @@ use anyhow::{bail, Ok, Result}; -use libcst_native::{Codegen, CodegenState, DictElement, Expression}; +use libcst_native::{DictElement, Expression}; use ruff_text_size::TextRange; -use rustpython_format::{ - FieldName, FieldNamePart, FieldType, FormatPart, FormatString, FromTemplate, -}; use rustpython_parser::ast::{Excepthandler, Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; @@ -11,9 +8,8 @@ use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; use ruff_python_ast::str::raw_contents; -use crate::cst::matchers::{ - match_attribute, match_call_mut, match_dict, match_expression, match_simple_string, -}; +use crate::autofix::codemods::CodegenStylist; +use crate::cst::matchers::{match_call_mut, match_dict, match_expression}; /// Generate a [`Edit`] to remove unused keys from format dict. pub(crate) fn remove_unused_format_arguments_from_dict( @@ -33,14 +29,10 @@ pub(crate) fn remove_unused_format_arguments_from_dict( } if raw_contents(name.value).map_or(false, |name| unused_arguments.contains(&name))) }); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), stmt.range())) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + stmt.range(), + )) } /// Generate a [`Edit`] to remove unused keyword arguments from a `format` call. @@ -57,72 +49,10 @@ pub(crate) fn remove_unused_keyword_arguments_from_format_call( call.args .retain(|e| !matches!(&e.keyword, Some(kw) if unused_arguments.contains(&kw.value))); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), location)) -} - -fn unparse_format_part(format_part: FormatPart) -> String { - match format_part { - FormatPart::Literal(literal) => literal, - FormatPart::Field { - field_name, - conversion_spec, - format_spec, - } => { - let mut field_name = field_name; - if let Some(conversion) = conversion_spec { - field_name.push_str(&format!("!{conversion}")); - } - if !format_spec.is_empty() { - field_name.push_str(&format!(":{format_spec}")); - } - format!("{{{field_name}}}") - } - } -} - -fn update_field_types(format_string: &FormatString, min_unused: usize) -> String { - format_string - .format_parts - .iter() - .map(|part| match part { - FormatPart::Literal(literal) => FormatPart::Literal(literal.to_string()), - FormatPart::Field { - field_name, - conversion_spec, - format_spec, - } => { - let new_field_name = FieldName::parse(field_name).unwrap(); // This should never fail because we parsed it before - let mut new_field_name_string = match new_field_name.field_type { - FieldType::Auto => String::new(), - FieldType::Index(i) => (i - min_unused).to_string(), - FieldType::Keyword(keyword) => keyword, - }; - for field_name_part in &new_field_name.parts { - let field_name_part_string = match field_name_part { - FieldNamePart::Attribute(attribute) => format!(".{attribute}"), - FieldNamePart::Index(i) => format!("[{i}]"), - FieldNamePart::StringIndex(s) => format!("[{s}]"), - }; - new_field_name_string.push_str(&field_name_part_string); - } - let new_format_spec = FormatString::from_str(format_spec).unwrap(); // This should never fail because we parsed it before - let new_format_spec_string = update_field_types(&new_format_spec, min_unused); - FormatPart::Field { - field_name: new_field_name_string, - conversion_spec: *conversion_spec, - format_spec: new_format_spec_string, - } - } - }) - .map(unparse_format_part) - .collect() + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + location, + )) } /// Generate a [`Edit`] to remove unused positional arguments from a `format` call. @@ -131,44 +61,23 @@ pub(crate) fn remove_unused_positional_arguments_from_format_call( location: TextRange, locator: &Locator, stylist: &Stylist, - format_string: &FormatString, ) -> Result { let module_text = locator.slice(location); let mut tree = match_expression(module_text)?; let call = match_call_mut(&mut tree)?; + // Remove any unused arguments. let mut index = 0; call.args.retain(|_| { + let is_unused = unused_arguments.contains(&index); index += 1; - !unused_arguments.contains(&(index - 1)) + !is_unused }); - let mut min_unused_index = 0; - for index in unused_arguments { - if *index == min_unused_index { - min_unused_index += 1; - } else { - break; - } - } - - let mut new_format_string; - if min_unused_index > 0 { - let func = match_attribute(&mut call.func)?; - let simple_string = match_simple_string(&mut func.value)?; - new_format_string = update_field_types(format_string, min_unused_index); - new_format_string = format!(r#""{new_format_string}""#); - simple_string.value = new_format_string.as_str(); - } - - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Ok(Edit::range_replacement(state.to_string(), location)) + Ok(Edit::range_replacement( + tree.codegen_stylist(stylist), + location, + )) } /// Generate a [`Edit`] to remove the binding from an exception handler. diff --git a/crates/ruff/src/rules/pyflakes/format.rs b/crates/ruff/src/rules/pyflakes/format.rs index 2bd1bc9b62..03d1f30f95 100644 --- a/crates/ruff/src/rules/pyflakes/format.rs +++ b/crates/ruff/src/rules/pyflakes/format.rs @@ -26,7 +26,6 @@ pub(crate) struct FormatSummary { pub(crate) indices: Vec, pub(crate) keywords: Vec, pub(crate) has_nested_parts: bool, - pub(crate) format_string: FormatString, } impl TryFrom<&str> for FormatSummary { @@ -75,7 +74,6 @@ impl TryFrom<&str> for FormatSummary { indices, keywords, has_nested_parts, - format_string, }) } } diff --git a/crates/ruff/src/rules/pyflakes/mod.rs b/crates/ruff/src/rules/pyflakes/mod.rs index bcaea72222..fe890582f6 100644 --- a/crates/ruff/src/rules/pyflakes/mod.rs +++ b/crates/ruff/src/rules/pyflakes/mod.rs @@ -40,6 +40,7 @@ mod tests { #[test_case(Rule::UnusedImport, Path::new("F401_13.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_14.py"))] #[test_case(Rule::UnusedImport, Path::new("F401_15.py"))] + #[test_case(Rule::UnusedImport, Path::new("F401_16.py"))] #[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))] #[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))] #[test_case(Rule::LateFutureImport, Path::new("F404.py"))] diff --git a/crates/ruff/src/rules/pyflakes/rules/strings.rs b/crates/ruff/src/rules/pyflakes/rules/strings.rs index 7ff92c6867..5afec5c441 100644 --- a/crates/ruff/src/rules/pyflakes/rules/strings.rs +++ b/crates/ruff/src/rules/pyflakes/rules/strings.rs @@ -4,7 +4,7 @@ use ruff_text_size::TextRange; use rustc_hash::FxHashSet; use rustpython_parser::ast::{self, Constant, Expr, Identifier, Keyword}; -use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Violation}; +use ruff_diagnostics::{AlwaysAutofixableViolation, AutofixKind, Diagnostic, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use crate::checkers::ast::Checker; @@ -425,7 +425,9 @@ pub struct StringDotFormatExtraPositionalArguments { missing: Vec, } -impl AlwaysAutofixableViolation for StringDotFormatExtraPositionalArguments { +impl Violation for StringDotFormatExtraPositionalArguments { + const AUTOFIX: AutofixKind = AutofixKind::Sometimes; + #[derive_message_formats] fn message(&self) -> String { let StringDotFormatExtraPositionalArguments { missing } = self; @@ -433,10 +435,12 @@ impl AlwaysAutofixableViolation for StringDotFormatExtraPositionalArguments { format!("`.format` call has unused arguments at position(s): {message}") } - fn autofix_title(&self) -> String { + fn autofix_title(&self) -> Option { let StringDotFormatExtraPositionalArguments { missing } = self; let message = missing.join(", "); - format!("Remove extra positional arguments at position(s): {message}") + Some(format!( + "Remove extra positional arguments at position(s): {message}" + )) } } @@ -600,14 +604,14 @@ pub(crate) fn percent_format_extra_named_arguments( location, ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_unused_format_arguments_from_dict( + diagnostic.try_set_fix(|| { + let edit = remove_unused_format_arguments_from_dict( &missing, right, checker.locator, checker.stylist, - ) + )?; + Ok(Fix::automatic(edit)) }); } checker.diagnostics.push(diagnostic); @@ -766,14 +770,14 @@ pub(crate) fn string_dot_format_extra_named_arguments( location, ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_unused_keyword_arguments_from_format_call( + diagnostic.try_set_fix(|| { + let edit = remove_unused_keyword_arguments_from_format_call( &missing, location, checker.locator, checker.stylist, - ) + )?; + Ok(Fix::automatic(edit)) }); } checker.diagnostics.push(diagnostic); @@ -805,22 +809,48 @@ pub(crate) fn string_dot_format_extra_positional_arguments( StringDotFormatExtraPositionalArguments { missing: missing .iter() - .map(std::string::ToString::to_string) + .map(ToString::to_string) .collect::>(), }, location, ); if checker.patch(diagnostic.kind.rule()) { - #[allow(deprecated)] - diagnostic.try_set_fix_from_edit(|| { - remove_unused_positional_arguments_from_format_call( - &missing, - location, - checker.locator, - checker.stylist, - &summary.format_string, - ) - }); + // We can only fix if the positional arguments we're removing don't require re-indexing + // the format string itself. For example, we can't fix `"{1}{2}".format(0, 1, 2)"`, since + // this requires changing the format string to `"{0}{1}"`. But we can fix + // `"{0}{1}".format(0, 1, 2)`, since this only requires modifying the call arguments. + fn is_contiguous_from_end(indexes: &[usize], target: &[T]) -> bool { + if indexes.is_empty() { + return true; + } + + let mut expected_index = target.len() - 1; + for &index in indexes.iter().rev() { + if index != expected_index { + return false; + } + + if expected_index == 0 { + break; + } + + expected_index -= 1; + } + + true + } + + if is_contiguous_from_end(&missing, args) { + diagnostic.try_set_fix(|| { + let edit = remove_unused_positional_arguments_from_format_call( + &missing, + location, + checker.locator, + checker.stylist, + )?; + Ok(Fix::automatic(edit)) + }); + } } checker.diagnostics.push(diagnostic); } diff --git a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs index c7c36956c7..07ed283517 100644 --- a/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs +++ b/crates/ruff/src/rules/pyflakes/rules/undefined_export.rs @@ -48,18 +48,16 @@ impl Violation for UndefinedExport { } /// F822 -pub(crate) fn undefined_export(names: &[&str], range: TextRange, scope: &Scope) -> Vec { +pub(crate) fn undefined_export(name: &str, range: TextRange, scope: &Scope) -> Vec { let mut diagnostics = Vec::new(); if !scope.uses_star_imports() { - for name in names { - if !scope.defines(name) { - diagnostics.push(Diagnostic::new( - UndefinedExport { - name: (*name).to_string(), - }, - range, - )); - } + if !scope.defines(name) { + diagnostics.push(Diagnostic::new( + UndefinedExport { + name: (*name).to_string(), + }, + range, + )); } } diagnostics diff --git a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs index acb5e2c6d9..a8f25b8809 100644 --- a/crates/ruff/src/rules/pyflakes/rules/unused_import.rs +++ b/crates/ruff/src/rules/pyflakes/rules/unused_import.rs @@ -5,9 +5,7 @@ use rustpython_parser::ast::Ranged; use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, IsolationLevel, Violation}; use ruff_macros::{derive_message_formats, violation}; -use ruff_python_semantic::binding::{ - BindingKind, Exceptions, FromImportation, Importation, SubmoduleImportation, -}; +use ruff_python_semantic::binding::Exceptions; use ruff_python_semantic::node::NodeId; use ruff_python_semantic::scope::Scope; @@ -102,8 +100,8 @@ impl Violation for UnusedImport { } } -type SpannedName<'a> = (&'a str, &'a TextRange); -type BindingContext<'a> = (NodeId, Option, Exceptions); +type SpannedName<'a> = (&'a str, TextRange); +type BindingContext = (NodeId, Option, Exceptions); pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut Vec) { // Collect all unused imports by statement. @@ -117,11 +115,8 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut continue; } - let full_name = match &binding.kind { - BindingKind::Importation(Importation { full_name }) => full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => full_name, - _ => continue, + let Some(qualified_name) = binding.qualified_name() else { + continue; }; let stmt_id = binding.source.unwrap(); @@ -144,12 +139,12 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut ignored .entry((stmt_id, parent_id, exceptions)) .or_default() - .push((full_name, &binding.range)); + .push((qualified_name, binding.range)); } else { unused .entry((stmt_id, parent_id, exceptions)) .or_default() - .push((full_name, &binding.range)); + .push((qualified_name, binding.range)); } } @@ -170,7 +165,9 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let fix = if !in_init && !in_except_handler && checker.patch(Rule::UnusedImport) { autofix::edits::remove_unused_imports( - unused_imports.iter().map(|(full_name, _)| *full_name), + unused_imports + .iter() + .map(|(qualified_name, _)| *qualified_name), stmt, parent, checker.locator, @@ -182,10 +179,10 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut None }; - for (full_name, range) in unused_imports { + for (qualified_name, range) in unused_imports { let mut diagnostic = Diagnostic::new( UnusedImport { - name: full_name.to_string(), + name: qualified_name.to_string(), context: if in_except_handler { Some(UnusedImportContext::ExceptHandler) } else if in_init { @@ -195,7 +192,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }, multiple, }, - *range, + range, ); if stmt.is_import_from_stmt() { diagnostic.set_parent(stmt.start()); @@ -222,10 +219,10 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut let multiple = unused_imports.len() > 1; let in_except_handler = exceptions.intersects(Exceptions::MODULE_NOT_FOUND_ERROR | Exceptions::IMPORT_ERROR); - for (full_name, range) in unused_imports { + for (qualified_name, range) in unused_imports { let mut diagnostic = Diagnostic::new( UnusedImport { - name: full_name.to_string(), + name: qualified_name.to_string(), context: if in_except_handler { Some(UnusedImportContext::ExceptHandler) } else if in_init { @@ -235,7 +232,7 @@ pub(crate) fn unused_import(checker: &Checker, scope: &Scope, diagnostics: &mut }, multiple, }, - *range, + range, ); if stmt.is_import_from_stmt() { diagnostic.set_parent(stmt.start()); diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_16.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_16.py.snap new file mode 100644 index 0000000000..1976c4331d --- /dev/null +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F401_F401_16.py.snap @@ -0,0 +1,4 @@ +--- +source: crates/ruff/src/rules/pyflakes/mod.rs +--- + diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap index 8ab0155101..a8f8bc10aa 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F504.py.snap @@ -12,7 +12,7 @@ F504.py:3:1: F504 [*] `%`-format string has unused named argument(s): b | = help: Remove extra named arguments: b -ℹ Suggested fix +ℹ Fix 1 1 | # Ruff has no way of knowing if the following are F505s 2 2 | a = "wrong" 3 |-"%(a)s %(c)s" % {a: "?", "b": "!"} # F504 ("b" not used) @@ -31,7 +31,7 @@ F504.py:8:1: F504 [*] `%`-format string has unused named argument(s): b | = help: Remove extra named arguments: b -ℹ Suggested fix +ℹ Fix 5 5 | hidden = {"a": "!"} 6 6 | "%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat) 7 7 | @@ -47,7 +47,7 @@ F504.py:9:1: F504 [*] `%`-format string has unused named argument(s): b | = help: Remove extra named arguments: b -ℹ Suggested fix +ℹ Fix 6 6 | "%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat) 7 7 | 8 8 | "%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used) diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap index 94960c3067..7c0f848338 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F504_F50x.py.snap @@ -12,7 +12,7 @@ F50x.py:8:1: F504 [*] `%`-format string has unused named argument(s): baz | = help: Remove extra named arguments: baz -ℹ Suggested fix +ℹ Fix 5 5 | '%s %s' % (1,) # F507 6 6 | '%s %s' % (1, 2, 3) # F507 7 7 | '%(bar)s' % {} # F505 diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap index 2779b22c72..ca5ac6ec6e 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F522_F522.py.snap @@ -10,7 +10,7 @@ F522.py:1:1: F522 [*] `.format` call has unused named argument(s): bar | = help: Remove extra named arguments: bar -ℹ Suggested fix +ℹ Fix 1 |-"{}".format(1, bar=2) # F522 1 |+"{}".format(1, ) # F522 2 2 | "{bar}{}".format(1, bar=2, spam=3) # F522 @@ -27,7 +27,7 @@ F522.py:2:1: F522 [*] `.format` call has unused named argument(s): spam | = help: Remove extra named arguments: spam -ℹ Suggested fix +ℹ Fix 1 1 | "{}".format(1, bar=2) # F522 2 |-"{bar}{}".format(1, bar=2, spam=3) # F522 2 |+"{bar}{}".format(1, bar=2, ) # F522 @@ -43,7 +43,7 @@ F522.py:4:1: F522 [*] `.format` call has unused named argument(s): eggs, ham | = help: Remove extra named arguments: eggs, ham -ℹ Suggested fix +ℹ Fix 1 1 | "{}".format(1, bar=2) # F522 2 2 | "{bar}{}".format(1, bar=2, spam=3) # F522 3 3 | "{bar:{spam}}".format(bar=2, spam=3) # No issues diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap index 51163b5e06..42d7590315 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F523_F523.py.snap @@ -11,7 +11,7 @@ F523.py:2:1: F523 [*] `.format` call has unused arguments at position(s): 1 | = help: Remove extra positional arguments at position(s): 1 -ℹ Suggested fix +ℹ Fix 1 1 | # With indexes 2 |-"{0}".format(1, 2) # F523 2 |+"{0}".format(1, ) # F523 @@ -19,7 +19,7 @@ F523.py:2:1: F523 [*] `.format` call has unused arguments at position(s): 1 4 4 | "{1:{0}}".format(1, 2) # No issues 5 5 | "{1:{0}}".format(1, 2, 3) # F523 -F523.py:3:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 +F523.py:3:1: F523 `.format` call has unused arguments at position(s): 0, 2 | 3 | # With indexes 4 | "{0}".format(1, 2) # F523 @@ -30,15 +30,6 @@ F523.py:3:1: F523 [*] `.format` call has unused arguments at position(s): 0, 2 | = help: Remove extra positional arguments at position(s): 0, 2 -ℹ Suggested fix -1 1 | # With indexes -2 2 | "{0}".format(1, 2) # F523 -3 |-"{1}".format(1, 2, 3) # F523 - 3 |+"{0}".format(2, ) # F523 -4 4 | "{1:{0}}".format(1, 2) # No issues -5 5 | "{1:{0}}".format(1, 2, 3) # F523 -6 6 | "{0}{2}".format(1, 2) # F523, # F524 - F523.py:5:1: F523 [*] `.format` call has unused arguments at position(s): 2 | 5 | "{1}".format(1, 2, 3) # F523 @@ -50,7 +41,7 @@ F523.py:5:1: F523 [*] `.format` call has unused arguments at position(s): 2 | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix +ℹ Fix 2 2 | "{0}".format(1, 2) # F523 3 3 | "{1}".format(1, 2, 3) # F523 4 4 | "{1:{0}}".format(1, 2) # No issues @@ -70,7 +61,7 @@ F523.py:6:1: F523 [*] `.format` call has unused arguments at position(s): 1 | = help: Remove extra positional arguments at position(s): 1 -ℹ Suggested fix +ℹ Fix 3 3 | "{1}".format(1, 2, 3) # F523 4 4 | "{1:{0}}".format(1, 2) # No issues 5 5 | "{1:{0}}".format(1, 2, 3) # F523 @@ -80,7 +71,7 @@ F523.py:6:1: F523 [*] `.format` call has unused arguments at position(s): 1 8 8 | 9 9 | # With no indexes -F523.py:7:1: F523 [*] `.format` call has unused arguments at position(s): 0, 3 +F523.py:7:1: F523 `.format` call has unused arguments at position(s): 0, 3 | 7 | "{1:{0}}".format(1, 2, 3) # F523 8 | "{0}{2}".format(1, 2) # F523, # F524 @@ -91,16 +82,6 @@ F523.py:7:1: F523 [*] `.format` call has unused arguments at position(s): 0, 3 | = help: Remove extra positional arguments at position(s): 0, 3 -ℹ Suggested fix -4 4 | "{1:{0}}".format(1, 2) # No issues -5 5 | "{1:{0}}".format(1, 2, 3) # F523 -6 6 | "{0}{2}".format(1, 2) # F523, # F524 -7 |-"{1.arg[1]!r:0{2['arg']}{1}}".format(1, 2, 3, 4) # F523 - 7 |+"{0.arg[1]!r:0{1['arg']}{0}}".format(2, 3, ) # F523 -8 8 | -9 9 | # With no indexes -10 10 | "{}".format(1, 2) # F523 - F523.py:10:1: F523 [*] `.format` call has unused arguments at position(s): 1 | 10 | # With no indexes @@ -111,7 +92,7 @@ F523.py:10:1: F523 [*] `.format` call has unused arguments at position(s): 1 | = help: Remove extra positional arguments at position(s): 1 -ℹ Suggested fix +ℹ Fix 7 7 | "{1.arg[1]!r:0{2['arg']}{1}}".format(1, 2, 3, 4) # F523 8 8 | 9 9 | # With no indexes @@ -132,7 +113,7 @@ F523.py:11:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 | = help: Remove extra positional arguments at position(s): 1, 2 -ℹ Suggested fix +ℹ Fix 8 8 | 9 9 | # With no indexes 10 10 | "{}".format(1, 2) # F523 @@ -153,7 +134,7 @@ F523.py:13:1: F523 [*] `.format` call has unused arguments at position(s): 2 | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix +ℹ Fix 10 10 | "{}".format(1, 2) # F523 11 11 | "{}".format(1, 2, 3) # F523 12 12 | "{:{}}".format(1, 2) # No issues @@ -163,20 +144,117 @@ F523.py:13:1: F523 [*] `.format` call has unused arguments at position(s): 2 15 15 | # With *args 16 16 | "{0}{1}".format(*args) # No issues -F523.py:19:1: F523 [*] `.format` call has unused arguments at position(s): 2 +F523.py:19:1: F523 `.format` call has unused arguments at position(s): 2 | 19 | "{0}{1}".format(1, *args) # No issues 20 | "{0}{1}".format(1, 2, *args) # No issues 21 | "{0}{1}".format(1, 2, 3, *args) # F523 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +22 | +23 | # With nested quotes | = help: Remove extra positional arguments at position(s): 2 -ℹ Suggested fix -16 16 | "{0}{1}".format(*args) # No issues -17 17 | "{0}{1}".format(1, *args) # No issues -18 18 | "{0}{1}".format(1, 2, *args) # No issues -19 |-"{0}{1}".format(1, 2, 3, *args) # F523 - 19 |+"{0}{1}".format(1, 2, *args) # F523 +F523.py:22:1: F523 [*] `.format` call has unused arguments at position(s): 1, 2 + | +22 | # With nested quotes +23 | "''1{0}".format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^ F523 +24 | "\"\"{1}{0}".format(1, 2, 3) # F523 +25 | '""{1}{0}'.format(1, 2, 3) # F523 + | + = help: Remove extra positional arguments at position(s): 1, 2 + +ℹ Fix +19 19 | "{0}{1}".format(1, 2, 3, *args) # F523 +20 20 | +21 21 | # With nested quotes +22 |-"''1{0}".format(1, 2, 3) # F523 + 22 |+"''1{0}".format(1, ) # F523 +23 23 | "\"\"{1}{0}".format(1, 2, 3) # F523 +24 24 | '""{1}{0}'.format(1, 2, 3) # F523 +25 25 | + +F523.py:23:1: F523 [*] `.format` call has unused arguments at position(s): 2 + | +23 | # With nested quotes +24 | "''1{0}".format(1, 2, 3) # F523 +25 | "\"\"{1}{0}".format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +26 | '""{1}{0}'.format(1, 2, 3) # F523 + | + = help: Remove extra positional arguments at position(s): 2 + +ℹ Fix +20 20 | +21 21 | # With nested quotes +22 22 | "''1{0}".format(1, 2, 3) # F523 +23 |-"\"\"{1}{0}".format(1, 2, 3) # F523 + 23 |+"\"\"{1}{0}".format(1, 2, ) # F523 +24 24 | '""{1}{0}'.format(1, 2, 3) # F523 +25 25 | +26 26 | # With modified indexes + +F523.py:24:1: F523 [*] `.format` call has unused arguments at position(s): 2 + | +24 | "''1{0}".format(1, 2, 3) # F523 +25 | "\"\"{1}{0}".format(1, 2, 3) # F523 +26 | '""{1}{0}'.format(1, 2, 3) # F523 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +27 | +28 | # With modified indexes + | + = help: Remove extra positional arguments at position(s): 2 + +ℹ Fix +21 21 | # With nested quotes +22 22 | "''1{0}".format(1, 2, 3) # F523 +23 23 | "\"\"{1}{0}".format(1, 2, 3) # F523 +24 |-'""{1}{0}'.format(1, 2, 3) # F523 + 24 |+'""{1}{0}'.format(1, 2, ) # F523 +25 25 | +26 26 | # With modified indexes +27 27 | "{1}{2}".format(1, 2, 3) # F523, # F524 + +F523.py:27:1: F523 `.format` call has unused arguments at position(s): 0 + | +27 | # With modified indexes +28 | "{1}{2}".format(1, 2, 3) # F523, # F524 + | ^^^^^^^^^^^^^^^^^^^^^^^^ F523 +29 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 +30 | "{1} {8}".format(0, 1) # F523, # F524 + | + = help: Remove extra positional arguments at position(s): 0 + +F523.py:28:1: F523 `.format` call has unused arguments at position(s): 0, 2 + | +28 | # With modified indexes +29 | "{1}{2}".format(1, 2, 3) # F523, # F524 +30 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ F523 +31 | "{1} {8}".format(0, 1) # F523, # F524 + | + = help: Remove extra positional arguments at position(s): 0, 2 + +F523.py:29:1: F523 `.format` call has unused arguments at position(s): 0 + | +29 | "{1}{2}".format(1, 2, 3) # F523, # F524 +30 | "{1}{3}".format(1, 2, 3, 4) # F523, # F524 +31 | "{1} {8}".format(0, 1) # F523, # F524 + | ^^^^^^^^^^^^^^^^^^^^^^ F523 +32 | +33 | # Not fixable + | + = help: Remove extra positional arguments at position(s): 0 + +F523.py:32:2: F523 `.format` call has unused arguments at position(s): 0 + | +32 | # Not fixable +33 | ('' + | __^ +34 | | .format(2)) + | |__________^ F523 + | + = help: Remove extra positional arguments at position(s): 0 diff --git a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap index d4abbf0a61..5d37ffa6ed 100644 --- a/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap +++ b/crates/ruff/src/rules/pyflakes/snapshots/ruff__rules__pyflakes__tests__F524_F524.py.snap @@ -45,6 +45,7 @@ F524.py:5:1: F524 `.format` call is missing argument(s) for placeholder(s): 0, b 7 | "{0} {bar}".format() # F524 | ^^^^^^^^^^^^^^^^^^^^ F524 8 | "{bar} {0}".format() # F524 +9 | "{1} {8}".format(0, 1) | F524.py:6:1: F524 `.format` call is missing argument(s) for placeholder(s): 0, bar @@ -53,6 +54,15 @@ F524.py:6:1: F524 `.format` call is missing argument(s) for placeholder(s): 0, b 7 | "{0} {bar}".format() # F524 8 | "{bar} {0}".format() # F524 | ^^^^^^^^^^^^^^^^^^^^ F524 +9 | "{1} {8}".format(0, 1) + | + +F524.py:7:1: F524 `.format` call is missing argument(s) for placeholder(s): 8 + | +7 | "{0} {bar}".format() # F524 +8 | "{bar} {0}".format() # F524 +9 | "{1} {8}".format(0, 1) + | ^^^^^^^^^^^^^^^^^^^^^^ F524 | diff --git a/crates/ruff/src/rules/pylint/mod.rs b/crates/ruff/src/rules/pylint/mod.rs index 29145a3b35..c7536d1922 100644 --- a/crates/ruff/src/rules/pylint/mod.rs +++ b/crates/ruff/src/rules/pylint/mod.rs @@ -52,8 +52,8 @@ mod tests { #[test_case(Rule::ImportSelf, Path::new("import_self/module.py"))] #[test_case(Rule::InvalidAllFormat, Path::new("invalid_all_format.py"))] #[test_case(Rule::InvalidAllObject, Path::new("invalid_all_object.py"))] + #[test_case(Rule::InvalidStrReturnType, Path::new("invalid_return_type_str.py"))] #[test_case(Rule::DuplicateBases, Path::new("duplicate_bases.py"))] - #[test_case(Rule::DuplicateValue, Path::new("duplicate_value.py"))] #[test_case(Rule::InvalidCharacterBackspace, Path::new("invalid_characters.py"))] #[test_case(Rule::InvalidCharacterEsc, Path::new("invalid_characters.py"))] #[test_case(Rule::InvalidCharacterNul, Path::new("invalid_characters.py"))] diff --git a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs index cbb8b10b4e..137b346cda 100644 --- a/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs +++ b/crates/ruff/src/rules/pylint/rules/bad_string_format_type.rs @@ -3,12 +3,13 @@ use std::str::FromStr; use ruff_text_size::TextRange; use rustc_hash::FxHashMap; use rustpython_format::cformat::{CFormatPart, CFormatSpec, CFormatStrOrBytes, CFormatString}; -use rustpython_parser::ast::{self, Constant, Expr, Operator, Ranged}; +use rustpython_parser::ast::{self, Constant, Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; use ruff_diagnostics::{Diagnostic, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::str::{leading_quote, trailing_quote}; +use ruff_python_semantic::analyze::type_inference::PythonType; use crate::checkers::ast::Checker; @@ -38,87 +39,6 @@ impl Violation for BadStringFormatType { } } -#[derive(Debug, Copy, Clone)] -enum DataType { - String, - Integer, - Float, - Object, - Unknown, -} - -impl From<&Expr> for DataType { - fn from(expr: &Expr) -> Self { - match expr { - Expr::NamedExpr(ast::ExprNamedExpr { value, .. }) => (&**value).into(), - Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => (&**operand).into(), - Expr::Dict(_) => DataType::Object, - Expr::Set(_) => DataType::Object, - Expr::ListComp(_) => DataType::Object, - Expr::SetComp(_) => DataType::Object, - Expr::DictComp(_) => DataType::Object, - Expr::GeneratorExp(_) => DataType::Object, - Expr::JoinedStr(_) => DataType::String, - Expr::BinOp(ast::ExprBinOp { left, op, .. }) => { - // Ex) "a" % "b" - if matches!( - left.as_ref(), - Expr::Constant(ast::ExprConstant { - value: Constant::Str(..), - .. - }) - ) && matches!(op, Operator::Mod) - { - return DataType::String; - } - DataType::Unknown - } - Expr::Constant(ast::ExprConstant { value, .. }) => match value { - Constant::Str(_) => DataType::String, - Constant::Int(_) => DataType::Integer, - Constant::Float(_) => DataType::Float, - _ => DataType::Unknown, - }, - Expr::List(_) => DataType::Object, - Expr::Tuple(_) => DataType::Object, - _ => DataType::Unknown, - } - } -} - -impl DataType { - fn is_compatible_with(self, format: FormatType) -> bool { - match self { - DataType::String => matches!( - format, - FormatType::Unknown | FormatType::String | FormatType::Repr - ), - DataType::Object => matches!( - format, - FormatType::Unknown | FormatType::String | FormatType::Repr - ), - DataType::Integer => matches!( - format, - FormatType::Unknown - | FormatType::String - | FormatType::Repr - | FormatType::Integer - | FormatType::Float - | FormatType::Number - ), - DataType::Float => matches!( - format, - FormatType::Unknown - | FormatType::String - | FormatType::Repr - | FormatType::Float - | FormatType::Number - ), - DataType::Unknown => true, - } - } -} - #[derive(Debug, Copy, Clone)] enum FormatType { Repr, @@ -129,6 +49,45 @@ enum FormatType { Unknown, } +impl FormatType { + fn is_compatible_with(self, data_type: PythonType) -> bool { + match data_type { + PythonType::String + | PythonType::Bytes + | PythonType::List + | PythonType::Dict + | PythonType::Set + | PythonType::Tuple + | PythonType::Generator + | PythonType::Complex + | PythonType::Bool + | PythonType::Ellipsis + | PythonType::None => matches!( + self, + FormatType::Unknown | FormatType::String | FormatType::Repr + ), + PythonType::Integer => matches!( + self, + FormatType::Unknown + | FormatType::String + | FormatType::Repr + | FormatType::Integer + | FormatType::Float + | FormatType::Number + ), + PythonType::Float => matches!( + self, + FormatType::Unknown + | FormatType::String + | FormatType::Repr + | FormatType::Float + | FormatType::Number + ), + PythonType::Unknown => true, + } + } +} + impl From for FormatType { fn from(format: char) -> Self { match format { @@ -159,9 +118,9 @@ fn collect_specs(formats: &[CFormatStrOrBytes]) -> Vec<&CFormatSpec> { /// Return `true` if the format string is equivalent to the constant type fn equivalent(format: &CFormatSpec, value: &Expr) -> bool { - let constant: DataType = value.into(); let format: FormatType = format.format_char.into(); - constant.is_compatible_with(format) + let constant: PythonType = value.into(); + format.is_compatible_with(constant) } /// Return `true` if the [`Constnat`] aligns with the format type. diff --git a/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs new file mode 100644 index 0000000000..486c8a474e --- /dev/null +++ b/crates/ruff/src/rules/pylint/rules/invalid_str_return.rs @@ -0,0 +1,60 @@ +use rustpython_parser::ast::{Ranged, Stmt}; + +use ruff_diagnostics::{Diagnostic, Violation}; +use ruff_macros::{derive_message_formats, violation}; +use ruff_python_ast::{helpers::ReturnStatementVisitor, statement_visitor::StatementVisitor}; +use ruff_python_semantic::analyze::type_inference::PythonType; + +use crate::checkers::ast::Checker; + +/// ## What it does +/// Checks for `__str__` implementations that return a type other than `str`. +/// +/// ## Why is this bad? +/// The `__str__` method should return a `str` object. Returning a different +/// type may cause unexpected behavior. +#[violation] +pub struct InvalidStrReturnType; + +impl Violation for InvalidStrReturnType { + #[derive_message_formats] + fn message(&self) -> String { + format!("`__str__` does not return `str`") + } +} + +/// E0307 +pub(crate) fn invalid_str_return(checker: &mut Checker, name: &str, body: &[Stmt]) { + if name != "__str__" { + return; + } + + if !checker.semantic_model().scope().kind.is_class() { + return; + } + + let returns = { + let mut visitor = ReturnStatementVisitor::default(); + visitor.visit_body(body); + visitor.returns + }; + + for stmt in returns { + if let Some(value) = stmt.value.as_deref() { + // Disallow other, non- + if !matches!( + PythonType::from(value), + PythonType::String | PythonType::Unknown + ) { + checker + .diagnostics + .push(Diagnostic::new(InvalidStrReturnType, value.range())); + } + } else { + // Disallow implicit `None`. + checker + .diagnostics + .push(Diagnostic::new(InvalidStrReturnType, stmt.range())); + } + } +} diff --git a/crates/ruff/src/rules/pylint/rules/mod.rs b/crates/ruff/src/rules/pylint/rules/mod.rs index 5a26cdb2eb..70f088b313 100644 --- a/crates/ruff/src/rules/pylint/rules/mod.rs +++ b/crates/ruff/src/rules/pylint/rules/mod.rs @@ -9,7 +9,6 @@ pub(crate) use compare_to_empty_string::{compare_to_empty_string, CompareToEmpty pub(crate) use comparison_of_constant::{comparison_of_constant, ComparisonOfConstant}; pub(crate) use continue_in_finally::{continue_in_finally, ContinueInFinally}; pub(crate) use duplicate_bases::{duplicate_bases, DuplicateBases}; -pub(crate) use duplicate_value::{duplicate_value, DuplicateValue}; pub(crate) use global_statement::{global_statement, GlobalStatement}; pub(crate) use global_variable_not_assigned::GlobalVariableNotAssigned; pub(crate) use import_self::{import_from_self, import_self, ImportSelf}; @@ -17,6 +16,7 @@ pub(crate) use invalid_all_format::{invalid_all_format, InvalidAllFormat}; pub(crate) use invalid_all_object::{invalid_all_object, InvalidAllObject}; pub(crate) use invalid_envvar_default::{invalid_envvar_default, InvalidEnvvarDefault}; pub(crate) use invalid_envvar_value::{invalid_envvar_value, InvalidEnvvarValue}; +pub(crate) use invalid_str_return::{invalid_str_return, InvalidStrReturnType}; pub(crate) use invalid_string_characters::{ invalid_string_characters, InvalidCharacterBackspace, InvalidCharacterEsc, InvalidCharacterNul, InvalidCharacterSub, InvalidCharacterZeroWidthSpace, @@ -65,7 +65,6 @@ mod compare_to_empty_string; mod comparison_of_constant; mod continue_in_finally; mod duplicate_bases; -mod duplicate_value; mod global_statement; mod global_variable_not_assigned; mod import_self; @@ -73,6 +72,7 @@ mod invalid_all_format; mod invalid_all_object; mod invalid_envvar_default; mod invalid_envvar_value; +mod invalid_str_return; mod invalid_string_characters; mod iteration_over_set; mod load_before_global_declaration; diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap new file mode 100644 index 0000000000..69d872d03e --- /dev/null +++ b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLE0307_invalid_return_type_str.py.snap @@ -0,0 +1,44 @@ +--- +source: crates/ruff/src/rules/pylint/mod.rs +--- +invalid_return_type_str.py:3:16: PLE0307 `__str__` does not return `str` + | +3 | class Str: +4 | def __str__(self): +5 | return 1 + | ^ PLE0307 +6 | +7 | class Float: + | + +invalid_return_type_str.py:7:16: PLE0307 `__str__` does not return `str` + | + 7 | class Float: + 8 | def __str__(self): + 9 | return 3.05 + | ^^^^ PLE0307 +10 | +11 | class Int: + | + +invalid_return_type_str.py:11:16: PLE0307 `__str__` does not return `str` + | +11 | class Int: +12 | def __str__(self): +13 | return 0 + | ^ PLE0307 +14 | +15 | class Bool: + | + +invalid_return_type_str.py:15:16: PLE0307 `__str__` does not return `str` + | +15 | class Bool: +16 | def __str__(self): +17 | return False + | ^^^^^ PLE0307 +18 | +19 | class Str2: + | + + diff --git a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0130_duplicate_value.py.snap b/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0130_duplicate_value.py.snap deleted file mode 100644 index 6fdf6149d7..0000000000 --- a/crates/ruff/src/rules/pylint/snapshots/ruff__rules__pylint__tests__PLW0130_duplicate_value.py.snap +++ /dev/null @@ -1,23 +0,0 @@ ---- -source: crates/ruff/src/rules/pylint/mod.rs ---- -duplicate_value.py:4:35: PLW0130 Duplicate value `"value1"` in set - | -4 | # Errors. -5 | ### -6 | incorrect_set = {"value1", 23, 5, "value1"} - | ^^^^^^^^ PLW0130 -7 | incorrect_set = {1, 1} - | - -duplicate_value.py:5:21: PLW0130 Duplicate value `1` in set - | -5 | ### -6 | incorrect_set = {"value1", 23, 5, "value1"} -7 | incorrect_set = {1, 1} - | ^ PLW0130 -8 | -9 | ### - | - - diff --git a/crates/ruff/src/rules/pyupgrade/fixes.rs b/crates/ruff/src/rules/pyupgrade/fixes.rs index ce242175f1..5f37034a2d 100644 --- a/crates/ruff/src/rules/pyupgrade/fixes.rs +++ b/crates/ruff/src/rules/pyupgrade/fixes.rs @@ -1,9 +1,10 @@ use anyhow::Result; -use libcst_native::{Codegen, CodegenState, ParenthesizableWhitespace}; +use libcst_native::ParenthesizableWhitespace; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{Expr, Ranged}; use rustpython_parser::{lexer, Mode, Tok}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::Edit; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -29,14 +30,7 @@ pub(crate) fn adjust_indentation( let indented_block = match_indented_block(&mut embedding.body)?; indented_block.indent = Some(indentation); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..Default::default() - }; - indented_block.codegen(&mut state); - - let module_text = state.to_string(); + let module_text = indented_block.codegen_stylist(stylist); let module_text = module_text .strip_prefix(stylist.line_ending().as_str()) .unwrap() @@ -61,14 +55,10 @@ pub(crate) fn remove_super_arguments( body.whitespace_before_args = ParenthesizableWhitespace::default(); body.whitespace_after_func = ParenthesizableWhitespace::default(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - Some(Edit::range_replacement(state.to_string(), range)) + Some(Edit::range_replacement( + tree.codegen_stylist(stylist), + range, + )) } /// Remove any imports matching `members` from an import-from statement. diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs index ffc4ed6ddc..b2c5c62394 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_import.rs @@ -493,14 +493,14 @@ impl<'a> ImportReplacer<'a> { fn format_import_from(names: &[&Alias], module: &str) -> String { // Construct the whitespace strings. // Generate the formatted names. - let full_names: String = names + let qualified_names: String = names .iter() .map(|name| match &name.asname { Some(asname) => format!("{} as {}", name.name, asname), None => format!("{}", name.name), }) .join(", "); - format!("from {module} import {full_names}") + format!("from {module} import {qualified_names}") } } diff --git a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs index d99c272926..9680aa8998 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/deprecated_mock_import.rs @@ -1,11 +1,12 @@ use anyhow::Result; use libcst_native::{ - AsName, AssignTargetExpression, Attribute, Codegen, CodegenState, Dot, Expression, Import, - ImportAlias, ImportFrom, ImportNames, Name, NameOrAttribute, ParenthesizableWhitespace, + AsName, AssignTargetExpression, Attribute, Dot, Expression, Import, ImportAlias, ImportFrom, + ImportNames, Name, NameOrAttribute, ParenthesizableWhitespace, }; use log::error; use rustpython_parser::ast::{self, Expr, Ranged, Stmt}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::call_path::collect_call_path; @@ -127,7 +128,7 @@ fn format_import( ) -> Result { let module_text = locator.slice(stmt.range()); let mut tree = match_statement(module_text)?; - let mut import = match_import(&mut tree)?; + let import = match_import(&mut tree)?; let Import { names, .. } = import.clone(); let (clean_aliases, mock_aliases) = clean_import_aliases(names); @@ -137,14 +138,7 @@ fn format_import( } else { import.names = clean_aliases; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); content.push_str(&stylist.line_ending()); content.push_str(indent); content.push_str(&format_mocks(mock_aliases, indent, stylist)); @@ -161,7 +155,7 @@ fn format_import_from( ) -> Result { let module_text = locator.slice(stmt.range()); let mut tree = match_statement(module_text).unwrap(); - let mut import = match_import_from(&mut tree)?; + let import = match_import_from(&mut tree)?; if let ImportFrom { names: ImportNames::Star(..), @@ -187,13 +181,7 @@ fn format_import_from( lpar: vec![], rpar: vec![], }))); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - Ok(state.to_string()) + Ok(tree.codegen_stylist(stylist)) } else if let ImportFrom { names: ImportNames::Aliases(aliases), .. @@ -224,14 +212,7 @@ fn format_import_from( rpar: vec![], }))); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - tree.codegen(&mut state); - - let mut content = state.to_string(); + let mut content = tree.codegen_stylist(stylist); if !mock_aliases.is_empty() { content.push_str(&stylist.line_ending()); content.push_str(indent); diff --git a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs index 5352f3c008..0918d05078 100644 --- a/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs +++ b/crates/ruff/src/rules/pyupgrade/rules/format_literals.rs @@ -1,9 +1,10 @@ use anyhow::{anyhow, bail, Result}; -use libcst_native::{Arg, Codegen, CodegenState}; +use libcst_native::Arg; use once_cell::sync::Lazy; use regex::Regex; use rustpython_parser::ast::{Expr, Ranged}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -89,7 +90,7 @@ fn generate_call( ) -> Result { let module_text = locator.slice(expr.range()); let mut expression = match_expression(module_text)?; - let mut call = match_call_mut(&mut expression)?; + let call = match_call_mut(&mut expression)?; // Fix the call arguments. if !is_sequential(correct_order) { @@ -99,27 +100,16 @@ fn generate_call( // Fix the string itself. let item = match_attribute(&mut call.func)?; - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - item.codegen(&mut state); - let cleaned = remove_specifiers(&state.to_string()); + let cleaned = remove_specifiers(&item.codegen_stylist(stylist)); call.func = Box::new(match_expression(&cleaned)?); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - expression.codegen(&mut state); - if module_text == state.to_string() { + let state = expression.codegen_stylist(stylist); + if module_text == state { // Ex) `'{' '0}'.format(1)` bail!("Failed to generate call expression for: {module_text}") } - Ok(state.to_string()) + Ok(state) } /// UP030 diff --git a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs index a08071d29d..18e93ab3c6 100644 --- a/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs +++ b/crates/ruff/src/rules/ruff/rules/explicit_f_string_type_conversion.rs @@ -1,7 +1,7 @@ use anyhow::{bail, Result}; -use libcst_native::{Codegen, CodegenState}; use rustpython_parser::ast::{self, Expr, Ranged}; +use crate::autofix::codemods::CodegenStylist; use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix}; use ruff_macros::{derive_message_formats, violation}; use ruff_python_ast::source_code::{Locator, Stylist}; @@ -59,7 +59,7 @@ fn fix_explicit_f_string_type_conversion( let formatted_string = match_formatted_string(&mut expression)?; // Replace the formatted call expression at `index` with a conversion flag. - let mut formatted_string_expression = + let formatted_string_expression = match_formatted_string_expression(&mut formatted_string.parts[index])?; let call = match_call_mut(&mut formatted_string_expression.expression)?; let name = match_name(&call.func)?; @@ -77,15 +77,8 @@ fn fix_explicit_f_string_type_conversion( } formatted_string_expression.expression = call.args[0].value.clone(); - let mut state = CodegenState { - default_newline: &stylist.line_ending(), - default_indent: stylist.indentation(), - ..CodegenState::default() - }; - expression.codegen(&mut state); - Ok(Fix::automatic(Edit::range_replacement( - state.to_string(), + expression.codegen_stylist(stylist), range, ))) } @@ -104,9 +97,10 @@ pub(crate) fn explicit_f_string_type_conversion( }) = &formatted_value else { continue; }; + // Skip if there's already a conversion flag. if !conversion.is_none() { - return; + continue; } let Expr::Call(ast::ExprCall { @@ -115,24 +109,24 @@ pub(crate) fn explicit_f_string_type_conversion( keywords, .. }) = value.as_ref() else { - return; + continue; }; // Can't be a conversion otherwise. if args.len() != 1 || !keywords.is_empty() { - return; + continue; } let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else { - return; + continue; }; if !matches!(id.as_str(), "str" | "repr" | "ascii") { - return; + continue; }; if !checker.semantic_model().is_builtin(id) { - return; + continue; } let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range()); diff --git a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap index df895039e7..57f269e8dc 100644 --- a/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap +++ b/crates/ruff/src/rules/ruff/snapshots/ruff__rules__ruff__tests__RUF010_RUF010.py.snap @@ -128,7 +128,7 @@ RUF010.py:13:5: RUF010 [*] Use conversion in f-string 15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | ^^^^^^^^ RUF010 16 | -17 | f"{foo(bla)}" # OK +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | = help: Replace f-string function call with conversion @@ -139,7 +139,7 @@ RUF010.py:13:5: RUF010 [*] Use conversion in f-string 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 13 |+f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 14 14 | -15 15 | f"{foo(bla)}" # OK +15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | RUF010.py:13:19: RUF010 [*] Use conversion in f-string @@ -149,7 +149,7 @@ RUF010.py:13:19: RUF010 [*] Use conversion in f-string 15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | ^^^^^^^^^ RUF010 16 | -17 | f"{foo(bla)}" # OK +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | = help: Replace f-string function call with conversion @@ -160,7 +160,7 @@ RUF010.py:13:19: RUF010 [*] Use conversion in f-string 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 13 |+f"{(str(bla))}, {bla!r}, {(ascii(bla))}" # RUF010 14 14 | -15 15 | f"{foo(bla)}" # OK +15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | RUF010.py:13:34: RUF010 [*] Use conversion in f-string @@ -170,7 +170,7 @@ RUF010.py:13:34: RUF010 [*] Use conversion in f-string 15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | ^^^^^^^^^^ RUF010 16 | -17 | f"{foo(bla)}" # OK +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 | = help: Replace f-string function call with conversion @@ -181,7 +181,49 @@ RUF010.py:13:34: RUF010 [*] Use conversion in f-string 13 |-f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 13 |+f"{(str(bla))}, {(repr(bla))}, {bla!a}" # RUF010 14 14 | -15 15 | f"{foo(bla)}" # OK +15 15 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 16 16 | +RUF010.py:15:14: RUF010 [*] Use conversion in f-string + | +15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +16 | +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + | ^^^^^^^^^ RUF010 +18 | +19 | f"{foo(bla)}" # OK + | + = help: Replace f-string function call with conversion + +ℹ Fix +12 12 | +13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +14 14 | +15 |-f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + 15 |+f"{bla!s}, {bla!r}, {(ascii(bla))}" # RUF010 +16 16 | +17 17 | f"{foo(bla)}" # OK +18 18 | + +RUF010.py:15:29: RUF010 [*] Use conversion in f-string + | +15 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +16 | +17 | f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + | ^^^^^^^^^^ RUF010 +18 | +19 | f"{foo(bla)}" # OK + | + = help: Replace f-string function call with conversion + +ℹ Fix +12 12 | +13 13 | f"{(str(bla))}, {(repr(bla))}, {(ascii(bla))}" # RUF010 +14 14 | +15 |-f"{bla!s}, {(repr(bla))}, {(ascii(bla))}" # RUF010 + 15 |+f"{bla!s}, {(repr(bla))}, {bla!a}" # RUF010 +16 16 | +17 17 | f"{foo(bla)}" # OK +18 18 | + diff --git a/crates/ruff/src/settings/mod.rs b/crates/ruff/src/settings/mod.rs index 453a93dac5..f2e2aec2db 100644 --- a/crates/ruff/src/settings/mod.rs +++ b/crates/ruff/src/settings/mod.rs @@ -259,7 +259,10 @@ impl From<&Configuration> for RuleTable { // The select_set keeps track of which rules have been selected. let mut select_set: RuleSet = defaults::PREFIXES.iter().flatten().collect(); // The fixable set keeps track of which rules are fixable. - let mut fixable_set: RuleSet = RuleSelector::All.into_iter().collect(); + let mut fixable_set: RuleSet = RuleSelector::All + .into_iter() + .chain(RuleSelector::Nursery.into_iter()) + .collect(); // Ignores normally only subtract from the current set of selected // rules. By that logic the ignore in `select = [], ignore = ["E501"]` diff --git a/crates/ruff_benchmark/Cargo.toml b/crates/ruff_benchmark/Cargo.toml index 3e738d518c..f3d63336e0 100644 --- a/crates/ruff_benchmark/Cargo.toml +++ b/crates/ruff_benchmark/Cargo.toml @@ -20,6 +20,10 @@ harness = false name = "parser" harness = false +[[bench]] +name = "formatter" +harness = false + [dependencies] once_cell.workspace = true serde.workspace = true @@ -30,7 +34,8 @@ ureq = "2.6.2" [dev-dependencies] ruff.path = "../ruff" ruff_python_ast.path = "../ruff_python_ast" -criterion = { version = "0.4.0"} +ruff_python_formatter = { path = "../ruff_python_formatter" } +criterion = { version = "0.5.1"} rustpython-parser.workspace = true [target.'cfg(target_os = "windows")'.dev-dependencies] @@ -38,3 +43,4 @@ mimalloc = "0.1.34" [target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies] tikv-jemallocator = "0.5.0" + diff --git a/crates/ruff_benchmark/benches/formatter.rs b/crates/ruff_benchmark/benches/formatter.rs new file mode 100644 index 0000000000..15698dbba0 --- /dev/null +++ b/crates/ruff_benchmark/benches/formatter.rs @@ -0,0 +1,62 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use ruff_benchmark::{TestCase, TestCaseSpeed, TestFile, TestFileDownloadError}; +use ruff_python_formatter::format_module; +use std::time::Duration; + +#[cfg(target_os = "windows")] +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + +#[cfg(all( + not(target_os = "windows"), + not(target_os = "openbsd"), + any( + target_arch = "x86_64", + target_arch = "aarch64", + target_arch = "powerpc64" + ) +))] +#[global_allocator] +static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + +fn create_test_cases() -> Result, TestFileDownloadError> { + Ok(vec![ + TestCase::fast(TestFile::try_download("numpy/globals.py", "https://raw.githubusercontent.com/numpy/numpy/89d64415e349ca75a25250f22b874aa16e5c0973/numpy/_globals.py")?), + TestCase::normal(TestFile::try_download( + "pydantic/types.py", + "https://raw.githubusercontent.com/pydantic/pydantic/83b3c49e99ceb4599d9286a3d793cea44ac36d4b/pydantic/types.py", + )?), + TestCase::normal(TestFile::try_download("numpy/ctypeslib.py", "https://raw.githubusercontent.com/numpy/numpy/e42c9503a14d66adfd41356ef5640c6975c45218/numpy/ctypeslib.py")?), + TestCase::slow(TestFile::try_download( + "large/dataset.py", + "https://raw.githubusercontent.com/DHI/mikeio/b7d26418f4db2909b0aa965253dbe83194d7bb5b/tests/test_dataset.py", + )?), + ]) +} + +fn benchmark_formatter(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("formatter"); + let test_cases = create_test_cases().unwrap(); + + for case in test_cases { + group.throughput(Throughput::Bytes(case.code().len() as u64)); + group.measurement_time(match case.speed() { + TestCaseSpeed::Fast => Duration::from_secs(5), + TestCaseSpeed::Normal => Duration::from_secs(10), + TestCaseSpeed::Slow => Duration::from_secs(20), + }); + + group.bench_with_input( + BenchmarkId::from_parameter(case.name()), + &case, + |b, case| { + b.iter(|| format_module(case.code()).expect("Formatting to succeed")); + }, + ); + } + + group.finish(); +} + +criterion_group!(formatter, benchmark_formatter); +criterion_main!(formatter); diff --git a/crates/ruff_formatter/src/builders.rs b/crates/ruff_formatter/src/builders.rs index 02000019c4..24879eb868 100644 --- a/crates/ruff_formatter/src/builders.rs +++ b/crates/ruff_formatter/src/builders.rs @@ -2131,11 +2131,11 @@ impl<'a, 'buf, Context> FillBuilder<'a, 'buf, Context> { /// The first variant is the most flat, and the last is the most expanded variant. /// See [`best_fitting!`] macro for a more in-detail documentation #[derive(Copy, Clone)] -pub struct BestFitting<'a, Context> { +pub struct FormatBestFitting<'a, Context> { variants: Arguments<'a, Context>, } -impl<'a, Context> BestFitting<'a, Context> { +impl<'a, Context> FormatBestFitting<'a, Context> { /// Creates a new best fitting IR with the given variants. The method itself isn't unsafe /// but it is to discourage people from using it because the printer will panic if /// the slice doesn't contain at least the least and most expanded variants. @@ -2154,7 +2154,7 @@ impl<'a, Context> BestFitting<'a, Context> { } } -impl Format for BestFitting<'_, Context> { +impl Format for FormatBestFitting<'_, Context> { fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { let mut buffer = VecBuffer::new(f.state_mut()); let variants = self.variants.items(); diff --git a/crates/ruff_formatter/src/lib.rs b/crates/ruff_formatter/src/lib.rs index a5e9401412..e1f9e4f21e 100644 --- a/crates/ruff_formatter/src/lib.rs +++ b/crates/ruff_formatter/src/lib.rs @@ -48,7 +48,7 @@ pub use buffer::{ Buffer, BufferExtensions, BufferSnapshot, Inspect, PreambleBuffer, RemoveSoftLinesBuffer, VecBuffer, }; -pub use builders::BestFitting; +pub use builders::FormatBestFitting; pub use source_code::{SourceCode, SourceCodeSlice}; pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, PrintError}; diff --git a/crates/ruff_formatter/src/macros.rs b/crates/ruff_formatter/src/macros.rs index 88ad961240..fb6c66e6fa 100644 --- a/crates/ruff_formatter/src/macros.rs +++ b/crates/ruff_formatter/src/macros.rs @@ -320,7 +320,7 @@ macro_rules! format { /// the content up to the first non-soft line break without exceeding the configured print width. /// This definition differs from groups as that non-soft line breaks make group expand. /// -/// [crate::BestFitting] acts as a "break" boundary, meaning that it is considered to fit +/// [crate::FormatBestFitting] acts as a "break" boundary, meaning that it is considered to fit /// /// /// [`Flat`]: crate::format_element::PrintMode::Flat @@ -330,7 +330,7 @@ macro_rules! format { macro_rules! best_fitting { ($least_expanded:expr, $($tail:expr),+ $(,)?) => {{ unsafe { - $crate::BestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) + $crate::FormatBestFitting::from_arguments_unchecked($crate::format_args!($least_expanded, $($tail),+)) } }} } diff --git a/crates/ruff_python_ast/src/helpers.rs b/crates/ruff_python_ast/src/helpers.rs index 6b873603ff..0f3340c1f7 100644 --- a/crates/ruff_python_ast/src/helpers.rs +++ b/crates/ruff_python_ast/src/helpers.rs @@ -800,7 +800,7 @@ pub fn format_import_from(level: Option, module: Option<&str>) -> String { /// assert_eq!(format_import_from_member(Some(1), Some("foo"), "bar"), ".foo.bar".to_string()); /// ``` pub fn format_import_from_member(level: Option, module: Option<&str>, member: &str) -> String { - let mut full_name = String::with_capacity( + let mut qualified_name = String::with_capacity( (level.unwrap_or(0) as usize) + module.as_ref().map_or(0, |module| module.len()) + 1 @@ -808,15 +808,15 @@ pub fn format_import_from_member(level: Option, module: Option<&str>, membe ); if let Some(level) = level { for _ in 0..level { - full_name.push('.'); + qualified_name.push('.'); } } if let Some(module) = module { - full_name.push_str(module); - full_name.push('.'); + qualified_name.push_str(module); + qualified_name.push('.'); } - full_name.push_str(member); - full_name + qualified_name.push_str(member); + qualified_name } /// Create a module path from a (package, path) pair. @@ -907,7 +907,7 @@ pub fn resolve_imported_module_path<'a>( /// A [`StatementVisitor`] that collects all `return` statements in a function or method. #[derive(Default)] pub struct ReturnStatementVisitor<'a> { - pub returns: Vec>, + pub returns: Vec<&'a ast::StmtReturn>, } impl<'a, 'b> StatementVisitor<'b> for ReturnStatementVisitor<'a> @@ -919,10 +919,7 @@ where Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_) => { // Don't recurse. } - Stmt::Return(ast::StmtReturn { - value, - range: _range, - }) => self.returns.push(value.as_deref()), + Stmt::Return(stmt) => self.returns.push(stmt), _ => walk_stmt(self, stmt), } } diff --git a/crates/ruff_python_ast/src/source_code/generator.rs b/crates/ruff_python_ast/src/source_code/generator.rs index ec7e92886d..7c3e1efde4 100644 --- a/crates/ruff_python_ast/src/source_code/generator.rs +++ b/crates/ruff_python_ast/src/source_code/generator.rs @@ -209,14 +209,13 @@ impl<'a> Generator<'a> { .. }) => { self.newlines(if self.indent_depth == 0 { 2 } else { 1 }); + for decorator in decorator_list { + statement!({ + self.p("@"); + self.unparse_expr(decorator, precedence::MAX); + }); + } statement!({ - for decorator in decorator_list { - statement!({ - self.p("@"); - self.unparse_expr(decorator, precedence::MAX); - }); - } - self.newline(); self.p("def "); self.p_id(name); self.p("("); @@ -242,13 +241,13 @@ impl<'a> Generator<'a> { .. }) => { self.newlines(if self.indent_depth == 0 { 2 } else { 1 }); + for decorator in decorator_list { + statement!({ + self.p("@"); + self.unparse_expr(decorator, precedence::MAX); + }); + } statement!({ - for decorator in decorator_list { - statement!({ - self.unparse_expr(decorator, precedence::MAX); - }); - } - self.newline(); self.p("async def "); self.p_id(name); self.p("("); @@ -274,13 +273,13 @@ impl<'a> Generator<'a> { range: _range, }) => { self.newlines(if self.indent_depth == 0 { 2 } else { 1 }); + for decorator in decorator_list { + statement!({ + self.p("@"); + self.unparse_expr(decorator, precedence::MAX); + }); + } statement!({ - for decorator in decorator_list { - statement!({ - self.unparse_expr(decorator, precedence::MAX); - }); - } - self.newline(); self.p("class "); self.p_id(name); let mut first = true; @@ -1614,6 +1613,29 @@ except* Exception as e: ); assert_eq!(round_trip(r#"x = (1, 2, 3)"#), r#"x = 1, 2, 3"#); assert_eq!(round_trip(r#"-(1) + ~(2) + +(3)"#), r#"-1 + ~2 + +3"#); + assert_round_trip!( + r#"def f(): + + def f(): + pass"# + ); + assert_round_trip!( + r#"@foo +def f(): + + @foo + def f(): + pass"# + ); + + assert_round_trip!( + r#"@foo +class Foo: + + @foo + def f(): + pass"# + ); } #[test] diff --git a/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py new file mode 100644 index 0000000000..79c0d09c8c --- /dev/null +++ b/crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/while.py @@ -0,0 +1,30 @@ +while 34: # trailing test comment + pass # trailing last statement comment + + # trailing while body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +while ( + some_condition(unformatted, args) and anotherCondition or aThirdCondition +): # comment + print("Do something") + + +while ( + some_condition(unformatted, args) # trailing some condition + and anotherCondition or aThirdCondition # trailing third condition +): # comment + print("Do something") diff --git a/crates/ruff_python_formatter/src/builders.rs b/crates/ruff_python_formatter/src/builders.rs index 62d7317f36..45a9ee248e 100644 --- a/crates/ruff_python_formatter/src/builders.rs +++ b/crates/ruff_python_formatter/src/builders.rs @@ -11,7 +11,7 @@ pub(crate) trait PyFormatterExtensions<'ast, 'buf> { /// empty lines between any two nodes. Separates any two nodes by at least a hard line break. /// /// * [`NodeLevel::Module`]: Up to two empty lines - /// * [`NodeLevel::Statement`]: Up to one empty line + /// * [`NodeLevel::CompoundStatement`]: Up to one empty line /// * [`NodeLevel::Parenthesized`]: No empty lines fn join_nodes<'fmt>(&'fmt mut self, level: NodeLevel) -> JoinNodesBuilder<'fmt, 'ast, 'buf>; } @@ -53,10 +53,12 @@ impl<'fmt, 'ast, 'buf> JoinNodesBuilder<'fmt, 'ast, 'buf> { 2 => empty_line().fmt(f), _ => write!(f, [empty_line(), empty_line()]), }, - NodeLevel::Statement => match lines_before(f.context().contents(), node.start()) { - 0 | 1 => hard_line_break().fmt(f), - _ => empty_line().fmt(f), - }, + NodeLevel::CompoundStatement => { + match lines_before(f.context().contents(), node.start()) { + 0 | 1 => hard_line_break().fmt(f), + _ => empty_line().fmt(f), + } + } NodeLevel::Parenthesized => hard_line_break().fmt(f), }); @@ -180,7 +182,7 @@ no_leading_newline = 30"# // Should keep at most one empty level #[test] fn ranged_builder_statement_level() { - let printed = format_ranged(NodeLevel::Statement); + let printed = format_ranged(NodeLevel::CompoundStatement); assert_eq!( &printed, diff --git a/crates/ruff_python_formatter/src/cli.rs b/crates/ruff_python_formatter/src/cli.rs index 87d7dc06aa..f605e66407 100644 --- a/crates/ruff_python_formatter/src/cli.rs +++ b/crates/ruff_python_formatter/src/cli.rs @@ -1,11 +1,75 @@ +#![allow(clippy::print_stdout)] + use std::path::PathBuf; -use clap::{command, Parser}; +use anyhow::{bail, Context, Result}; +use clap::{command, Parser, ValueEnum}; +use rustpython_parser::lexer::lex; +use rustpython_parser::{parse_tokens, Mode}; + +use ruff_formatter::SourceCode; +use ruff_python_ast::source_code::CommentRangesBuilder; + +use crate::format_node; + +#[derive(ValueEnum, Clone, Debug)] +pub enum Emit { + /// Write back to the original files + Files, + /// Write to stdout + Stdout, +} #[derive(Parser)] #[command(author, version, about, long_about = None)] pub struct Cli { - /// Python file to round-trip. - #[arg(required = true)] - pub file: PathBuf, + /// Python files to format. If there are none, stdin will be used. `-` as stdin is not supported + pub files: Vec, + #[clap(long)] + pub emit: Option, + /// Run in 'check' mode. Exits with 0 if input is formatted correctly. Exits with 1 and prints + /// a diff if formatting is required. + #[clap(long)] + pub check: bool, + #[clap(long)] + pub print_ir: bool, + #[clap(long)] + pub print_comments: bool, +} + +pub fn format_and_debug_print(input: &str, cli: &Cli) -> Result { + let mut tokens = Vec::new(); + let mut comment_ranges = CommentRangesBuilder::default(); + + for result in lex(input, Mode::Module) { + let (token, range) = match result { + Ok((token, range)) => (token, range), + Err(err) => bail!("Source contains syntax errors {err:?}"), + }; + + comment_ranges.visit_token(&token, range); + tokens.push(Ok((token, range))); + } + + let comment_ranges = comment_ranges.finish(); + + // Parse the AST. + let python_ast = parse_tokens(tokens, Mode::Module, "") + .with_context(|| "Syntax error in input")?; + + let formatted = format_node(&python_ast, &comment_ranges, input)?; + if cli.print_ir { + println!("{}", formatted.document().display(SourceCode::new(input))); + } + if cli.print_comments { + println!( + "{:?}", + formatted.context().comments().debug(SourceCode::new(input)) + ); + } + Ok(formatted + .print() + .with_context(|| "Failed to print the formatter IR")? + .as_code() + .to_string()) } diff --git a/crates/ruff_python_formatter/src/comments/debug.rs b/crates/ruff_python_formatter/src/comments/debug.rs index 8583461a44..6e37ac5ff6 100644 --- a/crates/ruff_python_formatter/src/comments/debug.rs +++ b/crates/ruff_python_formatter/src/comments/debug.rs @@ -183,7 +183,6 @@ mod tests { use ruff_python_ast::node::AnyNode; use ruff_text_size::{TextRange, TextSize}; use rustpython_parser::ast::{StmtBreak, StmtContinue}; - use std::cell::Cell; #[test] fn debug() { @@ -210,7 +209,7 @@ break; SourceComment { slice: source_code.slice(TextRange::at(TextSize::new(0), TextSize::new(17))), #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), position: CommentTextPosition::OwnLine, }, ); @@ -220,7 +219,7 @@ break; SourceComment { slice: source_code.slice(TextRange::at(TextSize::new(28), TextSize::new(10))), #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), position: CommentTextPosition::EndOfLine, }, ); @@ -230,7 +229,7 @@ break; SourceComment { slice: source_code.slice(TextRange::at(TextSize::new(39), TextSize::new(15))), #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), position: CommentTextPosition::OwnLine, }, ); diff --git a/crates/ruff_python_formatter/src/comments/format.rs b/crates/ruff_python_formatter/src/comments/format.rs index 1eb2f44184..286905ce85 100644 --- a/crates/ruff_python_formatter/src/comments/format.rs +++ b/crates/ruff_python_formatter/src/comments/format.rs @@ -6,27 +6,37 @@ use ruff_formatter::{format_args, write, FormatError, SourceCode}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::prelude::AstNode; use ruff_text_size::{TextLen, TextRange, TextSize}; +use rustpython_parser::ast::Ranged; /// Formats the leading comments of a node. -pub(crate) fn leading_comments(node: &T) -> FormatLeadingComments +pub(crate) fn leading_node_comments(node: &T) -> FormatLeadingComments where T: AstNode, { - FormatLeadingComments { - node: node.as_any_node_ref(), - } + FormatLeadingComments::Node(node.as_any_node_ref()) +} + +/// Formats the passed comments as leading comments +pub(crate) const fn leading_comments(comments: &[SourceComment]) -> FormatLeadingComments { + FormatLeadingComments::Comments(comments) } #[derive(Copy, Clone, Debug)] -pub(crate) struct FormatLeadingComments<'a> { - node: AnyNodeRef<'a>, +pub(crate) enum FormatLeadingComments<'a> { + Node(AnyNodeRef<'a>), + Comments(&'a [SourceComment]), } impl Format> for FormatLeadingComments<'_> { fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { let comments = f.context().comments().clone(); - for comment in comments.leading_comments(self.node) { + let leading_comments = match self { + FormatLeadingComments::Node(node) => comments.leading_comments(*node), + FormatLeadingComments::Comments(comments) => comments, + }; + + for comment in leading_comments { let slice = comment.slice(); let lines_after_comment = lines_after(f.context().contents(), slice.end()); @@ -42,32 +52,88 @@ impl Format> for FormatLeadingComments<'_> { } } -/// Formats the trailing comments of `node` -pub(crate) fn trailing_comments(node: &T) -> FormatTrailingComments +/// Formats the leading `comments` of an alternate branch and ensures that it preserves the right +/// number of empty lines before. The `last_node` is the last node of the preceding body. +/// +/// For example, `last_node` is the last statement in the if body when formatting the leading +/// comments of the `else` branch. +pub(crate) fn leading_alternate_branch_comments<'a, T>( + comments: &'a [SourceComment], + last_node: Option, +) -> FormatLeadingAlternateBranchComments<'a> where - T: AstNode, + T: Into>, { - FormatTrailingComments { - node: node.as_any_node_ref(), + FormatLeadingAlternateBranchComments { + comments, + last_node: last_node.map(std::convert::Into::into), } } -pub(crate) struct FormatTrailingComments<'a> { - node: AnyNodeRef<'a>, +pub(crate) struct FormatLeadingAlternateBranchComments<'a> { + comments: &'a [SourceComment], + last_node: Option>, +} + +impl Format> for FormatLeadingAlternateBranchComments<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + if let Some(first_leading) = self.comments.first() { + // Leading comments only preserves the lines after the comment but not before. + // Insert the necessary lines. + if lines_before(f.context().contents(), first_leading.slice().start()) > 1 { + write!(f, [empty_line()])?; + } + + write!(f, [leading_comments(self.comments)])?; + } else if let Some(last_preceding) = self.last_node { + // The leading comments formatting ensures that it preserves the right amount of lines after + // We need to take care of this ourselves, if there's no leading `else` comment. + if lines_after(f.context().contents(), last_preceding.end()) > 1 { + write!(f, [empty_line()])?; + } + } + + Ok(()) + } +} + +/// Formats the trailing comments of `node` +pub(crate) fn trailing_node_comments(node: &T) -> FormatTrailingComments +where + T: AstNode, +{ + FormatTrailingComments::Node(node.as_any_node_ref()) +} + +/// Formats the passed comments as trailing comments +pub(crate) fn trailing_comments(comments: &[SourceComment]) -> FormatTrailingComments { + FormatTrailingComments::Comments(comments) +} + +pub(crate) enum FormatTrailingComments<'a> { + Node(AnyNodeRef<'a>), + Comments(&'a [SourceComment]), } impl Format> for FormatTrailingComments<'_> { fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { let comments = f.context().comments().clone(); - let mut has_empty_lines_before = false; - for trailing in comments.trailing_comments(self.node) { + let trailing_comments = match self { + FormatTrailingComments::Node(node) => comments.trailing_comments(*node), + FormatTrailingComments::Comments(comments) => comments, + }; + + let mut has_trailing_own_line_comment = false; + + for trailing in trailing_comments { let slice = trailing.slice(); - let lines_before_comment = lines_before(f.context().contents(), slice.start()); - has_empty_lines_before |= lines_before_comment > 0; + has_trailing_own_line_comment |= trailing.position().is_own_line(); + + if has_trailing_own_line_comment { + let lines_before_comment = lines_before(f.context().contents(), slice.start()); - if has_empty_lines_before { // A trailing comment at the end of a body or list // ```python // def test(): @@ -105,7 +171,7 @@ impl Format> for FormatTrailingComments<'_> { } /// Formats the dangling comments of `node`. -pub(crate) fn dangling_comments(node: &T) -> FormatDanglingComments +pub(crate) fn dangling_node_comments(node: &T) -> FormatDanglingComments where T: AstNode, { @@ -229,7 +295,7 @@ impl Format> for FormatEmptyLines { _ => write!(f, [empty_line(), empty_line()]), }, - NodeLevel::Statement => match self.lines { + NodeLevel::CompoundStatement => match self.lines { 0 | 1 => write!(f, [hard_line_break()]), _ => write!(f, [empty_line()]), }, diff --git a/crates/ruff_python_formatter/src/comments/mod.rs b/crates/ruff_python_formatter/src/comments/mod.rs index fdc5ea2190..ae61709e01 100644 --- a/crates/ruff_python_formatter/src/comments/mod.rs +++ b/crates/ruff_python_formatter/src/comments/mod.rs @@ -88,7 +88,6 @@ //! It is possible to add an additional optional label to [`SourceComment`] If ever the need arises to distinguish two *dangling comments* in the formatting logic, use rustpython_parser::ast::Mod; -use std::cell::Cell; use std::fmt::Debug; use std::rc::Rc; @@ -103,7 +102,10 @@ use crate::comments::debug::{DebugComment, DebugComments}; use crate::comments::map::MultiMap; use crate::comments::node_key::NodeRefEqualityKey; use crate::comments::visitor::CommentsVisitor; -pub(crate) use format::{dangling_comments, leading_comments, trailing_comments}; +pub(crate) use format::{ + dangling_node_comments, leading_alternate_branch_comments, leading_node_comments, + trailing_comments, trailing_node_comments, +}; use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::CommentRanges; @@ -116,13 +118,11 @@ pub(crate) struct SourceComment { /// Whether the comment has been formatted or not. #[cfg(debug_assertions)] - formatted: Cell, + formatted: std::cell::Cell, position: CommentTextPosition, } -#[allow(unused)] -// TODO(micha): Remove after using the new comments infrastructure in the formatter. impl SourceComment { /// Returns the location of the comment in the original source code. /// Allows retrieving the text of the comment. @@ -136,7 +136,7 @@ impl SourceComment { #[cfg(not(debug_assertions))] #[inline(always)] - pub fn mark_formatted(&self) {} + pub(crate) fn mark_formatted(&self) {} /// Marks the comment as formatted #[cfg(debug_assertions)] @@ -184,8 +184,6 @@ pub(crate) enum CommentTextPosition { OwnLine, } -#[allow(unused)] -// TODO(micha): Remove after using the new comments infrastructure in the formatter. impl CommentTextPosition { pub(crate) const fn is_own_line(self) -> bool { matches!(self, CommentTextPosition::OwnLine) @@ -858,4 +856,33 @@ a = ( assert_debug_snapshot!(comments.debug(test_case.source_code)); } + + #[test] + fn while_trailing_end_of_line_comment() { + let source = r#"while True: + if something.changed: + do.stuff() # trailing comment +"#; + + let test_case = CommentsTestCase::from_code(source); + + let comments = test_case.to_comments(); + + assert_debug_snapshot!(comments.debug(test_case.source_code)); + } + + #[test] + fn while_trailing_else_end_of_line_comment() { + let source = r#"while True: + pass +else: # trailing comment + pass +"#; + + let test_case = CommentsTestCase::from_code(source); + + let comments = test_case.to_comments(); + + assert_debug_snapshot!(comments.debug(test_case.source_code)); + } } diff --git a/crates/ruff_python_formatter/src/comments/placement.rs b/crates/ruff_python_formatter/src/comments/placement.rs index f689d7c127..062cd17a8d 100644 --- a/crates/ruff_python_formatter/src/comments/placement.rs +++ b/crates/ruff_python_formatter/src/comments/placement.rs @@ -5,7 +5,7 @@ use crate::trivia::find_first_non_trivia_character_in_range; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::source_code::Locator; use ruff_python_ast::whitespace; -use ruff_text_size::{TextRange, TextSize}; +use ruff_text_size::{TextLen, TextRange, TextSize}; use rustpython_parser::ast::Ranged; use std::cmp::Ordering; @@ -16,8 +16,11 @@ pub(super) fn place_comment<'a>( ) -> CommentPlacement<'a> { handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment(comment, locator) .or_else(|comment| handle_match_comment(comment, locator)) - .or_else(|comment| handle_in_between_bodies_comment(comment, locator)) + .or_else(|comment| handle_in_between_bodies_own_line_comment(comment, locator)) + .or_else(|comment| handle_in_between_bodies_end_of_line_comment(comment, locator)) .or_else(|comment| handle_trailing_body_comment(comment, locator)) + .or_else(handle_trailing_end_of_line_body_comment) + .or_else(|comment| handle_trailing_end_of_line_condition_comment(comment, locator)) .or_else(|comment| handle_positional_only_arguments_separator_comment(comment, locator)) .or_else(|comment| { handle_trailing_binary_expression_left_or_operator_comment(comment, locator) @@ -177,7 +180,7 @@ fn handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_commen CommentPlacement::Default(comment) } -/// Handles comments between the last statement and the first statement of two bodies. +/// Handles own line comments between the last statement and the first statement of two bodies. /// /// ```python /// if x == y: @@ -187,15 +190,11 @@ fn handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_commen /// else: /// print("I have no comments") /// ``` -fn handle_in_between_bodies_comment<'a>( +fn handle_in_between_bodies_own_line_comment<'a>( comment: DecoratedComment<'a>, locator: &Locator, ) -> CommentPlacement<'a> { - use ruff_python_ast::prelude::*; - - // The rule only applies to own line comments. The default logic associates end of line comments - // correctly. - if comment.text_position().is_end_of_line() { + if !comment.text_position().is_own_line() { return CommentPlacement::Default(comment); } @@ -203,39 +202,7 @@ fn handle_in_between_bodies_comment<'a>( if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) { // ...and the following statement must be the first statement in an alternate body of the parent... - let is_following_the_first_statement_in_a_parents_alternate_body = - match comment.enclosing_node() { - AnyNodeRef::StmtIf(StmtIf { orelse, .. }) - | AnyNodeRef::StmtFor(StmtFor { orelse, .. }) - | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { orelse, .. }) - | AnyNodeRef::StmtWhile(StmtWhile { orelse, .. }) => { - are_same_optional(following, orelse.first()) - } - - AnyNodeRef::StmtTry(StmtTry { - handlers, - orelse, - finalbody, - .. - }) - | AnyNodeRef::StmtTryStar(StmtTryStar { - handlers, - orelse, - finalbody, - .. - }) => { - are_same_optional(following, handlers.first()) - // Comments between the handlers and the `else`, or comments between the `handlers` and the `finally` - // are already handled by `handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment` - || handlers.is_empty() && are_same_optional(following, orelse.first()) - || (handlers.is_empty() || !orelse.is_empty()) - && are_same_optional(following, finalbody.first()) - } - - _ => false, - }; - - if !is_following_the_first_statement_in_a_parents_alternate_body { + if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { // ```python // if test: // a @@ -304,6 +271,75 @@ fn handle_in_between_bodies_comment<'a>( CommentPlacement::Default(comment) } +/// Handles end of line comments comments between the last statement and the first statement of two bodies. +/// +/// ```python +/// if x == y: +/// pass # trailing comment of pass +/// else: # trailing comment of `else` +/// print("I have no comments") +/// ``` +fn handle_in_between_bodies_end_of_line_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + if !comment.text_position().is_end_of_line() { + return CommentPlacement::Default(comment); + } + + // The comment must be between two statements... + if let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) + { + // ...and the following statement must be the first statement in an alternate body of the parent... + if !is_first_statement_in_enclosing_alternate_body(following, comment.enclosing_node()) { + // ```python + // if test: + // a + // # comment + // b + // ``` + return CommentPlacement::Default(comment); + } + + if !locator.contains_line_break(TextRange::new(preceding.end(), comment.slice().start())) { + // Trailing comment of the preceding statement + // ```python + // while test: + // a # comment + // else: + // b + // ``` + CommentPlacement::trailing(preceding, comment) + } else if following.is_stmt_if() || following.is_except_handler() { + // The `elif` or except handlers have their own body to which we can attach the trailing comment + // ```python + // if test: + // a + // elif c: # comment + // b + // ``` + CommentPlacement::trailing(following, comment) + } else { + // There are no bodies for the "else" branch and other bodies that are represented as a `Vec`. + // This means, there's no good place to attach the comments to. + // Make this a dangling comments and manually format the comment in + // in the enclosing node's formatting logic. For `try`, it's the formatters responsibility + // to correctly identify the comments for the `finally` and `orelse` block by looking + // at the comment's range. + // + // ```python + // while x == y: + // pass + // else: # trailing + // print("nooop") + // ``` + CommentPlacement::dangling(comment.enclosing_node(), comment) + } + } else { + CommentPlacement::Default(comment) + } +} + /// Handles trailing comments at the end of a body block (or any other block that is indented). /// ```python /// def test(): @@ -401,6 +437,126 @@ fn handle_trailing_body_comment<'a>( } } +/// Handles end of line comments of the last statement in an indented body: +/// +/// ```python +/// while True: +/// if something.changed: +/// do.stuff() # trailing comment +/// ``` +fn handle_trailing_end_of_line_body_comment(comment: DecoratedComment<'_>) -> CommentPlacement<'_> { + // Must be an end of line comment + if comment.text_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + // Must be *after* a statement + let Some(preceding) = comment.preceding_node() else { + return CommentPlacement::Default(comment); + }; + + // Recursively get the last child of statements with a body. + let last_children = std::iter::successors(last_child_in_body(preceding), |parent| { + last_child_in_body(*parent) + }); + + if let Some(last_child) = last_children.last() { + CommentPlacement::trailing(last_child, comment) + } else { + // End of line comment of a statement that has no body. This is not what we're looking for. + // ```python + // a # trailing comment + // b + // ``` + CommentPlacement::Default(comment) + } +} + +/// Handles end of line comments after the `:` of a condition +/// +/// ```python +/// while True: # comment +/// pass +/// ``` +/// +/// It attaches the comment as dangling comment to the enclosing `while` statement. +fn handle_trailing_end_of_line_condition_comment<'a>( + comment: DecoratedComment<'a>, + locator: &Locator, +) -> CommentPlacement<'a> { + use ruff_python_ast::prelude::*; + + // Must be an end of line comment + if comment.text_position().is_own_line() { + return CommentPlacement::Default(comment); + } + + // Must be between the condition expression and the first body element + let (Some(preceding), Some(following)) = (comment.preceding_node(), comment.following_node()) else { + return CommentPlacement::Default(comment); + }; + + let expression_before_colon = match comment.enclosing_node() { + AnyNodeRef::StmtIf(StmtIf { test: expr, .. }) + | AnyNodeRef::StmtWhile(StmtWhile { test: expr, .. }) + | AnyNodeRef::StmtFor(StmtFor { iter: expr, .. }) + | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { iter: expr, .. }) => { + Some(AnyNodeRef::from(expr.as_ref())) + } + + AnyNodeRef::StmtWith(StmtWith { items, .. }) + | AnyNodeRef::StmtAsyncWith(StmtAsyncWith { items, .. }) => { + items.last().map(AnyNodeRef::from) + } + _ => None, + }; + + let Some(last_before_colon) = expression_before_colon else { + return CommentPlacement::Default(comment); + }; + + // If the preceding is the node before the `colon` + // `while true:` The node before the `colon` is the `true` constant. + if preceding.ptr_eq(last_before_colon) { + let mut start = preceding.end(); + while let Some((offset, c)) = find_first_non_trivia_character_in_range( + locator.contents(), + TextRange::new(start, following.start()), + ) { + match c { + ':' => { + if comment.slice().start() > offset { + // Comment comes after the colon + // ```python + // while a: # comment + // ... + // ``` + return CommentPlacement::dangling(comment.enclosing_node(), comment); + } + + // Comment comes before the colon + // ```python + // while ( + // a # comment + // ): + // ... + // ``` + break; + } + ')' => { + // Skip over any closing parentheses + start = offset + ')'.text_len(); + } + _ => { + unreachable!("Only ')' or ':' should follow the condition") + } + } + } + } + + CommentPlacement::Default(comment) +} + /// Attaches comments for the positional-only arguments separator `/` as trailing comments to the /// enclosing [`Arguments`] node. /// @@ -667,3 +823,42 @@ fn last_child_in_body(node: AnyNodeRef) -> Option { body.last().map(AnyNodeRef::from) } + +/// Returns `true` if `following` is the first statement in an alternate `body` (e.g. the else of an if statement) of the `enclosing` node. +fn is_first_statement_in_enclosing_alternate_body( + following: AnyNodeRef, + enclosing: AnyNodeRef, +) -> bool { + use ruff_python_ast::prelude::*; + + match enclosing { + AnyNodeRef::StmtIf(StmtIf { orelse, .. }) + | AnyNodeRef::StmtFor(StmtFor { orelse, .. }) + | AnyNodeRef::StmtAsyncFor(StmtAsyncFor { orelse, .. }) + | AnyNodeRef::StmtWhile(StmtWhile { orelse, .. }) => { + are_same_optional(following, orelse.first()) + } + + AnyNodeRef::StmtTry(StmtTry { + handlers, + orelse, + finalbody, + .. + }) + | AnyNodeRef::StmtTryStar(StmtTryStar { + handlers, + orelse, + finalbody, + .. + }) => { + are_same_optional(following, handlers.first()) + // Comments between the handlers and the `else`, or comments between the `handlers` and the `finally` + // are already handled by `handle_in_between_excepthandlers_or_except_handler_and_else_or_finally_comment` + || handlers.is_empty() && are_same_optional(following, orelse.first()) + || (handlers.is_empty() || !orelse.is_empty()) + && are_same_optional(following, finalbody.first()) + } + + _ => false, + } +} diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap index 51d2b14f09..ef24b1dbef 100644 --- a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__base_test.snap @@ -19,19 +19,19 @@ expression: comments.debug(test_case.source_code) "trailing": [], }, Node { - kind: ExprCompare, - range: 51..57, - source: `x == y`, + kind: StmtIf, + range: 48..212, + source: `if x == y: # if statement e...ne comment⏎`, }: { "leading": [], - "dangling": [], - "trailing": [ + "dangling": [ SourceComment { text: "# if statement end of line comment", position: EndOfLine, formatted: false, }, ], + "trailing": [], }, Node { kind: StmtIf, diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_else_end_of_line_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_else_end_of_line_comment.snap new file mode 100644 index 0000000000..33cd36edc4 --- /dev/null +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_else_end_of_line_comment.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_formatter/src/comments/mod.rs +expression: comments.debug(test_case.source_code) +--- +{ + Node { + kind: StmtWhile, + range: 0..54, + source: `while True:⏎`, + }: { + "leading": [], + "dangling": [ + SourceComment { + text: "# trailing comment", + position: EndOfLine, + formatted: false, + }, + ], + "trailing": [], + }, +} diff --git a/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_end_of_line_comment.snap b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_end_of_line_comment.snap new file mode 100644 index 0000000000..41e845c4b4 --- /dev/null +++ b/crates/ruff_python_formatter/src/comments/snapshots/ruff_python_formatter__comments__tests__while_trailing_end_of_line_comment.snap @@ -0,0 +1,21 @@ +--- +source: crates/ruff_python_formatter/src/comments/mod.rs +expression: comments.debug(test_case.source_code) +--- +{ + Node { + kind: StmtExpr, + range: 46..56, + source: `do.stuff()`, + }: { + "leading": [], + "dangling": [], + "trailing": [ + SourceComment { + text: "# trailing comment", + position: EndOfLine, + formatted: false, + }, + ], + }, +} diff --git a/crates/ruff_python_formatter/src/comments/visitor.rs b/crates/ruff_python_formatter/src/comments/visitor.rs index 4e2c75ffc6..fa1901a41c 100644 --- a/crates/ruff_python_formatter/src/comments/visitor.rs +++ b/crates/ruff_python_formatter/src/comments/visitor.rs @@ -5,7 +5,6 @@ use ruff_formatter::{SourceCode, SourceCodeSlice}; use ruff_python_ast::node::AnyNodeRef; use ruff_python_ast::prelude::*; use ruff_python_ast::source_code::{CommentRanges, Locator}; -use std::cell::Cell; // The interface is designed to only export the members relevant for iterating nodes in // pre-order. #[allow(clippy::wildcard_imports)] @@ -418,7 +417,7 @@ impl From> for SourceComment { slice: decorated.slice, position: decorated.text_position, #[cfg(debug_assertions)] - formatted: Cell::new(false), + formatted: std::cell::Cell::new(false), } } } diff --git a/crates/ruff_python_formatter/src/context.rs b/crates/ruff_python_formatter/src/context.rs index 21d03fec72..bef99c4514 100644 --- a/crates/ruff_python_formatter/src/context.rs +++ b/crates/ruff_python_formatter/src/context.rs @@ -25,7 +25,6 @@ impl<'a> PyFormatContext<'a> { } } - #[allow(unused)] pub(crate) fn contents(&self) -> &'a str { self.contents } @@ -35,7 +34,6 @@ impl<'a> PyFormatContext<'a> { Locator::new(self.contents) } - #[allow(unused)] pub(crate) fn set_node_level(&mut self, level: NodeLevel) { self.node_level = level; } @@ -44,7 +42,6 @@ impl<'a> PyFormatContext<'a> { self.node_level } - #[allow(unused)] pub(crate) fn comments(&self) -> &Comments<'a> { &self.comments } @@ -80,11 +77,10 @@ pub(crate) enum NodeLevel { #[default] TopLevel, - /// Formatting nodes that are enclosed by a statement. - #[allow(unused)] - Statement, + /// Formatting the body statements of a [compound statement](https://docs.python.org/3/reference/compound_stmts.html#compound-statements) + /// (`if`, `while`, `match`, etc.). + CompoundStatement, /// Formatting nodes that are enclosed in a parenthesized expression. - #[allow(unused)] Parenthesized, } diff --git a/crates/ruff_python_formatter/src/expression/expr_name.rs b/crates/ruff_python_formatter/src/expression/expr_name.rs index b19333ff4c..18605603e5 100644 --- a/crates/ruff_python_formatter/src/expression/expr_name.rs +++ b/crates/ruff_python_formatter/src/expression/expr_name.rs @@ -1,5 +1,6 @@ -use crate::{verbatim_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::{write, FormatContext}; use rustpython_parser::ast::ExprName; #[derive(Default)] @@ -7,6 +8,44 @@ pub struct FormatExprName; impl FormatNodeRule for FormatExprName { fn fmt_fields(&self, item: &ExprName, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [verbatim_text(item.range)]) + let ExprName { id, range, ctx: _ } = item; + + debug_assert_eq!( + id.as_str(), + f.context() + .source_code() + .slice(*range) + .text(f.context().source_code()) + ); + + write!(f, [source_text_slice(*range, ContainsNewlines::No)]) + } +} + +#[cfg(test)] +mod tests { + use ruff_text_size::{TextRange, TextSize}; + use rustpython_parser::ast::{ModModule, Ranged}; + use rustpython_parser::Parse; + + #[test] + fn name_range_with_comments() { + let source = ModModule::parse("a # comment", "file.py").unwrap(); + + let expression_statement = source + .body + .first() + .expect("Expected non-empty body") + .as_expr_stmt() + .unwrap(); + let name = expression_statement + .value + .as_name_expr() + .expect("Expected name expression"); + + assert_eq!( + name.range(), + TextRange::at(TextSize::new(0), TextSize::new(1)) + ); } } diff --git a/crates/ruff_python_formatter/src/expression/maybe_parenthesize.rs b/crates/ruff_python_formatter/src/expression/maybe_parenthesize.rs new file mode 100644 index 0000000000..aa7b4f5702 --- /dev/null +++ b/crates/ruff_python_formatter/src/expression/maybe_parenthesize.rs @@ -0,0 +1,53 @@ +use crate::context::NodeLevel; +use crate::prelude::*; +use ruff_formatter::{format_args, write}; +use rustpython_parser::ast::Expr; + +/// Formats the passed expression. Adds parentheses if the expression doesn't fit on a line. +pub(crate) const fn maybe_parenthesize(expression: &Expr) -> MaybeParenthesize { + MaybeParenthesize { expression } +} + +pub(crate) struct MaybeParenthesize<'a> { + expression: &'a Expr, +} + +impl Format> for MaybeParenthesize<'_> { + fn fmt(&self, f: &mut Formatter>) -> FormatResult<()> { + let saved_level = f.context().node_level(); + f.context_mut().set_node_level(NodeLevel::Parenthesized); + + let result = if needs_parentheses(self.expression) { + write!( + f, + [group(&format_args![ + if_group_breaks(&text("(")), + soft_block_indent(&self.expression.format()), + if_group_breaks(&text(")")) + ])] + ) + } else { + // Don't add parentheses around expressions that have parentheses on their own (e.g. list, dict, tuple, call expression) + self.expression.format().fmt(f) + }; + + f.context_mut().set_node_level(saved_level); + + result + } +} + +const fn needs_parentheses(expr: &Expr) -> bool { + !matches!( + expr, + Expr::Tuple(_) + | Expr::List(_) + | Expr::Set(_) + | Expr::Dict(_) + | Expr::ListComp(_) + | Expr::SetComp(_) + | Expr::DictComp(_) + | Expr::GeneratorExp(_) + | Expr::Call(_) + ) +} diff --git a/crates/ruff_python_formatter/src/expression/mod.rs b/crates/ruff_python_formatter/src/expression/mod.rs index 362b1daa86..039c104d55 100644 --- a/crates/ruff_python_formatter/src/expression/mod.rs +++ b/crates/ruff_python_formatter/src/expression/mod.rs @@ -29,6 +29,7 @@ pub(crate) mod expr_tuple; pub(crate) mod expr_unary_op; pub(crate) mod expr_yield; pub(crate) mod expr_yield_from; +pub(crate) mod maybe_parenthesize; #[derive(Default)] pub struct FormatExpr; diff --git a/crates/ruff_python_formatter/src/lib.rs b/crates/ruff_python_formatter/src/lib.rs index 49cc9fadc9..75de5ac303 100644 --- a/crates/ruff_python_formatter/src/lib.rs +++ b/crates/ruff_python_formatter/src/lib.rs @@ -3,18 +3,23 @@ use ruff_text_size::TextRange; use rustpython_parser::ast::Mod; use rustpython_parser::lexer::lex; use rustpython_parser::{parse_tokens, Mode}; +use std::borrow::Cow; use ruff_formatter::format_element::tag::VerbatimKind; use ruff_formatter::formatter::Formatter; -use ruff_formatter::prelude::{source_position, source_text_slice, ContainsNewlines, Tag}; +use ruff_formatter::prelude::{ + dynamic_text, source_position, source_text_slice, ContainsNewlines, Tag, +}; use ruff_formatter::{ - format, write, Buffer, Format, FormatContext, FormatElement, FormatResult, Formatted, + format, normalize_newlines, write, Buffer, Format, FormatElement, FormatResult, Formatted, IndentStyle, Printed, SimpleFormatOptions, SourceCode, }; use ruff_python_ast::node::AstNode; use ruff_python_ast::source_code::{CommentRanges, CommentRangesBuilder, Locator}; -use crate::comments::{dangling_comments, leading_comments, trailing_comments, Comments}; +use crate::comments::{ + dangling_node_comments, leading_node_comments, trailing_node_comments, Comments, +}; use crate::context::PyFormatContext; pub(crate) mod builders; @@ -64,7 +69,7 @@ where /// You may want to override this method if you want to manually handle the formatting of comments /// inside of the `fmt_fields` method or customize the formatting of the leading comments. fn fmt_leading_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - leading_comments(node).fmt(f) + leading_node_comments(node).fmt(f) } /// Formats the [dangling comments](comments#dangling-comments) of the node. @@ -75,7 +80,7 @@ where /// /// A node can have dangling comments if all its children are tokens or if all node childrens are optional. fn fmt_dangling_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - dangling_comments(node).fmt(f) + dangling_node_comments(node).fmt(f) } /// Formats the [trailing comments](comments#trailing-comments) of the node. @@ -83,7 +88,7 @@ where /// You may want to override this method if you want to manually handle the formatting of comments /// inside of the `fmt_fields` method or customize the formatting of the trailing comments. fn fmt_trailing_comments(&self, node: &N, f: &mut PyFormatter) -> FormatResult<()> { - trailing_comments(node).fmt(f) + trailing_node_comments(node).fmt(f) } } @@ -143,14 +148,29 @@ pub(crate) const fn verbatim_text(range: TextRange) -> VerbatimText { VerbatimText(range) } -impl Format for VerbatimText { - fn fmt(&self, f: &mut Formatter) -> FormatResult<()> { +impl Format> for VerbatimText { + fn fmt(&self, f: &mut PyFormatter) -> FormatResult<()> { f.write_element(FormatElement::Tag(Tag::StartVerbatim( VerbatimKind::Verbatim { length: self.0.len(), }, )))?; - write!(f, [source_text_slice(self.0, ContainsNewlines::Detect)])?; + + match normalize_newlines(f.context().locator().slice(self.0), ['\r']) { + Cow::Borrowed(_) => { + write!(f, [source_text_slice(self.0, ContainsNewlines::Detect)])?; + } + Cow::Owned(cleaned) => { + write!( + f, + [ + dynamic_text(&cleaned, Some(self.0.start())), + source_position(self.0.end()) + ] + )?; + } + } + f.write_element(FormatElement::Tag(Tag::EndVerbatim))?; Ok(()) } @@ -285,14 +305,58 @@ Formatted twice: Ok(()) } + #[fixture(pattern = "resources/test/fixtures/ruff/**/*.py")] + #[test] + fn ruff_test(input_path: &Path) -> Result<()> { + let content = fs::read_to_string(input_path)?; + + let printed = format_module(&content)?; + let formatted_code = printed.as_code(); + + let reformatted = + format_module(formatted_code).expect("Expected formatted code to be valid syntax"); + + if reformatted.as_code() != formatted_code { + let diff = TextDiff::from_lines(formatted_code, reformatted.as_code()) + .unified_diff() + .header("Formatted once", "Formatted twice") + .to_string(); + panic!( + r#"Reformatting the formatted code a second time resulted in formatting changes. +{diff} + +Formatted once: +{formatted_code} + +Formatted twice: +{}"#, + reformatted.as_code() + ); + } + + let snapshot = format!( + r#"## Input +{} + +## Output +{}"#, + CodeFrame::new("py", &content), + CodeFrame::new("py", formatted_code) + ); + assert_snapshot!(snapshot); + + Ok(()) + } + /// Use this test to debug the formatting of some snipped #[ignore] #[test] fn quick_test() { let src = r#" -{ - k: v for k, v in a_very_long_variable_name_that_exceeds_the_line_length_by_far_keep_going -} +while True: + if something.changed: + do.stuff() # trailing comment +other "#; // Tokenize once let mut tokens = Vec::new(); @@ -320,10 +384,10 @@ Formatted twice: assert_eq!( printed.as_code(), - r#"{ - k: v - for k, v in a_very_long_variable_name_that_exceeds_the_line_length_by_far_keep_going -}"# + r#"while True: + if something.changed: + do.stuff() # trailing comment +"# ); } diff --git a/crates/ruff_python_formatter/src/main.rs b/crates/ruff_python_formatter/src/main.rs index 19178aa6ac..63b789888c 100644 --- a/crates/ruff_python_formatter/src/main.rs +++ b/crates/ruff_python_formatter/src/main.rs @@ -1,15 +1,53 @@ -use std::fs; +use std::io::{stdout, Read, Write}; +use std::{fs, io}; -use anyhow::Result; +use anyhow::{bail, Context, Result}; use clap::Parser as ClapParser; -use ruff_python_formatter::cli::Cli; -use ruff_python_formatter::format_module; +use ruff_python_formatter::cli::{format_and_debug_print, Cli, Emit}; + +/// Read a `String` from `stdin`. +pub(crate) fn read_from_stdin() -> Result { + let mut buffer = String::new(); + io::stdin().lock().read_to_string(&mut buffer)?; + Ok(buffer) +} #[allow(clippy::print_stdout)] fn main() -> Result<()> { - let cli = Cli::parse(); - let contents = fs::read_to_string(cli.file)?; - println!("{}", format_module(&contents)?.as_code()); + let cli: Cli = Cli::parse(); + + if cli.files.is_empty() { + if !matches!(cli.emit, None | Some(Emit::Stdout)) { + bail!( + "Can only write to stdout when formatting from stdin, but you asked for {:?}", + cli.emit + ); + } + let input = read_from_stdin()?; + let formatted = format_and_debug_print(&input, &cli)?; + if cli.check { + if formatted == input { + return Ok(()); + } + bail!("Content not correctly formatted") + } + stdout().lock().write_all(formatted.as_bytes())?; + } else { + for file in &cli.files { + let input = fs::read_to_string(file) + .with_context(|| format!("Could not read {}: ", file.display()))?; + let formatted = format_and_debug_print(&input, &cli)?; + match cli.emit { + Some(Emit::Stdout) => stdout().lock().write_all(formatted.as_bytes())?, + None | Some(Emit::Files) => { + fs::write(file, formatted.as_bytes()).with_context(|| { + format!("Could not write to {}, exiting", file.display()) + })?; + } + } + } + } + Ok(()) } diff --git a/crates/ruff_python_formatter/src/module/mod_module.rs b/crates/ruff_python_formatter/src/module/mod_module.rs index a2d0370b5c..0d0fee7c00 100644 --- a/crates/ruff_python_formatter/src/module/mod_module.rs +++ b/crates/ruff_python_formatter/src/module/mod_module.rs @@ -1,8 +1,7 @@ -use crate::AsFormat; -use crate::{FormatNodeRule, PyFormatter}; +use crate::statement::suite::SuiteLevel; +use crate::{AsFormat, FormatNodeRule, PyFormatter}; use ruff_formatter::prelude::hard_line_break; use ruff_formatter::{write, Buffer, FormatResult}; - use rustpython_parser::ast::ModModule; #[derive(Default)] @@ -10,9 +9,20 @@ pub struct FormatModModule; impl FormatNodeRule for FormatModModule { fn fmt_fields(&self, item: &ModModule, f: &mut PyFormatter) -> FormatResult<()> { - for stmt in &item.body { - write!(f, [stmt.format(), hard_line_break()])?; - } - Ok(()) + let ModModule { + range: _, + body, + type_ignores, + } = item; + // https://docs.python.org/3/library/ast.html#ast-helpers + debug_assert!(type_ignores.is_empty()); + write!( + f, + [ + body.format().with_options(SuiteLevel::TopLevel), + // Trailing newline at the end of the file + hard_line_break() + ] + ) } } diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap index 2849790d1f..0128d6ed45 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__attribute_access_on_number_literals_py.snap @@ -35,7 +35,7 @@ y = 100(no) ```diff --- Black +++ Ruff -@@ -1,22 +1,20 @@ +@@ -1,21 +1,21 @@ -x = (123456789).bit_count() +x = 123456789 .bit_count() x = (123456).__abs__() @@ -53,8 +53,6 @@ y = 100(no) -x = 0o777.real -x = (0.000000006).hex() -x = -100.0000j -- --if (10).real: +x = .1.is_integer() +x = 1. .imag +x = 1E+1.imag @@ -69,11 +67,12 @@ y = 100(no) +x = 0O777 .real +x = 0.000000006 .hex() +x = -100.0000J + +-if (10).real: +if 10 .real: ... -- + y = 100[no] - y = 100(no) ``` ## Ruff Output @@ -95,8 +94,10 @@ x = 0B1011 .conjugate() x = 0O777 .real x = 0.000000006 .hex() x = -100.0000J + if 10 .real: ... + y = 100[no] y = 100(no) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap index 5e1f3eff02..c40c09ee3a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_blank_parentheses_py.snap @@ -36,12 +36,12 @@ class NormalClass ( ```diff --- Black +++ Ruff -@@ -1,30 +1,21 @@ +@@ -1,16 +1,16 @@ -class SimpleClassWithBlankParentheses: +class SimpleClassWithBlankParentheses(): pass -- -- + + -class ClassWithSpaceParentheses: +class ClassWithSpaceParentheses ( ): first_test_data = 90 @@ -49,24 +49,22 @@ class NormalClass ( - def test_func(self): return None -- -- + + class ClassWithEmptyFunc(object): + def func_with_blank_parentheses(): return 5 -- -- - def public_func_with_blank_parentheses(): - return None -- -- + +@@ -20,11 +20,12 @@ + + def class_under_the_func_with_blank_parentheses(): - class InsideFunc: + class InsideFunc(): pass -- -- + + -class NormalClass: +class NormalClass ( +): @@ -80,20 +78,30 @@ class NormalClass ( ```py class SimpleClassWithBlankParentheses(): pass + + class ClassWithSpaceParentheses ( ): first_test_data = 90 second_test_data = 100 def test_func(self): return None + + class ClassWithEmptyFunc(object): def func_with_blank_parentheses(): return 5 + + def public_func_with_blank_parentheses(): return None + + def class_under_the_func_with_blank_parentheses(): class InsideFunc(): pass + + class NormalClass ( ): def func_for_testing(self, first, second): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap index 5cdc7dd0c2..c569564e5c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__class_methods_new_line_py.snap @@ -113,38 +113,22 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: ```diff --- Black +++ Ruff -@@ -1,165 +1,100 @@ - class ClassSimplest: - pass -- -- - class ClassWithSingleField: - a = 1 -- -- - class ClassWithJustTheDocstring: - """Just a docstring.""" -- -- - class ClassWithInit: - def __init__(self): - pass -- -- +@@ -17,23 +17,19 @@ + class ClassWithTheDocstringAndInit: """Just a docstring.""" - def __init__(self): pass -- -- + + class ClassWithInitAndVars: cls_var = 100 - def __init__(self): pass -- -- + + class ClassWithInitAndVarsAndDocstring: """Test class""" - @@ -152,22 +136,17 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: - def __init__(self): pass -- -- - class ClassWithDecoInit: - @deco - def __init__(self): - pass -- -- + +@@ -46,7 +42,6 @@ + class ClassWithDecoInitAndVars: cls_var = 100 - @deco def __init__(self): pass -- -- +@@ -54,9 +49,7 @@ + class ClassWithDecoInitAndVarsAndDocstring: """Test class""" - @@ -176,43 +155,37 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- - class ClassSimplestWithInner: - class Inner: - pass -- -- +@@ -70,21 +63,18 @@ class ClassSimplestWithInnerWithDocstring: class Inner: """Just a docstring.""" - def __init__(self): pass -- -- + + class ClassWithSingleFieldWithInner: a = 1 - class Inner: pass -- -- + + class ClassWithJustTheDocstringWithInner: """Just a docstring.""" - class Inner: pass -- -- + +@@ -92,29 +82,23 @@ class ClassWithInitWithInner: class Inner: pass - def __init__(self): pass -- -- + + class ClassWithInitAndVarsWithInner: cls_var = 100 - @@ -221,8 +194,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: - def __init__(self): pass -- -- + + class ClassWithInitAndVarsAndDocstringWithInner: """Test class""" - @@ -233,8 +206,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: - def __init__(self): pass -- -- + +@@ -122,7 +106,6 @@ class ClassWithDecoInitWithInner: class Inner: pass @@ -242,8 +215,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- +@@ -130,10 +113,8 @@ + class ClassWithDecoInitAndVarsWithInner: cls_var = 100 - @@ -253,8 +226,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- +@@ -141,12 +122,9 @@ + class ClassWithDecoInitAndVarsAndDocstringWithInner: """Test class""" - @@ -266,8 +239,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: @deco def __init__(self): pass -- -- +@@ -154,12 +132,9 @@ + class ClassWithDecoInitAndVarsAndDocstringWithInner2: """Test class""" - @@ -286,68 +259,100 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner2: ```py class ClassSimplest: pass + + class ClassWithSingleField: a = 1 + + class ClassWithJustTheDocstring: """Just a docstring.""" + + class ClassWithInit: def __init__(self): pass + + class ClassWithTheDocstringAndInit: """Just a docstring.""" def __init__(self): pass + + class ClassWithInitAndVars: cls_var = 100 def __init__(self): pass + + class ClassWithInitAndVarsAndDocstring: """Test class""" cls_var = 100 def __init__(self): pass + + class ClassWithDecoInit: @deco def __init__(self): pass + + class ClassWithDecoInitAndVars: cls_var = 100 @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsAndDocstring: """Test class""" cls_var = 100 @deco def __init__(self): pass + + class ClassSimplestWithInner: class Inner: pass + + class ClassSimplestWithInnerWithDocstring: class Inner: """Just a docstring.""" def __init__(self): pass + + class ClassWithSingleFieldWithInner: a = 1 class Inner: pass + + class ClassWithJustTheDocstringWithInner: """Just a docstring.""" class Inner: pass + + class ClassWithInitWithInner: class Inner: pass def __init__(self): pass + + class ClassWithInitAndVarsWithInner: cls_var = 100 class Inner: pass def __init__(self): pass + + class ClassWithInitAndVarsAndDocstringWithInner: """Test class""" cls_var = 100 @@ -355,12 +360,16 @@ class ClassWithInitAndVarsAndDocstringWithInner: pass def __init__(self): pass + + class ClassWithDecoInitWithInner: class Inner: pass @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsWithInner: cls_var = 100 class Inner: @@ -368,6 +377,8 @@ class ClassWithDecoInitAndVarsWithInner: @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsAndDocstringWithInner: """Test class""" cls_var = 100 @@ -376,6 +387,8 @@ class ClassWithDecoInitAndVarsAndDocstringWithInner: @deco def __init__(self): pass + + class ClassWithDecoInitAndVarsAndDocstringWithInner2: """Test class""" class Inner: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap index 5daf501dc3..50f5ab1f05 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__collections_py.snap @@ -84,9 +84,9 @@ if True: ```diff --- Black +++ Ruff -@@ -1,99 +1,61 @@ +@@ -1,75 +1,49 @@ import core, time, a -- + from . import A, B, C - # keeps existing trailing comma @@ -104,7 +104,7 @@ if True: from foo import ( xyzzy as magic, ) -- + -a = { - 1, - 2, @@ -162,24 +162,22 @@ if True: pass for (x,) in (1,), (2,), (3,): pass -- + -[ - 1, - 2, - 3, -] -- --division_result_tuple = (6 / 2,) +[1, 2, 3,] + +-division_result_tuple = (6 / 2,) +division_result_tuple = (6/2,) print("foo %r", (foo.bar,)) -- + if True: - IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( - Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING - | {pylons.controllers.WSGIController} +@@ -79,21 +53,15 @@ ) -- + if True: - ec2client.get_waiter("instance_stopped").wait( + ec2client.get_waiter('instance_stopped').wait( @@ -210,6 +208,7 @@ if True: ```py import core, time, a + from . import A, B, C # keeps existing trailing comma from foo import ( @@ -224,6 +223,7 @@ from foo import ( from foo import ( xyzzy as magic, ) + a = {1,2,3,} b = { 1,2, @@ -249,14 +249,18 @@ for x in (1,): pass for (x,) in (1,), (2,), (3,): pass + [1, 2, 3,] + division_result_tuple = (6/2,) print("foo %r", (foo.bar,)) + if True: IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING = ( Config.IGNORED_TYPES_FOR_ATTRIBUTE_CHECKING | {pylons.controllers.WSGIController} ) + if True: ec2client.get_waiter('instance_stopped').wait( InstanceIds=[instance.id], diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap index 86d56db433..5633eef26a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comment_after_escaped_newline_py.snap @@ -22,13 +22,13 @@ def bobtwo(): \ ```diff --- Black +++ Ruff -@@ -1,6 +1,7 @@ +@@ -1,6 +1,9 @@ -def bob(): # pylint: disable=W9016 +def bob(): \ + # pylint: disable=W9016 pass -- -- + + -def bobtwo(): # some comment here +def bobtwo(): \ + \ @@ -42,6 +42,8 @@ def bobtwo(): \ def bob(): \ # pylint: disable=W9016 pass + + def bobtwo(): \ \ # some comment here diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap index 808c55756f..cd228ca42b 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments2_py.snap @@ -178,7 +178,7 @@ instruction()#comment with bad spacing ```diff --- Black +++ Ruff -@@ -1,39 +1,38 @@ +@@ -1,39 +1,40 @@ from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY + MyLovelyCompanyTeamProjectComponent # NOT DRY @@ -187,7 +187,7 @@ instruction()#comment with bad spacing - MyLovelyCompanyTeamProjectComponent as component, # DRY + MyLovelyCompanyTeamProjectComponent as component # DRY ) -- + # Please keep __all__ alphabetized within each category. __all__ = [ @@ -227,7 +227,7 @@ instruction()#comment with bad spacing + 'NamedTuple', # Not really a type. + 'Generator', ] -- + not_shareables = [ # singletons True, @@ -238,11 +238,10 @@ instruction()#comment with bad spacing # builtin types and objects type, object, -@@ -47,21 +46,21 @@ - Cheese("Wensleydale"), +@@ -48,20 +49,23 @@ SubBytes(b"spam"), ] -- + -if "PYTHON" in os.environ: +if 'PYTHON' in os.environ: add_compiler(compiler_from_env()) @@ -252,8 +251,8 @@ instruction()#comment with bad spacing + # add_compiler(compiler) add_compiler(compilers[(7.0, 32)]) - # add_compiler(compilers[(7.1, 64)]) -- -- + + # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: @@ -267,7 +266,7 @@ instruction()#comment with bad spacing children[0], body, children[-1], # type: ignore -@@ -73,49 +72,42 @@ +@@ -73,49 +77,42 @@ parameters.children[-1], # )2 ] parameters.children = [parameters.what_if_this_was_actually_long.children[0], body, parameters.children[-1]] # type: ignore @@ -340,7 +339,7 @@ instruction()#comment with bad spacing ] lcomp2 = [ # hello -@@ -127,7 +119,7 @@ +@@ -127,7 +124,7 @@ ] lcomp3 = [ # This one is actually too long to fit in a single line. @@ -349,7 +348,7 @@ instruction()#comment with bad spacing # yup for element in collection.select_elements() # right -@@ -140,28 +132,20 @@ +@@ -140,25 +137,23 @@ # and round and round we go # and round and round we go @@ -363,8 +362,8 @@ instruction()#comment with bad spacing + Leaf(token.NEWLINE, '\n') # FIXME: \r\n? + ], ) -- -- + + -CONFIG_FILES = ( - [ - CONFIG_FILE, @@ -372,20 +371,17 @@ instruction()#comment with bad spacing - + SHARED_CONFIG_FILES - + USER_CONFIG_FILES -) # type: Final -- -- +CONFIG_FILES = [CONFIG_FILE, ] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final + + class Test: def _init_host(self, parsed) -> None: - if parsed.hostname is None or not parsed.hostname.strip(): # type: ignore + if (parsed.hostname is None or # type: ignore + not parsed.hostname.strip()): pass -- -- - ####################### - ### SECTION COMMENT ### - ####################### + + ``` ## Ruff Output @@ -397,6 +393,7 @@ from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( MyLovelyCompanyTeamProjectComponent as component # DRY ) + # Please keep __all__ alphabetized within each category. __all__ = [ @@ -421,6 +418,7 @@ __all__ = [ 'NamedTuple', # Not really a type. 'Generator', ] + not_shareables = [ # singletons True, @@ -439,12 +437,15 @@ not_shareables = [ Cheese("Wensleydale"), SubBytes(b"spam"), ] + if 'PYTHON' in os.environ: add_compiler(compiler_from_env()) else: # for compiler in compilers.values(): # add_compiler(compiler) add_compiler(compilers[(7.0, 32)]) + + # Comment before function. def inline_comments_in_brackets_ruin_everything(): if typedargslist: @@ -533,12 +534,18 @@ short Leaf(token.NEWLINE, '\n') # FIXME: \r\n? ], ) + + CONFIG_FILES = [CONFIG_FILE, ] + SHARED_CONFIG_FILES + USER_CONFIG_FILES # type: Final + + class Test: def _init_host(self, parsed) -> None: if (parsed.hostname is None or # type: ignore not parsed.hostname.strip()): pass + + ####################### ### SECTION COMMENT ### ####################### diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap deleted file mode 100644 index 77be147ea1..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments4_py.snap +++ /dev/null @@ -1,338 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/comments4.py ---- -## Input - -```py -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY -) - - -class C: - @pytest.mark.parametrize( - ("post_data", "message"), - [ - # metadata_version errors. - ( - {}, - "None is an invalid value for Metadata-Version. Error: This field is" - " required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "-1"}, - "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" - " Version see" - " https://packaging.python.org/specifications/core-metadata", - ), - # name errors. - ( - {"metadata_version": "1.2"}, - "'' is an invalid value for Name. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "foo-"}, - "'foo-' is an invalid value for Name. Error: Must start and end with a" - " letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - # version errors. - ( - {"metadata_version": "1.2", "name": "example"}, - "'' is an invalid value for Version. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "example", "version": "dog"}, - "'dog' is an invalid value for Version. Error: Must start and end with" - " a letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - ], - ) - def test_fails_invalid_post_data( - self, pyramid_config, db_request, post_data, message - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) - - -def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() - ) - return results - - -def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) - - -def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -4,8 +4,6 @@ - from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY - ) -- -- - class C: - @pytest.mark.parametrize( - ("post_data", "message"), -@@ -54,8 +52,6 @@ - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) -- -- - def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") -@@ -70,8 +66,6 @@ - .all() - ) - return results -- -- - def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( -@@ -81,8 +75,6 @@ - ) - .filter(User.xyz.is_(None)) - ) -- -- - def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. -``` - -## Ruff Output - -```py -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY -) -class C: - @pytest.mark.parametrize( - ("post_data", "message"), - [ - # metadata_version errors. - ( - {}, - "None is an invalid value for Metadata-Version. Error: This field is" - " required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "-1"}, - "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" - " Version see" - " https://packaging.python.org/specifications/core-metadata", - ), - # name errors. - ( - {"metadata_version": "1.2"}, - "'' is an invalid value for Name. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "foo-"}, - "'foo-' is an invalid value for Name. Error: Must start and end with a" - " letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - # version errors. - ( - {"metadata_version": "1.2", "name": "example"}, - "'' is an invalid value for Version. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "example", "version": "dog"}, - "'dog' is an invalid value for Version. Error: Must start and end with" - " a letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - ], - ) - def test_fails_invalid_post_data( - self, pyramid_config, db_request, post_data, message - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) -def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() - ) - return results -def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -``` - -## Black Output - -```py -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent, # NOT DRY -) -from com.my_lovely_company.my_lovely_team.my_lovely_project.my_lovely_component import ( - MyLovelyCompanyTeamProjectComponent as component, # DRY -) - - -class C: - @pytest.mark.parametrize( - ("post_data", "message"), - [ - # metadata_version errors. - ( - {}, - "None is an invalid value for Metadata-Version. Error: This field is" - " required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "-1"}, - "'-1' is an invalid value for Metadata-Version. Error: Unknown Metadata" - " Version see" - " https://packaging.python.org/specifications/core-metadata", - ), - # name errors. - ( - {"metadata_version": "1.2"}, - "'' is an invalid value for Name. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "foo-"}, - "'foo-' is an invalid value for Name. Error: Must start and end with a" - " letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - # version errors. - ( - {"metadata_version": "1.2", "name": "example"}, - "'' is an invalid value for Version. Error: This field is required. see" - " https://packaging.python.org/specifications/core-metadata", - ), - ( - {"metadata_version": "1.2", "name": "example", "version": "dog"}, - "'dog' is an invalid value for Version. Error: Must start and end with" - " a letter or numeral and contain only ascii numeric and '.', '_' and" - " '-'. see https://packaging.python.org/specifications/core-metadata", - ), - ], - ) - def test_fails_invalid_post_data( - self, pyramid_config, db_request, post_data, message - ): - pyramid_config.testing_securitypolicy(userid=1) - db_request.POST = MultiDict(post_data) - - -def foo(list_a, list_b): - results = ( - User.query.filter(User.foo == "bar") - .filter( # Because foo. - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - # Another comment about the filtering on is_quux goes here. - .filter(db.not_(User.is_pending.astext.cast(db.Boolean).is_(True))) - .order_by(User.created_at.desc()) - .with_for_update(key_share=True) - .all() - ) - return results - - -def foo2(list_a, list_b): - # Standalone comment reasonably placed. - return ( - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) - - -def foo3(list_a, list_b): - return ( - # Standalone comment but weirdly placed. - User.query.filter(User.foo == "bar") - .filter( - db.or_(User.field_a.astext.in_(list_a), User.field_b.astext.in_(list_b)) - ) - .filter(User.xyz.is_(None)) - ) -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap index df1348d88f..f310ae68e4 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments5_py.snap @@ -86,74 +86,31 @@ if __name__ == "__main__": ```diff --- Black +++ Ruff -@@ -1,11 +1,6 @@ +@@ -1,7 +1,6 @@ while True: if something.changed: - do.stuff() # trailing comment +- do.stuff() # trailing comment - # Comment belongs to the `if` block. -- # This one belongs to the `while` block. -- -- # Should this one, too? I guess so. -- - # This one is properly standalone now. ++ do.stuff() + # This one belongs to the `while` block. - for i in range(100): -@@ -15,27 +10,18 @@ + # Should this one, too? I guess so. +@@ -15,7 +14,6 @@ # then we do this print(i) - # and finally we loop around -- + with open(some_temp_file) as f: data = f.read() -- - try: - with open(some_other_file) as w: - w.write(data) - - except OSError: - print("problems") -- - import sys -- -- +@@ -33,7 +31,6 @@ # leading function comment def wat(): ... - # trailing function comment -- -- + + # SECTION COMMENT - - -@@ -47,8 +33,6 @@ - @deco3 - def decorated1(): - ... -- -- - # leading 1 - @deco1 - # leading 2 -@@ -56,18 +40,12 @@ - # leading function comment - def decorated1(): - ... -- -- - # Note: this is fixed in - # Preview.empty_lines_before_class_or_def_with_leading_comments. - # In the current style, the user will have to split those lines by hand. - some_instruction -- -- - # This comment should be split from `some_instruction` by two lines but isn't. - def g(): - ... -- -- - if __name__ == "__main__": - main() ``` ## Ruff Output @@ -161,7 +118,11 @@ if __name__ == "__main__": ```py while True: if something.changed: - do.stuff() # trailing comment + do.stuff() + # This one belongs to the `while` block. + + # Should this one, too? I guess so. + # This one is properly standalone now. for i in range(100): @@ -171,18 +132,25 @@ for i in range(100): # then we do this print(i) + with open(some_temp_file) as f: data = f.read() + try: with open(some_other_file) as w: w.write(data) except OSError: print("problems") + import sys + + # leading function comment def wat(): ... + + # SECTION COMMENT @@ -194,6 +162,8 @@ def wat(): @deco3 def decorated1(): ... + + # leading 1 @deco1 # leading 2 @@ -201,13 +171,19 @@ def decorated1(): # leading function comment def decorated1(): ... + + # Note: this is fixed in # Preview.empty_lines_before_class_or_def_with_leading_comments. # In the current style, the user will have to split those lines by hand. some_instruction + + # This comment should be split from `some_instruction` by two lines but isn't. def g(): ... + + if __name__ == "__main__": main() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap index 24247fc646..d0727290f2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments6_py.snap @@ -131,98 +131,35 @@ aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*ite ```diff --- Black +++ Ruff -@@ -1,18 +1,12 @@ - from typing import Any, Tuple -- -- - def f( - a, # type: int - ): - pass -- -- - # test type comments - def f(a, b, c, d, e, f, g, h, i): - # type: (int, int, int, int, int, int, int, int, int) -> None - pass -- -- - def f( - a, # type: int - b, # type: int -@@ -26,8 +20,6 @@ - ): - # type: (...) -> None - pass -- -- - def f( - arg, # type: int - *args, # type: *Any -@@ -36,8 +28,6 @@ - ): - # type: (...) -> None - pass -- -- - def f( - a, # type: int - b, # type: int -@@ -67,22 +57,16 @@ +@@ -66,7 +66,7 @@ + + element + another_element + another_element_with_long_name - ) # type: int -- -- +- ) # type: int ++ ) + + def f( - x, # not a type comment - y, # type: int - ): - # type: (...) -> None - pass -- -- - def f( - x, # not a type comment - ): # type: (int) -> None - pass -- -- - def func( - a=some_list[0], # type: int - ): # type: () -> int -@@ -102,17 +86,12 @@ - c = call( - "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore - ) -- -- - result = ( # aaa - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ) -- - AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore -- - call_to_some_function_asdf( - foo, - [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore - ) -- - aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` ## Ruff Output ```py from typing import Any, Tuple + + def f( a, # type: int ): pass + + # test type comments def f(a, b, c, d, e, f, g, h, i): # type: (int, int, int, int, int, int, int, int, int) -> None pass + + def f( a, # type: int b, # type: int @@ -236,6 +173,8 @@ def f( ): # type: (...) -> None pass + + def f( arg, # type: int *args, # type: *Any @@ -244,6 +183,8 @@ def f( ): # type: (...) -> None pass + + def f( a, # type: int b, # type: int @@ -272,17 +213,23 @@ def f( + element + another_element + another_element_with_long_name - ) # type: int + ) + + def f( x, # not a type comment y, # type: int ): # type: (...) -> None pass + + def f( x, # not a type comment ): # type: (int) -> None pass + + def func( a=some_list[0], # type: int ): # type: () -> int @@ -302,14 +249,19 @@ def func( c = call( "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa", "aaaaaaaa" # type: ignore ) + + result = ( # aaa "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ) + AAAAAAAAAAAAA = [AAAAAAAAAAAAA] + SHARED_AAAAAAAAAAAAA + USER_AAAAAAAAAAAAA + AAAAAAAAAAAAA # type: ignore + call_to_some_function_asdf( foo, [AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, AAAAAAAAAAAAAAAAAAAAAAA, BBBBBBBBBBBB], # type: ignore ) + aaaaaaaaaaaaa, bbbbbbbbb = map(list, map(itertools.chain.from_iterable, zip(*items))) # type: ignore[arg-type] ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap index adc7467642..97ea9a42fb 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments9_py.snap @@ -152,99 +152,19 @@ def bar(): ```diff --- Black +++ Ruff -@@ -1,60 +1,35 @@ - # Test for https://github.com/psf/black/issues/246. +@@ -35,9 +35,10 @@ + some = statement -- -- - # This comment should be split from the statement above by two lines. - def function(): - pass -- -- - some = statement -- -- - # This multiline comments section - # should be split from the statement - # above by two lines. - def function(): - pass -- -- - some = statement -- -- - # This comment should be split from the statement above by two lines. - async def async_function(): - pass -- -- - some = statement -- -- - # This comment should be split from the statement above by two lines. - class MyClass: - pass -- -- - some = statement - # This should be stick to the statement above +-# This should be stick to the statement above -- + ++# This should be stick to the statement above ++ # This should be split from the above by two lines class MyClassWithComplexLeadingComments: pass -- -- - class ClassWithDocstring: - """A docstring.""" -- -- - # Leading comment after a class with just a docstring - class MyClassAfterAnotherClassWithDocstring: - pass -- -- - some = statement -- -- - # leading 1 - @deco1 - # leading 2 -@@ -65,11 +40,7 @@ - # leading 4 - def decorated(): - pass -- -- - some = statement -- -- - # leading 1 - @deco1 - # leading 2 -@@ -80,11 +51,7 @@ - # leading 4 - def decorated_with_split_leading_comments(): - pass -- -- - some = statement -- -- - # leading 1 - @deco1 - # leading 2 -@@ -95,66 +62,44 @@ - # leading 4 that already has an empty line - def decorated_with_split_leading_comments(): - pass -- -- - def main(): - if a: +@@ -102,11 +103,9 @@ # Leading comment before inline function def inline(): pass @@ -256,10 +176,7 @@ def bar(): else: # More leading comments def inline_after_else(): - pass -- -- - if a: +@@ -117,11 +116,9 @@ # Leading comment before "top-level inline" function def top_level_quote_inline(): pass @@ -271,39 +188,24 @@ def bar(): else: # More leading comments def top_level_quote_inline_after_else(): - pass -- -- - class MyClass: - # First method has no empty lines between bare class def. - # More comments. - def first_method(self): - pass -- -- +@@ -138,7 +135,6 @@ # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass - # Trailing comment that belongs to this function -- -- + + @decorator1 - @decorator2 # fmt: skip - def bar(): - pass -- -- +@@ -150,9 +146,6 @@ # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass - # Trailing comment that belongs to this function. - # NOTE this comment only has one empty line below, and the formatter - # should enforce two blank lines. -- -- + + @decorator1 - # A standalone comment - def bar(): ``` ## Ruff Output @@ -312,35 +214,61 @@ def bar(): # Test for https://github.com/psf/black/issues/246. some = statement + + # This comment should be split from the statement above by two lines. def function(): pass + + some = statement + + # This multiline comments section # should be split from the statement # above by two lines. def function(): pass + + some = statement + + # This comment should be split from the statement above by two lines. async def async_function(): pass + + some = statement + + # This comment should be split from the statement above by two lines. class MyClass: pass + + some = statement + + # This should be stick to the statement above # This should be split from the above by two lines class MyClassWithComplexLeadingComments: pass + + class ClassWithDocstring: """A docstring.""" + + # Leading comment after a class with just a docstring class MyClassAfterAnotherClassWithDocstring: pass + + some = statement + + # leading 1 @deco1 # leading 2 @@ -351,7 +279,11 @@ some = statement # leading 4 def decorated(): pass + + some = statement + + # leading 1 @deco1 # leading 2 @@ -362,7 +294,11 @@ some = statement # leading 4 def decorated_with_split_leading_comments(): pass + + some = statement + + # leading 1 @deco1 # leading 2 @@ -373,6 +309,8 @@ some = statement # leading 4 that already has an empty line def decorated_with_split_leading_comments(): pass + + def main(): if a: # Leading comment before inline function @@ -385,6 +323,8 @@ def main(): # More leading comments def inline_after_else(): pass + + if a: # Leading comment before "top-level inline" function def top_level_quote_inline(): @@ -396,21 +336,31 @@ else: # More leading comments def top_level_quote_inline_after_else(): pass + + class MyClass: # First method has no empty lines between bare class def. # More comments. def first_method(self): pass + + # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass + + @decorator1 @decorator2 # fmt: skip def bar(): pass + + # Regression test for https://github.com/psf/black/issues/3454. def foo(): pass + + @decorator1 # A standalone comment def bar(): diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap index 306cbb3e56..3709a25f65 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_non_breaking_space_py.snap @@ -32,7 +32,7 @@ def function(a:int=42): ```diff --- Black +++ Ruff -@@ -1,23 +1,16 @@ +@@ -1,23 +1,20 @@ -from .config import ( - ConfigTypeAttributes, - Int, @@ -41,16 +41,16 @@ def function(a:int=42): +from .config import ( ConfigTypeAttributes, Int, Path, # String, + # DEFAULT_TYPE_ATTRIBUTES, ) -- + result = 1 # A simple comment -result = (1,) # Another one -- +result = ( 1, ) # Another one + result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore square = Square(4) #  type: Optional[Square] -- -- + + -def function(a: int = 42): - """This docstring is already formatted - a @@ -72,11 +72,15 @@ def function(a:int=42): from .config import ( ConfigTypeAttributes, Int, Path, # String, # DEFAULT_TYPE_ATTRIBUTES, ) + result = 1 # A simple comment result = ( 1, ) # Another one + result = 1 #  type: ignore result = 1 # This comment is talking about type: ignore square = Square(4) #  type: Optional[Square] + + def function(a:int=42): """ This docstring is already formatted a diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap index 419cf8868b..12a415a057 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__comments_py.snap @@ -109,18 +109,7 @@ async def wat(): ```diff --- Black +++ Ruff -@@ -8,27 +8,17 @@ - - Possibly also many, many lines. - """ -- - import os.path - import sys -- - import a - from b.c import X # some noqa comment -- - try: +@@ -19,14 +19,9 @@ import fast except ImportError: import slow as fast @@ -132,36 +121,12 @@ async def wat(): - # some strings - y # type: ignore -) -- -- +# some strings +y # type: ignore + + def function(default=None): - """Docstring comes first. - -@@ -45,12 +35,8 @@ - - # This return is also commented for some reason. - return default -- -- - # Explains why we use global state. - GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} -- -- - # Another comment! - # This time two lines. - -@@ -73,8 +59,6 @@ - - self.spam = 4 - """Docstring for instance attribute spam.""" -- -- - #'

This is pweave!

- - -@@ -93,4 +77,4 @@ +@@ -93,4 +88,4 @@ # Some closing comments. # Maybe Vim or Emacs directives for formatting. @@ -183,10 +148,13 @@ async def wat(): Possibly also many, many lines. """ + import os.path import sys + import a from b.c import X # some noqa comment + try: import fast except ImportError: @@ -194,6 +162,8 @@ except ImportError: y = 1 # some strings y # type: ignore + + def function(default=None): """Docstring comes first. @@ -210,8 +180,12 @@ def function(default=None): # This return is also commented for some reason. return default + + # Explains why we use global state. GLOBAL_STATE = {"a": a(1), "b": a(2), "c": a(3)} + + # Another comment! # This time two lines. @@ -234,6 +208,8 @@ class Foo: self.spam = 4 """Docstring for instance attribute spam.""" + + #'

This is pweave!

diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap index 3a06521d11..b95e94cf32 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_preview_py.snap @@ -63,57 +63,23 @@ def single_quote_docstring_over_line_limit2(): ```diff --- Black +++ Ruff -@@ -1,48 +1,32 @@ +@@ -1,9 +1,11 @@ def docstring_almost_at_line_limit(): - """long docstring.................................................................""" -- -- + """long docstring................................................................. + """ + + def docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................""" -- -- + f"""long docstring................................................................ + """ + + def mulitline_docstring_almost_at_line_limit(): - """long docstring................................................................. +@@ -45,4 +47,4 @@ - .................................................................................. - """ -- -- - def mulitline_docstring_almost_at_line_limit_with_prefix(): - f"""long docstring................................................................ - .................................................................................. - """ -- -- - def docstring_at_line_limit(): - """long docstring................................................................""" -- -- - def docstring_at_line_limit_with_prefix(): - f"""long docstring...............................................................""" -- -- - def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" -- -- - def multiline_docstring_at_line_limit_with_prefix(): - f"""first line---------------------------------------------------------------------- - - second line----------------------------------------------------------------------""" -- -- - def single_quote_docstring_over_line_limit(): - "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." -- -- def single_quote_docstring_over_line_limit2(): - "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + 'We do not want to put the closing quote on a new line as that is invalid (see GH-3141).' @@ -125,33 +91,51 @@ def single_quote_docstring_over_line_limit2(): def docstring_almost_at_line_limit(): """long docstring................................................................. """ + + def docstring_almost_at_line_limit_with_prefix(): f"""long docstring................................................................ """ + + def mulitline_docstring_almost_at_line_limit(): """long docstring................................................................. .................................................................................. """ + + def mulitline_docstring_almost_at_line_limit_with_prefix(): f"""long docstring................................................................ .................................................................................. """ + + def docstring_at_line_limit(): """long docstring................................................................""" + + def docstring_at_line_limit_with_prefix(): f"""long docstring...............................................................""" + + def multiline_docstring_at_line_limit(): """first line----------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + def multiline_docstring_at_line_limit_with_prefix(): f"""first line---------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + def single_quote_docstring_over_line_limit(): "We do not want to put the closing quote on a new line as that is invalid (see GH-3141)." + + def single_quote_docstring_over_line_limit2(): 'We do not want to put the closing quote on a new line as that is invalid (see GH-3141).' ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap index 933f6529e7..58b5b5f29c 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__docstring_py.snap @@ -234,7 +234,7 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): ```diff --- Black +++ Ruff -@@ -1,219 +1,157 @@ +@@ -1,83 +1,85 @@ class MyClass: + """ Multiline + class docstring @@ -251,64 +251,64 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - method docstring - """ - pass -- -- + pass + + def foo(): - """This is a docstring with - some lines of text here - """ - return -- -- + """This is a docstring with + some lines of text here + """ + return + + def bar(): - """This is another docstring - with more lines of text - """ - return -- -- + '''This is another docstring + with more lines of text + ''' + return + + def baz(): - '''"This" is a string with some - embedded "quotes"''' - return -- -- + '''"This" is a string with some + embedded "quotes"''' + return + + def troz(): - """Indentation with tabs - is just as OK - """ - return -- -- + '''Indentation with tabs + is just as OK + ''' + return + + def zort(): - """Another - multiline - docstring - """ - pass -- -- + """Another + multiline + docstring + """ + pass + + def poit(): - """ - Lorem ipsum dolor sit amet. @@ -323,8 +323,6 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - - aliquip ex ea commodo consequat - """ - pass -- -- + Consectetur adipiscing elit: + - sed do eiusmod tempor incididunt ut labore + - dolore magna aliqua @@ -333,16 +331,26 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): + - aliquip ex ea commodo consequat + """ + pass + + def under_indent(): - """ - These lines are indented in a way that does not - make sense. +- """ +- pass + """ + These lines are indented in a way that does not +make sense. + """ + pass -+def over_indent(): + + + def over_indent(): +- """ +- This has a shallow indent +- - But some lines are deeper +- - And the closing quote is too deep + """ + This has a shallow indent + - But some lines are deeper @@ -350,90 +358,78 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): """ - pass + pass -+def single_line(): -+ """But with a newline after it! -- --def over_indent(): -- """ -- This has a shallow indent -- - But some lines are deeper -- - And the closing quote is too deep - """ - pass -- -- --def single_line(): + + def single_line(): - """But with a newline after it!""" -- pass -- -- - def this(): - r""" - 'hey ho' - """ -- -- ++ """But with a newline after it! ++ ++ """ + pass + + +@@ -88,25 +90,30 @@ + + def that(): - """ "hey yah" """ -- -- + """ "hey yah" """ + + def and_that(): - """ - "hey yah" """ -- -- + """ + "hey yah" """ + + def and_this(): +- ''' +- "hey yah"''' + ''' + "hey yah"''' -+def multiline_whitespace(): + + + def multiline_whitespace(): +- """ """ + ''' + + + + - ''' -- "hey yah"''' -- -- --def multiline_whitespace(): -- """ """ -- -- ++ ''' + + def oneline_whitespace(): - """ """ -- -- + ''' ''' + + def empty(): - """""" -- -- +@@ -114,12 +121,11 @@ + + def single_quotes(): - "testing" -- -- ++ 'testing' + + -def believe_it_or_not_this_is_in_the_py_stdlib(): - ''' - "hey yah"''' -- -- -+ 'testing' +def believe_it_or_not_this_is_in_the_py_stdlib(): ''' +"hey yah"''' + + def ignored_docstring(): - """a => \ - b""" -- -- +@@ -128,32 +134,32 @@ + + def single_line_docstring_with_whitespace(): - """This should be stripped""" -- -- + """ This should be stripped """ + + def docstring_with_inline_tabs_and_space_indentation(): """hey @@ -449,8 +445,8 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): + + line ends with some tabs """ -- -- + + def docstring_with_inline_tabs_and_tab_indentation(): - """hey + """hey @@ -463,8 +459,6 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): - line ends with some tabs - """ - pass -- -- + tab separated value + tab at start of line and then a tab separated value + multiple tabs at the beginning and inline @@ -473,69 +467,51 @@ def stable_quote_normalization_with_immediate_inner_single_quote(self): + line ends with some tabs + """ + pass + + def backslash_space(): - """\ """ -- -- +@@ -161,14 +167,14 @@ + + def multiline_backslash_1(): - """ + ''' hey\there\ - \ """ -- -- + \ ''' + + def multiline_backslash_2(): - """ - hey there \ """ -- -- + ''' + hey there \ ''' + + # Regression test for #3425 - def multiline_backslash_really_long_dont_crash(): - """ - hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ -- -- +@@ -178,8 +184,8 @@ + + def multiline_backslash_3(): - """ - already escaped \\""" -- -- + ''' + already escaped \\ ''' + + def my_god_its_full_of_stars_1(): - "I'm sorry Dave\u2001" -- -- +@@ -188,7 +194,7 @@ + # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): - "I'm sorry Dave" -- -- + "I'm sorry Dave " + + def docstring_almost_at_line_limit(): - """long docstring.................................................................""" -- -- - def docstring_almost_at_line_limit2(): - """long docstring................................................................. +@@ -213,7 +219,7 @@ - .................................................................................. - """ -- -- - def docstring_at_line_limit(): - """long docstring................................................................""" -- -- - def multiline_docstring_at_line_limit(): - """first line----------------------------------------------------------------------- - second line----------------------------------------------------------------------""" -- -- def stable_quote_normalization_with_immediate_inner_single_quote(self): - """' + '''' @@ -558,31 +534,43 @@ class MyClass: method docstring """ pass + + def foo(): """This is a docstring with some lines of text here """ return + + def bar(): '''This is another docstring with more lines of text ''' return + + def baz(): '''"This" is a string with some embedded "quotes"''' return + + def troz(): '''Indentation with tabs is just as OK ''' return + + def zort(): """Another multiline docstring """ pass + + def poit(): """ Lorem ipsum dolor sit amet. @@ -595,12 +583,16 @@ def poit(): - aliquip ex ea commodo consequat """ pass + + def under_indent(): """ These lines are indented in a way that does not make sense. """ pass + + def over_indent(): """ This has a shallow indent @@ -608,23 +600,35 @@ def over_indent(): - And the closing quote is too deep """ pass + + def single_line(): """But with a newline after it! """ pass + + def this(): r""" 'hey ho' """ + + def that(): """ "hey yah" """ + + def and_that(): """ "hey yah" """ + + def and_this(): ''' "hey yah"''' + + def multiline_whitespace(): ''' @@ -632,19 +636,33 @@ def multiline_whitespace(): ''' + + def oneline_whitespace(): ''' ''' + + def empty(): """""" + + def single_quotes(): 'testing' + + def believe_it_or_not_this_is_in_the_py_stdlib(): ''' "hey yah"''' + + def ignored_docstring(): """a => \ b""" + + def single_line_docstring_with_whitespace(): """ This should be stripped """ + + def docstring_with_inline_tabs_and_space_indentation(): """hey @@ -655,6 +673,8 @@ def docstring_with_inline_tabs_and_space_indentation(): line ends with some tabs """ + + def docstring_with_inline_tabs_and_tab_indentation(): """hey @@ -666,40 +686,64 @@ def docstring_with_inline_tabs_and_tab_indentation(): line ends with some tabs """ pass + + def backslash_space(): """\ """ + + def multiline_backslash_1(): ''' hey\there\ \ ''' + + def multiline_backslash_2(): ''' hey there \ ''' + + # Regression test for #3425 def multiline_backslash_really_long_dont_crash(): """ hey there hello guten tag hi hoow are you ola zdravstvuyte ciao como estas ca va \ """ + + def multiline_backslash_3(): ''' already escaped \\ ''' + + def my_god_its_full_of_stars_1(): "I'm sorry Dave\u2001" + + # the space below is actually a \u2001, removed in output def my_god_its_full_of_stars_2(): "I'm sorry Dave " + + def docstring_almost_at_line_limit(): """long docstring.................................................................""" + + def docstring_almost_at_line_limit2(): """long docstring................................................................. .................................................................................. """ + + def docstring_at_line_limit(): """long docstring................................................................""" + + def multiline_docstring_at_line_limit(): """first line----------------------------------------------------------------------- second line----------------------------------------------------------------------""" + + def stable_quote_normalization_with_immediate_inner_single_quote(self): '''' diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap index 741cc1757f..9782dc0171 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__empty_lines_py.snap @@ -105,10 +105,8 @@ def g(): ```diff --- Black +++ Ruff -@@ -1,11 +1,9 @@ - """Docstring.""" -- -- +@@ -3,9 +3,9 @@ + # leading comment def f(): - NO = "" @@ -120,7 +118,7 @@ def g(): t = leaf.type p = leaf.parent # trailing comment -@@ -16,14 +14,19 @@ +@@ -16,14 +16,19 @@ if t == token.COMMENT: # another trailing comment return DOUBLESPACE @@ -140,17 +138,12 @@ def g(): if prevp.type == token.EQUAL: if prevp.parent and prevp.parent.type in { syms.typedargslist, -@@ -43,17 +46,14 @@ - syms.dictsetmaker, - }: - return NO -- -- +@@ -48,12 +53,11 @@ ############################################################################### # SECTION BECAUSE SECTIONS ############################################################################### - - + def g(): - NO = "" - SPACE = " " @@ -161,7 +154,7 @@ def g(): t = leaf.type p = leaf.parent -@@ -67,7 +67,7 @@ +@@ -67,7 +71,7 @@ return DOUBLESPACE # Another comment because more comments @@ -176,6 +169,8 @@ def g(): ```py """Docstring.""" + + # leading comment def f(): NO = '' @@ -223,6 +218,8 @@ def f(): syms.dictsetmaker, }: return NO + + ############################################################################### # SECTION BECAUSE SECTIONS ############################################################################### diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap index cf47dd1cbe..e59d18b772 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__expression_py.snap @@ -511,26 +511,22 @@ last_call() Ø = set() authors.łukasz.say_thanks() mapping = { -@@ -233,138 +170,83 @@ - C: 0.1 * (10.0 / 12), - D: 0.1 * (10.0 / 12), - } -- -- +@@ -237,134 +174,86 @@ + def gen(): yield from outside_of_generator - a = yield - b = yield - c = yield -- -- + a = (yield) + b = ((yield)) + c = (((yield))) + + async def f(): await some.complicated[0].call(with_args=(True or (1 is not 1))) -- -- + + -print(*[] or [1]) +print(* [] or [1]) print(**{1: 3} if False else {x: x for x in range(3)}) @@ -550,15 +546,6 @@ last_call() - ... -for j in 1 + (2 + 3): - ... --while this and that: -- ... --for ( -- addr_family, -- addr_type, -- addr_proto, -- addr_canonname, -- addr_sockaddr, --) in socket.getaddrinfo("google.com", "http"): +print(* lambda x: x) +assert(not Test),("Short message") +assert this is ComplexTest and not requirements.fit_in_a_single_line(force=False), "Short message" @@ -568,7 +555,15 @@ last_call() +for z in (i for i in (1, 2, 3)): ... +for i in (call()): ... +for j in (1 + (2 + 3)): ... -+while(this and that): ... + while this and that: + ... +-for ( +- addr_family, +- addr_type, +- addr_proto, +- addr_canonname, +- addr_sockaddr, +-) in socket.getaddrinfo("google.com", "http"): +for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): pass -a = ( @@ -861,13 +856,19 @@ mapping = { C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } + + def gen(): yield from outside_of_generator a = (yield) b = ((yield)) c = (((yield))) + + async def f(): await some.complicated[0].call(with_args=(True or (1 is not 1))) + + print(* [] or [1]) print(**{1: 3} if False else {x: x for x in range(3)}) print(* lambda x: x) @@ -879,7 +880,8 @@ for y in (): ... for z in (i for i in (1, 2, 3)): ... for i in (call()): ... for j in (1 + (2 + 3)): ... -while(this and that): ... +while this and that: + ... for addr_family, addr_type, addr_proto, addr_canonname, addr_sockaddr in socket.getaddrinfo('google.com', 'http'): pass a = aaaa.bbbb.cccc.dddd.eeee.ffff.gggg.hhhh.iiii.jjjj.kkkk.llll.mmmm.nnnn.oooo.pppp in qqqq.rrrr.ssss.tttt.uuuu.vvvv.xxxx.yyyy.zzzz diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap index b7f2c2732c..33388de664 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff2_py.snap @@ -53,32 +53,34 @@ def test_calculate_fades(): ```diff --- Black +++ Ruff -@@ -1,8 +1,6 @@ - import pytest -- +@@ -3,6 +3,7 @@ TmSt = 1 TmEx = 2 -- + ++ # fmt: off # Test data: -@@ -17,19 +15,15 @@ - ]) +@@ -18,18 +19,22 @@ def test_fader(test): pass -- + ++ def check_fader(test): pass -- + ++ def verify_fader(test): # misaligned comment pass -- + ++ def verify_fader(test): """Hey, ho.""" assert test.passed() -- + ++ def test_calculate_fades(): calcs = [ # one is zero/none @@ -88,8 +90,11 @@ def test_calculate_fades(): ```py import pytest + TmSt = 1 TmEx = 2 + + # fmt: off # Test data: @@ -104,15 +109,23 @@ TmEx = 2 ]) def test_fader(test): pass + + def check_fader(test): pass + + def verify_fader(test): # misaligned comment pass + + def verify_fader(test): """Hey, ho.""" assert test.passed() + + def test_calculate_fades(): calcs = [ # one is zero/none diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap index 772a2828ad..a092c47a2a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff3_py.snap @@ -30,8 +30,11 @@ x = [ ```diff --- Black +++ Ruff -@@ -12,4 +12,6 @@ +@@ -10,6 +10,9 @@ + 1, 2, + 3, 4, ] ++ # fmt: on -x = [1, 2, 3, 4] @@ -55,6 +58,7 @@ x = [ 1, 2, 3, 4, ] + # fmt: on x = [ diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap index 6f14577620..4c768c3d7d 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff4_py.snap @@ -26,14 +26,15 @@ def f(): pass ```diff --- Black +++ Ruff -@@ -4,17 +4,9 @@ +@@ -4,17 +4,11 @@ 3, 4, ]) # fmt: on -def f(): - pass -- -- ++def f(): pass + + -@test( - [ - 1, @@ -44,7 +45,6 @@ def f(): pass -) -def f(): - pass -+def f(): pass +@test([ + 1, 2, + 3, 4, @@ -62,6 +62,8 @@ def f(): pass ]) # fmt: on def f(): pass + + @test([ 1, 2, 3, 4, diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap index cab9d6960c..db5263f639 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff5_py.snap @@ -106,60 +106,37 @@ elif unformatted: # Regression test for https://github.com/psf/black/issues/2015. run( # fmt: off -@@ -22,8 +20,6 @@ - + path, - check=True, - ) -- -- - # Regression test for https://github.com/psf/black/issues/3026. - def test_func(): - # yapf: disable -@@ -34,8 +30,6 @@ - return True - - return False -- -- - # Regression test for https://github.com/psf/black/issues/2567. - if True: - # fmt: off -@@ -44,9 +38,7 @@ +@@ -44,7 +42,7 @@ print ( "This won't be formatted" ) print ( "This won't be formatted either" ) else: - print("This will be formatted") -- -- + print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/3184. - class A: - async def call(param): -@@ -61,27 +53,18 @@ +@@ -61,7 +59,7 @@ elif param[0:4] in ("ZZZZ",): print ( "This won't be formatted either" ) - print("This will be formatted") -- -- + print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/2985. - class Named(t.Protocol): - # fmt: off - @property - def this_wont_be_formatted ( self ) -> str: ... -- -- +@@ -72,10 +70,7 @@ + + class Factory(t.Protocol): - def this_will_be_formatted(self, **kwargs) -> Named: - ... - - # fmt: on -- -- + def this_will_be_formatted ( self, **kwargs ) -> Named: ... + + # Regression test for https://github.com/psf/black/issues/3436. - if x: +@@ -83,5 +78,5 @@ return x # fmt: off elif unformatted: @@ -194,6 +171,8 @@ run( + path, check=True, ) + + # Regression test for https://github.com/psf/black/issues/3026. def test_func(): # yapf: disable @@ -204,6 +183,8 @@ def test_func(): return True return False + + # Regression test for https://github.com/psf/black/issues/2567. if True: # fmt: off @@ -213,6 +194,8 @@ if True: print ( "This won't be formatted either" ) else: print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/3184. class A: async def call(param): @@ -228,13 +211,19 @@ class A: print ( "This won't be formatted either" ) print ( "This will be formatted" ) + + # Regression test for https://github.com/psf/black/issues/2985. class Named(t.Protocol): # fmt: off @property def this_wont_be_formatted ( self ) -> str: ... + + class Factory(t.Protocol): def this_will_be_formatted ( self, **kwargs ) -> Named: ... + + # Regression test for https://github.com/psf/black/issues/3436. if x: return x diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap index 362982ef50..71c0fceef3 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtonoff_py.snap @@ -199,13 +199,10 @@ d={'a':1, ```diff --- Black +++ Ruff -@@ -1,20 +1,17 @@ - #!/usr/bin/env python3 - import asyncio - import sys -- +@@ -4,17 +4,18 @@ + from third_party import X, Y, Z -- + -from library import some_connection, some_decorator - +from library import some_connection, \ @@ -216,6 +213,8 @@ d={'a':1, # fmt: on -f"trigger 3.6 mode" +f'trigger 3.6 mode' ++ ++ # Comment 1 # Comment 2 @@ -223,15 +222,37 @@ d={'a':1, # fmt: off def func_no_args(): -@@ -39,72 +36,41 @@ +@@ -26,11 +27,15 @@ + continue + exec('new-style exec', {}, {}) + return None ++ ++ + async def coroutine(arg, exec=False): + 'Single-line docstring. Multiline is harder to reformat.' + async with some_connection() as conn: + await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) + await asyncio.sleep(1) ++ ++ + @asyncio.coroutine + @some_decorator( + with_args=True, +@@ -38,28 +43,19 @@ + ) def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: return text[number:-1] ++ ++ # fmt: on -def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(1, 2))) - assert task._cancel_stack[: len(old_stack)] == old_stack -- -- ++def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): ++ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(1, 2))) ++ assert task._cancel_stack[:len(old_stack)] == old_stack + + -def spaces_types( - a: int = 1, - b: tuple = (), @@ -244,53 +265,43 @@ d={'a':1, - i: str = r"", -): - ... -- -- ++def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + -def spaces2(result=_core.Value(None)): - ... -- -- -+def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): -+ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(1, 2))) -+ assert task._cancel_stack[:len(old_stack)] == old_stack -+def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... +def spaces2(result= _core.Value(None)): + ... + + something = { - # fmt: off - key: 'value', - } -- -- - def subscriptlist(): - atom[ - # fmt: off +@@ -74,23 +70,20 @@ 'some big and', 'complex subscript', # fmt: on - goes + here, - andhere, - ] -- -- + goes + here, andhere, + ] + + def import_as_names(): # fmt: off from hello import a, b 'unformatted' - # fmt: on -- -- + + def testlist_star_expr(): # fmt: off a , b = *hello 'unformatted' - # fmt: on -- -- + + def yield_expr(): - # fmt: off +@@ -98,11 +91,10 @@ yield hello 'unformatted' # fmt: on @@ -300,22 +311,18 @@ d={'a':1, ( yield hello ) 'unformatted' - # fmt: on -- -- + + def example(session): - # fmt: off - result = session\ -@@ -113,9 +79,6 @@ +@@ -113,7 +105,6 @@ models.Customer.email == email_address)\ .order_by(models.Customer.id.asc())\ .all() - # fmt: on -- -- - def off_and_on_without_data(): - """All comments here are technically on the same prefix. -@@ -123,12 +86,12 @@ + + def off_and_on_without_data(): +@@ -123,8 +114,10 @@ """ # fmt: off @@ -326,12 +333,8 @@ d={'a':1, + # fmt: on pass -- -- - def on_and_off_broken(): - """Another known limitation.""" - # fmt: on -@@ -137,21 +100,10 @@ + +@@ -137,21 +130,12 @@ and_=indeed . it is not formatted because . the . handling . inside . generate_ignored_nodes() now . considers . multiple . fmt . directives . within . one . prefix @@ -339,8 +342,8 @@ d={'a':1, - # fmt: off - # ...but comments still get reformatted even though they should not be - # fmt: on -- -- + + def long_lines(): if True: typedargslist.extend( @@ -354,22 +357,18 @@ d={'a':1, ) # fmt: off a = ( -@@ -182,24 +134,19 @@ - re.MULTILINE|re.VERBOSE - # fmt: on - ) -- -- +@@ -186,20 +170,19 @@ + def single_literal_yapf_disable(): """Black does not support this.""" - BAZ = {(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12)} # yapf: disable -- -- + BAZ = { + (1, 2, 3, 4), + (5, 6, 7, 8), + (9, 10, 11, 12) -+ } # yapf: disable ++ } + + cfg.rule( - "Default", - "address", @@ -388,7 +387,7 @@ d={'a':1, # fmt: off xxxxxxx_xxxxxxxxxxxx={ "xxxxxxxx": { -@@ -214,7 +161,7 @@ +@@ -214,7 +197,7 @@ }, }, # fmt: on @@ -405,7 +404,9 @@ d={'a':1, #!/usr/bin/env python3 import asyncio import sys + from third_party import X, Y, Z + from library import some_connection, \ some_decorator # fmt: off @@ -413,6 +414,8 @@ from third_party import (X, Y, Z) # fmt: on f'trigger 3.6 mode' + + # Comment 1 # Comment 2 @@ -427,11 +430,15 @@ def func_no_args(): continue exec('new-style exec', {}, {}) return None + + async def coroutine(arg, exec=False): 'Single-line docstring. Multiline is harder to reformat.' async with some_connection() as conn: await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) await asyncio.sleep(1) + + @asyncio.coroutine @some_decorator( with_args=True, @@ -439,17 +446,27 @@ many_args=[1,2,3] ) def function_signature_stress_test(number:int,no_annotation=None,text:str='default',* ,debug:bool=False,**kwargs) -> str: return text[number:-1] + + # fmt: on def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): offset = attr.ib(default=attr.Factory( lambda: _r.uniform(1, 2))) assert task._cancel_stack[:len(old_stack)] == old_stack + + def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + def spaces2(result= _core.Value(None)): ... + + something = { # fmt: off key: 'value', } + + def subscriptlist(): atom[ # fmt: off @@ -458,14 +475,20 @@ def subscriptlist(): # fmt: on goes + here, andhere, ] + + def import_as_names(): # fmt: off from hello import a, b 'unformatted' + + def testlist_star_expr(): # fmt: off a , b = *hello 'unformatted' + + def yield_expr(): # fmt: off yield hello @@ -475,6 +498,8 @@ def yield_expr(): # fmt: off ( yield hello ) 'unformatted' + + def example(session): # fmt: off result = session\ @@ -483,6 +508,8 @@ def example(session): models.Customer.email == email_address)\ .order_by(models.Customer.id.asc())\ .all() + + def off_and_on_without_data(): """All comments here are technically on the same prefix. @@ -496,6 +523,8 @@ def off_and_on_without_data(): # fmt: on pass + + def on_and_off_broken(): """Another known limitation.""" # fmt: on @@ -504,6 +533,8 @@ def on_and_off_broken(): and_=indeed . it is not formatted because . the . handling . inside . generate_ignored_nodes() now . considers . multiple . fmt . directives . within . one . prefix + + def long_lines(): if True: typedargslist.extend( @@ -538,13 +569,17 @@ def long_lines(): re.MULTILINE|re.VERBOSE # fmt: on ) + + def single_literal_yapf_disable(): """Black does not support this.""" BAZ = { (1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12) - } # yapf: disable + } + + cfg.rule( "Default", "address", xxxx_xxxx=["xxx-xxxxxx-xxxxxxxxxx"], diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap new file mode 100644 index 0000000000..46b8920531 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip6_py.snap @@ -0,0 +1,49 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/fmtskip6.py +--- +## Input + +```py +class A: + def f(self): + for line in range(10): + if True: + pass # fmt: skip +``` + +## Black Differences + +```diff +--- Black ++++ Ruff +@@ -2,4 +2,4 @@ + def f(self): + for line in range(10): + if True: +- pass # fmt: skip ++ pass +``` + +## Ruff Output + +```py +class A: + def f(self): + for line in range(10): + if True: + pass +``` + +## Black Output + +```py +class A: + def f(self): + for line in range(10): + if True: + pass # fmt: skip +``` + + diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap index 358018b4e0..0ebe890404 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__fmtskip8_py.snap @@ -75,65 +75,42 @@ async def test_async_with(): ```diff --- Black +++ Ruff -@@ -2,15 +2,10 @@ +@@ -2,7 +2,6 @@ def some_func( unformatted, args ): # fmt: skip print("I am some_func") return 0 - # Make sure this comment is not removed. -- -- + + # Make sure a leading comment is not removed. - async def some_async_func( unformatted, args): # fmt: skip - print("I am some_async_func") - await asyncio.sleep(1) -- -- - # Make sure a leading comment is not removed. - class SomeClass( Unformatted, SuperClasses ): # fmt: skip - def some_method( self, unformatted, args ): # fmt: skip -@@ -20,8 +15,6 @@ - async def some_async_method( self, unformatted, args ): # fmt: skip - print("I am some_async_method") - await asyncio.sleep(1) -- -- - # Make sure a leading comment is not removed. - if unformatted_call( args ): # fmt: skip - print("First branch") -@@ -30,33 +23,21 @@ +@@ -29,15 +28,15 @@ + elif another_unformatted_call( args ): # fmt: skip print("Second branch") else : # fmt: skip - print("Last branch") -- -- - while some_condition( unformatted, args ): # fmt: skip +- print("Last branch") ++ print("Last branch") # fmt: skip + + +-while some_condition( unformatted, args ): # fmt: skip ++while some_condition( unformatted, args ): # fmt: skip print("Do something") -- -- + + for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") -- -- +- print("Do something") ++ print("Do something") # fmt: skip + + async def test_async_for(): - async for i in some_async_iter( unformatted, args ): # fmt: skip - print("Do something") -- -- - try : # fmt: skip - some_call() - except UnformattedError as ex: # fmt: skip - handle_exception() - finally : # fmt: skip - finally_call() -- -- +@@ -54,7 +53,7 @@ + + with give_me_context( unformatted, args ): # fmt: skip - print("Do something") -- -- +- print("Do something") ++ print("Do something") # fmt: skip + + async def test_async_with(): - async with give_me_async_context( unformatted, args ): # fmt: skip - print("Do something") ``` ## Ruff Output @@ -143,10 +120,14 @@ async def test_async_with(): def some_func( unformatted, args ): # fmt: skip print("I am some_func") return 0 + + # Make sure a leading comment is not removed. async def some_async_func( unformatted, args): # fmt: skip print("I am some_async_func") await asyncio.sleep(1) + + # Make sure a leading comment is not removed. class SomeClass( Unformatted, SuperClasses ): # fmt: skip def some_method( self, unformatted, args ): # fmt: skip @@ -156,6 +137,8 @@ class SomeClass( Unformatted, SuperClasses ): # fmt: skip async def some_async_method( self, unformatted, args ): # fmt: skip print("I am some_async_method") await asyncio.sleep(1) + + # Make sure a leading comment is not removed. if unformatted_call( args ): # fmt: skip print("First branch") @@ -163,22 +146,34 @@ if unformatted_call( args ): # fmt: skip elif another_unformatted_call( args ): # fmt: skip print("Second branch") else : # fmt: skip - print("Last branch") -while some_condition( unformatted, args ): # fmt: skip + print("Last branch") # fmt: skip + + +while some_condition( unformatted, args ): # fmt: skip print("Do something") + + for i in some_iter( unformatted, args ): # fmt: skip - print("Do something") + print("Do something") # fmt: skip + + async def test_async_for(): async for i in some_async_iter( unformatted, args ): # fmt: skip print("Do something") + + try : # fmt: skip some_call() except UnformattedError as ex: # fmt: skip handle_exception() finally : # fmt: skip finally_call() + + with give_me_context( unformatted, args ): # fmt: skip - print("Do something") + print("Do something") # fmt: skip + + async def test_async_with(): async with give_me_async_context( unformatted, args ): # fmt: skip print("Do something") diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap index 6af0b38a4e..1a6c83f39e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function2_py.snap @@ -82,12 +82,8 @@ with hmm_but_this_should_get_two_preceding_newlines(): ) limited.append(-limited.pop()) # negate top return A( -@@ -13,34 +13,22 @@ - very_long_argument_name2=-very.long.value.for_the_argument, - **kwargs, - ) -- -- +@@ -17,30 +17,24 @@ + def g(): "Docstring." - @@ -95,15 +91,15 @@ with hmm_but_this_should_get_two_preceding_newlines(): pass - print("Inner defs should breathe a little.") -- -- + + def h(): def inner(): pass - print("Inner defs should breathe a little.") -- -- + + if os.name == "posix": import termios - @@ -117,7 +113,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): def i_should_be_followed_by_only_one_newline(): pass -@@ -54,12 +42,9 @@ +@@ -54,12 +48,10 @@ class IHopeYouAreHavingALovelyDay: def __call__(self): print("i_should_be_followed_by_only_one_newline") @@ -127,7 +123,7 @@ with hmm_but_this_should_get_two_preceding_newlines(): def foo(): pass - -- + with hmm_but_this_should_get_two_preceding_newlines(): pass ``` @@ -150,15 +146,21 @@ def f( very_long_argument_name2=-very.long.value.for_the_argument, **kwargs, ) + + def g(): "Docstring." def inner(): pass print("Inner defs should breathe a little.") + + def h(): def inner(): pass print("Inner defs should breathe a little.") + + if os.name == "posix": import termios def i_should_be_followed_by_only_one_newline(): @@ -183,6 +185,7 @@ else: def foo(): pass + with hmm_but_this_should_get_two_preceding_newlines(): pass ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap index a1ca74dcab..ceeff81fdc 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_py.snap @@ -108,21 +108,18 @@ def __await__(): return (yield) ```diff --- Black +++ Ruff -@@ -1,100 +1,52 @@ - #!/usr/bin/env python3 - import asyncio - import sys -- +@@ -4,97 +4,67 @@ + from third_party import X, Y, Z -- + -from library import some_connection, some_decorator -- --f"trigger 3.6 mode" -- -- +from library import some_connection, \ + some_decorator +f'trigger 3.6 mode' + +-f"trigger 3.6 mode" +- + def func_no_args(): - a - b @@ -136,8 +133,6 @@ def __await__(): return (yield) - continue - exec("new-style exec", {}, {}) - return None -- -- + a; b; c + if True: raise RuntimeError + if False: ... @@ -146,17 +141,19 @@ def __await__(): return (yield) + continue + exec("new-style exec", {}, {}) + return None + + async def coroutine(arg, exec=False): - "Single-line docstring. Multiline is harder to reformat." - async with some_connection() as conn: - await conn.do_what_i_mean("SELECT bobby, tables FROM xkcd", timeout=2) - await asyncio.sleep(1) -- -- + "Single-line docstring. Multiline is harder to reformat." + async with some_connection() as conn: + await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) + await asyncio.sleep(1) + + @asyncio.coroutine -@some_decorator(with_args=True, many_args=[1, 2, 3]) -def function_signature_stress_test( @@ -168,13 +165,22 @@ def __await__(): return (yield) - **kwargs, -) -> str: - return text[number:-1] -- -- ++@some_decorator( ++with_args=True, ++many_args=[1,2,3] ++) ++def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str: ++ return text[number:-1] + + -def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r""): - offset = attr.ib(default=attr.Factory(lambda: _r.uniform(10000, 200000))) - assert task._cancel_stack[: len(old_stack)] == old_stack -- -- ++def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): ++ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(10000, 200000))) ++ assert task._cancel_stack[:len(old_stack)] == old_stack + + -def spaces_types( - a: int = 1, - b: tuple = (), @@ -187,24 +193,15 @@ def __await__(): return (yield) - i: str = r"", -): - ... -- -- ++def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + -def spaces2(result=_core.Value(None)): - assert fut is self._read_fut, (fut, self._read_fut) -- -- -+@some_decorator( -+with_args=True, -+many_args=[1,2,3] -+) -+def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str: -+ return text[number:-1] -+def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): -+ offset = attr.ib(default=attr.Factory( lambda: _r.uniform(10000, 200000))) -+ assert task._cancel_stack[:len(old_stack)] == old_stack -+def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... +def spaces2(result= _core.Value(None)): + assert fut is self._read_fut, (fut, self._read_fut) + + def example(session): - result = ( - session.query(models.Customer.id) @@ -215,14 +212,14 @@ def __await__(): return (yield) - .order_by(models.Customer.id.asc()) - .all() - ) -- -- + result = session.query(models.Customer.id).filter( + models.Customer.account_id == account_id, + models.Customer.email == email_address, + ).order_by( + models.Customer.id.asc() + ).all() + + def long_lines(): if True: typedargslist.extend( @@ -244,7 +241,7 @@ def __await__(): return (yield) # trailing standalone comment ) ) -@@ -117,23 +69,18 @@ +@@ -117,23 +87,22 @@ \n? ) $ @@ -252,8 +249,8 @@ def __await__(): return (yield) - re.MULTILINE | re.VERBOSE, + """, re.MULTILINE | re.VERBOSE ) -- -- + + def trailing_comma(): mapping = { - A: 0.25 * (10.0 / 12), @@ -261,13 +258,13 @@ def __await__(): return (yield) - C: 0.1 * (10.0 / 12), - D: 0.1 * (10.0 / 12), - } -- -- + A: 0.25 * (10.0 / 12), + B: 0.1 * (10.0 / 12), + C: 0.1 * (10.0 / 12), + D: 0.1 * (10.0 / 12), +} + + def f( - a, - **kwargs, @@ -276,12 +273,10 @@ def __await__(): return (yield) ) -> A: return ( yield from A( -@@ -142,7 +89,4 @@ - **kwargs, - ) +@@ -144,5 +113,4 @@ ) -- -- + + -def __await__(): - return (yield) +def __await__(): return (yield) @@ -293,10 +288,14 @@ def __await__(): return (yield) #!/usr/bin/env python3 import asyncio import sys + from third_party import X, Y, Z + from library import some_connection, \ some_decorator f'trigger 3.6 mode' + + def func_no_args(): a; b; c if True: raise RuntimeError @@ -306,11 +305,15 @@ def func_no_args(): continue exec("new-style exec", {}, {}) return None + + async def coroutine(arg, exec=False): "Single-line docstring. Multiline is harder to reformat." async with some_connection() as conn: await conn.do_what_i_mean('SELECT bobby, tables FROM xkcd', timeout=2) await asyncio.sleep(1) + + @asyncio.coroutine @some_decorator( with_args=True, @@ -318,12 +321,20 @@ many_args=[1,2,3] ) def function_signature_stress_test(number:int,no_annotation=None,text:str="default",* ,debug:bool=False,**kwargs) -> str: return text[number:-1] + + def spaces(a=1, b=(), c=[], d={}, e=True, f=-1, g=1 if False else 2, h="", i=r''): offset = attr.ib(default=attr.Factory( lambda: _r.uniform(10000, 200000))) assert task._cancel_stack[:len(old_stack)] == old_stack + + def spaces_types(a: int = 1, b: tuple = (), c: list = [], d: dict = {}, e: bool = True, f: int = -1, g: int = 1 if False else 2, h: str = "", i: str = r''): ... + + def spaces2(result= _core.Value(None)): assert fut is self._read_fut, (fut, self._read_fut) + + def example(session): result = session.query(models.Customer.id).filter( models.Customer.account_id == account_id, @@ -331,6 +342,8 @@ def example(session): ).order_by( models.Customer.id.asc() ).all() + + def long_lines(): if True: typedargslist.extend( @@ -363,6 +376,8 @@ def long_lines(): $ """, re.MULTILINE | re.VERBOSE ) + + def trailing_comma(): mapping = { A: 0.25 * (10.0 / 12), @@ -370,6 +385,8 @@ def trailing_comma(): C: 0.1 * (10.0 / 12), D: 0.1 * (10.0 / 12), } + + def f( a, **kwargs, @@ -381,6 +398,8 @@ def f( **kwargs, ) ) + + def __await__(): return (yield) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap index d5edb4eca1..6de2f2bfd1 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__function_trailing_comma_py.snap @@ -74,7 +74,7 @@ some_module.some_function( ```diff --- Black +++ Ruff -@@ -1,114 +1,46 @@ +@@ -1,69 +1,28 @@ -def f( - a, -): @@ -84,8 +84,8 @@ some_module.some_function( +def f(a,): + d = {'key': 'value',} tup = (1,) -- -- + + -def f2( - a, - b, @@ -98,8 +98,11 @@ some_module.some_function( - 1, - 2, - ) -- -- ++def f2(a,b,): ++ d = {'key': 'value', 'key2': 'value2',} ++ tup = (1,2,) + + -def f( - a: int = 1, -): @@ -111,9 +114,6 @@ some_module.some_function( - call2( - arg=[1, 2, 3], - ) -+def f2(a,b,): -+ d = {'key': 'value', 'key2': 'value2',} -+ tup = (1,2,) +def f(a:int=1,): + call(arg={'explode': 'this',}) + call2(arg=[1,2,3],) @@ -136,8 +136,8 @@ some_module.some_function( - ): + if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: pass -- -- + + -def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> ( - Set["xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] -): @@ -150,20 +150,17 @@ some_module.some_function( - } - } - } -- -- +def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +]: + json = {"k": {"k2": {"k3": [1,]}}} + + # The type annotation shouldn't get a trailing comma since that would change its type. - # Relevant bug report: https://github.com/psf/black/issues/2381. - def some_function_with_a_really_long_name() -> ( - returning_a_deeply_nested_import_of_a_type_i_suppose - ): +@@ -74,24 +33,21 @@ pass -- -- + + -def some_method_with_a_really_long_name( - very_long_parameter_so_yeah: str, another_long_parameter: int -) -> another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not: @@ -171,8 +168,8 @@ some_module.some_function( + another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not +): pass -- -- + + def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( - this_shouldn_t_get_a_trailing_comma_too @@ -180,8 +177,8 @@ some_module.some_function( + also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too) ): pass -- -- + + -def func() -> ( - also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( +def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( @@ -190,9 +187,8 @@ some_module.some_function( + )) ): pass -- -- - # Make sure inner one-element tuple won't explode + +@@ -100,15 +56,7 @@ some_module.some_function( argument1, (one_element_tuple,), argument4, argument5, argument6 ) @@ -217,9 +213,13 @@ some_module.some_function( def f(a,): d = {'key': 'value',} tup = (1,) + + def f2(a,b,): d = {'key': 'value', 'key2': 'value2',} tup = (1,2,) + + def f(a:int=1,): call(arg={'explode': 'this',}) call2(arg=[1,2,3],) @@ -229,29 +229,41 @@ def f(a:int=1,): }["a"] if a == {"a": 1,"b": 2,"c": 3,"d": 4,"e": 5,"f": 6,"g": 7,"h": 8,}["a"]: pass + + def xxxxxxxxxxxxxxxxxxxxxxxxxxxx() -> Set[ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ]: json = {"k": {"k2": {"k3": [1,]}}} + + # The type annotation shouldn't get a trailing comma since that would change its type. # Relevant bug report: https://github.com/psf/black/issues/2381. def some_function_with_a_really_long_name() -> ( returning_a_deeply_nested_import_of_a_type_i_suppose ): pass + + def some_method_with_a_really_long_name(very_long_parameter_so_yeah: str, another_long_parameter: int) -> ( another_case_of_returning_a_deeply_nested_import_of_a_type_i_suppose_cause_why_not ): pass + + def func() -> ( also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black(this_shouldn_t_get_a_trailing_comma_too) ): pass + + def func() -> ((also_super_long_type_annotation_that_may_cause_an_AST_related_crash_in_black( this_shouldn_t_get_a_trailing_comma_too )) ): pass + + # Make sure inner one-element tuple won't explode some_module.some_function( argument1, (one_element_tuple,), argument4, argument5, argument6 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap index b7cde495e0..352abb2541 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__import_spacing_py.snap @@ -62,9 +62,8 @@ __all__ = ( ```diff --- Black +++ Ruff -@@ -1,55 +1,33 @@ - """The asyncio package, tracking PEP 3156.""" -- +@@ -2,12 +2,13 @@ + # flake8: noqa -from logging import WARNING @@ -79,16 +78,9 @@ __all__ = ( # This relies on each of the submodules having an __all__ variable. from .base_events import * from .coroutines import * - from .events import * # comment here -- - from .futures import * - from .locks import * # comment here - from .protocols import * -- - from ..runners import * # comment here - from ..queues import * +@@ -22,33 +23,16 @@ from ..streams import * -- + from some_library import ( - Just, - Enough, @@ -111,7 +103,7 @@ __all__ = ( -) +from name_of_a_company.extremely_long_project_name.component.ttypes import CuteLittleServiceHandlerFactoryyy from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * -- + from .a.b.c.subprocess import * -from . import tasks -from . import A, B, C @@ -119,20 +111,20 @@ __all__ = ( - SomeVeryLongNameAndAllOfItsAdditionalLetters1, - SomeVeryLongNameAndAllOfItsAdditionalLetters2, -) -- +from . import (tasks) +from . import (A, B, C) +from . import SomeVeryLongNameAndAllOfItsAdditionalLetters1, \ + SomeVeryLongNameAndAllOfItsAdditionalLetters2 + __all__ = ( base_events.__all__ - + coroutines.__all__ ``` ## Ruff Output ```py """The asyncio package, tracking PEP 3156.""" + # flake8: noqa from logging import ( @@ -146,22 +138,27 @@ import sys from .base_events import * from .coroutines import * from .events import * # comment here + from .futures import * from .locks import * # comment here from .protocols import * + from ..runners import * # comment here from ..queues import * from ..streams import * + from some_library import ( Just, Enough, Libraries, To, Fit, In, This, Nice, Split, Which, We, No, Longer, Use ) from name_of_a_company.extremely_long_project_name.component.ttypes import CuteLittleServiceHandlerFactoryyy from name_of_a_company.extremely_long_project_name.extremely_long_component_name.ttypes import * + from .a.b.c.subprocess import * from . import (tasks) from . import (A, B, C) from . import SomeVeryLongNameAndAllOfItsAdditionalLetters1, \ SomeVeryLongNameAndAllOfItsAdditionalLetters2 + __all__ = ( base_events.__all__ + coroutines.__all__ diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap index e2e7b79f7e..4f258ac888 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__power_op_spacing_py.snap @@ -76,34 +76,20 @@ return np.divide( ```diff --- Black +++ Ruff -@@ -1,16 +1,10 @@ +@@ -1,10 +1,10 @@ def function(**kwargs): t = a**2 + b**3 - return t**2 -- -- + return t ** 2 + + def function_replace_spaces(**kwargs): - t = a**2 + b**3 + c**4 -- -- + t = a **2 + b** 3 + c ** 4 + + def function_dont_replace_spaces(): - {**a, **b, **c} -- -- - a = 5**~4 - b = 5 ** f() - c = -(5**2) -@@ -29,7 +23,6 @@ - p = {(k, k**2): v**2 for k, v in pairs} - q = [10**i for i in range(6)] - r = x**y -- - a = 5.0**~4.0 - b = 5.0 ** f() - c = -(5.0**2.0) -@@ -47,8 +40,6 @@ +@@ -47,8 +47,6 @@ o = settings(max_examples=10**6.0) p = {(k, k**2): v**2.0 for k, v in pairs} q = [10.5**i for i in range(6)] @@ -112,14 +98,6 @@ return np.divide( # WE SHOULD DEFINITELY NOT EAT THESE COMMENTS (https://github.com/psf/black/issues/2873) if hasattr(view, "sum_of_weights"): return np.divide( # type: ignore[no-any-return] -@@ -57,7 +48,6 @@ - out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] - where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] - ) -- - return np.divide( - where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore - ) ``` ## Ruff Output @@ -128,10 +106,16 @@ return np.divide( def function(**kwargs): t = a**2 + b**3 return t ** 2 + + def function_replace_spaces(**kwargs): t = a **2 + b** 3 + c ** 4 + + def function_dont_replace_spaces(): {**a, **b, **c} + + a = 5**~4 b = 5 ** f() c = -(5**2) @@ -150,6 +134,7 @@ o = settings(max_examples=10**6) p = {(k, k**2): v**2 for k, v in pairs} q = [10**i for i in range(6)] r = x**y + a = 5.0**~4.0 b = 5.0 ** f() c = -(5.0**2.0) @@ -175,6 +160,7 @@ if hasattr(view, "sum_of_weights"): out=np.full(view.sum_of_weights.shape, np.nan), # type: ignore[union-attr] where=view.sum_of_weights**2 > view.sum_of_weights_squared, # type: ignore[union-attr] ) + return np.divide( where=view.sum_of_weights_of_weight_long**2 > view.sum_of_weights_squared, # type: ignore ) diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap index 62096f8534..42f7589dcd 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_await_parens_py.snap @@ -94,56 +94,49 @@ async def main(): ```diff --- Black +++ Ruff -@@ -1,93 +1,64 @@ - import asyncio -- -- - # Control example - async def main(): - await asyncio.sleep(1) -- -- +@@ -8,59 +8,64 @@ + # Remove brackets for short coroutine/task async def main(): - await asyncio.sleep(1) -- -- + await (asyncio.sleep(1)) + + async def main(): - await asyncio.sleep(1) -- -- + await ( + asyncio.sleep(1) + ) + + async def main(): - await asyncio.sleep(1) -- -- + await (asyncio.sleep(1) + ) + + # Check comments async def main(): - await asyncio.sleep(1) # Hello -- -- + await ( # Hello + asyncio.sleep(1) + ) + + async def main(): - await asyncio.sleep(1) # Hello -- -- + await ( + asyncio.sleep(1) # Hello + ) + + async def main(): - await asyncio.sleep(1) # Hello -- -- + await ( + asyncio.sleep(1) -+ ) # Hello ++ ) + + # Long lines async def main(): - await asyncio.gather( @@ -155,9 +148,9 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - ) -- -- + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1)) + + # Same as above but with magic trailing comma in function async def main(): - await asyncio.gather( @@ -169,14 +162,12 @@ async def main(): - asyncio.sleep(1), - asyncio.sleep(1), - ) -- -- + await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1),) + + # Cr@zY Br@ck3Tz async def main(): - await black(1) -- -- + await ( + ((((((((((((( + ((( ((( @@ -190,74 +181,81 @@ async def main(): + ))) ))) + ))))))))))))) + ) + + # Keep brackets around non power operations and nested awaits - async def main(): - await (set_of_tasks | other_set) -- -- - async def main(): - await (await asyncio.sleep(1)) -- -- - # It's awaits all the way down... - async def main(): - await (await x) -- -- - async def main(): - await (yield x) -- -- +@@ -82,11 +87,11 @@ + + async def main(): - await (await asyncio.sleep(1)) -- -- + await (await (asyncio.sleep(1))) + + async def main(): - await (await (await (await (await asyncio.sleep(1))))) -- -- + await (await (await (await (await (asyncio.sleep(1)))))) + + async def main(): - await (yield) ``` ## Ruff Output ```py import asyncio + + # Control example async def main(): await asyncio.sleep(1) + + # Remove brackets for short coroutine/task async def main(): await (asyncio.sleep(1)) + + async def main(): await ( asyncio.sleep(1) ) + + async def main(): await (asyncio.sleep(1) ) + + # Check comments async def main(): await ( # Hello asyncio.sleep(1) ) + + async def main(): await ( asyncio.sleep(1) # Hello ) + + async def main(): await ( asyncio.sleep(1) - ) # Hello + ) + + # Long lines async def main(): await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1)) + + # Same as above but with magic trailing comma in function async def main(): await asyncio.gather(asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1), asyncio.sleep(1),) + + # Cr@zY Br@ck3Tz async def main(): await ( @@ -273,20 +271,34 @@ async def main(): ))) ))) ))))))))))))) ) + + # Keep brackets around non power operations and nested awaits async def main(): await (set_of_tasks | other_set) + + async def main(): await (await asyncio.sleep(1)) + + # It's awaits all the way down... async def main(): await (await x) + + async def main(): await (yield x) + + async def main(): await (await (asyncio.sleep(1))) + + async def main(): await (await (await (await (await (asyncio.sleep(1)))))) + + async def main(): await (yield) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap index f7724c3116..7f435c1e2f 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_except_parens_py.snap @@ -48,7 +48,7 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov ```diff --- Black +++ Ruff -@@ -1,42 +1,25 @@ +@@ -1,42 +1,27 @@ # These brackets are redundant, therefore remove. try: a.something @@ -78,7 +78,7 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov -) as err: +except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: raise err -- + try: a.something -except ( @@ -86,7 +86,7 @@ except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.ov -) as err: +except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,) as err: raise err -- + try: a.something -except ( @@ -117,10 +117,12 @@ try: a.something except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: raise err + try: a.something except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error,) as err: raise err + try: a.something except (some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error, some.really.really.really.looooooooooooooooooooooooooooooooong.module.over89.chars.Error) as err: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap index df7d50d6f2..d432c040c7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_for_brackets_py.snap @@ -32,7 +32,7 @@ for (((((k, v))))) in d.items(): ```diff --- Black +++ Ruff -@@ -1,27 +1,15 @@ +@@ -1,27 +1,16 @@ # Only remove tuple brackets after `for` -for k, v in d.items(): +for (k, v) in d.items(): @@ -50,7 +50,7 @@ for (((((k, v))))) in d.items(): -) in d.items(): +for (why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do) in d.items(): print(k, v) -- + -for ( - k, - v, @@ -79,6 +79,7 @@ for module in (core, _unicodefun): # Brackets remain for long for loop lines for (why_would_anyone_choose_to_name_a_loop_variable_with_a_name_this_long, i_dont_know_but_we_should_still_check_the_behaviour_if_they_do) in d.items(): print(k, v) + for (k, v) in dfkasdjfldsjflkdsjflkdsjfdslkfjldsjfgkjdshgkljjdsfldgkhsdofudsfudsofajdslkfjdslkfjldisfjdffjsdlkfjdlkjjkdflskadjldkfjsalkfjdasj.items(): print(k, v) # Test deeply nested brackets diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap index bb3d5adbec..7e1a6486b2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_newline_after_code_block_open_py.snap @@ -121,104 +121,88 @@ with open("/path/to/file.txt", mode="r") as read_file: ```diff --- Black +++ Ruff -@@ -1,78 +1,74 @@ - import random -- -- +@@ -2,20 +2,26 @@ + + def foo1(): -- print("The newline above me should be deleted!") -- - -+ print("The newline above me should be deleted!") - def foo2(): -- print("All the newlines above me should be deleted!") - - + -+ print("All the newlines above me should be deleted!") + print("The newline above me should be deleted!") + + + def foo2(): ++ ++ ++ + print("All the newlines above me should be deleted!") + + def foo3(): + print("No newline above me!") print("There is a newline above me, and that's OK!") -+def foo4(): -- --def foo4(): + + def foo4(): ++ # There is a comment here print("The newline above me should not be deleted!") -- -- +@@ -23,27 +29,39 @@ + class Foo: def bar(self): + print("The newline above me should be deleted!") -- -- + + for i in range(5): -- print(f"{i}) The line above me should be removed!") -- - -+ print(f"{i}) The line above me should be removed!") - for i in range(5): -- print(f"{i}) The lines above me should be removed!") - - + -+ print(f"{i}) The lines above me should be removed!") + print(f"{i}) The line above me should be removed!") + + + for i in range(5): ++ ++ ++ + print(f"{i}) The lines above me should be removed!") + + for i in range(5): + for j in range(7): + print(f"{i}) The lines above me should be removed!") -+if random.randint(0, 3) == 0: -- -+ print("The new line above me is about to be removed!") + if random.randint(0, 3) == 0: -- print("The new line above me is about to be removed!") ++ + print("The new line above me is about to be removed!") --if random.randint(0, 3) == 0: -- print("The new lines above me is about to be removed!") - - -+ print("The new lines above me is about to be removed!") if random.randint(0, 3) == 0: - if random.uniform(0, 1) > 0.5: - print("Two lines above me are about to be removed!") -- -+while True: - -+ print("The newline above me should be deleted!") - while True: -- print("The newline above me should be deleted!") - - --while True: + - print("The newlines above me should be deleted!") -+while True: - -- --while True: - while False: -- print("The newlines above me should be deleted!") -- - -+ print("The newlines above me should be deleted!") - with open("/path/to/file.txt", mode="w") as file: -- file.write("The new line above me is about to be removed!") -- - -+ file.write("The new line above me is about to be removed!") - with open("/path/to/file.txt", mode="w") as file: -- file.write("The new lines above me is about to be removed!") - - + -+ file.write("The new lines above me is about to be removed!") ++ ++ + print("The new lines above me is about to be removed!") + + +@@ -66,13 +84,19 @@ + + + with open("/path/to/file.txt", mode="w") as file: ++ + file.write("The new line above me is about to be removed!") + + + with open("/path/to/file.txt", mode="w") as file: ++ ++ ++ + file.write("The new lines above me is about to be removed!") + + with open("/path/to/file.txt", mode="r") as read_file: + with open("/path/to/output_file.txt", mode="w") as write_file: @@ -230,74 +214,102 @@ with open("/path/to/file.txt", mode="r") as read_file: ```py import random + + def foo1(): print("The newline above me should be deleted!") + + def foo2(): print("All the newlines above me should be deleted!") + + def foo3(): print("No newline above me!") print("There is a newline above me, and that's OK!") + + def foo4(): # There is a comment here print("The newline above me should not be deleted!") + + class Foo: def bar(self): print("The newline above me should be deleted!") + + for i in range(5): print(f"{i}) The line above me should be removed!") + + for i in range(5): print(f"{i}) The lines above me should be removed!") + + for i in range(5): for j in range(7): print(f"{i}) The lines above me should be removed!") + + if random.randint(0, 3) == 0: print("The new line above me is about to be removed!") + + if random.randint(0, 3) == 0: print("The new lines above me is about to be removed!") + + if random.randint(0, 3) == 0: if random.uniform(0, 1) > 0.5: print("Two lines above me are about to be removed!") -while True: + +while True: print("The newline above me should be deleted!") + + while True: - - - print("The newlines above me should be deleted!") + + while True: - while False: - print("The newlines above me should be deleted!") + + with open("/path/to/file.txt", mode="w") as file: file.write("The new line above me is about to be removed!") + + with open("/path/to/file.txt", mode="w") as file: file.write("The new lines above me is about to be removed!") + + with open("/path/to/file.txt", mode="r") as read_file: with open("/path/to/output_file.txt", mode="w") as write_file: diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap index d06c27e3a0..7b2de231ae 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__remove_parens_py.snap @@ -68,19 +68,15 @@ def example8(): ```diff --- Black +++ Ruff -@@ -1,85 +1,34 @@ +@@ -1,5 +1,5 @@ -x = 1 -x = 1.2 -- +x = (1) +x = (1.2) + data = ( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ).encode() -- -- - async def show_status(): - while True: +@@ -11,75 +11,47 @@ try: if report_host: data = ( @@ -90,52 +86,52 @@ def example8(): + ).encode() except Exception as e: pass -- -- + + def example(): - return "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -- -- + return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example1(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 -- -- + return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example1point5(): - return 1111111111111111111111111111111111111111111111111111111111111111111111111111111111111 -- -- + return ((((((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)))))) + + def example2(): - return ( - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - ) -- -- + return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example3(): - return ( - 1111111111111111111111111111111111111111111111111111111111111111111111111111111 - ) -- -- + return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example4(): - return True -- -- + return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((True)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example5(): - return () -- -- + return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((())))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example6(): - return {a: a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]} -- -- + return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]}))))))))) + + def example7(): - return { - a: a @@ -162,9 +158,9 @@ def example8(): - 20000000000000000000, - ] - } -- -- + return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20000000000000000000]}))))))))) + + def example8(): - return None + return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) @@ -175,9 +171,12 @@ def example8(): ```py x = (1) x = (1.2) + data = ( "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ).encode() + + async def show_status(): while True: try: @@ -187,24 +186,44 @@ async def show_status(): ).encode() except Exception as e: pass + + def example(): return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example1(): return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example1point5(): return ((((((1111111111111111111111111111111111111111111111111111111111111111111111111111111111111)))))) + + def example2(): return (("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")) + + def example3(): return ((1111111111111111111111111111111111111111111111111111111111111111111111111111111)) + + def example4(): return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((True)))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example5(): return ((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((())))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) + + def example6(): return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]}))))))))) + + def example7(): return ((((((((({a:a for a in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20000000000000000000]}))))))))) + + def example8(): return (((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((None))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))) ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap index f80e25a7c9..d09d9dfaa7 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__return_annotation_brackets_py.snap @@ -101,82 +101,80 @@ def foo() -> tuple[int, int, int,]: ```diff --- Black +++ Ruff -@@ -1,120 +1,70 @@ +@@ -1,33 +1,41 @@ # Control def double(a: int) -> int: - return 2 * a -- -- + return 2*a + + # Remove the brackets -def double(a: int) -> int: - return 2 * a -- -- +def double(a: int) -> (int): + return 2*a + + # Some newline variations -def double(a: int) -> int: - return 2 * a -- -- --def double(a: int) -> int: -- return 2 * a -- -- --def double(a: int) -> int: -- return 2 * a -- -- +def double(a: int) -> ( + int): + return 2*a + + +-def double(a: int) -> int: +- return 2 * a +def double(a: int) -> (int +): + return 2*a + + +-def double(a: int) -> int: +- return 2 * a +def double(a: int) -> ( + int +): + return 2*a + + # Don't lose the comments -def double(a: int) -> int: # Hello - return 2 * a -- -- --def double(a: int) -> int: # Hello -- return 2 * a -- -- +def double(a: int) -> ( # Hello + int +): + return 2*a + + +-def double(a: int) -> int: # Hello +- return 2 * a +def double(a: int) -> ( + int # Hello +): + return 2*a + + # Really long annotations - def foo() -> ( - intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds - ): +@@ -37,84 +45,62 @@ return 2 -- -- + + -def foo() -> ( - intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds -): +def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + -def foo() -> ( - intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds - | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds -): +def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + -def foo( - a: int, - b: int, @@ -184,8 +182,8 @@ def foo() -> tuple[int, int, int,]: -) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: +def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + -def foo( - a: int, - b: int, @@ -196,8 +194,8 @@ def foo() -> tuple[int, int, int,]: -): +def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 -- -- + + # Split args but no need to split return -def foo( - a: int, @@ -206,20 +204,18 @@ def foo() -> tuple[int, int, int,]: -) -> int: +def foo(a: int, b: int, c: int,) -> int: return 2 -- -- + + # Deeply nested brackets # with *interesting* spacing -def double(a: int) -> int: - return 2 * a -- -- --def double(a: int) -> int: -- return 2 * a -- -- +def double(a: int) -> (((((int))))): + return 2*a + + +-def double(a: int) -> int: +- return 2 * a +def double(a: int) -> ( + ( ( + ((int) @@ -228,6 +224,8 @@ def foo() -> tuple[int, int, int,]: + ) + ): + return 2*a + + def foo() -> ( + ( ( intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds @@ -235,26 +233,27 @@ def foo() -> tuple[int, int, int,]: +) +)): return 2 -- -- + + # Return type with commas -def foo() -> tuple[int, int, int]: -- return 2 -- -- - def foo() -> ( ++def foo() -> ( ++ tuple[int, int, int] ++): + return 2 + + +-def foo() -> ( - tuple[ - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, - ] -+ tuple[int, int, int] - ): - return 2 -- -- +-): +def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong]: -+ return 2 + return 2 + + # Magic trailing comma example -def foo() -> ( - tuple[ @@ -273,49 +272,77 @@ def foo() -> tuple[int, int, int,]: # Control def double(a: int) -> int: return 2*a + + # Remove the brackets def double(a: int) -> (int): return 2*a + + # Some newline variations def double(a: int) -> ( int): return 2*a + + def double(a: int) -> (int ): return 2*a + + def double(a: int) -> ( int ): return 2*a + + # Don't lose the comments def double(a: int) -> ( # Hello int ): return 2*a + + def double(a: int) -> ( int # Hello ): return 2*a + + # Really long annotations def foo() -> ( intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds ): return 2 + + def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + def foo() -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + def foo(a: int, b: int, c: int,) -> intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds | intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds: return 2 + + # Split args but no need to split return def foo(a: int, b: int, c: int,) -> int: return 2 + + # Deeply nested brackets # with *interesting* spacing def double(a: int) -> (((((int))))): return 2*a + + def double(a: int) -> ( ( ( ((int) @@ -324,19 +351,27 @@ def double(a: int) -> ( ) ): return 2*a + + def foo() -> ( ( ( intsdfsafafafdfdsasdfsfsdfasdfafdsafdfdsfasdskdsdsfdsafdsafsdfdasfffsfdsfdsafafhdskfhdsfjdslkfdlfsdkjhsdfjkdshfkljds ) )): return 2 + + # Return type with commas def foo() -> ( tuple[int, int, int] ): return 2 + + def foo() -> tuple[loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong, loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong]: return 2 + + # Magic trailing comma example def foo() -> tuple[int, int, int,]: return 2 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap index fbac7ed57e..26cfa4faca 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__skip_magic_trailing_comma_py.snap @@ -60,7 +60,7 @@ func( ```diff --- Black +++ Ruff -@@ -1,25 +1,40 @@ +@@ -1,25 +1,43 @@ # We should not remove the trailing comma in a single-element subscript. a: tuple[int,] b = tuple[int,] @@ -86,12 +86,6 @@ func( - # Trailing commas in multiple chained non-nested parens. -zero(one).two(three).four(five) -- --func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) -- --(a, b, c, d) = func1(arg1) and func2(arg2) -- --func(argument1, (one, two), argument4, argument5, argument6) +zero( + one, +).two( @@ -99,7 +93,11 @@ func( +).four( + five, +) + +-func1(arg1).func2(arg2).func3(arg3).func4(arg4).func5(arg5) +func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) + +-(a, b, c, d) = func1(arg1) and func2(arg2) +( + a, + b, @@ -108,6 +106,8 @@ func( +) = func1( + arg1 +) and func2(arg2) + +-func(argument1, (one, two), argument4, argument5, argument6) +func( + argument1, + ( @@ -144,7 +144,9 @@ zero( ).four( five, ) + func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) + ( a, b, @@ -153,6 +155,7 @@ func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) ) = func1( arg1 ) and func2(arg2) + func( argument1, ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap index ee4cb215cc..902e4035e2 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__slices_py.snap @@ -76,35 +76,7 @@ x[ ```diff --- Black +++ Ruff -@@ -17,19 +17,14 @@ - slice[not so_simple : 1 < val <= 10] - slice[(1 for i in range(42)) : x] - slice[:: [i for i in range(42)]] -- -- - async def f(): - slice[await x : [i async for i in arange(42)] : 42] -- -- - # These are from PEP-8: - ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] - ham[lower:upper], ham[lower:upper:], ham[lower::step] - # ham[lower+offset : upper+offset] - ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] - ham[lower + offset : upper + offset] -- - slice[::, ::] - slice[ - # A -@@ -46,7 +41,6 @@ - # C - 3 - ] -- - slice[ - # A - 1 -@@ -56,4 +50,8 @@ +@@ -56,4 +56,8 @@ # C 4 ] @@ -138,14 +110,19 @@ slice[1 or 2 : True and False] slice[not so_simple : 1 < val <= 10] slice[(1 for i in range(42)) : x] slice[:: [i for i in range(42)]] + + async def f(): slice[await x : [i async for i in arange(42)] : 42] + + # These are from PEP-8: ham[1:9], ham[1:9:3], ham[:9:3], ham[1::3], ham[1:9:] ham[lower:upper], ham[lower:upper:], ham[lower::step] # ham[lower+offset : upper+offset] ham[: upper_fn(x) : step_fn(x)], ham[:: step_fn(x)] ham[lower + offset : upper + offset] + slice[::, ::] slice[ # A @@ -162,6 +139,7 @@ slice[ # C 3 ] + slice[ # A 1 diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap index be3dbc6901..07e263b90e 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__string_prefixes_py.snap @@ -33,7 +33,7 @@ def docstring_multiline(): ```diff --- Black +++ Ruff -@@ -1,19 +1,14 @@ +@@ -1,13 +1,13 @@ #!/usr/bin/env python3 name = "Łukasz" @@ -44,20 +44,14 @@ def docstring_multiline(): +(b"", B"") +(u"", U"") (r"", R"") -- + -(rf"", rf"", Rf"", Rf"", rf"", rf"", Rf"", Rf"") -(rb"", rb"", Rb"", Rb"", rb"", rb"", Rb"", Rb"") -- -- +(rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"") +(rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"") + + def docstring_singleline(): - R"""2020 was one hell of a year. The good news is that we were able to""" -- -- - def docstring_multiline(): - R""" - clear out all of the issues opened in that time :p ``` ## Ruff Output @@ -70,10 +64,15 @@ name = "Łukasz" (b"", B"") (u"", U"") (r"", R"") + (rf"", fr"", Rf"", fR"", rF"", Fr"", RF"", FR"") (rb"", br"", Rb"", bR"", rB"", Br"", RB"", BR"") + + def docstring_singleline(): R"""2020 was one hell of a year. The good news is that we were able to""" + + def docstring_multiline(): R""" clear out all of the issues opened in that time :p diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap index 304ee0a3cb..95a7d91907 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__torture_py.snap @@ -42,15 +42,15 @@ assert ( ```diff --- Black +++ Ruff -@@ -1,58 +1,25 @@ +@@ -1,58 +1,33 @@ importA -( - () - << 0 - ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 -) # -- +() << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 # + assert sort_by_dependency( { - "1": {"2", "3"}, @@ -64,13 +64,13 @@ assert ( + "2a": set(), "2b": set(), "3a": set(), "3b": set() } ) == ["2a", "2b", "2", "3a", "3b", "3", "1"] -- + importA 0 -0 ^ 0 # -- -- +0^0 # + + class A: def foo(self): for _ in range(10): @@ -78,9 +78,9 @@ assert ( + aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member xxxxxxxxxxxx - ) # pylint: disable=no-member -- -- + ) + + def test(self, othr): - return 1 == 2 and ( - name, @@ -101,15 +101,15 @@ assert ( - othr.meta_data, - othr.schedule, - ) -- -- ++ return (1 == 2 and ++ (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == ++ (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) + + -assert a_function( - very_long_arguments_that_surpass_the_limit, - which_is_eighty_eight_in_this_case_plus_a_bit_more, -) == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} -+ return (1 == 2 and -+ (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == -+ (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) +assert ( + a_function(very_long_arguments_that_surpass_the_limit, which_is_eighty_eight_in_this_case_plus_a_bit_more) + == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} @@ -121,25 +121,33 @@ assert ( ```py importA () << 0 ** 101234234242352525425252352352525234890264906820496920680926538059059209922523523525 # + assert sort_by_dependency( { "1": {"2", "3"}, "2": {"2a", "2b"}, "3": {"3a", "3b"}, "2a": set(), "2b": set(), "3a": set(), "3b": set() } ) == ["2a", "2b", "2", "3a", "3b", "3", "1"] + importA 0 0^0 # + + class A: def foo(self): for _ in range(10): aaaaaaaaaaaaaaaaaaa = bbbbbbbbbbbbbbb.cccccccccc( # pylint: disable=no-member xxxxxxxxxxxx ) + + def test(self, othr): return (1 == 2 and (name, description, self.default, self.selected, self.auto_generated, self.parameters, self.meta_data, self.schedule) == (name, description, othr.default, othr.selected, othr.auto_generated, othr.parameters, othr.meta_data, othr.schedule)) + + assert ( a_function(very_long_arguments_that_surpass_the_limit, which_is_eighty_eight_in_this_case_plus_a_bit_more) == {"x": "this need to pass the line limit as well", "b": "but only by a little bit"} diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap index 7fea6d8abe..a6989d984a 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_comma_optional_parens1_py.snap @@ -38,7 +38,7 @@ class A: ```diff --- Black +++ Ruff -@@ -1,29 +1,17 @@ +@@ -1,18 +1,11 @@ -if e1234123412341234.winerror not in ( - _winapi.ERROR_SEM_TIMEOUT, - _winapi.ERROR_PIPE_BUSY, @@ -46,7 +46,7 @@ class A: +if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, + _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): pass -- + if x: if y: - new_id = ( @@ -56,23 +56,20 @@ class A: - ) - + 1 - ) -- -- + new_id = max(Vegetable.objects.order_by('-id')[0].id, + Mineral.objects.order_by('-id')[0].id) + 1 + + class X: - def get_help_text(self): - return ngettext( +@@ -21,7 +14,7 @@ "Your password must contain at least %(min_length)d character.", "Your password must contain at least %(min_length)d characters.", self.min_length, - ) % {"min_length": self.min_length} -- -- + ) % {'min_length': self.min_length} + + class A: - def b(self): - if self.connection.mysql_is_mariadb and ( ``` ## Ruff Output @@ -81,10 +78,13 @@ class A: if e1234123412341234.winerror not in (_winapi.ERROR_SEM_TIMEOUT, _winapi.ERROR_PIPE_BUSY) or _check_timeout(t): pass + if x: if y: new_id = max(Vegetable.objects.order_by('-id')[0].id, Mineral.objects.order_by('-id')[0].id) + 1 + + class X: def get_help_text(self): return ngettext( @@ -92,6 +92,8 @@ class X: "Your password must contain at least %(min_length)d characters.", self.min_length, ) % {'min_length': self.min_length} + + class A: def b(self): if self.connection.mysql_is_mariadb and ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap index 73853e4f0e..eea12aab00 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__trailing_commas_in_leading_parts_py.snap @@ -46,7 +46,7 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```diff --- Black +++ Ruff -@@ -1,30 +1,8 @@ +@@ -1,28 +1,10 @@ -zero( - one, -).two( @@ -60,12 +60,12 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -).func3(arg3).func4( - arg4, -).func5(arg5) -- +zero(one,).two(three,).four(five,) + +func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) # Inner one-element tuple shouldn't explode func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) -- + -( - a, - b, @@ -74,21 +74,11 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( -) = func1( - arg1 -) and func2(arg2) -- -- +(a, b, c, d,) = func1(arg1) and func2(arg2) + + # Example from https://github.com/psf/black/issues/3229 - def refresh_token(self, device_family, refresh_token, api_key): - return self.orchestration.refresh_token( -@@ -33,15 +11,12 @@ - }, - api_key=api_key, - )["extensions"]["sdk"]["token"] -- -- - # Edge case where a bug in a working-in-progress version of - # https://github.com/psf/black/pull/3370 causes an infinite recursion. - assert ( +@@ -41,7 +23,6 @@ long_module.long_class.long_func().another_func() == long_module.long_class.long_func()["some_key"].another_func(arg1) ) @@ -102,10 +92,14 @@ assert xxxxxxxxx.xxxxxxxxx.xxxxxxxxx( ```py zero(one,).two(three,).four(five,) + func1(arg1).func2(arg2,).func3(arg3).func4(arg4,).func5(arg5) # Inner one-element tuple shouldn't explode func1(arg1).func2(arg1, (one_tuple,)).func3(arg3) + (a, b, c, d,) = func1(arg1) and func2(arg2) + + # Example from https://github.com/psf/black/issues/3229 def refresh_token(self, device_family, refresh_token, api_key): return self.orchestration.refresh_token( @@ -114,6 +108,8 @@ def refresh_token(self, device_family, refresh_token, api_key): }, api_key=api_key, )["extensions"]["sdk"]["token"] + + # Edge case where a bug in a working-in-progress version of # https://github.com/psf/black/pull/3370 causes an infinite recursion. assert ( diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tricky_unicode_symbols_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tricky_unicode_symbols_py.snap deleted file mode 100644 index 01c1dbbadd..0000000000 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tricky_unicode_symbols_py.snap +++ /dev/null @@ -1,61 +0,0 @@ ---- -source: crates/ruff_python_formatter/src/lib.rs -expression: snapshot -input_file: crates/ruff_python_formatter/resources/test/fixtures/black/simple_cases/tricky_unicode_symbols.py ---- -## Input - -```py -ä = 1 -µ = 2 -蟒 = 3 -x󠄀 = 4 -មុ = 1 -Q̇_per_meter = 4 - -A᧚ = 3 -A፩ = 8 -``` - -## Black Differences - -```diff ---- Black -+++ Ruff -@@ -4,6 +4,5 @@ - x󠄀 = 4 - មុ = 1 - Q̇_per_meter = 4 -- - A᧚ = 3 - A፩ = 8 -``` - -## Ruff Output - -```py -ä = 1 -µ = 2 -蟒 = 3 -x󠄀 = 4 -មុ = 1 -Q̇_per_meter = 4 -A᧚ = 3 -A፩ = 8 -``` - -## Black Output - -```py -ä = 1 -µ = 2 -蟒 = 3 -x󠄀 = 4 -មុ = 1 -Q̇_per_meter = 4 - -A᧚ = 3 -A፩ = 8 -``` - - diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap index a1a1c9cf2f..084d542640 100644 --- a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__black_test__tupleassign_py.snap @@ -20,7 +20,7 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") ```diff --- Black +++ Ruff -@@ -1,12 +1,5 @@ +@@ -1,12 +1,6 @@ # This is a standalone comment. -( - sdfjklsdfsjldkflkjsf, @@ -32,8 +32,8 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") +sdfjklsdfsjldkflkjsf, sdfjsdfjlksdljkfsdlkf, sdfsdjfklsdfjlksdljkf, sdsfsdfjskdflsfsdf = 1, 2, 3 # This is as well. -(this_will_be_wrapped_in_parens,) = struct.unpack(b"12345678901234567890") -- +this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") + (a,) = call() ``` @@ -44,6 +44,7 @@ this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") sdfjklsdfsjldkflkjsf, sdfjsdfjlksdljkfsdlkf, sdfsdjfklsdfjlksdljkf, sdsfsdfjskdflsfsdf = 1, 2, 3 # This is as well. this_will_be_wrapped_in_parens, = struct.unpack(b"12345678901234567890") + (a,) = call() ``` diff --git a/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap new file mode 100644 index 0000000000..9ef6ca4e90 --- /dev/null +++ b/crates/ruff_python_formatter/src/snapshots/ruff_python_formatter__tests__ruff_test__while_py.snap @@ -0,0 +1,75 @@ +--- +source: crates/ruff_python_formatter/src/lib.rs +expression: snapshot +--- +## Input +```py +while 34: # trailing test comment + pass # trailing last statement comment + + # trailing while body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +while aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn: # trailing comment + pass + +else: + ... + +while ( + some_condition(unformatted, args) and anotherCondition or aThirdCondition +): # comment + print("Do something") + + +while ( + some_condition(unformatted, args) # trailing some condition + and anotherCondition or aThirdCondition # trailing third condition +): # comment + print("Do something") +``` + + + +## Output +```py +while 34: # trailing test comment + pass # trailing last statement comment + + # trailing while body comment + +# leading else comment + +else: # trailing else comment + pass + + # trailing else body comment + + +while ( + aVeryLongConditionThatSpillsOverToTheNextLineBecauseItIsExtremelyLongAndGoesOnAndOnAndOnAndOnAndOnAndOnAndOnAndOnAndOn +): # trailing comment + pass + +else: + ... + +while some_condition(unformatted, args) and anotherCondition or aThirdCondition: # comment + print("Do something") + + +while ( + some_condition(unformatted, args) # trailing some condition + and anotherCondition or aThirdCondition # trailing third condition +): # comment + print("Do something") +``` + + diff --git a/crates/ruff_python_formatter/src/statement/stmt_while.rs b/crates/ruff_python_formatter/src/statement/stmt_while.rs index c11cd9cbf4..7ff55a21de 100644 --- a/crates/ruff_python_formatter/src/statement/stmt_while.rs +++ b/crates/ruff_python_formatter/src/statement/stmt_while.rs @@ -1,12 +1,68 @@ -use crate::{verbatim_text, FormatNodeRule, PyFormatter}; -use ruff_formatter::{write, Buffer, FormatResult}; -use rustpython_parser::ast::StmtWhile; +use crate::comments::{leading_alternate_branch_comments, trailing_comments}; +use crate::expression::maybe_parenthesize::maybe_parenthesize; +use crate::prelude::*; +use crate::FormatNodeRule; +use ruff_formatter::write; +use ruff_python_ast::node::AstNode; +use rustpython_parser::ast::{Ranged, Stmt, StmtWhile}; #[derive(Default)] pub struct FormatStmtWhile; impl FormatNodeRule for FormatStmtWhile { fn fmt_fields(&self, item: &StmtWhile, f: &mut PyFormatter) -> FormatResult<()> { - write!(f, [verbatim_text(item.range)]) + let StmtWhile { + range: _, + test, + body, + orelse, + } = item; + + let comments = f.context().comments().clone(); + let dangling_comments = comments.dangling_comments(item.as_any_node_ref()); + + let body_start = body.first().map_or(test.end(), Stmt::start); + let or_else_comments_start = + dangling_comments.partition_point(|comment| comment.slice().end() < body_start); + + let (trailing_condition_comments, or_else_comments) = + dangling_comments.split_at(or_else_comments_start); + + write!( + f, + [ + text("while"), + space(), + maybe_parenthesize(test), + text(":"), + trailing_comments(trailing_condition_comments), + block_indent(&body.format()) + ] + )?; + + if !orelse.is_empty() { + // Split between leading comments before the `else` keyword and end of line comments at the end of + // the `else:` line. + let trailing_start = + or_else_comments.partition_point(|comment| comment.position().is_own_line()); + let (leading, trailing) = or_else_comments.split_at(trailing_start); + + write!( + f, + [ + leading_alternate_branch_comments(leading, body.last()), + text("else:"), + trailing_comments(trailing), + block_indent(&orelse.format()) + ] + )?; + } + + Ok(()) + } + + fn fmt_dangling_comments(&self, _node: &StmtWhile, _f: &mut PyFormatter) -> FormatResult<()> { + // Handled in `fmt_fields` + Ok(()) } } diff --git a/crates/ruff_python_formatter/src/statement/suite.rs b/crates/ruff_python_formatter/src/statement/suite.rs index 86c7654c71..7f53ec0da3 100644 --- a/crates/ruff_python_formatter/src/statement/suite.rs +++ b/crates/ruff_python_formatter/src/statement/suite.rs @@ -28,10 +28,15 @@ impl Default for FormatSuite { impl FormatRule> for FormatSuite { fn fmt(&self, statements: &Suite, f: &mut PyFormatter) -> FormatResult<()> { - let mut joiner = f.join_nodes(match self.level { + let node_level = match self.level { SuiteLevel::TopLevel => NodeLevel::TopLevel, - SuiteLevel::Nested => NodeLevel::Statement, - }); + SuiteLevel::Nested => NodeLevel::CompoundStatement, + }; + + let saved_level = f.context().node_level(); + f.context_mut().set_node_level(node_level); + + let mut joiner = f.join_nodes(node_level); let mut iter = statements.iter(); let Some(first) = iter.next() else { @@ -67,7 +72,11 @@ impl FormatRule> for FormatSuite { is_last_function_or_class_definition = is_current_function_or_class_definition; } - joiner.finish() + let result = joiner.finish(); + + f.context_mut().set_node_level(saved_level); + + result } } diff --git a/crates/ruff_python_formatter/src/trivia.rs b/crates/ruff_python_formatter/src/trivia.rs index db99c85198..e1224883b0 100644 --- a/crates/ruff_python_formatter/src/trivia.rs +++ b/crates/ruff_python_formatter/src/trivia.rs @@ -41,7 +41,6 @@ pub(crate) fn find_first_non_trivia_character_in_range( } /// Returns the number of newlines between `offset` and the first non whitespace character in the source code. -#[allow(unused)] // TODO(micha) Remove after using for statements. pub(crate) fn lines_before(code: &str, offset: TextSize) -> u32 { let head = &code[TextRange::up_to(offset)]; let mut newlines = 0u32; diff --git a/crates/ruff_python_semantic/src/analyze/mod.rs b/crates/ruff_python_semantic/src/analyze/mod.rs index a4cd2fdf50..f8cb066480 100644 --- a/crates/ruff_python_semantic/src/analyze/mod.rs +++ b/crates/ruff_python_semantic/src/analyze/mod.rs @@ -1,5 +1,6 @@ pub mod branch_detection; pub mod function_type; pub mod logging; +pub mod type_inference; pub mod typing; pub mod visibility; diff --git a/crates/ruff_python_semantic/src/analyze/type_inference.rs b/crates/ruff_python_semantic/src/analyze/type_inference.rs new file mode 100644 index 0000000000..e040fe2805 --- /dev/null +++ b/crates/ruff_python_semantic/src/analyze/type_inference.rs @@ -0,0 +1,96 @@ +//! Analysis rules to perform basic type inference on individual expressions. + +use rustpython_parser::ast; +use rustpython_parser::ast::{Constant, Expr}; + +/// An extremely simple type inference system for individual expressions. +/// +/// This system can only represent and infer the types of simple data types +/// such as strings, integers, floats, and containers. It cannot infer the +/// types of variables or expressions that are not statically known from +/// individual AST nodes alone. +#[derive(Debug, Copy, Clone)] +pub enum PythonType { + /// A string literal, such as `"hello"`. + String, + /// A bytes literal, such as `b"hello"`. + Bytes, + /// An integer literal, such as `1` or `0x1`. + Integer, + /// A floating-point literal, such as `1.0` or `1e10`. + Float, + /// A complex literal, such as `1j` or `1+1j`. + Complex, + /// A boolean literal, such as `True` or `False`. + Bool, + /// A `None` literal, such as `None`. + None, + /// An ellipsis literal, such as `...`. + Ellipsis, + /// A dictionary literal, such as `{}` or `{"a": 1}`. + Dict, + /// A list literal, such as `[]` or `[i for i in range(3)]`. + List, + /// A set literal, such as `set()` or `{i for i in range(3)}`. + Set, + /// A tuple literal, such as `()` or `(1, 2, 3)`. + Tuple, + /// A generator expression, such as `(x for x in range(10))`. + Generator, + /// An unknown type, such as a variable or function call. + Unknown, +} + +impl From<&Expr> for PythonType { + fn from(expr: &Expr) -> Self { + match expr { + Expr::NamedExpr(ast::ExprNamedExpr { value, .. }) => (&**value).into(), + Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => (&**operand).into(), + Expr::Dict(_) => PythonType::Dict, + Expr::DictComp(_) => PythonType::Dict, + Expr::Set(_) => PythonType::Set, + Expr::SetComp(_) => PythonType::Set, + Expr::List(_) => PythonType::List, + Expr::ListComp(_) => PythonType::List, + Expr::Tuple(_) => PythonType::Tuple, + Expr::GeneratorExp(_) => PythonType::Generator, + Expr::JoinedStr(_) => PythonType::String, + Expr::BinOp(ast::ExprBinOp { left, op, .. }) => { + // Ex) "a" % "b" + if op.is_mod() { + if matches!( + left.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Str(..), + .. + }) + ) { + return PythonType::String; + } + if matches!( + left.as_ref(), + Expr::Constant(ast::ExprConstant { + value: Constant::Bytes(..), + .. + }) + ) { + return PythonType::Bytes; + } + } + PythonType::Unknown + } + Expr::Constant(ast::ExprConstant { value, .. }) => match value { + Constant::Str(_) => PythonType::String, + Constant::Int(_) => PythonType::Integer, + Constant::Float(_) => PythonType::Float, + Constant::Bool(_) => PythonType::Bool, + Constant::Complex { .. } => PythonType::Complex, + Constant::None => PythonType::None, + Constant::Ellipsis => PythonType::Ellipsis, + Constant::Bytes(_) => PythonType::Bytes, + Constant::Tuple(_) => PythonType::Tuple, + }, + _ => PythonType::Unknown, + } + } +} diff --git a/crates/ruff_python_semantic/src/analyze/typing.rs b/crates/ruff_python_semantic/src/analyze/typing.rs index 6e3ce7ad19..694e97e650 100644 --- a/crates/ruff_python_semantic/src/analyze/typing.rs +++ b/crates/ruff_python_semantic/src/analyze/typing.rs @@ -1,3 +1,5 @@ +//! Analysis rules for the `typing` module. + use rustpython_parser::ast::{self, Constant, Expr, Operator}; use num_traits::identities::Zero; diff --git a/crates/ruff_python_semantic/src/binding.rs b/crates/ruff_python_semantic/src/binding.rs index a384c54bc4..6d30793aa4 100644 --- a/crates/ruff_python_semantic/src/binding.rs +++ b/crates/ruff_python_semantic/src/binding.rs @@ -2,6 +2,7 @@ use std::ops::{Deref, DerefMut}; use bitflags::bitflags; use ruff_text_size::TextRange; +use rustpython_parser::ast::Ranged; use ruff_index::{newtype_index, IndexSlice, IndexVec}; use ruff_python_ast::helpers; @@ -48,36 +49,36 @@ impl<'a> Binding<'a> { /// Return `true` if this binding redefines the given binding. pub fn redefines(&self, existing: &'a Binding) -> bool { match &self.kind { - BindingKind::Importation(Importation { full_name }) => { + BindingKind::Importation(Importation { qualified_name }) => { if let BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: existing, + qualified_name: existing, }) = &existing.kind { - return full_name == existing; + return qualified_name == existing; } } - BindingKind::FromImportation(FromImportation { full_name }) => { + BindingKind::FromImportation(FromImportation { qualified_name }) => { if let BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: existing, + qualified_name: existing, }) = &existing.kind { - return full_name == existing; + return qualified_name == existing; } } - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => { + BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { match &existing.kind { BindingKind::Importation(Importation { - full_name: existing, + qualified_name: existing, }) | BindingKind::SubmoduleImportation(SubmoduleImportation { - full_name: existing, + qualified_name: existing, }) => { - return full_name == existing; + return qualified_name == existing; } BindingKind::FromImportation(FromImportation { - full_name: existing, + qualified_name: existing, }) => { - return full_name == existing; + return qualified_name == existing; } _ => {} } @@ -101,6 +102,37 @@ impl<'a> Binding<'a> { ) } + /// Returns the fully-qualified symbol name, if this symbol was imported from another module. + pub fn qualified_name(&self) -> Option<&str> { + match &self.kind { + BindingKind::Importation(Importation { qualified_name }) => Some(qualified_name), + BindingKind::FromImportation(FromImportation { qualified_name }) => { + Some(qualified_name) + } + BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + Some(qualified_name) + } + _ => None, + } + } + + /// Returns the fully-qualified name of the module from which this symbol was imported, if this + /// symbol was imported from another module. + pub fn module_name(&self) -> Option<&str> { + match &self.kind { + BindingKind::Importation(Importation { qualified_name }) + | BindingKind::SubmoduleImportation(SubmoduleImportation { qualified_name }) => { + Some(qualified_name.split('.').next().unwrap_or(qualified_name)) + } + BindingKind::FromImportation(FromImportation { qualified_name }) => Some( + qualified_name + .rsplit_once('.') + .map_or(qualified_name, |(module, _)| module), + ), + _ => None, + } + } + /// Returns the appropriate visual range for highlighting this binding. pub fn trimmed_range(&self, semantic_model: &SemanticModel, locator: &Locator) -> TextRange { match self.kind { @@ -112,6 +144,19 @@ impl<'a> Binding<'a> { _ => self.range, } } + + /// Returns the range of the binding's parent. + pub fn parent_range(&self, semantic_model: &SemanticModel) -> Option { + self.source + .map(|node_id| semantic_model.stmts[node_id]) + .and_then(|parent| { + if parent.is_import_from_stmt() { + Some(parent.range()) + } else { + None + } + }) + } } bitflags! { @@ -212,9 +257,9 @@ pub struct Export<'a> { #[derive(Clone, Debug)] pub struct Importation<'a> { /// The full name of the module being imported. - /// Ex) Given `import foo`, `full_name` would be "foo". - /// Ex) Given `import foo as bar`, `full_name` would be "foo". - pub full_name: &'a str, + /// Ex) Given `import foo`, `qualified_name` would be "foo". + /// Ex) Given `import foo as bar`, `qualified_name` would be "foo". + pub qualified_name: &'a str, } /// A binding for a member imported from a module, keyed on the name to which the member is bound. @@ -223,9 +268,9 @@ pub struct Importation<'a> { #[derive(Clone, Debug)] pub struct FromImportation { /// The full name of the member being imported. - /// Ex) Given `from foo import bar`, `full_name` would be "foo.bar". - /// Ex) Given `from foo import bar as baz`, `full_name` would be "foo.bar". - pub full_name: String, + /// Ex) Given `from foo import bar`, `qualified_name` would be "foo.bar". + /// Ex) Given `from foo import bar as baz`, `qualified_name` would be "foo.bar". + pub qualified_name: String, } /// A binding for a submodule imported from a module, keyed on the name of the parent module. @@ -233,8 +278,8 @@ pub struct FromImportation { #[derive(Clone, Debug)] pub struct SubmoduleImportation<'a> { /// The full name of the submodule being imported. - /// Ex) Given `import foo.bar`, `full_name` would be "foo.bar". - pub full_name: &'a str, + /// Ex) Given `import foo.bar`, `qualified_name` would be "foo.bar". + pub qualified_name: &'a str, } #[derive(Clone, Debug, is_macro::Is)] diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index ccb7a9ea8e..7b23c81b6a 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -47,7 +47,7 @@ pub struct SemanticModel<'a> { // Arena of global bindings. globals: GlobalsArena<'a>, // Map from binding index to indexes of bindings that shadow it in other scopes. - pub shadowed_bindings: HashMap, BuildNoHashHasher>, + pub shadowed_bindings: HashMap>, // Body iteration; used to peek at siblings. pub body: &'a [Stmt], pub body_index: usize, @@ -114,13 +114,54 @@ impl<'a> SemanticModel<'a> { false } - /// Return the current `Binding` for a given `name`. + /// Create a new [`Binding`] for a builtin. + pub fn push_builtin(&mut self) -> BindingId { + self.bindings.push(Binding { + range: TextRange::default(), + kind: BindingKind::Builtin, + references: Vec::new(), + flags: BindingFlags::empty(), + source: None, + context: ExecutionContext::Runtime, + exceptions: Exceptions::empty(), + }) + } + + /// Create a new [`Binding`] for the given `name` and `range`. + pub fn push_binding( + &mut self, + range: TextRange, + kind: BindingKind<'a>, + flags: BindingFlags, + ) -> BindingId { + self.bindings.push(Binding { + range, + kind, + flags, + references: Vec::new(), + source: self.stmt_id, + context: self.execution_context(), + exceptions: self.exceptions(), + }) + } + + /// Return the current [`Binding`] for a given `name`. pub fn find_binding(&self, member: &str) -> Option<&Binding> { self.scopes() .find_map(|scope| scope.get(member)) .map(|binding_id| &self.bindings[binding_id]) } + /// Return the [`Binding`] that the given [`BindingId`] shadows, if any. + /// + /// Note that this will only return bindings that are shadowed by a binding in a parent scope. + pub fn shadowed_binding(&self, binding_id: BindingId) -> Option<&Binding> { + self.shadowed_bindings + .get(&binding_id) + .copied() + .map(|id| &self.bindings[id]) + } + /// Return `true` if `member` is bound as a builtin. pub fn is_builtin(&self, member: &str) -> bool { self.find_binding(member) @@ -256,14 +297,8 @@ impl<'a> SemanticModel<'a> { // import pyarrow.csv // print(pa.csv.read_csv("test.csv")) // ``` - let full_name = match &self.bindings[binding_id].kind { - BindingKind::Importation(Importation { full_name }) => *full_name, - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name }) => *full_name, - BindingKind::FromImportation(FromImportation { full_name }) => full_name.as_str(), - _ => return None, - }; - - let has_alias = full_name + let qualified_name = self.bindings[binding_id].qualified_name()?; + let has_alias = qualified_name .split('.') .last() .map(|segment| segment != symbol) @@ -272,7 +307,7 @@ impl<'a> SemanticModel<'a> { return None; } - self.scopes[scope_id].get(full_name) + self.scopes[scope_id].get(qualified_name) } /// Resolves the [`Expr`] to a fully-qualified symbol-name, if `value` resolves to an imported @@ -292,7 +327,9 @@ impl<'a> SemanticModel<'a> { let head = call_path.first()?; let binding = self.find_binding(head)?; match &binding.kind { - BindingKind::Importation(Importation { full_name: name }) => { + BindingKind::Importation(Importation { + qualified_name: name, + }) => { if name.starts_with('.') { let mut source_path = from_relative_import(self.module_path?, name); if source_path.is_empty() { @@ -307,13 +344,17 @@ impl<'a> SemanticModel<'a> { Some(source_path) } } - BindingKind::SubmoduleImportation(SubmoduleImportation { full_name: name }) => { + BindingKind::SubmoduleImportation(SubmoduleImportation { + qualified_name: name, + }) => { let name = name.split('.').next().unwrap_or(name); let mut source_path: CallPath = from_unqualified_name(name); source_path.extend(call_path.into_iter().skip(1)); Some(source_path) } - BindingKind::FromImportation(FromImportation { full_name: name }) => { + BindingKind::FromImportation(FromImportation { + qualified_name: name, + }) => { if name.starts_with('.') { let mut source_path = from_relative_import(self.module_path?, name); if source_path.is_empty() { @@ -362,8 +403,8 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="sys"` and `object="exit"`: // `import sys` -> `sys.exit` // `import sys as sys2` -> `sys2.exit` - BindingKind::Importation(Importation { full_name }) => { - if full_name == &module { + BindingKind::Importation(Importation { qualified_name }) => { + if qualified_name == &module { if let Some(source) = binding.source { // Verify that `sys` isn't bound in an inner scope. if self @@ -383,8 +424,9 @@ impl<'a> SemanticModel<'a> { // Ex) Given `module="os.path"` and `object="join"`: // `from os.path import join` -> `join` // `from os.path import join as join2` -> `join2` - BindingKind::FromImportation(FromImportation { full_name }) => { - if let Some((target_module, target_member)) = full_name.split_once('.') { + BindingKind::FromImportation(FromImportation { qualified_name }) => { + if let Some((target_module, target_member)) = qualified_name.split_once('.') + { if target_module == module && target_member == member { if let Some(source) = binding.source { // Verify that `join` isn't bound in an inner scope. diff --git a/crates/ruff_python_semantic/src/scope.rs b/crates/ruff_python_semantic/src/scope.rs index 6d6c2bde29..09cadf47cf 100644 --- a/crates/ruff_python_semantic/src/scope.rs +++ b/crates/ruff_python_semantic/src/scope.rs @@ -1,3 +1,5 @@ +use nohash_hasher::{BuildNoHashHasher, IntMap}; +use std::collections::HashMap; use std::ops::{Deref, DerefMut}; use rustc_hash::FxHashMap; @@ -17,10 +19,10 @@ pub struct Scope<'a> { /// A list of star imports in this scope. These represent _module_ imports (e.g., `sys` in /// `from sys import *`), rather than individual bindings (e.g., individual members in `sys`). star_imports: Vec>, - /// A map from bound name to binding index, for current bindings. + /// A map from bound name to binding ID. bindings: FxHashMap<&'a str, BindingId>, - /// A map from bound name to binding index, for bindings that were shadowed later in the scope. - shadowed_bindings: FxHashMap<&'a str, Vec>, + /// A map from binding ID to binding ID that it shadows. + shadowed_bindings: HashMap>, /// Index into the globals arena, if the scope contains any globally-declared symbols. globals_id: Option, } @@ -33,7 +35,7 @@ impl<'a> Scope<'a> { uses_locals: false, star_imports: Vec::default(), bindings: FxHashMap::default(), - shadowed_bindings: FxHashMap::default(), + shadowed_bindings: IntMap::default(), globals_id: None, } } @@ -45,7 +47,7 @@ impl<'a> Scope<'a> { uses_locals: false, star_imports: Vec::default(), bindings: FxHashMap::default(), - shadowed_bindings: FxHashMap::default(), + shadowed_bindings: IntMap::default(), globals_id: None, } } @@ -57,9 +59,9 @@ impl<'a> Scope<'a> { /// Adds a new binding with the given name to this scope. pub fn add(&mut self, name: &'a str, id: BindingId) -> Option { - if let Some(id) = self.bindings.insert(name, id) { - self.shadowed_bindings.entry(name).or_default().push(id); - Some(id) + if let Some(shadowed) = self.bindings.insert(name, id) { + self.shadowed_bindings.insert(id, shadowed); + Some(shadowed) } else { None } @@ -88,11 +90,9 @@ impl<'a> Scope<'a> { /// Returns an iterator over all [bindings](BindingId) bound to the given name, including /// those that were shadowed by later bindings. pub fn bindings_for_name(&self, name: &str) -> impl Iterator + '_ { - self.bindings - .get(name) - .into_iter() - .chain(self.shadowed_bindings.get(name).into_iter().flatten().rev()) - .copied() + std::iter::successors(self.bindings.get(name).copied(), |id| { + self.shadowed_bindings.get(id).copied() + }) } /// Adds a reference to a star import (e.g., `from sys import *`) to this scope. diff --git a/ruff.schema.json b/ruff.schema.json index 63c49b5a87..737f0fdc4a 100644 --- a/ruff.schema.json +++ b/ruff.schema.json @@ -1639,6 +1639,7 @@ "B030", "B031", "B032", + "B033", "B9", "B90", "B904", @@ -2063,6 +2064,7 @@ "PLE03", "PLE030", "PLE0302", + "PLE0307", "PLE06", "PLE060", "PLE0604", @@ -2136,7 +2138,6 @@ "PLW0120", "PLW0129", "PLW013", - "PLW0130", "PLW0131", "PLW04", "PLW040", @@ -2241,6 +2242,7 @@ "PYI021", "PYI024", "PYI025", + "PYI029", "PYI03", "PYI032", "PYI033", diff --git a/rust-toolchain b/rust-toolchain index 883d7d6539..bfe79d0bdd 100644 --- a/rust-toolchain +++ b/rust-toolchain @@ -1 +1 @@ -1.69 +1.70