From 4aa4a39851a00049017182a9bc30a4e4cea8e305 Mon Sep 17 00:00:00 2001 From: Maxwell Muoto <41130755+max-muoto@users.noreply.github.com> Date: Mon, 27 Jan 2025 22:41:05 -0600 Subject: [PATCH] Upgrade Ruff creates --- Cargo.lock | 1437 ++++++++++++++++++++++---- Cargo.toml | 9 +- pyproject.toml | 2 +- python/tach/check_external.py | 4 +- python/tach/cli.py | 175 +--- python/tach/extension.pyi | 71 +- python/tach/modularity.py | 98 +- python/tests/test_check.py | 23 +- python/tests/test_cli.py | 70 +- python/tests/test_sync.py | 3 - rust-toolchain.toml | 2 + src/cli.rs | 52 + src/commands/check/check_external.rs | 246 ++--- src/commands/check/check_internal.rs | 236 ++--- src/commands/check/checks.rs | 348 +++++-- src/commands/check/diagnostics.rs | 215 ---- src/commands/check/error.rs | 2 + src/commands/check/format.rs | 190 ++++ src/commands/check/mod.rs | 3 +- src/commands/sync.rs | 18 +- src/diagnostics.rs | 397 +++++++ src/imports.rs | 4 +- src/interfaces/data_types.rs | 14 +- src/lib.rs | 23 +- src/lsp/server.rs | 62 +- src/modularity/diagnostics.rs | 81 ++ src/modularity/mod.rs | 3 + 27 files changed, 2629 insertions(+), 1159 deletions(-) create mode 100644 rust-toolchain.toml delete mode 100644 src/commands/check/diagnostics.rs create mode 100644 src/commands/check/format.rs create mode 100644 src/diagnostics.rs create mode 100644 src/modularity/diagnostics.rs create mode 100644 src/modularity/mod.rs diff --git a/Cargo.lock b/Cargo.lock index d5248136..08c442f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,12 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +version = 4 [[package]] name = "ahash" @@ -31,21 +25,48 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.18" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7021ce4924a3f25f802b2cccd1af585e39ea1a363a1aa2e72afe54b67a3a7a7" + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" @@ -55,15 +76,21 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" + +[[package]] +name = "boxcar" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "2721c3c5a6f0e7f7e607125d963fedeb765f545f67adc9d71ed934693881eb42" [[package]] name = "bstr" -version = "1.9.1" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0" dependencies = [ "memchr", "regex-automata", @@ -92,7 +119,7 @@ dependencies = [ "cached_proc_macro", "cached_proc_macro_types", "directories", - "hashbrown", + "hashbrown 0.14.5", "once_cell", "rmp-serde", "serde", @@ -119,6 +146,24 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -131,6 +176,70 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chic" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b5db619f3556839cb2223ae86ff3f9a09da2c5013be42bc9af08c9589bf70c" +dependencies = [ + "annotate-snippets", +] + +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.6", +] + +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "serde", + "static_assertions", +] + +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.0", + "windows-sys 0.59.0", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crc32fast" version = "1.4.2" @@ -170,9 +279,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "ctrlc" @@ -186,9 +295,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ "darling_core", "darling_macro", @@ -196,9 +305,9 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", @@ -210,9 +319,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", @@ -240,12 +349,29 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.1" @@ -254,19 +380,40 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fern" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "filetime" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.59.0", +] [[package]] name = "fixedbitset" @@ -289,6 +436,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs2" version = "0.4.3" @@ -301,9 +457,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -316,9 +472,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -326,15 +482,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -349,9 +505,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -366,9 +522,9 @@ checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -378,9 +534,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -409,7 +565,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ - "unicode-width", + "unicode-width 0.1.14", ] [[package]] @@ -419,8 +575,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -452,26 +610,205 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "imperative" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a1f6526af721f9aec9ceed7ab8ebfca47f3399d08b80056c2acca3fcb694a9" +dependencies = [ + "phf", + "rust-stemmers", +] + [[package]] name = "indexmap" -version = "2.5.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", + "serde", ] [[package]] @@ -489,23 +826,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + [[package]] name = "is-macro" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a85abdc13717906baccb5a1e435556ce0df215f242892f721dff62bf25288f" +checksum = "1d57a3e447e24c22647738e4607f1df1e0ec6f72e16182c4cd199f647cdfb0e4" dependencies = [ - "Inflector", + "heck", "proc-macro2", "quote", "syn", ] +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itertools" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -521,24 +877,50 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ + "once_cell", "wasm-bindgen", ] [[package]] name = "libc" -version = "0.2.167" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d6582e104315a817dff97f75133544b2e094ee22447d2acf4a74e189ba06fc" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" + +[[package]] +name = "libcst" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649801a698a649791541a3125d396d5db065ed7cea53faca3652b0179394922a" +dependencies = [ + "chic", + "libcst_derive", + "memchr", + "paste", + "peg", + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "libcst_derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf66548c351bcaed792ef3e2b430cc840fbde504e09da6b29ed114ca60dcd4b" +dependencies = [ + "quote", + "syn", +] [[package]] name = "libredox" @@ -546,15 +928,22 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "libc", + "redox_syscall 0.5.8", ] [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -568,19 +957,20 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "lsp-server" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "550446e84739dcaf6d48a4a093973850669e13e8a34d8f8d64851041be267cd9" +checksum = "9462c4dc73e17f971ec1f171d44bfffb72e65a130117233388a0ebc7ec5656f9" dependencies = [ "crossbeam-channel", "log", "serde", + "serde_derive", "serde_json", ] @@ -597,6 +987,12 @@ dependencies = [ "serde_repr", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "memchr" version = "2.7.4" @@ -612,13 +1008,28 @@ dependencies = [ "autocfg", ] +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + +[[package]] +name = "newtype-uuid" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3224f0e8be7c2a1ebc77ef9c3eecb90f55c6594399ee825de964526b3c9056" +dependencies = [ + "uuid", +] + [[package]] name = "nix" version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "cfg_aliases", "libc", @@ -699,11 +1110,103 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "path-absolutize" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4af381fe79fa195b4909485d99f73a80792331df0625188e707854f0b3383f5" +dependencies = [ + "path-dedot", +] + +[[package]] +name = "path-dedot" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ba0ad7e047712414213ff67533e6dd477af0a4e1d14fb52343e53d30ea9397" +dependencies = [ + "once_cell", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "peg" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" + +[[package]] +name = "pep440_rs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c" +dependencies = [ + "once_cell", + "serde", + "unicode-width 0.2.0", + "unscanny", + "version-ranges", +] + +[[package]] +name = "pep508_rs" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faee7227064121fcadcd2ff788ea26f0d8f2bd23a0574da11eca23bc935bcc05" +dependencies = [ + "boxcar", + "indexmap", + "itertools 0.13.0", + "once_cell", + "pep440_rs", + "regex", + "rustc-hash", + "serde", + "smallvec", + "thiserror 1.0.69", + "unicode-width 0.2.0", + "url", + "urlencoding", + "version-ranges", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "petgraph" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b1374ec32450264534c67d1ccb5ca09818c4db8fd87cf97478d0df2fa44c65" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", "indexmap", @@ -711,18 +1214,18 @@ dependencies = [ [[package]] name = "phf" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ "phf_generator", "phf_shared", @@ -730,9 +1233,9 @@ dependencies = [ [[package]] name = "phf_generator" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared", "rand", @@ -740,18 +1243,18 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ "siphasher", ] [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -761,15 +1264,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "proc-macro-crate" @@ -782,18 +1288,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d922163ba1f79c04bc49073ba7b32fd5a8d3b76a87c955921234b8e77333c51" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" dependencies = [ "cfg-if", "indoc", @@ -809,9 +1315,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc38c5feeb496c8321091edf3d63e9a6829eab4b863b4a6a65f26f3e9cc6b179" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" dependencies = [ "once_cell", "target-lexicon", @@ -819,9 +1325,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94845622d88ae274d2729fcefc850e63d7a3ddff5e3ce11bd88486db9f1d357d" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" dependencies = [ "libc", "pyo3-build-config", @@ -829,9 +1335,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e655aad15e09b94ffdb3ce3d217acf652e26bbc37697ef012f5e5e348c716e5e" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -841,9 +1347,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1e3f09eecd94618f60a455a23def79f79eba4dc561a97324bf9ac8c6df30ce" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" dependencies = [ "heck", "proc-macro2", @@ -852,6 +1358,44 @@ dependencies = [ "syn", ] +[[package]] +name = "pyproject-toml" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643af57c3f36ba90a8b53e972727d8092f7408a9ebfbaf4c3d2c17b07c58d835" +dependencies = [ + "indexmap", + "pep440_rs", + "pep508_rs", + "serde", + "thiserror 1.0.69", + "toml", +] + +[[package]] +name = "quick-junit" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed1a693391a16317257103ad06a88c6529ac640846021da7c435a06fffdacd7" +dependencies = [ + "chrono", + "indexmap", + "newtype-uuid", + "quick-xml", + "strip-ansi-escapes", + "thiserror 2.0.11", + "uuid", +] + +[[package]] +name = "quick-xml" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.38" @@ -926,14 +1470,14 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] name = "redox_users" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", "libredox", @@ -954,9 +1498,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -1027,34 +1571,204 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ruff_annotate_snippets" +version = "0.1.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "anstyle", + "memchr", + "unicode-width 0.2.0", +] + +[[package]] +name = "ruff_cache" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "filetime", + "glob", + "globset", + "itertools 0.14.0", + "regex", + "seahash", +] + +[[package]] +name = "ruff_diagnostics" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "anyhow", + "is-macro", + "log", + "ruff_text_size", + "serde", +] + +[[package]] +name = "ruff_index" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "ruff_macros", +] + +[[package]] +name = "ruff_linter" +version = "0.9.3" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "aho-corasick", + "anyhow", + "bitflags 2.8.0", + "chrono", + "colored", + "fern", + "glob", + "globset", + "imperative", + "is-macro", + "is-wsl", + "itertools 0.14.0", + "libcst", + "log", + "memchr", + "natord", + "path-absolutize", + "pathdiff", + "pep440_rs", + "pyproject-toml", + "quick-junit", + "regex", + "ruff_annotate_snippets", + "ruff_cache", + "ruff_diagnostics", + "ruff_index", + "ruff_macros", + "ruff_notebook", + "ruff_python_ast", + "ruff_python_codegen", + "ruff_python_index", + "ruff_python_literal", + "ruff_python_parser", + "ruff_python_semantic", + "ruff_python_stdlib", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", + "rustc-hash", + "serde", + "serde_json", + "similar", + "smallvec", + "strum", + "strum_macros", + "thiserror 2.0.11", + "toml", + "typed-arena", + "unicode-normalization", + "unicode-width 0.2.0", + "unicode_names2", + "url", +] + +[[package]] +name = "ruff_macros" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "itertools 0.14.0", + "proc-macro2", + "quote", + "ruff_python_trivia", + "syn", +] + +[[package]] +name = "ruff_notebook" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "rand", + "ruff_diagnostics", + "ruff_source_file", + "ruff_text_size", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.11", + "uuid", +] + [[package]] name = "ruff_python_ast" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?tag=v0.4.5#550aa871d32b53a2f042fb0e7fea1080eadfa11d" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" dependencies = [ "aho-corasick", - "bitflags 2.6.0", + "bitflags 2.8.0", + "compact_str", "is-macro", - "itertools 0.12.1", - "once_cell", + "itertools 0.14.0", + "memchr", + "ruff_cache", + "ruff_macros", "ruff_python_trivia", "ruff_source_file", "ruff_text_size", "rustc-hash", + "serde", +] + +[[package]] +name = "ruff_python_codegen" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "ruff_python_ast", + "ruff_python_literal", + "ruff_python_parser", + "ruff_source_file", + "ruff_text_size", +] + +[[package]] +name = "ruff_python_index" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "ruff_python_ast", + "ruff_python_parser", + "ruff_python_trivia", + "ruff_source_file", + "ruff_text_size", +] + +[[package]] +name = "ruff_python_literal" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "bitflags 2.8.0", + "itertools 0.14.0", + "ruff_python_ast", + "unic-ucd-category", ] [[package]] name = "ruff_python_parser" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?tag=v0.4.5#550aa871d32b53a2f042fb0e7fea1080eadfa11d" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" dependencies = [ - "anyhow", - "bitflags 2.6.0", + "bitflags 2.8.0", "bstr", - "is-macro", - "itertools 0.12.1", + "compact_str", "memchr", "ruff_python_ast", + "ruff_python_trivia", "ruff_text_size", "rustc-hash", "static_assertions", @@ -1063,12 +1777,39 @@ dependencies = [ "unicode_names2", ] +[[package]] +name = "ruff_python_semantic" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "bitflags 2.8.0", + "is-macro", + "ruff_cache", + "ruff_index", + "ruff_macros", + "ruff_python_ast", + "ruff_python_parser", + "ruff_python_stdlib", + "ruff_text_size", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "ruff_python_stdlib" +version = "0.0.0" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "bitflags 2.8.0", + "unicode-ident", +] + [[package]] name = "ruff_python_trivia" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?tag=v0.4.5#550aa871d32b53a2f042fb0e7fea1080eadfa11d" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" dependencies = [ - "itertools 0.12.1", + "itertools 0.14.0", "ruff_source_file", "ruff_text_size", "unicode-ident", @@ -1077,23 +1818,36 @@ dependencies = [ [[package]] name = "ruff_source_file" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?tag=v0.4.5#550aa871d32b53a2f042fb0e7fea1080eadfa11d" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" dependencies = [ "memchr", - "once_cell", "ruff_text_size", + "serde", ] [[package]] name = "ruff_text_size" version = "0.0.0" -source = "git+https://github.com/astral-sh/ruff.git?tag=v0.4.5#550aa871d32b53a2f042fb0e7fea1080eadfa11d" +source = "git+https://github.com/astral-sh/ruff.git?tag=0.9.3#90589372daf58ec4d314cbd15db8d2ef572c33cc" +dependencies = [ + "serde", +] + +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] [[package]] name = "rustc-hash" -version = "1.1.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" [[package]] name = "rustc_version" @@ -1106,17 +1860,23 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] +[[package]] +name = "rustversion" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" + [[package]] name = "ryu" version = "1.0.18" @@ -1153,26 +1913,32 @@ version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "478f121bb72bbf63c52c93011ea1791dca40140dfe13f8336c4c5ac952c33aa9" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "semver" -version = "1.0.23" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -1181,9 +1947,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.134" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -1204,11 +1970,34 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_with" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "serde", + "serde_derive", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -1236,11 +2025,23 @@ dependencies = [ "syn", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" @@ -1273,34 +2074,83 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "syn" -version = "2.0.87" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tach" version = "0.23.0" dependencies = [ "cached", + "console", "crossbeam-channel", "ctrlc", "glob", @@ -1315,14 +2165,14 @@ dependencies = [ "rayon", "regex", "rstest", + "ruff_linter", "ruff_python_ast", "ruff_python_parser", - "ruff_source_file", "serde", "serde_json", "serial_test", "tempfile", - "thiserror 2.0.7", + "thiserror 2.0.11", "toml", "toml_edit", "walkdir", @@ -1330,9 +2180,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" @@ -1359,11 +2209,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.7", + "thiserror-impl 2.0.11", ] [[package]] @@ -1379,20 +2229,30 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] @@ -1437,32 +2297,86 @@ dependencies = [ "winnow", ] +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-category" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" +dependencies = [ + "matches", + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "11cd88e12b17c6494200a9c1b683a04fcac9573ed74cd1b62aeb2727c5592243" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode_names2" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addeebf294df7922a1164f729fb27ebbbcea99cc32b3bf08afab62757f707677" +checksum = "d1673eca9782c84de5f81b82e4109dcfb3611c8ba0d52930ec4a9478f547b2dd" dependencies = [ "phf", "unicode_names2_generator", @@ -1470,9 +2384,9 @@ dependencies = [ [[package]] name = "unicode_names2_generator" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f444b8bba042fe3c1251ffaca35c603f2dc2ccc08d595c65a8c4f76f3e8426c0" +checksum = "b91e5b84611016120197efd7dc93ef76774f4e084cd73c9fb3ea4a86c570c56e" dependencies = [ "getopts", "log", @@ -1486,11 +2400,88 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" +dependencies = [ + "getrandom", + "rand", + "uuid-macro-internal", + "wasm-bindgen", +] + +[[package]] +name = "uuid-macro-internal" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a86d88347b61a0e17b9908a67efcc594130830bf1045653784358dd023e294" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "version-ranges" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d079415ceb2be83fc355adbadafe401307d5c309c7e6ade6638e6f9f42f42d" +dependencies = [ + "smallvec", +] + [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vte" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] [[package]] name = "walkdir" @@ -1510,23 +2501,24 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -1535,9 +2527,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1545,9 +2537,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", @@ -1558,9 +2550,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-time" @@ -1590,11 +2585,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1604,21 +2599,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] name = "windows-sys" -version = "0.52.0" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -1753,19 +2748,56 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ + "byteorder", "zerocopy-derive", ] @@ -1779,3 +2811,46 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index 3b23bd46..e11f4bfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,16 +13,16 @@ pyo3 = { version = "0.22.5", features = ["abi3-py37"] } regex = "1.11.1" once_cell = "1.20.2" walkdir = "2.5.0" -ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.5" } -ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.5" } -ruff_source_file = { git = "https://github.com/astral-sh/ruff.git", tag = "v0.4.5" } +ruff_python_ast = { git = "https://github.com/astral-sh/ruff.git", tag = "0.9.3" } +ruff_python_parser = { git = "https://github.com/astral-sh/ruff.git", tag = "0.9.3" } +ruff_linter = { git = "https://github.com/astral-sh/ruff.git", tag = "0.9.3" } cached = { version = "0.54.0", features = ["disk_store"] } globset = "0.4.15" toml = "0.8.19" thiserror = "2.0.7" serde = { version = "1.0.216", features = ["derive"] } glob = "0.3.2" -petgraph = "0.7.0" +petgraph = "0.7.1" serde_json = "1.0.134" tempfile = "3.15.0" lsp-server = "0.7.7" @@ -33,6 +33,7 @@ rayon = "1.10.0" parking_lot = "0.12.3" itertools = "0.14.0" toml_edit = "0.22.22" +console = "0.15.10" [features] extension-module = ["pyo3/extension-module"] diff --git a/pyproject.toml b/pyproject.toml index 4a487fe6..f7e326d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dev = [ "pip==24.0", # Code Quality "pyright==1.1.389", - "ruff==0.8.3", + "ruff==0.9.3", # Build/Release "setuptools==69.5.1; python_version > '3.7'", "twine==5.1.1; python_version > '3.7'", diff --git a/python/tach/check_external.py b/python/tach/check_external.py index dedaed0a..d55a5642 100644 --- a/python/tach/check_external.py +++ b/python/tach/check_external.py @@ -4,7 +4,7 @@ from tach.errors import TachError from tach.extension import ( - ExternalCheckDiagnostics, + Diagnostic, check_external_dependencies, set_excluded_paths, ) @@ -34,7 +34,7 @@ def check_external( project_root: Path, project_config: ProjectConfig, exclude_paths: list[str], -) -> ExternalCheckDiagnostics: +) -> list[Diagnostic]: set_excluded_paths( project_root=str(project_root), exclude_paths=exclude_paths, diff --git a/python/tach/cli.py b/python/tach/cli.py index cae576b0..31109b77 100644 --- a/python/tach/cli.py +++ b/python/tach/cli.py @@ -27,7 +27,9 @@ check_computation_cache, create_computation_cache_key, detect_unused_dependencies, + format_diagnostics, run_server, + serialize_diagnostics_json, update_computation_cache, ) from tach.filesystem import install_pre_commit @@ -42,101 +44,9 @@ ) from tach.sync import sync_project from tach.test import run_affected_tests -from tach.utils.display import create_clickable_link if TYPE_CHECKING: - from tach.extension import BoundaryError, UnusedDependencies - - -def build_error_message(error: BoundaryError, source_roots: list[Path]) -> str: - absolute_error_path = next( - ( - source_root / error.file_path - for source_root in source_roots - if (source_root / error.file_path).exists() - ), - None, - ) - - if absolute_error_path is None: - # This is an unexpected case, - # all errors should have originated from within a source root - error_location = error.file_path - else: - error_location = create_clickable_link( - absolute_error_path, - display_path=error.file_path, - line=error.line_number, - ) - - error_template = ( - f"{icons.FAIL} {BCOLORS.FAIL}{error_location}{BCOLORS.ENDC}{BCOLORS.WARNING}: " - f"{{message}} {BCOLORS.ENDC}" - ) - warning_template = ( - f"{icons.WARNING} {BCOLORS.FAIL}{error_location}{BCOLORS.ENDC}{BCOLORS.WARNING}: " - f"{{message}} {BCOLORS.ENDC}" - ) - error_info = error.error_info - if error_info.is_deprecated(): - return warning_template.format(message=error_info.to_pystring()) - return error_template.format(message=error_info.to_pystring()) - - -def print_warnings(warning_list: list[str]) -> None: - for warning in warning_list: - print(f"{BCOLORS.WARNING}{warning}{BCOLORS.ENDC}", file=sys.stderr) - - -def print_errors(error_list: list[str]) -> None: - for error in error_list: - print(f"{BCOLORS.FAIL}{error}{BCOLORS.ENDC}", file=sys.stderr) - - -def print_boundary_errors( - error_list: list[BoundaryError], source_roots: list[Path] -) -> None: - if not error_list: - return - - interface_errors: list[BoundaryError] = [] - dependency_errors: list[BoundaryError] = [] - for error in sorted(error_list, key=lambda e: e.file_path): - if error.error_info.is_interface_error(): - interface_errors.append(error) - else: - dependency_errors.append(error) - - if interface_errors: - print(f"{BCOLORS.FAIL}Interface Errors:{BCOLORS.ENDC}", file=sys.stderr) - for error in interface_errors: - print( - build_error_message(error, source_roots=source_roots), - file=sys.stderr, - ) - print( - f"{BCOLORS.WARNING}\nIf you intended to change an interface, edit the '[[interfaces]]' section of {CONFIG_FILE_NAME}.toml." - f"\nOtherwise, remove any disallowed imports and consider refactoring.\n{BCOLORS.ENDC}", - file=sys.stderr, - ) - - if dependency_errors: - print(f"{BCOLORS.FAIL}Dependency Errors:{BCOLORS.ENDC}", file=sys.stderr) - has_real_errors = False - for error in dependency_errors: - if not error.error_info.is_deprecated(): - has_real_errors = True - print( - build_error_message(error, source_roots=source_roots), - file=sys.stderr, - ) - print(file=sys.stderr) - if has_real_errors: - print( - f"{BCOLORS.WARNING}If you intended to add a new dependency, run 'tach sync' to update your module configuration." - f"\nOtherwise, remove any disallowed imports and consider refactoring.\n{BCOLORS.ENDC}", - file=sys.stderr, - ) + from tach.extension import UnusedDependencies def print_unused_dependencies( @@ -246,46 +156,6 @@ def print_visibility_errors( ) -def print_undeclared_dependencies( - undeclared_dependencies: dict[str, list[str]], -) -> None: - any_undeclared = False - for file_path, dependencies in undeclared_dependencies.items(): - if dependencies: - any_undeclared = True - print( - f"{icons.FAIL}: {BCOLORS.FAIL}Undeclared dependencies in {BCOLORS.ENDC}{BCOLORS.WARNING}'{file_path}'{BCOLORS.ENDC}:" - ) - for dependency in dependencies: - print(f"\t{BCOLORS.FAIL}{dependency}{BCOLORS.ENDC}") - if any_undeclared: - print( - f"{BCOLORS.WARNING}\nAdd the undeclared dependencies to the corresponding pyproject.toml file, " - f"or consider ignoring the dependencies by adding them to the 'external.exclude' list in {CONFIG_FILE_NAME}.toml.\n{BCOLORS.ENDC}", - file=sys.stderr, - ) - - -def print_unused_external_dependencies( - unused_dependencies: dict[str, list[str]], -) -> None: - any_unused = False - for pyproject_path, dependencies in unused_dependencies.items(): - if dependencies: - any_unused = True - print( - f"{icons.WARNING} {BCOLORS.WARNING}Unused dependencies from project at {BCOLORS.OKCYAN}'{pyproject_path}'{BCOLORS.ENDC}{BCOLORS.ENDC}:" - ) - for dependency in dependencies: - print(f"\t{BCOLORS.WARNING}{dependency}{BCOLORS.ENDC}") - if any_unused: - print( - f"{BCOLORS.OKCYAN}\nRemove the unused dependencies from the corresponding pyproject.toml file, " - f"or consider ignoring the dependencies by adding them to the 'external.exclude' list in {CONFIG_FILE_NAME}.toml.\n{BCOLORS.ENDC}", - file=sys.stderr, - ) - - def add_base_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( "-e", @@ -626,33 +496,28 @@ def tach_check( try: exact |= project_config.exact - check_result = check( + diagnostics = check( project_root=project_root, project_config=project_config, dependencies=dependencies, interfaces=interfaces, exclude_paths=exclude_paths, ) + has_errors = any(diagnostic.is_error() for diagnostic in diagnostics) if output_format == "json": try: - print(check_result.serialize_json(pretty_print=True)) + print(serialize_diagnostics_json(diagnostics, pretty_print=True)) except ValueError as e: json.dump({"error": str(e)}, sys.stdout) - sys.exit(1 if len(check_result.errors) > 0 else 0) - - if check_result.warnings: - print_warnings(check_result.warnings) - - source_roots = [ - project_root / source_root for source_root in project_config.source_roots - ] + sys.exit(1 if has_errors else 0) - print_boundary_errors( - check_result.errors + check_result.deprecated_warnings, - source_roots=source_roots, - ) - exit_code = 1 if len(check_result.errors) > 0 else 0 + if diagnostics: + print( + format_diagnostics(project_root=project_root, diagnostics=diagnostics), + file=sys.stderr, + ) + exit_code = 1 if has_errors else 0 # If we're checking in exact mode, we want to verify that there are no unused dependencies if dependencies and exact: @@ -697,18 +562,20 @@ def tach_check_external( }, ) try: - result = check_external( + diagnostics = check_external( project_root=project_root, project_config=project_config, exclude_paths=exclude_paths, ) - print_warnings(result.warnings) - print_errors(result.errors) - print_unused_external_dependencies(result.unused_dependencies) - print_undeclared_dependencies(result.undeclared_dependencies) + if diagnostics: + print( + format_diagnostics(project_root=project_root, diagnostics=diagnostics), + file=sys.stderr, + ) - if result.errors or result.undeclared_dependencies: + has_errors = any(diagnostic.is_error() for diagnostic in diagnostics) + if has_errors: sys.exit(1) else: print( diff --git a/python/tach/extension.pyi b/python/tach/extension.pyi index eb81741d..0c896c7b 100644 --- a/python/tach/extension.pyi +++ b/python/tach/extension.pyi @@ -21,12 +21,6 @@ def get_normalized_imports( def set_excluded_paths( project_root: str, exclude_paths: list[str], use_regex_matching: bool ) -> None: ... -def check_external_dependencies( - project_root: str, - project_config: ProjectConfig, - module_mappings: dict[str, list[str]], - stdlib_modules: list[str], -) -> ExternalCheckDiagnostics: ... def create_dependency_report( project_root: str, project_config: ProjectConfig, @@ -60,7 +54,17 @@ def check( dependencies: bool, interfaces: bool, exclude_paths: list[str], -) -> CheckDiagnostics: ... +) -> list[Diagnostic]: ... +def check_external_dependencies( + project_root: str, + project_config: ProjectConfig, + module_mappings: dict[str, list[str]], + stdlib_modules: list[str], +) -> list[Diagnostic]: ... +def format_diagnostics( + project_root: Path, + diagnostics: list[Diagnostic], +) -> str: ... def detect_unused_dependencies( project_root: Path, project_config: ProjectConfig, @@ -75,38 +79,37 @@ def sync_project( def run_server(project_root: Path, project_config: ProjectConfig) -> None: ... def serialize_modules_json(modules: list[ModuleConfig]) -> str: ... -class ErrorInfo: +class Diagnostic: + def is_code(self) -> bool: ... + def is_configuration(self) -> bool: ... def is_dependency_error(self) -> bool: ... def is_interface_error(self) -> bool: ... - def to_pystring(self) -> str: ... + def is_warning(self) -> bool: ... + def is_error(self) -> bool: ... def is_deprecated(self) -> bool: ... + def usage_module(self) -> str | None: ... + def definition_module(self) -> str | None: ... + def to_string(self) -> str: ... + def pyfile_path(self) -> str | None: ... + def pyline_number(self) -> int | None: ... + +def serialize_diagnostics_json( + diagnostics: list[Diagnostic], pretty_print: bool +) -> str: ... -class BoundaryError: - file_path: Path - line_number: int - import_mod_path: str - error_info: ErrorInfo - -class CheckDiagnostics: - errors: list[BoundaryError] - deprecated_warnings: list[BoundaryError] - warnings: list[str] - - def serialize_json(self, pretty_print: bool = False) -> str: ... - -class ExternalCheckDiagnostics: - undeclared_dependencies: dict[str, list[str]] - unused_dependencies: dict[str, list[str]] - errors: list[str] - warnings: list[str] +ErrorKind = Literal["DEPENDENCY", "INTERFACE"] - def __new__( - cls, - undeclared_dependencies: dict[str, list[str]], - unused_dependencies: dict[str, list[str]], - errors: list[str], - warnings: list[str], - ) -> ExternalCheckDiagnostics: ... +class UsageError: + file: str + line_number: int + member: str + # The module that contains the usage + usage_module: str + # The module that contains the definition + definition_module: str + error_type: ErrorKind + +def into_usage_errors(diagnostics: list[Diagnostic]) -> list[UsageError]: ... class DependencyConfig: path: str diff --git a/python/tach/modularity.py b/python/tach/modularity.py index bc89fd8f..a9ef0d79 100644 --- a/python/tach/modularity.py +++ b/python/tach/modularity.py @@ -8,15 +8,20 @@ from typing import TYPE_CHECKING, Any from urllib import parse +from typing_extensions import Literal + from tach import filesystem as fs from tach.colors import BCOLORS from tach.constants import GAUGE_API_BASE_URL from tach.errors import TachClosedBetaError, TachError from tach.extension import ( - CheckDiagnostics, ProjectConfig, check, get_project_imports, + into_usage_errors, +) +from tach.extension import ( + UsageError as ExtUsageError, ) from tach.filesystem.git_ops import get_current_branch_info from tach.parsing import extend_and_validate @@ -54,7 +59,7 @@ def upload_report_to_gauge( GAUGE_API_KEY = os.getenv("GAUGE_API_KEY", "") -GAUGE_UPLOAD_URL = f"{GAUGE_API_BASE_URL}/api/client/tach-upload/1.3" +GAUGE_UPLOAD_URL = f"{GAUGE_API_BASE_URL}/api/client/tach-upload/1.4" def post_json_to_gauge_api( @@ -125,8 +130,6 @@ class Dependency: @dataclass class Module: path: str - # [1.2] Deprecated - is_strict: bool = False # [1.2] Replaces 'is_strict' has_interface: bool = False interface_members: list[str] = field(default_factory=list) @@ -134,7 +137,7 @@ class Module: depends_on: list[Dependency] = field(default_factory=list) -REPORT_VERSION = "1.3" +REPORT_VERSION = "1.4" @dataclass @@ -143,25 +146,27 @@ class ReportMetadata: configuration_format: str = "json" +# This is unfortunately necessary because we use 'asdict' on the report +# This will be removed once we move report generation to Rust @dataclass -class ErrorInfo: - is_deprecated: bool - pystring: str - - -@dataclass -class BoundaryError: - file_path: str +class UsageError: + file: str line_number: int - import_mod_path: str - error_info: ErrorInfo - - -@dataclass -class CheckResult: - errors: list[BoundaryError] = field(default_factory=list) - deprecated_warnings: list[BoundaryError] = field(default_factory=list) - warnings: list[str] = field(default_factory=list) + member: str + usage_module: str + definition_module: str + error_type: Literal["DEPENDENCY", "INTERFACE"] + + @classmethod + def from_extension(cls, ext_usage_error: ExtUsageError) -> UsageError: + return cls( + file=ext_usage_error.file, + line_number=ext_usage_error.line_number, + member=ext_usage_error.member, + usage_module=ext_usage_error.usage_module, + definition_module=ext_usage_error.definition_module, + error_type=ext_usage_error.error_type, + ) @dataclass @@ -176,11 +181,8 @@ class Report: full_configuration: str modules: list[Module] = field(default_factory=list) usages: list[Usage] = field(default_factory=list) - # [1.3] Check result for dependency errors - check_result: CheckResult = field(default_factory=CheckResult) - metadata: ReportMetadata = field(default_factory=ReportMetadata) - # [1.2] Deprecated - interface_rules: list[Any] = field(default_factory=list) + # [1.4] Diagnostics + diagnostics: list[UsageError] = field(default_factory=list) metadata: ReportMetadata = field(default_factory=ReportMetadata) @@ -195,7 +197,8 @@ def build_modules(project_config: ProjectConfig) -> list[Module]: interface_members: set[str] = set() for interface in project_config.all_interfaces(): if any( - re.match(pattern, module.path) for pattern in interface.from_modules + re.match(r"^" + pattern + r"$", module.path) + for pattern in interface.from_modules ): has_interface = True interface_members.update(interface.expose) @@ -273,36 +276,6 @@ def get_containing_module(mod_path: str) -> str | None: return usages -def process_check_result(check_diagnostics: CheckDiagnostics) -> CheckResult: - return CheckResult( - errors=[ - BoundaryError( - file_path=str(error.file_path), - line_number=error.line_number, - import_mod_path=error.import_mod_path, - error_info=ErrorInfo( - is_deprecated=error.error_info.is_deprecated(), - pystring=error.error_info.to_pystring(), - ), - ) - for error in check_diagnostics.errors - ], - deprecated_warnings=[ - BoundaryError( - file_path=str(warning.file_path), - line_number=warning.line_number, - import_mod_path=warning.import_mod_path, - error_info=ErrorInfo( - is_deprecated=warning.error_info.is_deprecated(), - pystring=warning.error_info.to_pystring(), - ), - ) - for warning in check_diagnostics.deprecated_warnings - ], - warnings=check_diagnostics.warnings, - ) - - def generate_modularity_report( project_root: Path, project_config: ProjectConfig, force: bool = False ) -> Report: @@ -329,9 +302,14 @@ def generate_modularity_report( project_config=project_config, exclude_paths=exclude_paths, dependencies=True, - interfaces=False, # for now leave this as a separate concern + interfaces=True, + ) + report.diagnostics = list( + map( + lambda ext_usage_error: UsageError.from_extension(ext_usage_error), + into_usage_errors(check_diagnostics), + ) ) - report.check_result = process_check_result(check_diagnostics) print(f"{BCOLORS.OKGREEN} > Report generated!{BCOLORS.ENDC}") return report diff --git a/python/tests/test_check.py b/python/tests/test_check.py index 9220efb0..60d325ac 100644 --- a/python/tests/test_check.py +++ b/python/tests/test_check.py @@ -7,7 +7,7 @@ from tach.cli import tach_check from tach.errors import TachCircularDependencyError, TachVisibilityError -from tach.extension import CheckDiagnostics +from tach.extension import Diagnostic from tach.icons import SUCCESS, WARNING from tach.parsing.config import parse_project_config @@ -25,7 +25,7 @@ def test_valid_example_dir(example_dir, capfd): assert exc_info.value.code == 0 captured = capfd.readouterr() assert SUCCESS in captured.out - assert WARNING in captured.err + assert WARNING in captured.err or "WARN" in captured.err def test_valid_example_dir_monorepo(example_dir): @@ -46,9 +46,11 @@ def test_check_json_output(example_dir, capfd, mocker): project_config = parse_project_config(root=project_root) assert project_config is not None - mock_diagnostics = NonCallableMagicMock(spec=CheckDiagnostics) - mock_diagnostics.serialize_json.return_value = json.dumps( - {"errors": [], "warnings": []} + mock_diagnostics = [NonCallableMagicMock(spec=Diagnostic)] + mock_diagnostics[0].is_error.return_value = False + mocker.patch( + "tach.cli.serialize_diagnostics_json", + return_value=json.dumps([{"hello": "world"}]), ) mocker.patch("tach.cli.check", return_value=mock_diagnostics) @@ -62,7 +64,7 @@ def test_check_json_output(example_dir, capfd, mocker): assert exc_info.value.code == 0 captured = capfd.readouterr() - assert json.loads(captured.out) == {"errors": [], "warnings": []} + assert json.loads(captured.out) == [{"hello": "world"}] def test_check_json_with_errors(example_dir, capfd, mocker): @@ -70,9 +72,12 @@ def test_check_json_with_errors(example_dir, capfd, mocker): project_config = parse_project_config(root=project_root) assert project_config is not None - mock_diagnostics = NonCallableMagicMock(spec=CheckDiagnostics) - mock_diagnostics.serialize_json.return_value = json.dumps( - {"errors": ["error1", "error2"], "warnings": ["warning1"]} + mock_diagnostics = [NonCallableMagicMock(spec=Diagnostic)] + mocker.patch( + "tach.cli.serialize_diagnostics_json", + return_value=json.dumps( + {"errors": ["error1", "error2"], "warnings": ["warning1"]} + ), ) mocker.patch("tach.cli.check", return_value=mock_diagnostics) diff --git a/python/tests/test_cli.py b/python/tests/test_cli.py index 079c71d0..6ed04640 100644 --- a/python/tests/test_cli.py +++ b/python/tests/test_cli.py @@ -1,6 +1,5 @@ from __future__ import annotations -from dataclasses import dataclass from pathlib import Path from unittest.mock import Mock @@ -10,46 +9,9 @@ from tach.extension import ProjectConfig -@dataclass -class ErrorInfo: - exception_message: str - dependency_error: bool = False - interface_error: bool = False - deprecated: bool = False - - def is_dependency_error(self) -> bool: - return self.dependency_error - - def is_interface_error(self) -> bool: - return self.interface_error - - def is_deprecated(self) -> bool: - return self.deprecated - - def to_pystring(self) -> str: - return self.exception_message - - -@dataclass -class BoundaryError: - file_path: Path - line_number: int - import_mod_path: str - error_info: ErrorInfo - - -@dataclass -class CheckDiagnostics: - errors: list[BoundaryError] - deprecated_warnings: list[BoundaryError] - warnings: list[str] - - @pytest.fixture def mock_check(mocker) -> Mock: - mock = Mock( - return_value=CheckDiagnostics(errors=[], deprecated_warnings=[], warnings=[]) - ) # default to a return with no errors + mock = Mock(return_value=[]) # default to a return with no errors mocker.patch("tach.cli.check", mock) return mock @@ -79,36 +41,6 @@ def test_execute_with_config(capfd, mock_check, mock_project_config): assert "All modules validated!" in captured.out -def test_execute_with_error(capfd, mock_check, mock_project_config): - # Mock an error returned from check - location = Path("valid_dir/file.py") - message = "Import valid_dir in valid_dir/file.py is blocked by boundary" - mock_check.return_value = CheckDiagnostics( - deprecated_warnings=[], - warnings=[], - errors=[ - BoundaryError( - file_path=location, - line_number=0, - import_mod_path="valid_dir", - error_info=ErrorInfo( - exception_message="Import valid_dir in valid_dir/file.py is blocked by boundary", - ), - ) - ], - ) - with pytest.raises(SystemExit) as sys_exit: - cli.tach_check( - project_root=Path(), - project_config=mock_project_config, - exclude_paths=mock_project_config.exclude, - ) - captured = capfd.readouterr() - assert sys_exit.value.code == 1 - assert str(location) in captured.err - assert message in captured.err - - def test_invalid_command(capfd): with pytest.raises(SystemExit) as sys_exit: # Test with an invalid command diff --git a/python/tests/test_sync.py b/python/tests/test_sync.py index 33063a7c..39bcb2f4 100644 --- a/python/tests/test_sync.py +++ b/python/tests/test_sync.py @@ -62,12 +62,9 @@ def test_distributed_config_dir(example_dir, capfd): assert exc_info.value.code == 0 captured = capfd.readouterr() - print(captured.err) - print(captured.out) assert "✅" in captured.out # success state project_config = parse_project_config(root=temp_project_root) - print(project_config.serialize_json()) assert project_config is not None modules = project_config.filtered_modules([]) diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..e0222de2 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.84" diff --git a/src/cli.rs b/src/cli.rs index cb5e1c56..feaa0d75 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,6 +1,8 @@ use std::env; use std::path::Path; +use console::Term; + #[derive(Debug, PartialEq, Eq)] enum TerminalEnvironment { Unknown, @@ -41,3 +43,53 @@ pub fn create_clickable_link(file_path: &Path, abs_path: &Path, line: &usize) -> let display_with_line = format!("{}[L{}]", file_path_str, line); format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", link, display_with_line) } + +pub fn supports_emoji() -> bool { + let term = Term::stdout(); + term.is_term() && term.features().wants_emoji() +} + +pub fn supports_colors() -> bool { + let term = Term::stdout(); + term.is_term() && term.features().colors_supported() +} + +pub struct EmojiIcons; + +impl EmojiIcons { + pub const SUCCESS: &str = "✅"; + pub const WARNING: &str = "⚠️ "; + pub const FAIL: &str = "❌"; +} + +pub struct SimpleIcons; + +impl SimpleIcons { + pub const SUCCESS: &str = "[OK]"; + pub const WARNING: &str = "[WARN]"; + pub const FAIL: &str = "[FAIL]"; +} + +pub fn success() -> &'static str { + if supports_emoji() { + EmojiIcons::SUCCESS + } else { + SimpleIcons::SUCCESS + } +} + +pub fn warning() -> &'static str { + if supports_emoji() { + EmojiIcons::WARNING + } else { + SimpleIcons::WARNING + } +} + +pub fn fail() -> &'static str { + if supports_emoji() { + EmojiIcons::FAIL + } else { + SimpleIcons::FAIL + } +} diff --git a/src/commands/check/check_external.rs b/src/commands/check/check_external.rs index b55ed306..2d61c0a8 100644 --- a/src/commands/check/check_external.rs +++ b/src/commands/check/check_external.rs @@ -1,45 +1,36 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; -use crate::config::{ProjectConfig, RuleSetting}; -use crate::external::parsing::ProjectInfo; -use crate::external::{parsing::normalize_package_name, parsing::parse_pyproject_toml}; +use crate::config::ProjectConfig; +use crate::external::parsing::parse_pyproject_toml; use crate::filesystem::relative_to; -use crate::imports::NormalizedImport; use crate::{filesystem, imports}; -use super::checks::check_missing_ignore_directive_reason; -use super::diagnostics::ExternalCheckDiagnostics; +use super::checks::{ + check_import_external, check_missing_ignore_directive_reason, + check_unused_ignore_directive_external, ImportProcessResult, +}; use super::error::ExternalCheckError; +use crate::diagnostics::{CodeDiagnostic, Diagnostic, DiagnosticDetails}; pub type Result = std::result::Result; -#[derive(Debug)] -enum ImportProcessResult { - UndeclaredDependency(String), - UsedDependencies(Vec), - Excluded(Vec), -} - pub fn check( project_root: &Path, project_config: &ProjectConfig, module_mappings: &HashMap>, stdlib_modules: &[String], -) -> Result { +) -> Result> { let stdlib_modules: HashSet = stdlib_modules.iter().cloned().collect(); let excluded_external_modules: HashSet = project_config.external.exclude.iter().cloned().collect(); let source_roots: Vec = project_config.prepend_roots(project_root); - let mut diagnostics = ExternalCheckDiagnostics::default(); + let mut diagnostics = vec![]; for pyproject in filesystem::walk_pyprojects(project_root.to_str().unwrap()) { let project_info = parse_pyproject_toml(&pyproject)?; let mut all_dependencies = project_info.dependencies.clone(); for source_root in &project_info.source_paths { for file_path in filesystem::walk_pyfiles(source_root.to_str().unwrap()) { let absolute_file_path = source_root.join(&file_path); - let display_file_path = relative_to(&absolute_file_path, project_root)? - .display() - .to_string(); if let Ok(project_imports) = imports::get_external_imports( &source_roots, @@ -47,7 +38,7 @@ pub fn check( project_config.ignore_type_checking_imports, ) { for import in project_imports.active_imports() { - match process_import( + match check_import_external( import, &project_info, module_mappings, @@ -55,11 +46,15 @@ pub fn check( &stdlib_modules, ) { ImportProcessResult::UndeclaredDependency(module_name) => { - let undeclared_dep_entry: &mut Vec = diagnostics - .undeclared_dependencies - .entry(display_file_path.to_string()) - .or_default(); - undeclared_dep_entry.push(module_name); + diagnostics.push(Diagnostic::new_located_error( + relative_to(&absolute_file_path, project_root)?, + import.import_line_no, + DiagnosticDetails::Code( + CodeDiagnostic::UndeclaredExternalDependency { + import_mod_path: module_name, + }, + ), + )); } ImportProcessResult::UsedDependencies(deps) | ImportProcessResult::Excluded(deps) => { @@ -71,145 +66,77 @@ pub fn check( } for directive_ignored_import in project_imports.directive_ignored_imports() { - if project_config.rules.require_ignore_directive_reasons != RuleSetting::Off - { - if let Err(e) = - check_missing_ignore_directive_reason(&directive_ignored_import) - { - match &project_config.rules.require_ignore_directive_reasons { - RuleSetting::Error => { - diagnostics.errors.push(format!( - "{}:{}: {}", - display_file_path, - directive_ignored_import.import.line_no, - e - )); - } - RuleSetting::Warn => { - diagnostics.warnings.push(format!( - "{}:{}: {}", - display_file_path, - directive_ignored_import.import.line_no, - e - )); - } - RuleSetting::Off => {} - } + match check_missing_ignore_directive_reason( + &directive_ignored_import, + project_config, + ) { + Ok(()) => {} + Err(diagnostic) => { + diagnostics.push(diagnostic.into_located( + relative_to(&absolute_file_path, project_root)?, + directive_ignored_import.import.line_no, + )); } } - if project_config.rules.unused_ignore_directives != RuleSetting::Off { - if let ImportProcessResult::UsedDependencies(_) - | ImportProcessResult::Excluded(_) = process_import( - directive_ignored_import.import, - &project_info, - module_mappings, - &excluded_external_modules, - &stdlib_modules, - ) { - match project_config.rules.unused_ignore_directives { - RuleSetting::Error => { - diagnostics.errors.push(format!( - "{}:{}: Unnecessary ignore directive for package: '{}'", - display_file_path, - directive_ignored_import.import.line_no, - directive_ignored_import.import.top_level_module_name() - )); - } - RuleSetting::Warn => { - diagnostics.warnings.push(format!( - "{}:{}: Unnecessary ignore directive for package: '{}'", - display_file_path, - directive_ignored_import.import.line_no, - directive_ignored_import.import.top_level_module_name() - )); - } - RuleSetting::Off => {} - } + match check_unused_ignore_directive_external( + &directive_ignored_import, + &project_info, + module_mappings, + &excluded_external_modules, + &stdlib_modules, + project_config, + ) { + Ok(()) => {} + Err(diagnostic) => { + diagnostics.push(diagnostic.into_located( + relative_to(&absolute_file_path, project_root)?, + directive_ignored_import.import.line_no, + )); } } } for unused_directive in project_imports.unused_ignore_directives() { - match project_config.rules.unused_ignore_directives { - RuleSetting::Error => { - diagnostics.errors.push(format!( - "{}:{}: Unused ignore directive: '{}'", - display_file_path, - unused_directive.line_no, - unused_directive.modules.join(",") - )); - } - RuleSetting::Warn => { - diagnostics.warnings.push(format!( - "{}:{}: Unused ignore directive: '{}'", - display_file_path, - unused_directive.line_no, - unused_directive.modules.join(",") - )); - } - RuleSetting::Off => {} + if let Ok(severity) = + (&project_config.rules.unused_ignore_directives).try_into() + { + diagnostics.push(Diagnostic::new_located( + severity, + DiagnosticDetails::Code(CodeDiagnostic::UnusedIgnoreDirective()), + relative_to(&absolute_file_path, project_root)?, + unused_directive.line_no, + )); } } } } } - diagnostics.unused_dependencies.insert( - relative_to(&pyproject, project_root)? - .to_string_lossy() - .to_string(), + diagnostics.extend( all_dependencies .into_iter() .filter(|dep| !excluded_external_modules.contains(dep)) // 'exclude' should hide unused errors unconditionally - .collect(), + .map(|dep| { + Diagnostic::new_global_error(DiagnosticDetails::Code( + CodeDiagnostic::UnusedExternalDependency { + package_module_name: dep, + }, + )) + }) + .collect::>(), ); } Ok(diagnostics) } -fn process_import( - import: &NormalizedImport, - project_info: &ProjectInfo, - module_mappings: &HashMap>, - excluded_external_modules: &HashSet, - stdlib_modules: &HashSet, -) -> ImportProcessResult { - let top_level_module_name = import.top_level_module_name().to_string(); - let default_distribution_names = vec![top_level_module_name.clone()]; - let distribution_names: Vec = module_mappings - .get(&top_level_module_name) - .unwrap_or(&default_distribution_names) - .iter() - .map(|dist_name| normalize_package_name(dist_name)) - .collect(); - - if distribution_names - .iter() - .any(|dist_name| excluded_external_modules.contains(dist_name)) - || stdlib_modules.contains(&top_level_module_name) - { - return ImportProcessResult::Excluded(distribution_names); - } - - let is_declared = distribution_names - .iter() - .any(|dist_name| project_info.dependencies.contains(dist_name)); - - if !is_declared { - ImportProcessResult::UndeclaredDependency(top_level_module_name.to_string()) - } else { - ImportProcessResult::UsedDependencies(distribution_names) - } -} - #[cfg(test)] mod tests { + use super::*; use crate::config::ProjectConfig; + use crate::diagnostics::Severity; use crate::tests::fixtures::example_dir; - - use super::*; use rstest::*; #[fixture] @@ -244,13 +171,21 @@ mod tests { module_mapping: HashMap>, ) { let project_root = example_dir.join("multi_package"); - let result = check(&project_root, &project_config, &module_mapping, &[]); - assert!(result.as_ref().unwrap().undeclared_dependencies.is_empty()); - let unused_dependency_root = "src/pack-a/pyproject.toml"; - assert!(result - .unwrap() - .unused_dependencies - .contains_key(unused_dependency_root)); + let result = check(&project_root, &project_config, &module_mapping, &[]).unwrap(); + assert_eq!(result.len(), 1); + assert!(matches!( + result[0], + Diagnostic::Global { + severity: Severity::Error, + details: DiagnosticDetails::Code(CodeDiagnostic::UnusedExternalDependency { .. }) + } + )); + assert_eq!( + result[0].details(), + &DiagnosticDetails::Code(CodeDiagnostic::UnusedExternalDependency { + package_module_name: "unused".to_string() + }) + ); } #[rstest] @@ -259,16 +194,19 @@ mod tests { project_config: ProjectConfig, ) { let project_root = example_dir.join("multi_package"); - let result = check(&project_root, &project_config, &HashMap::new(), &[]); - let expected_failure_path = "src/pack-a/src/myorg/pack_a/__init__.py"; - let r = result.unwrap(); - assert_eq!( - r.undeclared_dependencies.keys().collect::>(), - vec![expected_failure_path] - ); - assert_eq!( - r.undeclared_dependencies[expected_failure_path], - vec!["git"] - ); + let result = check(&project_root, &project_config, &HashMap::new(), &[]).unwrap(); + assert_eq!(result.len(), 3); + assert!(result.iter().any(|d| d.details() + == &DiagnosticDetails::Code(CodeDiagnostic::UndeclaredExternalDependency { + import_mod_path: "git".to_string() + }))); + assert!(result.iter().any(|d| d.details() + == &DiagnosticDetails::Code(CodeDiagnostic::UnusedExternalDependency { + package_module_name: "gitpython".to_string() + }))); + assert!(result.iter().any(|d| d.details() + == &DiagnosticDetails::Code(CodeDiagnostic::UnusedExternalDependency { + package_module_name: "unused".to_string() + }))); } } diff --git a/src/commands/check/check_internal.rs b/src/commands/check/check_internal.rs index 143081a1..5efce0bd 100644 --- a/src/commands/check/check_internal.rs +++ b/src/commands/check/check_internal.rs @@ -1,8 +1,10 @@ use super::checks::{ - check_import, check_missing_ignore_directive_reason, check_unused_ignore_directive, + check_import_internal, check_missing_ignore_directive_reason, + check_unused_ignore_directive_internal, }; -use super::diagnostics::{BoundaryError, CheckDiagnostics, ImportCheckError}; use super::error::CheckError; +use crate::diagnostics::{CodeDiagnostic, ConfigurationDiagnostic, Diagnostic, DiagnosticDetails}; +use crate::filesystem::relative_to; use std::{ path::{Path, PathBuf}, @@ -12,8 +14,9 @@ use std::{ use rayon::prelude::*; +use crate::modules::error::ModuleTreeError; use crate::{ - config::{root_module::RootModuleTreatment, ProjectConfig, RuleSetting}, + config::{root_module::RootModuleTreatment, ProjectConfig}, exclusion::set_excluded_paths, filesystem as fs, imports::{get_project_imports, ImportParseError}, @@ -22,8 +25,11 @@ use crate::{ modules::{build_module_tree, ModuleTree}, }; +pub type Result = std::result::Result; + fn process_file( file_path: PathBuf, + project_root: &Path, source_root: &Path, source_roots: &[PathBuf], module_tree: &ModuleTree, @@ -31,20 +37,25 @@ fn process_file( interface_checker: &Option, check_dependencies: bool, found_imports: &AtomicBool, -) -> Option { +) -> Result> { let abs_file_path = &source_root.join(&file_path); - let mod_path = fs::file_to_module_path(source_roots, abs_file_path).ok()?; - let nearest_module = module_tree.find_nearest(&mod_path)?; + let relative_file_path = relative_to(abs_file_path, project_root)?; + let mod_path = fs::file_to_module_path(source_roots, abs_file_path)?; + let nearest_module = module_tree + .find_nearest(&mod_path) + .ok_or(CheckError::ModuleTree(ModuleTreeError::ModuleNotFound( + mod_path.to_string(), + )))?; if nearest_module.is_unchecked() { - return None; + return Ok(vec![]); } if nearest_module.is_root() && project_config.root_module == RootModuleTreatment::Ignore { - return None; + return Ok(vec![]); } - let mut diagnostics = CheckDiagnostics::default(); + let mut diagnostics = vec![]; let project_imports = match get_project_imports( source_roots, abs_file_path, @@ -60,30 +71,23 @@ fn process_file( project_imports } Err(ImportParseError::Parsing { .. }) => { - diagnostics.warnings.push(format!( - "Skipped '{}' due to a syntax error.", - file_path.display() - )); - return Some(diagnostics); + return Ok(vec![Diagnostic::new_global_warning( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::SkippedFileSyntaxError { + file_path: relative_file_path.display().to_string(), + }), + )]); } Err(ImportParseError::Filesystem(_)) => { - diagnostics.warnings.push(format!( - "Skipped '{}' due to an I/O error.", - file_path.display() - )); - return Some(diagnostics); - } - Err(ImportParseError::Exclusion(_)) => { - diagnostics.warnings.push(format!( - "Skipped '{}'. Failed to check if the path is excluded.", - file_path.display(), - )); - return Some(diagnostics); + return Ok(vec![Diagnostic::new_global_warning( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::SkippedFileIoError { + file_path: relative_file_path.display().to_string(), + }), + )]); } }; project_imports.active_imports().for_each(|import| { - if let Err(error_info) = check_import( + if let Err(import_diagnostics) = check_import_internal( &import.module_path, module_tree, Arc::clone(&nearest_module), @@ -92,99 +96,72 @@ fn process_file( interface_checker, check_dependencies, ) { - let boundary_error = BoundaryError { - file_path: file_path.clone(), - line_number: import.line_no, - import_mod_path: import.module_path.to_string(), - error_info, - }; - if boundary_error.is_deprecated() { - diagnostics.deprecated_warnings.push(boundary_error); - } else { - diagnostics.errors.push(boundary_error); - } - }; + import_diagnostics + .into_iter() + .for_each(|diagnostic| match &diagnostic { + Diagnostic::Global { + details: DiagnosticDetails::Code(_), + .. + } => { + diagnostics.push( + diagnostic.into_located(relative_file_path.clone(), import.line_no), + ); + } + Diagnostic::Global { + details: DiagnosticDetails::Configuration(_), + .. + } => { + diagnostics.push(diagnostic); + } + _ => {} + }); + } }); project_imports .directive_ignored_imports() .for_each(|directive_ignored_import| { - if project_config.rules.unused_ignore_directives != RuleSetting::Off { - let check_result = check_unused_ignore_directive( - &directive_ignored_import, - module_tree, - Arc::clone(&nearest_module), - project_config, - interface_checker, - check_dependencies, - ); - match (check_result, &project_config.rules.unused_ignore_directives) { - (Err(e), RuleSetting::Error) => { - diagnostics.errors.push(BoundaryError { - file_path: file_path.clone(), - line_number: directive_ignored_import.import.line_no, - import_mod_path: directive_ignored_import - .import - .module_path - .to_string(), - error_info: e, - }); - } - (Err(e), RuleSetting::Warn) => { - diagnostics.warnings.push(e.to_string()); - } - (Ok(()), _) | (_, RuleSetting::Off) => {} + match check_unused_ignore_directive_internal( + &directive_ignored_import, + module_tree, + Arc::clone(&nearest_module), + project_config, + interface_checker, + check_dependencies, + ) { + Ok(()) => {} + Err(diagnostic) => { + diagnostics.push(diagnostic.into_located( + relative_file_path.clone(), + directive_ignored_import.import.line_no, + )); } } - if project_config.rules.require_ignore_directive_reasons != RuleSetting::Off { - let check_result = check_missing_ignore_directive_reason(&directive_ignored_import); - match ( - check_result, - &project_config.rules.require_ignore_directive_reasons, - ) { - (Err(e), RuleSetting::Error) => { - diagnostics.errors.push(BoundaryError { - file_path: file_path.clone(), - line_number: directive_ignored_import.import.line_no, - import_mod_path: directive_ignored_import - .import - .module_path - .to_string(), - error_info: e, - }); - } - (Err(e), RuleSetting::Warn) => { - diagnostics.warnings.push(e.to_string()); - } - (Ok(()), _) | (_, RuleSetting::Off) => {} + match check_missing_ignore_directive_reason(&directive_ignored_import, project_config) { + Ok(()) => {} + Err(diagnostic) => { + diagnostics.push(diagnostic.into_located( + relative_file_path.clone(), + directive_ignored_import.import.line_no, + )); } } }); project_imports .unused_ignore_directives() - .for_each( - |ignore_directive| match project_config.rules.unused_ignore_directives { - RuleSetting::Error => { - diagnostics.errors.push(BoundaryError { - file_path: file_path.clone(), - line_number: ignore_directive.line_no, - import_mod_path: ignore_directive.modules.join(", "), - error_info: ImportCheckError::UnusedIgnoreDirective(), - }); - } - RuleSetting::Warn => { - diagnostics.warnings.push(format!( - "Unused ignore directive: '{}' in file '{}'", - ignore_directive.modules.join(","), - file_path.display() - )); - } - RuleSetting::Off => {} - }, - ); + .for_each(|ignore_directive| { + if let Ok(severity) = (&project_config.rules.unused_ignore_directives).try_into() { + diagnostics.push(Diagnostic::new_located( + severity, + DiagnosticDetails::Code(CodeDiagnostic::UnusedIgnoreDirective()), + relative_file_path.clone(), + ignore_directive.line_no, + )); + } + }); - Some(diagnostics) + Ok(diagnostics) } pub fn check( @@ -193,21 +170,18 @@ pub fn check( dependencies: bool, interfaces: bool, exclude_paths: Vec, -) -> Result { +) -> Result> { if !dependencies && !interfaces { - return Ok(CheckDiagnostics { - errors: Vec::new(), - deprecated_warnings: Vec::new(), - warnings: vec!["WARNING: No checks enabled. At least one of dependencies or interfaces must be enabled.".to_string()], - }); + return Err(CheckError::NoChecksEnabled()); } + if !project_root.is_dir() { return Err(CheckError::InvalidDirectory( project_root.display().to_string(), )); } - let mut diagnostics = CheckDiagnostics::default(); + let mut warnings = Vec::new(); let found_imports = AtomicBool::new(false); let exclude_paths = exclude_paths.iter().map(PathBuf::from).collect::>(); let source_roots: Vec = project_config.prepend_roots(&project_root); @@ -217,9 +191,10 @@ pub fn check( ); for module in &invalid_modules { - diagnostics.warnings.push(format!( - "Module '{}' not found. It will be ignored.", - module.path + warnings.push(Diagnostic::new_global_warning( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::ModuleNotFound { + file_mod_path: module.path.to_string(), + }), )); } @@ -246,18 +221,19 @@ pub fn check( None }; - for source_root in &source_roots { - let source_root_diagnostics = fs::walk_pyfiles(&source_root.display().to_string()) + let diagnostics = source_roots.par_iter().flat_map(|source_root| { + fs::walk_pyfiles(&source_root.display().to_string()) .par_bridge() - .filter_map(|file_path| { + .flat_map(|file_path| { if check_interrupt().is_err() { // Since files are being processed in parallel, // this will essentially short-circuit all remaining files. // Then, we check for an interrupt right after, and return the Err if it is set - return None; + return vec![]; } process_file( file_path, + &project_root, source_root, &source_roots, &module_tree, @@ -266,18 +242,20 @@ pub fn check( dependencies, &found_imports, ) - }); - check_interrupt().map_err(|_| CheckError::Interrupt)?; - diagnostics.par_extend(source_root_diagnostics); - check_interrupt().map_err(|_| CheckError::Interrupt)?; + .unwrap_or_default() + }) + }); + + if check_interrupt().is_err() { + return Err(CheckError::Interrupt); } + let mut final_diagnostics: Vec = diagnostics.collect(); if !found_imports.load(Ordering::Relaxed) { - diagnostics.warnings.push( - "WARNING: No first-party imports were found. You may need to use 'tach mod' to update your Python source roots. Docs: https://docs.gauge.sh/usage/configuration#source-roots" - .to_string(), - ); + final_diagnostics.push(Diagnostic::new_global_warning( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::NoFirstPartyImportsFound()), + )); } - Ok(diagnostics) + Ok(final_diagnostics) } diff --git a/src/commands/check/checks.rs b/src/commands/check/checks.rs index 79051f10..fe4feb63 100644 --- a/src/commands/check/checks.rs +++ b/src/commands/check/checks.rs @@ -1,9 +1,16 @@ -use std::sync::Arc; +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; -use super::diagnostics::ImportCheckError; +use crate::diagnostics::{CodeDiagnostic, ConfigurationDiagnostic, Diagnostic, DiagnosticDetails}; use crate::{ - config::{root_module::RootModuleTreatment, DependencyConfig, ModuleConfig, ProjectConfig}, - imports::DirectiveIgnoredImport, + config::{ + root_module::RootModuleTreatment, rules::RuleSetting, DependencyConfig, ModuleConfig, + ProjectConfig, + }, + external::parsing::{normalize_package_name, ProjectInfo}, + imports::{DirectiveIgnoredImport, NormalizedImport}, interfaces::{ check::CheckResult as InterfaceCheckResult, data_types::TypeCheckResult, InterfaceChecker, }, @@ -15,7 +22,7 @@ fn check_dependencies( file_module_config: &ModuleConfig, import_module_config: &ModuleConfig, layers: &[String], -) -> Result<(), ImportCheckError> { +) -> Result<(), Diagnostic> { // Layer check should take precedence over other dependency checks match check_layers(layers, file_module_config, import_module_config) { LayerCheckResult::Ok => return Ok(()), // Higher layers can unconditionally import lower layers @@ -40,17 +47,21 @@ fn check_dependencies( { Some(DependencyConfig { deprecated: true, .. - }) => Err(ImportCheckError::DeprecatedImport { - import_mod_path: import_mod_path.to_string(), - source_module: file_nearest_module_path.to_string(), - invalid_module: import_nearest_module_path.to_string(), - }), + }) => Err(Diagnostic::new_global_warning(DiagnosticDetails::Code( + CodeDiagnostic::DeprecatedImport { + import_mod_path: import_mod_path.to_string(), + usage_module: file_nearest_module_path.to_string(), + definition_module: import_nearest_module_path.to_string(), + }, + ))), Some(_) => Ok(()), - None => Err(ImportCheckError::InvalidImport { - import_mod_path: import_mod_path.to_string(), - source_module: file_nearest_module_path.to_string(), - invalid_module: import_nearest_module_path.to_string(), - }), + None => Err(Diagnostic::new_global_error(DiagnosticDetails::Code( + CodeDiagnostic::InvalidImport { + import_mod_path: import_mod_path.to_string(), + usage_module: file_nearest_module_path.to_string(), + definition_module: import_nearest_module_path.to_string(), + }, + ))), } } @@ -59,7 +70,7 @@ fn check_interfaces( import_nearest_module: &ModuleNode, file_nearest_module: &ModuleNode, interface_checker: &InterfaceChecker, -) -> Result<(), ImportCheckError> { +) -> Result<(), Diagnostic> { let import_member = import_mod_path .strip_prefix(&import_nearest_module.full_path) .and_then(|s| s.strip_prefix('.')) @@ -67,23 +78,28 @@ fn check_interfaces( let check_result = interface_checker.check_member(import_member, &import_nearest_module.full_path); match check_result { - InterfaceCheckResult::NotExposed => Err(ImportCheckError::PrivateImport { - import_mod_path: import_mod_path.to_string(), - import_nearest_module_path: import_nearest_module.full_path.to_string(), - file_nearest_module_path: file_nearest_module.full_path.to_string(), - }), + InterfaceCheckResult::NotExposed => Err(Diagnostic::new_global_error( + DiagnosticDetails::Code(CodeDiagnostic::PrivateImport { + import_mod_path: import_mod_path.to_string(), + usage_module: file_nearest_module.full_path.to_string(), + definition_module: import_nearest_module.full_path.to_string(), + }), + )), InterfaceCheckResult::Exposed { type_check_result: TypeCheckResult::DidNotMatchInterface { expected }, - } => Err(ImportCheckError::InvalidDataTypeExport { - import_mod_path: import_mod_path.to_string(), - import_nearest_module_path: import_nearest_module.full_path.to_string(), - expected_data_type: expected.to_string(), - }), + } => Err(Diagnostic::new_global_error(DiagnosticDetails::Code( + CodeDiagnostic::InvalidDataTypeExport { + import_mod_path: import_mod_path.to_string(), + usage_module: file_nearest_module.full_path.to_string(), + definition_module: import_nearest_module.full_path.to_string(), + expected_data_type: expected.to_string(), + }, + ))), _ => Ok(()), } } -pub(super) fn check_import( +pub(super) fn check_import_internal( import_mod_path: &str, module_tree: &ModuleTree, file_nearest_module: Arc, @@ -91,9 +107,13 @@ pub(super) fn check_import( root_module_treatment: RootModuleTreatment, interface_checker: &Option, should_check_dependencies: bool, -) -> Result<(), ImportCheckError> { +) -> Result<(), Vec> { + let mut diagnostics = Vec::new(); + if !should_check_dependencies && interface_checker.is_none() { - return Err(ImportCheckError::NoChecksEnabled()); + return Err(vec![Diagnostic::new_global_error( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::NoChecksEnabled()), + )]); } let import_nearest_module = match module_tree.find_nearest(import_mod_path) { @@ -112,45 +132,66 @@ pub(super) fn check_import( return Ok(()); } - let file_module_config = file_nearest_module - .config - .as_ref() - .ok_or(ImportCheckError::ModuleConfigNotFound())?; - let import_module_config = import_nearest_module - .config - .as_ref() - .ok_or(ImportCheckError::ModuleConfigNotFound())?; + let file_module_config = match file_nearest_module.config.as_ref() { + Some(config) => config, + None => { + return Err(vec![Diagnostic::new_global_error( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::ModuleConfigNotFound()), + )]); + } + }; + + let import_module_config = match import_nearest_module.config.as_ref() { + Some(config) => config, + None => { + return Err(vec![Diagnostic::new_global_error( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::ModuleConfigNotFound()), + )]); + } + }; if let Some(interface_checker) = interface_checker { - check_interfaces( + if let Err(err) = check_interfaces( import_mod_path, &import_nearest_module, &file_nearest_module, interface_checker, - )? + ) { + diagnostics.push(err); + } } if should_check_dependencies { - check_dependencies( + if let Err(err) = check_dependencies( import_mod_path, file_module_config, import_module_config, layers, - )? + ) { + diagnostics.push(err); + } } - Ok(()) + if diagnostics.is_empty() { + Ok(()) + } else { + Err(diagnostics) + } } -pub(super) fn check_unused_ignore_directive( +pub(super) fn check_unused_ignore_directive_internal( directive_ignored_import: &DirectiveIgnoredImport, module_tree: &ModuleTree, nearest_module: Arc, project_config: &ProjectConfig, interface_checker: &Option, check_dependencies: bool, -) -> Result<(), ImportCheckError> { - match check_import( +) -> Result<(), Diagnostic> { + if project_config.rules.unused_ignore_directives == RuleSetting::Off { + return Ok(()); + } + + match check_import_internal( &directive_ignored_import.import.module_path, module_tree, Arc::clone(&nearest_module), @@ -159,20 +200,118 @@ pub(super) fn check_unused_ignore_directive( interface_checker, check_dependencies, ) { - Ok(()) => Err(ImportCheckError::UnnecessarilyIgnoredImport { - import_mod_path: directive_ignored_import.import.module_path.to_string(), - }), + Ok(()) => Err(Diagnostic::new_global( + (&project_config.rules.unused_ignore_directives) + .try_into() + .unwrap(), + DiagnosticDetails::Code(CodeDiagnostic::UnnecessarilyIgnoredImport { + import_mod_path: directive_ignored_import.import.module_path.to_string(), + }), + )), Err(_) => Ok(()), } } +#[derive(Debug)] +pub enum ImportProcessResult { + UndeclaredDependency(String), + UsedDependencies(Vec), + Excluded(Vec), +} + +pub(super) fn check_import_external( + import: &NormalizedImport, + project_info: &ProjectInfo, + module_mappings: &HashMap>, + excluded_external_modules: &HashSet, + stdlib_modules: &HashSet, +) -> ImportProcessResult { + let top_level_module_name = import.top_level_module_name().to_string(); + let default_distribution_names = vec![top_level_module_name.clone()]; + let distribution_names: Vec = module_mappings + .get(&top_level_module_name) + .unwrap_or(&default_distribution_names) + .iter() + .map(|dist_name| normalize_package_name(dist_name)) + .collect(); + + if distribution_names + .iter() + .any(|dist_name| excluded_external_modules.contains(dist_name)) + || stdlib_modules.contains(&top_level_module_name) + { + return ImportProcessResult::Excluded(distribution_names); + } + + let is_declared = distribution_names + .iter() + .any(|dist_name| project_info.dependencies.contains(dist_name)); + + if !is_declared { + ImportProcessResult::UndeclaredDependency(top_level_module_name.to_string()) + } else { + ImportProcessResult::UsedDependencies(distribution_names) + } +} + +pub(super) fn check_unused_ignore_directive_external( + directive_ignored_import: &DirectiveIgnoredImport, + project_info: &ProjectInfo, + module_mappings: &HashMap>, + excluded_external_modules: &HashSet, + stdlib_modules: &HashSet, + project_config: &ProjectConfig, +) -> Result<(), Diagnostic> { + if let ImportProcessResult::UsedDependencies(_) | ImportProcessResult::Excluded(_) = + check_import_external( + directive_ignored_import.import, + project_info, + module_mappings, + excluded_external_modules, + stdlib_modules, + ) + { + match project_config.rules.unused_ignore_directives { + RuleSetting::Error => Err(Diagnostic::new_global( + (&project_config.rules.unused_ignore_directives) + .try_into() + .unwrap(), + DiagnosticDetails::Code(CodeDiagnostic::UnnecessarilyIgnoredImport { + import_mod_path: directive_ignored_import.import.module_path.to_string(), + }), + )), + RuleSetting::Warn => Err(Diagnostic::new_global( + (&project_config.rules.unused_ignore_directives) + .try_into() + .unwrap(), + DiagnosticDetails::Code(CodeDiagnostic::UnnecessarilyIgnoredImport { + import_mod_path: directive_ignored_import.import.module_path.to_string(), + }), + )), + RuleSetting::Off => Ok(()), + } + } else { + Ok(()) + } +} + pub(super) fn check_missing_ignore_directive_reason( directive_ignored_import: &DirectiveIgnoredImport, -) -> Result<(), ImportCheckError> { + project_config: &ProjectConfig, +) -> Result<(), Diagnostic> { + if project_config.rules.require_ignore_directive_reasons == RuleSetting::Off { + return Ok(()); + } + if directive_ignored_import.reason.is_empty() { - Err(ImportCheckError::MissingIgnoreDirectiveReason { - import_mod_path: directive_ignored_import.import.module_path.to_string(), - }) + Err(Diagnostic::new_global( + (&project_config.rules.require_ignore_directive_reasons) + .try_into() + .unwrap(), + DiagnosticDetails::Code(CodeDiagnostic::MissingIgnoreDirectiveReason { + import_mod_path: directive_ignored_import.import.module_path.to_string(), + }), + )) } else { Ok(()) } @@ -183,8 +322,8 @@ enum LayerCheckResult { Ok, SameLayer, LayerNotSpecified, - LayerViolation(ImportCheckError), - UnknownLayer(ImportCheckError), + LayerViolation(Diagnostic), + UnknownLayer(Diagnostic), } fn check_layers( @@ -204,25 +343,33 @@ fn check_layers( } else if source_index < target_index { LayerCheckResult::Ok } else { - LayerCheckResult::LayerViolation(ImportCheckError::LayerViolation { - import_mod_path: target_module_config.path.clone(), - source_module: source_module_config.path.clone(), - source_layer: source_layer.clone(), - invalid_module: target_module_config.path.clone(), - invalid_layer: target_layer.clone(), - }) + LayerCheckResult::LayerViolation(Diagnostic::new_global_error( + DiagnosticDetails::Code(CodeDiagnostic::LayerViolation { + import_mod_path: target_module_config.path.clone(), + usage_module: source_module_config.path.clone(), + usage_layer: source_layer.clone(), + definition_module: target_module_config.path.clone(), + definition_layer: target_layer.clone(), + }), + )) } } // If either index is not found, the layer is unknown - (Some(_), None) => LayerCheckResult::UnknownLayer(ImportCheckError::UnknownLayer { - layer: target_layer.clone(), - }), - (None, Some(_)) => LayerCheckResult::UnknownLayer(ImportCheckError::UnknownLayer { - layer: source_layer.clone(), - }), - _ => LayerCheckResult::UnknownLayer(ImportCheckError::UnknownLayer { - layer: source_layer.clone(), - }), + (Some(_), None) => LayerCheckResult::UnknownLayer(Diagnostic::new_global_error( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::UnknownLayer { + layer: target_layer.clone(), + }), + )), + (None, Some(_)) => LayerCheckResult::UnknownLayer(Diagnostic::new_global_error( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::UnknownLayer { + layer: source_layer.clone(), + }), + )), + _ => LayerCheckResult::UnknownLayer(Diagnostic::new_global_error( + DiagnosticDetails::Configuration(ConfigurationDiagnostic::UnknownLayer { + layer: source_layer.clone(), + }), + )), } } _ => LayerCheckResult::LayerNotSpecified, // At least one module does not have a layer @@ -232,8 +379,8 @@ fn check_layers( #[cfg(test)] mod tests { use super::*; - use crate::commands::check::diagnostics::ImportCheckError; use crate::config::{InterfaceConfig, ModuleConfig}; + use crate::diagnostics::Diagnostic; use crate::modules::ModuleTree; use crate::tests::check_internal::fixtures::{ interface_config, layers, module_config, module_tree, @@ -269,7 +416,7 @@ mod tests { .unwrap(), ); - let check_error = check_import( + let check_error = check_import_internal( import_mod_path, &module_tree, file_module.clone(), @@ -296,7 +443,7 @@ mod tests { .unwrap(), ); - let check_error = check_import( + let check_error = check_import_internal( "domain_one.subdomain", &module_tree, file_module.clone(), @@ -306,7 +453,10 @@ mod tests { true, ); assert!(check_error.is_err()); - assert!(check_error.unwrap_err().is_deprecated()); + assert!(check_error + .unwrap_err() + .iter() + .any(|err| err.is_deprecated())); } #[rstest] @@ -314,27 +464,33 @@ mod tests { #[case("top", "middle", LayerCheckResult::Ok)] #[case("top", "bottom", LayerCheckResult::Ok)] #[case("middle", "bottom", LayerCheckResult::Ok)] - #[case("bottom", "top", LayerCheckResult::LayerViolation(ImportCheckError::LayerViolation { - source_layer: "bottom".to_string(), - invalid_layer: "top".to_string(), - import_mod_path: "".to_string(), - source_module: "".to_string(), - invalid_module: "".to_string(), - }))] - #[case("middle", "top", LayerCheckResult::LayerViolation(ImportCheckError::LayerViolation { - source_layer: "middle".to_string(), - invalid_layer: "top".to_string(), - import_mod_path: "".to_string(), - source_module: "".to_string(), - invalid_module: "".to_string(), - }))] - #[case("bottom", "middle", LayerCheckResult::LayerViolation(ImportCheckError::LayerViolation { - source_layer: "bottom".to_string(), - invalid_layer: "middle".to_string(), - import_mod_path: "".to_string(), - source_module: "".to_string(), - invalid_module: "".to_string(), - }))] + #[case("bottom", "top", LayerCheckResult::LayerViolation(Diagnostic::new_global_error( + DiagnosticDetails::Code(CodeDiagnostic::LayerViolation { + import_mod_path: "".to_string(), + usage_module: "".to_string(), + usage_layer: "bottom".to_string(), + definition_module: "".to_string(), + definition_layer: "top".to_string(), + }), + )))] + #[case("middle", "top", LayerCheckResult::LayerViolation(Diagnostic::new_global_error( + DiagnosticDetails::Code(CodeDiagnostic::LayerViolation { + import_mod_path: "".to_string(), + usage_module: "".to_string(), + usage_layer: "middle".to_string(), + definition_module: "".to_string(), + definition_layer: "top".to_string(), + }), + )))] + #[case("bottom", "middle", LayerCheckResult::LayerViolation(Diagnostic::new_global_error( + DiagnosticDetails::Code(CodeDiagnostic::LayerViolation { + import_mod_path: "".to_string(), + usage_module: "".to_string(), + usage_layer: "bottom".to_string(), + definition_module: "".to_string(), + definition_layer: "middle".to_string(), + }), + )))] fn test_check_layers_hierarchy( layers: Vec, #[case] source_layer: &str, @@ -393,7 +549,7 @@ mod tests { .unwrap(), ); - let check_error = check_import( + let check_error = check_import_internal( import_mod_path, &module_tree, file_module.clone(), diff --git a/src/commands/check/diagnostics.rs b/src/commands/check/diagnostics.rs deleted file mode 100644 index d1a4b26a..00000000 --- a/src/commands/check/diagnostics.rs +++ /dev/null @@ -1,215 +0,0 @@ -use std::collections::HashMap; -use std::path::PathBuf; - -use pyo3::exceptions::PyValueError; -use pyo3::{pyclass, pymethods, PyResult}; -use rayon::prelude::*; -use serde::Serialize; -use thiserror::Error; - -use crate::interrupt::check_interrupt; - -#[derive(Debug, Clone, Serialize)] -#[pyclass(get_all, module = "tach.extension")] -pub struct BoundaryError { - pub file_path: PathBuf, - pub line_number: usize, - pub import_mod_path: String, - pub error_info: ImportCheckError, -} - -impl BoundaryError { - pub fn is_deprecated(&self) -> bool { - self.error_info.is_deprecated() - } -} - -#[derive(Debug, Default, Serialize)] -#[pyclass(get_all, module = "tach.extension")] -pub struct CheckDiagnostics { - pub errors: Vec, - pub deprecated_warnings: Vec, - pub warnings: Vec, -} - -#[pymethods] -impl CheckDiagnostics { - #[pyo3(signature = (pretty_print = false))] - fn serialize_json(&self, pretty_print: bool) -> PyResult { - if pretty_print { - serde_json::to_string_pretty(&self) - .map_err(|_| PyValueError::new_err("Failed to serialize check results.")) - } else { - serde_json::to_string(&self) - .map_err(|_| PyValueError::new_err("Failed to serialize check results.")) - } - } -} - -impl ParallelExtend for CheckDiagnostics { - fn par_extend(&mut self, par_iter: I) - where - I: IntoParallelIterator, - { - // Reduce all diagnostics into a single one in parallel - let combined = - par_iter - .into_par_iter() - .reduce(CheckDiagnostics::default, |mut acc, item| { - if check_interrupt().is_err() { - return acc; - } - acc.errors.extend(item.errors); - acc.deprecated_warnings.extend(item.deprecated_warnings); - acc.warnings.extend(item.warnings); - acc - }); - - if check_interrupt().is_err() { - return; - } - // Extend self with the combined results - self.errors.extend(combined.errors); - self.deprecated_warnings - .extend(combined.deprecated_warnings); - self.warnings.extend(combined.warnings); - } -} - -#[derive(Error, Debug, Clone, Serialize)] -#[pyclass(module = "tach.extension")] -pub enum ImportCheckError { - #[error("Module containing '{file_mod_path}' not found in project.")] - ModuleNotFound { file_mod_path: String }, - - #[error("Module '{import_nearest_module_path}' has a defined public interface. Only imports from the public interface of this module are allowed. The import '{import_mod_path}' (in module '{file_nearest_module_path}') is not public.")] - PrivateImport { - import_mod_path: String, - import_nearest_module_path: String, - file_nearest_module_path: String, - }, - - #[error("The import '{import_mod_path}' (from module '{import_nearest_module_path}') matches an interface but does not match the expected data type ('{expected_data_type}').")] - InvalidDataTypeExport { - import_mod_path: String, - import_nearest_module_path: String, - expected_data_type: String, - }, - - #[error("Could not find module configuration.")] - ModuleConfigNotFound(), - - #[error("Cannot import '{import_mod_path}'. Module '{source_module}' cannot depend on '{invalid_module}'.")] - InvalidImport { - import_mod_path: String, - source_module: String, - invalid_module: String, - }, - - #[error("Import '{import_mod_path}' is deprecated. Module '{source_module}' should not depend on '{invalid_module}'.")] - DeprecatedImport { - import_mod_path: String, - source_module: String, - invalid_module: String, - }, - - #[error("Cannot import '{import_mod_path}'. Layer '{source_layer}' ('{source_module}') is lower than layer '{invalid_layer}' ('{invalid_module}').")] - LayerViolation { - import_mod_path: String, - source_module: String, - source_layer: String, - invalid_module: String, - invalid_layer: String, - }, - - #[error("Layer '{layer}' is not defined in the project.")] - UnknownLayer { layer: String }, - - #[error("Import '{import_mod_path}' is unnecessarily ignored by a directive.")] - UnnecessarilyIgnoredImport { import_mod_path: String }, - - #[error("Ignore directive is unused.")] - UnusedIgnoreDirective(), - - #[error("Import '{import_mod_path}' is ignored without providing a reason.")] - MissingIgnoreDirectiveReason { import_mod_path: String }, - - #[error("No checks enabled. At least one of dependencies or interfaces must be enabled.")] - NoChecksEnabled(), -} - -#[pymethods] -impl ImportCheckError { - pub fn is_dependency_error(&self) -> bool { - matches!( - self, - Self::InvalidImport { .. } - | Self::DeprecatedImport { .. } - | Self::LayerViolation { .. } - ) - } - - pub fn is_interface_error(&self) -> bool { - matches!( - self, - Self::PrivateImport { .. } | Self::InvalidDataTypeExport { .. } - ) - } - - pub fn source_path(&self) -> Option<&String> { - match self { - Self::InvalidImport { source_module, .. } => Some(source_module), - Self::DeprecatedImport { source_module, .. } => Some(source_module), - Self::LayerViolation { source_module, .. } => Some(source_module), - _ => None, - } - } - - pub fn invalid_path(&self) -> Option<&String> { - match self { - Self::InvalidImport { invalid_module, .. } => Some(invalid_module), - Self::DeprecatedImport { invalid_module, .. } => Some(invalid_module), - Self::LayerViolation { invalid_module, .. } => Some(invalid_module), - _ => None, - } - } - - pub fn is_deprecated(&self) -> bool { - matches!(self, Self::DeprecatedImport { .. }) - } - - pub fn to_pystring(&self) -> String { - self.to_string() - } -} - -#[derive(Default)] -#[pyclass(get_all, module = "tach.extension")] -pub struct ExternalCheckDiagnostics { - // Undeclared dependencies by source filepath - pub undeclared_dependencies: HashMap>, - // Unused dependencies by project configuration filepath - pub unused_dependencies: HashMap>, - // Other errors - pub errors: Vec, - // Other warnings - pub warnings: Vec, -} - -#[pymethods] -impl ExternalCheckDiagnostics { - #[new] - fn new( - undeclared_dependencies: HashMap>, - unused_dependencies: HashMap>, - errors: Vec, - warnings: Vec, - ) -> Self { - Self { - undeclared_dependencies, - unused_dependencies, - errors, - warnings, - } - } -} diff --git a/src/commands/check/error.rs b/src/commands/check/error.rs index ee5ca2a9..86879f4d 100644 --- a/src/commands/check/error.rs +++ b/src/commands/check/error.rs @@ -13,6 +13,8 @@ use crate::modules; pub enum CheckError { #[error("The path {0} is not a valid directory.")] InvalidDirectory(String), + #[error("No checks enabled.")] + NoChecksEnabled(), #[error("Filesystem error: {0}")] Filesystem(#[from] fs::FileSystemError), #[error("Module tree error: {0}")] diff --git a/src/commands/check/format.rs b/src/commands/check/format.rs new file mode 100644 index 00000000..ed2ec050 --- /dev/null +++ b/src/commands/check/format.rs @@ -0,0 +1,190 @@ +use crate::{ + cli::{create_clickable_link, fail, warning}, + diagnostics::{CodeDiagnostic, Diagnostic, DiagnosticDetails, Severity}, +}; +use std::{collections::HashMap, path::PathBuf}; + +use console::style; +use itertools::Itertools; + +#[derive(Debug, PartialEq, Eq, Ord, PartialOrd, Hash, Clone)] +enum DiagnosticGroupKind { + Other, + Configuration, + ExternalDependency, + Interface, + InternalDependency, +} + +impl From<&DiagnosticDetails> for DiagnosticGroupKind { + fn from(details: &DiagnosticDetails) -> Self { + match details { + DiagnosticDetails::Configuration(..) => Self::Configuration, + DiagnosticDetails::Code(code_diagnostic_details) => match code_diagnostic_details { + CodeDiagnostic::InvalidImport { .. } => Self::InternalDependency, + CodeDiagnostic::DeprecatedImport { .. } => Self::InternalDependency, + CodeDiagnostic::LayerViolation { .. } => Self::InternalDependency, + CodeDiagnostic::PrivateImport { .. } => Self::Interface, + CodeDiagnostic::InvalidDataTypeExport { .. } => Self::Interface, + CodeDiagnostic::UndeclaredExternalDependency { .. } => Self::ExternalDependency, + CodeDiagnostic::UnusedExternalDependency { .. } => Self::ExternalDependency, + CodeDiagnostic::UnnecessarilyIgnoredImport { .. } => Self::Other, + CodeDiagnostic::UnusedIgnoreDirective { .. } => Self::Other, + CodeDiagnostic::MissingIgnoreDirectiveReason { .. } => Self::Other, + }, + } + } +} + +#[derive(Debug)] +struct DiagnosticGroup<'a> { + kind: DiagnosticGroupKind, + severity: Severity, + header: String, + diagnostics: Vec<&'a Diagnostic>, + footer: Option, +} + +impl<'a> DiagnosticGroup<'a> { + fn new(severity: Severity, kind: DiagnosticGroupKind) -> Self { + let (header, footer) = match kind { + DiagnosticGroupKind::Configuration => (style("Configuration").red().bold(), None), + DiagnosticGroupKind::InternalDependency => ( + style("Internal Dependencies").red().bold(), + Some(style( + "If you intended to add a new dependency, run 'tach sync' to update your module configuration.\n\ + Otherwise, remove any disallowed imports and consider refactoring." + ).yellow()), + ), + DiagnosticGroupKind::ExternalDependency => ( + style("External Dependencies").red().bold(), + Some(style( + "Consider updating the corresponding pyproject.toml file,\n\ + or add the dependencies to the 'external.exclude' list in tach.toml." + ).yellow()), + ), + DiagnosticGroupKind::Interface => ( + style("Interfaces").red().bold(), + Some(style( + "If you intended to change an interface, edit the '[[interfaces]]' section of tach.toml.\n\ + Otherwise, remove any disallowed imports and consider refactoring." + ).yellow()), + ), + DiagnosticGroupKind::Other => (style("General").red().bold(), None), + }; + + Self { + kind, + severity, + header: header.to_string(), + diagnostics: vec![], + footer: footer.map(|f| f.to_string()), + } + } + + fn add_diagnostic(&mut self, diagnostic: &'a Diagnostic) { + self.diagnostics.push(diagnostic); + } + + fn sort_diagnostics(&mut self) { + self.diagnostics.sort_by(|a, b| { + // First sort by severity (warnings first) + let severity_order = b.severity().cmp(&a.severity()); + if severity_order != std::cmp::Ordering::Equal { + return severity_order; + } + + // Then sort by file path (None first) + match (a.file_path(), b.file_path()) { + (None, None) => std::cmp::Ordering::Equal, + (None, Some(_)) => std::cmp::Ordering::Less, + (Some(_), None) => std::cmp::Ordering::Greater, + (Some(a_path), Some(b_path)) => a_path.cmp(b_path), + } + }); + } +} + +pub struct DiagnosticFormatter { + project_root: PathBuf, +} + +impl DiagnosticFormatter { + pub fn new(project_root: PathBuf) -> Self { + Self { project_root } + } + + fn format_diagnostic(&self, diagnostic: &Diagnostic) -> String { + let local_error_path = diagnostic.file_path(); + + let error_location = match local_error_path { + Some(path) => { + let absolute_error_path = self.project_root.join(path); + create_clickable_link( + path, + &absolute_error_path, + &diagnostic.line_number().unwrap(), + ) + } + None => diagnostic.severity().to_string(), + }; + + match diagnostic.severity() { + Severity::Error => format!( + "{} {}{} {}", + fail(), + style(error_location).red().bold(), + style(":").yellow().bold(), + style(diagnostic.message()).yellow(), + ), + Severity::Warning => format!( + "{} {}{} {}", + warning(), + style(error_location).yellow().bold(), + style(":").yellow().bold(), + style(diagnostic.message()).yellow(), + ), + } + } + + fn format_diagnostic_group(&self, group: &mut DiagnosticGroup) -> String { + group.sort_diagnostics(); + let header = match group.severity { + Severity::Error => style(&group.header).red().bold(), + Severity::Warning => style(&group.header).yellow().bold(), + }; + let diagnostics = group + .diagnostics + .iter() + .map(|d| self.format_diagnostic(d)) + .collect::>() + .join("\n"); + + match &group.footer { + Some(footer) => format!("{}\n{}\n\n{}", header, diagnostics, footer), + None => format!("{}\n{}", header, diagnostics), + } + } + + pub fn format_diagnostics(&self, diagnostics: &[Diagnostic]) -> String { + let mut groups: HashMap = HashMap::new(); + + for diagnostic in diagnostics { + let group_kind = DiagnosticGroupKind::from(diagnostic.details()); + let group = groups + .entry(group_kind.clone()) + .or_insert_with(|| DiagnosticGroup::new(diagnostic.severity(), group_kind)); + group.add_diagnostic(diagnostic); + } + + let mut formatted_diagnostics = Vec::new(); + for group in groups + .values_mut() + .sorted_by_key(|group| group.kind.clone()) + { + formatted_diagnostics.push(self.format_diagnostic_group(group)); + } + + formatted_diagnostics.join("\n\n") + } +} diff --git a/src/commands/check/mod.rs b/src/commands/check/mod.rs index 54941aae..70bfcdaf 100644 --- a/src/commands/check/mod.rs +++ b/src/commands/check/mod.rs @@ -1,10 +1,9 @@ pub mod check_external; pub mod check_internal; pub mod checks; -pub mod diagnostics; pub mod error; +pub mod format; pub use check_external::check as check_external; pub use check_internal::check as check_internal; -pub use diagnostics::{BoundaryError, CheckDiagnostics, ExternalCheckDiagnostics}; pub use error::{CheckError, ExternalCheckError}; diff --git a/src/commands/sync.rs b/src/commands/sync.rs index 87b7d402..1a212a75 100644 --- a/src/commands/sync.rs +++ b/src/commands/sync.rs @@ -2,10 +2,11 @@ use thiserror::Error; use pyo3::prelude::*; -use crate::commands::check::{check_internal, BoundaryError, CheckError}; +use crate::commands::check::{check_internal, CheckError}; use crate::config::edit::{ConfigEditor, EditError}; use crate::config::root_module::{RootModuleTreatment, ROOT_MODULE_SENTINEL_TAG}; use crate::config::{DependencyConfig, ProjectConfig}; +use crate::diagnostics::Diagnostic; use crate::filesystem::validate_module_path; use std::collections::{HashMap, HashSet}; use std::path::PathBuf; @@ -57,13 +58,12 @@ fn handle_added_dependency( } } -fn detect_dependencies(boundary_errors: &[BoundaryError]) -> HashMap> { +fn detect_dependencies(diagnostics: &[Diagnostic]) -> HashMap> { let mut dependencies = HashMap::new(); - for error in boundary_errors { - let error_info = &error.error_info; - if error_info.is_dependency_error() { - let source_path = error_info.source_path().unwrap(); - let dep_path = error_info.invalid_path().unwrap(); + for diagnostic in diagnostics { + if diagnostic.is_dependency_error() { + let source_path = diagnostic.usage_module().unwrap(); + let dep_path = diagnostic.definition_module().unwrap(); dependencies .entry(source_path.to_string()) .or_insert(vec![]) @@ -95,7 +95,7 @@ pub fn detect_unused_dependencies( false, exclude_paths, )?; - let detected_dependencies = detect_dependencies(&check_result.errors); + let detected_dependencies = detect_dependencies(&check_result); let mut unused_dependencies: Vec = vec![]; for module_path in project_config.module_paths() { @@ -147,7 +147,7 @@ fn sync_dependency_constraints( false, exclude_paths, )?; - let detected_dependencies = detect_dependencies(&check_result.errors); + let detected_dependencies = detect_dependencies(&check_result); // Root module is a special case -- it may not be in module paths and still implicitly detect dependencies // If the root module is not in the module paths, but was detected, create it diff --git a/src/diagnostics.rs b/src/diagnostics.rs new file mode 100644 index 00000000..3199ae61 --- /dev/null +++ b/src/diagnostics.rs @@ -0,0 +1,397 @@ +use std::{fmt::Display, path::PathBuf}; + +use pyo3::prelude::*; +use serde::Serialize; +use thiserror::Error; + +use crate::config::RuleSetting; + +#[derive(Debug, Clone, Eq, PartialOrd, Ord, Serialize, PartialEq)] +#[pyclass(eq, eq_int, module = "tach.extension")] +pub enum Severity { + Error, + Warning, +} + +impl Display for Severity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Severity::Error => write!(f, "Error"), + Severity::Warning => write!(f, "Warning"), + } + } +} + +impl TryFrom<&RuleSetting> for Severity { + type Error = (); + + fn try_from(setting: &RuleSetting) -> Result>::Error> { + match setting { + RuleSetting::Error => Ok(Self::Error), + RuleSetting::Warn => Ok(Self::Warning), + RuleSetting::Off => Err(()), + } + } +} + +#[derive(Error, Debug, Clone, Serialize, PartialEq)] +#[pyclass(module = "tach.extension")] +pub enum ConfigurationDiagnostic { + #[error("Module containing '{file_mod_path}' not found in project.")] + ModuleNotFound { file_mod_path: String }, + + #[error("Could not find module configuration.")] + ModuleConfigNotFound(), + + #[error("Layer '{layer}' is not defined in the project.")] + UnknownLayer { layer: String }, + + #[error("No first-party imports were found. You may need to use 'tach mod' to update your Python source roots. Docs: https://docs.gauge.sh/usage/configuration#source-roots")] + NoFirstPartyImportsFound(), + + #[error("Unexpected error: No checks were enabled.")] + NoChecksEnabled(), + + #[error("Skipped '{file_path}' due to a syntax error.")] + SkippedFileSyntaxError { file_path: String }, + + #[error("Skipped '{file_path}' due to an I/O error.")] + SkippedFileIoError { file_path: String }, +} + +#[derive(Error, Debug, Clone, Serialize, PartialEq)] +#[pyclass(module = "tach.extension")] +pub enum CodeDiagnostic { + #[error("Module '{definition_module}' has a defined public interface. Only imports from the public interface of this module are allowed. The import '{import_mod_path}' (in module '{usage_module}') is not public.")] + PrivateImport { + import_mod_path: String, + definition_module: String, + usage_module: String, + }, + + #[error("The import '{import_mod_path}' (from module '{definition_module}') matches an interface but does not match the expected data type ('{expected_data_type}').")] + InvalidDataTypeExport { + import_mod_path: String, + definition_module: String, + usage_module: String, + expected_data_type: String, + }, + + #[error("Cannot import '{import_mod_path}'. Module '{usage_module}' cannot depend on '{definition_module}'.")] + InvalidImport { + import_mod_path: String, + usage_module: String, + definition_module: String, + }, + + #[error("Import '{import_mod_path}' is deprecated. Module '{usage_module}' should not depend on '{definition_module}'.")] + DeprecatedImport { + import_mod_path: String, + usage_module: String, + definition_module: String, + }, + + #[error("Cannot import '{import_mod_path}'. Layer '{usage_layer}' ('{usage_module}') is lower than layer '{definition_layer}' ('{definition_module}').")] + LayerViolation { + import_mod_path: String, + usage_module: String, + usage_layer: String, + definition_module: String, + definition_layer: String, + }, + + #[error("Import '{import_mod_path}' is unnecessarily ignored by a directive.")] + UnnecessarilyIgnoredImport { import_mod_path: String }, + + #[error("Ignore directive is unused.")] + UnusedIgnoreDirective(), + + #[error("Import '{import_mod_path}' is ignored without providing a reason.")] + MissingIgnoreDirectiveReason { import_mod_path: String }, + + #[error("Import '{import_mod_path}' does not match any declared dependency.")] + UndeclaredExternalDependency { import_mod_path: String }, + + #[error("External package '{package_module_name}' is not used.")] + UnusedExternalDependency { package_module_name: String }, +} + +impl CodeDiagnostic { + pub fn import_mod_path(&self) -> Option<&str> { + match self { + CodeDiagnostic::PrivateImport { + import_mod_path, .. + } + | CodeDiagnostic::InvalidDataTypeExport { + import_mod_path, .. + } + | CodeDiagnostic::InvalidImport { + import_mod_path, .. + } + | CodeDiagnostic::DeprecatedImport { + import_mod_path, .. + } + | CodeDiagnostic::LayerViolation { + import_mod_path, .. + } + | CodeDiagnostic::UnnecessarilyIgnoredImport { + import_mod_path, .. + } => Some(import_mod_path), + CodeDiagnostic::UnusedIgnoreDirective { .. } => None, + CodeDiagnostic::MissingIgnoreDirectiveReason { .. } => None, + CodeDiagnostic::UndeclaredExternalDependency { .. } => None, + CodeDiagnostic::UnusedExternalDependency { .. } => None, + } + } + + pub fn usage_module(&self) -> Option<&str> { + match self { + CodeDiagnostic::PrivateImport { usage_module, .. } + | CodeDiagnostic::InvalidDataTypeExport { usage_module, .. } + | CodeDiagnostic::InvalidImport { usage_module, .. } + | CodeDiagnostic::DeprecatedImport { usage_module, .. } + | CodeDiagnostic::LayerViolation { usage_module, .. } => Some(usage_module), + _ => None, + } + } + + pub fn definition_module(&self) -> Option<&str> { + match self { + CodeDiagnostic::PrivateImport { + definition_module, .. + } + | CodeDiagnostic::InvalidDataTypeExport { + definition_module, .. + } + | CodeDiagnostic::InvalidImport { + definition_module, .. + } + | CodeDiagnostic::DeprecatedImport { + definition_module, .. + } + | CodeDiagnostic::LayerViolation { + definition_module, .. + } => Some(definition_module), + _ => None, + } + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[pyclass(module = "tach.extension")] +pub enum DiagnosticDetails { + Code(CodeDiagnostic), + Configuration(ConfigurationDiagnostic), +} + +impl Display for DiagnosticDetails { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + DiagnosticDetails::Code(code) => write!(f, "{}", code), + DiagnosticDetails::Configuration(config) => write!(f, "{}", config), + } + } +} + +#[derive(Debug, Clone, Serialize, PartialEq)] +#[pyclass(module = "tach.extension")] +pub enum Diagnostic { + Global { + severity: Severity, + details: DiagnosticDetails, + }, + Located { + file_path: PathBuf, + line_number: usize, + severity: Severity, + details: DiagnosticDetails, + }, +} + +impl Diagnostic { + pub fn new_global(severity: Severity, details: DiagnosticDetails) -> Self { + Self::Global { severity, details } + } + + pub fn new_located( + severity: Severity, + details: DiagnosticDetails, + file_path: PathBuf, + line_number: usize, + ) -> Self { + Self::Located { + severity, + details, + file_path, + line_number, + } + } + + pub fn into_located(self, file_path: PathBuf, line_number: usize) -> Self { + match self { + Self::Global { severity, details } => { + Self::new_located(severity, details, file_path, line_number) + } + Self::Located { .. } => self, + } + } + + pub fn new_located_error( + file_path: PathBuf, + line_number: usize, + details: DiagnosticDetails, + ) -> Self { + Self::Located { + file_path, + line_number, + severity: Severity::Error, + details, + } + } + + pub fn new_located_warning( + file_path: PathBuf, + line_number: usize, + details: DiagnosticDetails, + ) -> Self { + Self::Located { + file_path, + line_number, + severity: Severity::Warning, + details, + } + } + + pub fn new_global_error(details: DiagnosticDetails) -> Self { + Self::Global { + severity: Severity::Error, + details, + } + } + + pub fn new_global_warning(details: DiagnosticDetails) -> Self { + Self::Global { + severity: Severity::Warning, + details, + } + } + + pub fn details(&self) -> &DiagnosticDetails { + match self { + Self::Global { details, .. } => details, + Self::Located { details, .. } => details, + } + } + + pub fn message(&self) -> String { + self.details().to_string() + } + + pub fn severity(&self) -> Severity { + match self { + Self::Global { severity, .. } => severity.clone(), + Self::Located { severity, .. } => severity.clone(), + } + } + + pub fn file_path(&self) -> Option<&PathBuf> { + match self { + Self::Global { .. } => None, + Self::Located { file_path, .. } => Some(file_path), + } + } + + pub fn line_number(&self) -> Option { + match self { + Self::Global { .. } => None, + Self::Located { line_number, .. } => Some(*line_number), + } + } + + pub fn import_mod_path(&self) -> Option<&str> { + match self.details() { + DiagnosticDetails::Code(details) => details.import_mod_path(), + _ => None, + } + } + + pub fn usage_module(&self) -> Option<&str> { + match self.details() { + DiagnosticDetails::Code(details) => details.usage_module(), + _ => None, + } + } + + pub fn definition_module(&self) -> Option<&str> { + match self.details() { + DiagnosticDetails::Code(details) => details.definition_module(), + _ => None, + } + } +} + +#[pymethods] +impl Diagnostic { + pub fn is_code(&self) -> bool { + matches!(self.details(), DiagnosticDetails::Code { .. }) + } + + pub fn is_configuration(&self) -> bool { + matches!(self.details(), DiagnosticDetails::Configuration { .. }) + } + + pub fn is_dependency_error(&self) -> bool { + matches!( + self.details(), + DiagnosticDetails::Code(CodeDiagnostic::InvalidImport { .. }) + | DiagnosticDetails::Code(CodeDiagnostic::DeprecatedImport { .. }) + | DiagnosticDetails::Code(CodeDiagnostic::LayerViolation { .. }) + ) + } + + pub fn is_interface_error(&self) -> bool { + matches!( + self.details(), + DiagnosticDetails::Code(CodeDiagnostic::PrivateImport { .. }) + | DiagnosticDetails::Code(CodeDiagnostic::InvalidDataTypeExport { .. }) + ) + } + + pub fn is_deprecated(&self) -> bool { + matches!( + self.details(), + DiagnosticDetails::Code(CodeDiagnostic::DeprecatedImport { .. }) + ) + } + + pub fn is_error(&self) -> bool { + matches!(self.severity(), Severity::Error) + } + + pub fn is_warning(&self) -> bool { + matches!(self.severity(), Severity::Warning) + } + + #[pyo3(name = "to_string")] + pub fn to_pystring(&self) -> String { + self.message() + } + + pub fn pyfile_path(&self) -> Option { + self.file_path() + .map(|path| path.to_string_lossy().to_string()) + } + + pub fn pyline_number(&self) -> Option { + self.line_number() + } +} + +#[pyfunction(signature = (diagnostics, pretty_print = false))] +pub fn serialize_diagnostics_json(diagnostics: Vec, pretty_print: bool) -> String { + if pretty_print { + serde_json::to_string_pretty(&diagnostics).unwrap() + } else { + serde_json::to_string(&diagnostics).unwrap() + } +} diff --git a/src/imports.rs b/src/imports.rs index 230469cc..9a0cfc4b 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -11,10 +11,10 @@ use pyo3::PyObject; use once_cell::sync::Lazy; use regex::Regex; +use ruff_linter::Locator; use ruff_python_ast::statement_visitor::{walk_stmt, StatementVisitor}; use ruff_python_ast::visitor::Visitor; use ruff_python_ast::{Expr, Mod, Stmt, StmtIf, StmtImport, StmtImportFrom}; -use ruff_source_file::Locator; use thiserror::Error; use crate::python::{error::ParsingError, parsing::parse_python_source}; @@ -30,8 +30,6 @@ pub enum ImportParseError { }, #[error("Failed to parse project imports.\n{0}")] Filesystem(#[from] filesystem::FileSystemError), - #[error("Failed to check if path is excluded.\n{0}")] - Exclusion(#[from] exclusion::PathExclusionError), } pub type Result = std::result::Result; diff --git a/src/interfaces/data_types.rs b/src/interfaces/data_types.rs index bb8db1c5..e6af5a1b 100644 --- a/src/interfaces/data_types.rs +++ b/src/interfaces/data_types.rs @@ -135,7 +135,7 @@ impl StatementVisitor<'_> for ModuleInterfaceVisitor<'_> { for target in &node.targets { self.current_interface_members.push(InterfaceMember { name: match target { - Expr::Name(name) => name.id.clone(), + Expr::Name(name) => name.id.to_string(), _ => panic!("Expected Expr::Name"), }, node: InterfaceMemberNode::Variable { annotation: None }, @@ -145,12 +145,12 @@ impl StatementVisitor<'_> for ModuleInterfaceVisitor<'_> { Stmt::AnnAssign(node) => { self.current_interface_members.push(InterfaceMember { name: match node.target.as_ref() { - Expr::Name(name) => name.id.clone(), + Expr::Name(name) => name.id.to_string(), _ => panic!("Expected Expr::Name"), }, node: InterfaceMemberNode::Variable { annotation: match node.annotation.as_ref() { - Expr::Name(name) => Some(name.id.clone()), + Expr::Name(name) => Some(name.id.to_string()), Expr::StringLiteral(s) => Some(s.value.to_string()), _ => None, }, @@ -159,7 +159,7 @@ impl StatementVisitor<'_> for ModuleInterfaceVisitor<'_> { } Stmt::FunctionDef(node) => { self.current_interface_members.push(InterfaceMember { - name: node.name.id.clone(), + name: node.name.id.to_string(), node: InterfaceMemberNode::Function { parameters: node .parameters @@ -168,7 +168,7 @@ impl StatementVisitor<'_> for ModuleInterfaceVisitor<'_> { _name: p.parameter.name.to_string(), annotation: match &p.parameter.annotation { Some(annotation) => match annotation.as_ref() { - Expr::Name(name) => Some(name.id.clone()), + Expr::Name(name) => Some(name.id.to_string()), Expr::StringLiteral(s) => Some(s.value.to_string()), _ => None, }, @@ -178,7 +178,7 @@ impl StatementVisitor<'_> for ModuleInterfaceVisitor<'_> { .collect(), return_type: match node.returns.as_ref() { Some(r) => match r.as_ref() { - Expr::Name(name) => Some(name.id.clone()), + Expr::Name(name) => Some(name.id.to_string()), Expr::StringLiteral(s) => Some(s.value.to_string()), _ => None, }, @@ -189,7 +189,7 @@ impl StatementVisitor<'_> for ModuleInterfaceVisitor<'_> { } Stmt::ClassDef(node) => { self.current_interface_members.push(InterfaceMember { - name: node.name.id.clone(), + name: node.name.id.to_string(), node: InterfaceMemberNode::Class, }); } diff --git a/src/lib.rs b/src/lib.rs index 59055ddb..d5cb649d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ pub mod cli; pub mod colors; pub mod commands; pub mod config; +pub mod diagnostics; pub mod exclusion; pub mod external; pub mod filesystem; @@ -10,6 +11,7 @@ pub mod imports; pub mod interfaces; pub mod interrupt; pub mod lsp; +pub mod modularity; pub mod modules; pub mod parsing; pub mod pattern; @@ -17,6 +19,8 @@ pub mod python; pub mod tests; use commands::{check, report, server, sync, test}; +use diagnostics::serialize_diagnostics_json; +use modularity::into_usage_errors; use std::collections::HashMap; use std::path::PathBuf; @@ -248,7 +252,7 @@ fn check_external_dependencies( project_config: config::ProjectConfig, module_mappings: HashMap>, stdlib_modules: Vec, -) -> check::check_external::Result { +) -> check::check_external::Result> { let project_root = PathBuf::from(project_root); check::check_external::check( &project_root, @@ -336,7 +340,7 @@ fn check_internal( dependencies: bool, interfaces: bool, exclude_paths: Vec, -) -> Result { +) -> check::check_internal::Result> { check::check_internal( project_root, project_config, @@ -346,6 +350,14 @@ fn check_internal( ) } +#[pyfunction] +pub fn format_diagnostics( + project_root: PathBuf, + diagnostics: Vec, +) -> String { + check::format::DiagnosticFormatter::new(project_root).format_diagnostics(&diagnostics) +} + #[pyfunction] #[pyo3(signature = (project_root, project_config, exclude_paths))] fn detect_unused_dependencies( @@ -390,9 +402,9 @@ fn extension(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction_bound!(parse_project_config, m)?)?; m.add_function(wrap_pyfunction_bound!(get_project_imports, m)?)?; m.add_function(wrap_pyfunction_bound!(get_external_imports, m)?)?; @@ -405,9 +417,12 @@ fn extension(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction_bound!(update_computation_cache, m)?)?; m.add_function(wrap_pyfunction_bound!(dump_project_config_to_toml, m)?)?; m.add_function(wrap_pyfunction_bound!(check_internal, m)?)?; + m.add_function(wrap_pyfunction_bound!(format_diagnostics, m)?)?; m.add_function(wrap_pyfunction_bound!(detect_unused_dependencies, m)?)?; m.add_function(wrap_pyfunction_bound!(sync_project, m)?)?; m.add_function(wrap_pyfunction_bound!(run_server, m)?)?; m.add_function(wrap_pyfunction_bound!(serialize_modules_json, m)?)?; + m.add_function(wrap_pyfunction_bound!(serialize_diagnostics_json, m)?)?; + m.add_function(wrap_pyfunction_bound!(into_usage_errors, m)?)?; Ok(()) } diff --git a/src/lsp/server.rs b/src/lsp/server.rs index 97a87ef5..7c2d72ee 100644 --- a/src/lsp/server.rs +++ b/src/lsp/server.rs @@ -8,6 +8,7 @@ use lsp_server::{Connection, Message, Notification as NotificationMessage, Reque use crate::commands::check::check_internal; use crate::config; +use crate::diagnostics::{Diagnostic, Severity}; use crate::interrupt::{check_interrupt, get_interrupt_channel}; use super::error::ServerError; @@ -52,6 +53,39 @@ fn uri_to_path(uri: &Uri) -> PathBuf { } } +impl From for lsp_types::DiagnosticSeverity { + fn from(severity: Severity) -> Self { + match severity { + Severity::Error => lsp_types::DiagnosticSeverity::ERROR, + Severity::Warning => lsp_types::DiagnosticSeverity::WARNING, + } + } +} + +impl From for Option { + fn from(diag: Diagnostic) -> Self { + match diag { + Diagnostic::Global { .. } => None, + Diagnostic::Located { line_number, .. } => Some(lsp_types::Diagnostic { + range: lsp_types::Range { + start: lsp_types::Position { + line: (line_number - 1) as u32, + character: 0, + }, + end: lsp_types::Position { + line: (line_number - 1) as u32, + character: 99999, + }, + }, + severity: Some(diag.severity().into()), + source: Some("tach".to_string()), + message: diag.details().to_string(), + ..Default::default() + }), + } + } +} + impl LSPServer { pub fn new(project_root: PathBuf, project_config: config::ProjectConfig) -> Self { Self { @@ -137,32 +171,14 @@ impl LSPServer { self.project_config.exclude.clone(), )?; let diagnostics = check_result - .errors .into_iter() .filter_map(|e| { - if self.project_config.source_roots.iter().any(|source_root| { - let full_path = self.project_root.join(source_root).join(&e.file_path); - uri_pathbuf == full_path - }) { - Some(lsp_types::Diagnostic { - range: lsp_types::Range { - start: lsp_types::Position { - line: (e.line_number - 1) as u32, - character: 0, - }, - end: lsp_types::Position { - line: (e.line_number - 1) as u32, - character: 99999, - }, - }, - severity: Some(lsp_types::DiagnosticSeverity::ERROR), - source: Some("tach".to_string()), - message: e.error_info.to_string(), - ..Default::default() - }) - } else { - None + if let Some(file_path) = e.file_path() { + if uri_pathbuf == self.project_root.join(file_path) { + return e.into(); + } } + None }) .collect(); Ok(lsp_types::PublishDiagnosticsParams { diff --git a/src/modularity/diagnostics.rs b/src/modularity/diagnostics.rs new file mode 100644 index 00000000..647c8e4a --- /dev/null +++ b/src/modularity/diagnostics.rs @@ -0,0 +1,81 @@ +use pyo3::prelude::*; + +use crate::diagnostics::Diagnostic; + +#[derive(Debug, Clone)] +pub enum ErrorKind { + Dependency, + Interface, +} + +impl IntoPy for ErrorKind { + fn into_py(self, py: Python) -> PyObject { + match self { + Self::Dependency => "DEPENDENCY".to_object(py), + Self::Interface => "INTERFACE".to_object(py), + } + } +} + +#[derive(Debug, Clone)] +#[pyclass(get_all, module = "tach.extension")] +pub struct UsageError { + pub file: String, + pub line_number: usize, + pub member: String, + pub usage_module: String, + pub definition_module: String, + pub error_type: ErrorKind, +} + +impl TryFrom for UsageError { + type Error = (); + + fn try_from(value: Diagnostic) -> Result { + if let ( + is_interface_error, + is_dependency_error, + Some(file_path), + Some(line_number), + Some(member), + Some(usage_module), + Some(definition_module), + ) = ( + value.is_interface_error(), + value.is_dependency_error(), + value.file_path(), + value.line_number(), + value.import_mod_path(), + value.usage_module(), + value.definition_module(), + ) { + let error_type = match (is_interface_error, is_dependency_error) { + (false, false) => { + return Err(()); + } + (true, false) => ErrorKind::Interface, + (false, true) => ErrorKind::Dependency, + _ => return Err(()), + }; + + Ok(Self { + file: file_path.to_string_lossy().to_string(), + line_number, + member: member.to_string(), + usage_module: usage_module.to_string(), + definition_module: definition_module.to_string(), + error_type, + }) + } else { + Err(()) + } + } +} + +#[pyfunction] +pub fn into_usage_errors(diagnostics: Vec) -> Vec { + diagnostics + .into_iter() + .filter_map(|d| UsageError::try_from(d).ok()) + .collect() +} diff --git a/src/modularity/mod.rs b/src/modularity/mod.rs new file mode 100644 index 00000000..12695ce7 --- /dev/null +++ b/src/modularity/mod.rs @@ -0,0 +1,3 @@ +pub mod diagnostics; + +pub use diagnostics::{into_usage_errors, ErrorKind, UsageError};