From 89037c459ba6115aefad32608a54dc81b4baf888 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sat, 31 May 2025 22:43:59 -0400 Subject: [PATCH 1/5] initial setup of clippy-annotation-reporter github action. This is not part of the libdatadog workspace. It's a github action that will run for CI. It reports counts of clippy allow annotations for both changed files in the PR and a repo overall. The goal is to call attention to excessive usage of allows, which could be a signal of reduced code quality. This action should be moved to its own repo when confident of its functionality. --- .../clippy-annotation-reporter/Cargo.lock | 2935 +++++++++++++++++ .../clippy-annotation-reporter/Cargo.toml | 28 + .../clippy-annotation-reporter/action.yml | 56 + .../clippy-annotation-reporter/rustfmt.toml | 8 + .../clippy-annotation-reporter/src/main.rs | 79 + .../workflows/clippy-annotation-reporter.yml | 37 + 6 files changed, 3143 insertions(+) create mode 100644 .github/actions/clippy-annotation-reporter/Cargo.lock create mode 100644 .github/actions/clippy-annotation-reporter/Cargo.toml create mode 100644 .github/actions/clippy-annotation-reporter/action.yml create mode 100644 .github/actions/clippy-annotation-reporter/rustfmt.toml create mode 100644 .github/actions/clippy-annotation-reporter/src/main.rs create mode 100644 .github/workflows/clippy-annotation-reporter.yml diff --git a/.github/actions/clippy-annotation-reporter/Cargo.lock b/.github/actions/clippy-annotation-reporter/Cargo.lock new file mode 100644 index 0000000000..b579e625a1 --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/Cargo.lock @@ -0,0 +1,2935 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "ascii-canvas" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" +dependencies = [ + "term", +] + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.3.0", + "futures-lite 2.6.0", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite 2.6.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.0", + "parking", + "polling 3.8.0", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.4.0", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-object-pool" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "333c456b97c3f2d50604e8b2624253b7f787208cb72eb75e64b0ad11b221652c" +dependencies = [ + "async-std", +] + +[[package]] +name = "async-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" +dependencies = [ + "async-channel 2.3.1", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener 5.4.0", + "futures-lite 2.6.0", + "rustix", + "tracing", +] + +[[package]] +name = "async-signal" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-std" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "async-process", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 2.6.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "basic-cookies" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bd8fd42c16bdb08688243dc5f0cc117a3ca9efeeaba3a345a18a6159ad96f7" +dependencies = [ + "lalrpop", + "lalrpop-util", + "regex", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.6.0", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "castaway" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" + +[[package]] +name = "cc" +version = "1.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "clippy-annotation-reporter" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "env_logger", + "http 1.3.1", + "httpmock", + "log", + "mockall", + "octocrab", + "regex", + "serde_json", + "tempfile", + "tokio", + "toml", + "url", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "curl" +version = "0.4.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9fb4d13a1be2b58f14d60adba57c9834b78c62fd86c3e76a148f732686e9265" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "windows-sys 0.52.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.80+curl-8.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f7df2eac63200c3ab25bde3b2268ef2ee56af3d238e76d61f01c3c49bff734" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.52.0", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "ena" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d248bdd43ce613d87415282f69b9bb99d947d290b10962dd6c56233312c2ad5" +dependencies = [ + "log", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.0", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +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 = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand 2.3.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "hashbrown" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "httpmock" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b02e044d3b4c2f94936fb05f9649efa658ca788f44eb6b87554e2033fc8ce93" +dependencies = [ + "assert-json-diff", + "async-object-pool", + "async-trait", + "base64 0.21.7", + "basic-cookies", + "crossbeam-utils", + "form_urlencoded", + "futures-util", + "hyper 0.14.32", + "isahc", + "lazy_static", + "levenshtein", + "log", + "regex", + "serde", + "serde_json", + "serde_regex", + "similar", + "tokio", + "url", +] + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +dependencies = [ + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper 1.6.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "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 = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "isahc" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" +dependencies = [ + "async-channel 1.9.0", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener 2.5.3", + "futures-lite 1.13.0", + "http 0.2.12", + "log", + "mime", + "once_cell", + "polling 2.8.0", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lalrpop" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" +dependencies = [ + "ascii-canvas", + "bit-set", + "ena", + "itertools", + "lalrpop-util", + "petgraph", + "pico-args", + "regex", + "regex-syntax", + "string_cache", + "term", + "tiny-keccak", + "unicode-xid", + "walkdir", +] + +[[package]] +name = "lalrpop-util" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + +[[package]] +name = "libc" +version = "0.2.172" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" + +[[package]] +name = "libnghttp2-sys" +version = "0.1.11+1.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b6c24e48a7167cffa7119da39d577fa482e66c688a4aac016bee862e1a713c4" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.9.1", + "libc", +] + +[[package]] +name = "libz-sys" +version = "1.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +dependencies = [ + "value-bag", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mockall" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "octocrab" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86996964f8b721067b6ed238aa0ccee56ecad6ee5e714468aa567992d05d2b91" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.1", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-rustls", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", + "web-time", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pem" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38af38e8470ac9dee3ce1bae1af9c1671fffc44ddfd8bd1d0a3445bf349a8ef3" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[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.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.3.0", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "redox_syscall" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustls" +version = "0.23.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_regex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf" +dependencies = [ + "regex", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sluice" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d7400c0eff44aa2fcb5e31a5f24ba9716ed90138769e4977a2ba6014ae63eb5" +dependencies = [ + "async-channel 1.9.0", + "futures-core", + "futures-io", +] + +[[package]] +name = "smallvec" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" + +[[package]] +name = "snafu" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320b01e011bf8d5d7a4a4a4be966d9160968935849c83b918827f6a435e7f627" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1961e2ef424c1424204d3a5d6975f934f56b6d50ff5732382d84ebf460e147f7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand 2.3.0", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "term" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.45.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/.github/actions/clippy-annotation-reporter/Cargo.toml b/.github/actions/clippy-annotation-reporter/Cargo.toml new file mode 100644 index 0000000000..07cfa1db37 --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "clippy-annotation-reporter" +rust-version = "1.78.0" +edition = "2021" +version = "0.1.0" + +[dependencies] +clap = { version = "4.3", features = ["derive", "env"] } +octocrab = "0.44" +anyhow = "1.0" +regex = "1.9" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +serde_json = "1.0" +log = "0.4" +env_logger = "0.10" +# Octocrab has a defined MSRV < 1.78 and depends on url 2.5.4 which also has a defined MSRV < 1.78 but url has a dependency on `idna` has a MSRV of 1.81. Not sure what is going on here. +url = "=2.5.2" +toml = "0.8.22" +mockall = "0.13.1" + +[dev-dependencies] +httpmock = "0.6" +http = "1.1.0" +tempfile = "3.20.0" + +# TODO: Remove this when move to separate repo +[workspace] +members = ["."] diff --git a/.github/actions/clippy-annotation-reporter/action.yml b/.github/actions/clippy-annotation-reporter/action.yml new file mode 100644 index 0000000000..467255524f --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/action.yml @@ -0,0 +1,56 @@ +name: 'Clippy Annotation Reporter' +description: 'Reports changes in Clippy allow annotations' +author: 'Datadog' + +inputs: + github-token: + description: 'GitHub token for PR comment access' + required: true + default: ${{ github.token }} + allow-annotation-rules: + description: 'Comma-separated list of clippy rules to track' + required: false + default: 'unwrap_used,expect_used,todo,unimplemented,panic,unreachable' + base-branch: + description: 'Base branch to compare against' + required: false + default: 'origin/main' + log_level: + description: 'Log level (error, warn, info, debug, trace)' + required: false + default: 'info' + +runs: + using: 'composite' + steps: + - name: Set up Rust + shell: bash + run: rustup install stable && rustup default stable + + - name: Fetch all branches + shell: bash + run: | + git fetch --all + echo "Available branches:" + git branch -a + + - name: Build annotation reporter + shell: bash + run: | + cd ${{ github.action_path }} + cargo build --release + - name: Debug step + shell: bash + run: | + FILE_PATH="data-pipeline/src/trace_exporter/mod.rs" + - name: Run annotation reporter + shell: bash + run: | + ${{ github.action_path }}/target/release/clippy-annotation-reporter \ + --token "${{ inputs.github-token }}" \ + --rules "${{ inputs.allow-annotation-rules }}" \ + --repo "${{ github.repository }}" \ + --pr "${{ github.event.number }}" \ + --base-branch "${{ inputs.base_branch }}" + env: + GITHUB_TOKEN: ${{ inputs.github-token }} diff --git a/.github/actions/clippy-annotation-reporter/rustfmt.toml b/.github/actions/clippy-annotation-reporter/rustfmt.toml new file mode 100644 index 0000000000..47d8649ac1 --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/rustfmt.toml @@ -0,0 +1,8 @@ +unstable_features = true + +format_macro_matchers = true +format_code_in_doc_comments=true +wrap_comments = true +max_width=100 +comment_width=100 +doc_comment_code_block_width=100 diff --git a/.github/actions/clippy-annotation-reporter/src/main.rs b/.github/actions/clippy-annotation-reporter/src/main.rs new file mode 100644 index 0000000000..9190aaceb8 --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/main.rs @@ -0,0 +1,79 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context as _, Result}; +use log::info; +use octocrab::Octocrab; +use std::env; + +mod analyzer; +mod commenter; +mod config; +mod report_generator; + +use crate::config::ConfigBuilder; +use crate::report_generator::generate_report; + +#[tokio::main] +async fn main() -> Result<()> { + let log_level = env::var("INPUT_LOG_LEVEL").unwrap_or_else(|_| "info".to_string()); + + // Set RUST_LOG if not already set + if env::var("RUST_LOG").is_err() { + env::set_var("RUST_LOG", &log_level); + } + + env_logger::init(); + + info!("Clippy Annotation Reporter starting..."); + + let config = ConfigBuilder::new().build()?; + + let octocrab = Octocrab::builder() + .personal_token(config.token.clone()) + .build() + .context("Failed to build GitHub API client")?; + + let analysis_result = match analyzer::run_analysis( + &octocrab, + &config.owner, + &config.repo, + config.pr_number, + &config.base_branch, + &config.head_branch, + &config.rules, + ) + .await + { + Ok(result) => result, + Err(e) => { + if e.to_string().contains("No Rust files changed") { + info!("No Rust files changed in this PR, nothing to analyze."); + return Ok(()); + } + return Err(e); + } + }; + + let report = generate_report( + &analysis_result, + &config.rules, + &config.repository, + &config.base_branch, + &config.head_branch, + ); + + commenter::post_comment( + &octocrab, + &config.owner, + &config.repo, + config.pr_number, + report, + None, + ) + .await?; + + info!("Process completed successfully!"); + + Ok(()) +} diff --git a/.github/workflows/clippy-annotation-reporter.yml b/.github/workflows/clippy-annotation-reporter.yml new file mode 100644 index 0000000000..47748de6cc --- /dev/null +++ b/.github/workflows/clippy-annotation-reporter.yml @@ -0,0 +1,37 @@ +name: Clippy Annotation Reporter + +on: + pull_request: + +permissions: + contents: read + pull-requests: write + +jobs: + clippy-annotation-reporter: + runs-on: ubuntu-latest + continue-on-error: true + env: + CARGO_TERM_COLOR: always + steps: + - name: Free Disk Space + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: true + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + with: + fetch-depth: 0 # Full history + - name: Install Rust + run: rustup install stable + - name: Run annotation reporter + uses: ./.github/actions/clippy-annotation-reporter + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + allow-annotation-rules: "expect_used, panic, todo, unimplemented, unwrap_used" + log_level: 'info' From 1ef0359ba1a4ce7598f5d0d96239297de2580903 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sat, 31 May 2025 22:44:25 -0400 Subject: [PATCH 2/5] add the analyzer module to clippy-annotation-reporter. This module reviews the files in the repo to parse and count the allow annotations usage. It also compares changed files to their base to determine the diffs of counts. --- .../src/analyzer/annotation.rs | 326 ++++++++++ .../src/analyzer/crate_detection.rs | 333 ++++++++++ .../src/analyzer/git.rs | 586 ++++++++++++++++++ .../src/analyzer/mod.rs | 203 ++++++ 4 files changed, 1448 insertions(+) create mode 100644 .github/actions/clippy-annotation-reporter/src/analyzer/annotation.rs create mode 100644 .github/actions/clippy-annotation-reporter/src/analyzer/crate_detection.rs create mode 100644 .github/actions/clippy-annotation-reporter/src/analyzer/git.rs create mode 100644 .github/actions/clippy-annotation-reporter/src/analyzer/mod.rs diff --git a/.github/actions/clippy-annotation-reporter/src/analyzer/annotation.rs b/.github/actions/clippy-annotation-reporter/src/analyzer/annotation.rs new file mode 100644 index 0000000000..f4e0ab485c --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/analyzer/annotation.rs @@ -0,0 +1,326 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Functions for finding and parsing clippy annotations + +use crate::analyzer::crate_detection::get_crate_for_file; +use crate::analyzer::ClippyAnnotation; +use anyhow::{Context, Result}; +use regex::Regex; +use std::collections::HashMap; +use std::rc::Rc; + +/// Find clippy annotations in file content +pub(super) fn find_annotations( + annotations: &mut Vec, + file: &str, + content: &str, + regex: &Regex, + rule_cache: &mut HashMap>, +) { + // Use Rc for file path + let file_rc = Rc::new(file.to_owned()); + + for line in content.lines() { + if let Some(captures) = regex.captures(line) { + if let Some(rule_match) = captures.get(1) { + let rule_str = rule_match.as_str().to_owned(); + + // Get or create Rc for this rule + let rule_rc = match rule_cache.get(&rule_str) { + Some(cached) => Rc::clone(cached), + None => { + let rc = Rc::new(rule_str.clone()); + rule_cache.insert(rule_str, Rc::clone(&rc)); + rc + } + }; + + annotations.push(ClippyAnnotation { + file: Rc::clone(&file_rc), + rule: rule_rc, + }); + } + } + } +} + +/// Count annotations by rule +pub(super) fn count_annotations_by_rule( + annotations: &[ClippyAnnotation], +) -> HashMap, usize> { + let mut counts = HashMap::with_capacity(annotations.len().min(20)); + + for annotation in annotations { + *counts.entry(Rc::clone(&annotation.rule)).or_insert(0) += 1; + } + + counts +} + +/// Count annotations by crate +pub(super) fn count_annotations_by_crate( + annotations: &[ClippyAnnotation], +) -> HashMap, usize> { + let mut counts = HashMap::new(); + let mut crate_cache: HashMap> = HashMap::new(); + + for annotation in annotations { + let file_path = annotation.file.as_str(); + + // Use cached crate name if we've seen this file before + let crate_name = match crate_cache.get(file_path) { + Some(name) => name.clone(), + None => { + let name = Rc::new(get_crate_for_file(file_path).to_owned()); + crate_cache.insert(file_path.to_owned(), Rc::clone(&name)); + + name + } + }; + + *counts.entry(crate_name).or_insert(0) += 1; + } + + counts +} + +/// Create a regex for matching clippy allow annotations +pub(super) fn create_annotation_regex(rules: &[String]) -> Result { + if rules.is_empty() { + return Err(anyhow::anyhow!("Cannot create regex with empty rules list")); + } + + let rule_pattern = rules.join("|"); + let regex = Regex::new(&format!( + r"#\s*\[\s*allow\s*\(\s*clippy\s*::\s*({})\s*\)\s*\]", + rule_pattern + )) + .context("Failed to compile annotation regex")?; + + Ok(regex) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::rc::Rc; + + #[test] + fn test_count_annotations_by_rule() { + // Create test annotations + let rule1 = Rc::new("clippy::unwrap_used".to_owned()); + let rule2 = Rc::new("clippy::match_bool".to_owned()); + let file = Rc::new("src/main.rs".to_owned()); + + let annotations = vec![ + ClippyAnnotation { + file: file.clone(), + rule: rule1.clone(), + }, + ClippyAnnotation { + file: file.clone(), + rule: rule1.clone(), + }, + ClippyAnnotation { + file: file.clone(), + rule: rule2.clone(), + }, + ClippyAnnotation { + file: file.clone(), + rule: rule1.clone(), + }, + ]; + + let counts = count_annotations_by_rule(&annotations); + + assert_eq!(counts.len(), 2, "Should have counts for 2 rules"); + assert_eq!(counts[&rule1], 3, "Rule1 should have 3 annotations"); + assert_eq!(counts[&rule2], 1, "Rule2 should have 1 annotation"); + } + + #[test] + fn test_count_annotations_by_rule_empty() { + // Test with empty annotations + let annotations: Vec = vec![]; + let counts = count_annotations_by_rule(&annotations); + + assert_eq!( + counts.len(), + 0, + "Empty annotations should produce empty counts" + ); + } + + #[test] + fn test_count_annotations_by_crate() { + use std::fs::{self, File}; + use std::io::Write; + use tempfile::TempDir; + + // Create a temporary directory structure for testing + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let temp_path = temp_dir.path(); + + // Create a directory structure with two different crates + // crate1 + // ├── Cargo.toml (package.name = "crate1") + // └── src + // ├── lib.rs + // └── module.rs + // crate2 + // ├── Cargo.toml (package.name = "crate2") + // └── src + // └── main.rs + + // Create directories + let crate1_dir = temp_path.join("crate1"); + let crate1_src_dir = crate1_dir.join("src"); + let crate2_dir = temp_path.join("crate2"); + let crate2_src_dir = crate2_dir.join("src"); + + fs::create_dir_all(&crate1_src_dir).expect("Failed to create crate1/src directory"); + fs::create_dir_all(&crate2_src_dir).expect("Failed to create crate2/src directory"); + + // Create Cargo.toml files with specific package names + let crate1_cargo = crate1_dir.join("Cargo.toml"); + let mut cargo1_file = + File::create(&crate1_cargo).expect("Failed to create crate1 Cargo.toml"); + writeln!( + cargo1_file, + r#"[package] +name = "crate1" +version = "0.1.0" +edition = "2021" +"# + ) + .expect("Failed to write to crate1 Cargo.toml"); + + let crate2_cargo = crate2_dir.join("Cargo.toml"); + let mut cargo2_file = + File::create(&crate2_cargo).expect("Failed to create crate2 Cargo.toml"); + writeln!( + cargo2_file, + r#"[package] +name = "crate2" +version = "0.1.0" +edition = "2021" +"# + ) + .expect("Failed to write to crate2 Cargo.toml"); + + // Create source files + let crate1_lib = crate1_src_dir.join("lib.rs"); + let mut lib_file = File::create(&crate1_lib).expect("Failed to create lib.rs"); + writeln!(lib_file, "// Empty lib file").expect("Failed to write to lib.rs"); + + let crate1_module = crate1_src_dir.join("module.rs"); + let mut module_file = File::create(&crate1_module).expect("Failed to create module.rs"); + writeln!(module_file, "// Empty module file").expect("Failed to write to module.rs"); + + let crate2_main = crate2_src_dir.join("main.rs"); + let mut main_file = File::create(&crate2_main).expect("Failed to create main.rs"); + writeln!(main_file, "// Empty main file").expect("Failed to write to main.rs"); + + // Create test annotations with the real file paths + let rule = Rc::new("clippy::unwrap_used".to_owned()); + + let crate1_lib_path = Rc::new(crate1_lib.to_string_lossy().to_string()); + let crate1_module_path = Rc::new(crate1_module.to_string_lossy().to_string()); + let crate2_main_path = Rc::new(crate2_main.to_string_lossy().to_string()); + + let annotations = vec![ + ClippyAnnotation { + file: crate1_lib_path.clone(), + rule: rule.clone(), + }, + ClippyAnnotation { + file: crate1_module_path.clone(), + rule: rule.clone(), + }, + ClippyAnnotation { + file: crate1_module_path.clone(), // Another annotation in the same file + rule: rule.clone(), + }, + ClippyAnnotation { + file: crate2_main_path.clone(), + rule: rule.clone(), + }, + ]; + + let counts = count_annotations_by_crate(&annotations); + + assert_eq!(counts.len(), 2, "Should have counts for 2 crates"); + + let crate1_count = counts + .iter() + .find(|(k, _)| k.contains("crate1")) + .map(|(_, v)| *v) + .unwrap_or(0); + + let crate2_count = counts + .iter() + .find(|(k, _)| k.contains("crate2")) + .map(|(_, v)| *v) + .unwrap_or(0); + + assert_eq!(crate1_count, 3, "crate1 should have 3 annotations"); + assert_eq!(crate2_count, 1, "crate2 should have 1 annotation"); + } + + #[test] + fn test_count_annotations_by_crate_empty() { + // Test with empty annotations + let annotations: Vec = vec![]; + let counts = count_annotations_by_crate(&annotations); + + assert_eq!( + counts.len(), + 0, + "Empty annotations should produce empty counts" + ); + } + + #[test] + fn test_create_annotation_regex_single_rule() { + let rules = vec!["unwrap_used".to_owned()]; // Rule without clippy:: prefix + let regex = create_annotation_regex(&rules).expect("Failed to create regex"); + + // Test matching + assert!(regex.is_match("#[allow(clippy::unwrap_used)]")); + assert!(regex.is_match("#[allow(clippy:: unwrap_used )]")); // With spaces + assert!(regex.is_match("# [ allow ( clippy :: unwrap_used ) ]")); // With more spaces + + // Test non-matching + assert!(!regex.is_match("#[allow(clippy::unused_imports)]")); + assert!(!regex.is_match("#[allow(unwrap_used)]")); // Missing clippy:: + assert!(!regex.is_match("clippy::unwrap_used")); // Missing #[allow()] + } + #[test] + fn test_create_annotation_regex_multiple_rules() { + let rules = vec!["unwrap_used".to_owned(), "match_bool".to_owned()]; + let regex = create_annotation_regex(&rules).expect("Failed to create regex"); + + assert!(regex.is_match("#[allow(clippy::unwrap_used)]")); + assert!(regex.is_match("#[allow(clippy::match_bool)]")); + + // Test mixed spacing and formatting + assert!(regex.is_match("#[allow(clippy:: unwrap_used )]")); // With spaces + assert!(regex.is_match("# [ allow ( clippy :: match_bool ) ]")); // With more spaces + + // Test non-matching + assert!(!regex.is_match("#[allow(clippy::unused_imports)]")); + assert!(!regex.is_match("#[allow(unwrap_used)]")); // Missing clippy:: + } + + #[test] + fn test_create_annotation_regex_empty_rules() { + let rules: Vec = vec![]; + let result = create_annotation_regex(&rules); + + assert!( + result.is_err(), + "Creating regex with empty rules should fail" + ); + } +} diff --git a/.github/actions/clippy-annotation-reporter/src/analyzer/crate_detection.rs b/.github/actions/clippy-annotation-reporter/src/analyzer/crate_detection.rs new file mode 100644 index 0000000000..3e5847bbdf --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/analyzer/crate_detection.rs @@ -0,0 +1,333 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use log::error; +use std::fs; +use std::path::{Path, PathBuf}; +use toml::Value; + +/// Get crate information for a given file path by finding the closest Cargo.toml +pub(super) fn get_crate_for_file(file_path: &str) -> String { + let path = Path::new(file_path); + + // Try to find the closest Cargo.toml file + if let Some(cargo_toml_path) = find_closest_cargo_toml(path) { + if let Some(crate_name) = extract_package_name(&cargo_toml_path) { + return crate_name; + } + } + + error!( + "Could not find crate for {}, falling back to unknown-crate", + file_path + ); + "unknown-crate".to_owned() +} + +/// Find the closest Cargo.toml file by traversing up the directory tree +fn find_closest_cargo_toml(mut path: &Path) -> Option { + // Start with the directory containing the file + if !path.is_dir() { + path = path.parent()?; + } + + // Traverse up the directory tree + loop { + let cargo_path = path.join("Cargo.toml"); + if cargo_path.exists() { + return Some(cargo_path); + } + + // Check if we've reached the root + let parent = path.parent()?; + if parent == path { + // We've reached the root without finding Cargo.toml + return None; + } + + // Move up one directory + path = parent; + } +} + +/// Extract package name from Cargo.toml +fn extract_package_name(cargo_toml_path: &Path) -> Option { + // Read the Cargo.toml file + let content = match fs::read_to_string(cargo_toml_path) { + Ok(content) => content, + Err(e) => { + log::warn!("Failed to read {}: {}", cargo_toml_path.display(), e); + return None; + } + }; + + // Parse the TOML + let toml_value: Value = match content.parse() { + Ok(value) => value, + Err(e) => { + log::warn!("Failed to parse {}: {}", cargo_toml_path.display(), e); + return None; + } + }; + + // Extract the package name + toml_value + .get("package")? + .get("name")? + .as_str() + .map(|s| s.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs::{self, File}; + use std::io::Write; + use tempfile::TempDir; + + // Helper function to create a Cargo.toml file with a specific package name + fn create_cargo_toml(path: &Path, package_name: &str) -> std::io::Result<()> { + let mut file = File::create(path)?; + writeln!( + file, + r#"[package] +name = "{}" +version = "0.1.0" +edition = "2021" +"#, + package_name + )?; + Ok(()) + } + + #[test] + fn test_get_crate_for_file_direct_parent() { + // Create a temporary directory with a specific crate structure + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let crate_root = temp_dir.path().join("my-crate"); + fs::create_dir_all(&crate_root).expect("Failed to create crate directory"); + + // Create Cargo.toml in the crate root + let cargo_toml_path = crate_root.join("Cargo.toml"); + create_cargo_toml(&cargo_toml_path, "my-awesome-crate") + .expect("Failed to create Cargo.toml"); + + // Create a source file in the same directory + let source_file_path = crate_root.join("lib.rs"); + File::create(&source_file_path).expect("Failed to create source file"); + + let crate_name = get_crate_for_file(&source_file_path.to_string_lossy()); + + assert_eq!( + crate_name, "my-awesome-crate", + "Should identify the correct crate name from direct parent" + ); + } + + #[test] + fn test_get_crate_for_file_nested_directory() { + // Create a temporary directory with a nested structure + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let crate_root = temp_dir.path().join("my-crate"); + let src_dir = crate_root.join("src"); + let module_dir = src_dir.join("module"); + fs::create_dir_all(&module_dir).expect("Failed to create nested directories"); + + // Create Cargo.toml in the crate root + let cargo_toml_path = crate_root.join("Cargo.toml"); + create_cargo_toml(&cargo_toml_path, "nested-crate").expect("Failed to create Cargo.toml"); + + // Create a source file in the nested directory + let source_file_path = module_dir.join("mod.rs"); + File::create(&source_file_path).expect("Failed to create source file"); + + let crate_name = get_crate_for_file(&source_file_path.to_string_lossy()); + + assert_eq!( + crate_name, "nested-crate", + "Should identify the correct crate name from nested directory" + ); + } + + #[test] + fn test_get_crate_for_file_multiple_cargo_tomls() { + // Create a temporary directory with nested crates + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let workspace_dir = temp_dir.path().join("workspace"); + let parent_crate_dir = workspace_dir.join("parent-crate"); + let child_crate_dir = parent_crate_dir.join("child-crate"); + let child_src_dir = child_crate_dir.join("src"); + + fs::create_dir_all(&parent_crate_dir).expect("Failed to create parent crate directory"); + fs::create_dir_all(&child_src_dir).expect("Failed to create child crate src directory"); + + // Create Cargo.toml in both crate directories + let parent_cargo_toml = parent_crate_dir.join("Cargo.toml"); + create_cargo_toml(&parent_cargo_toml, "parent-crate") + .expect("Failed to create parent Cargo.toml"); + + let child_cargo_toml = child_crate_dir.join("Cargo.toml"); + create_cargo_toml(&child_cargo_toml, "child-crate") + .expect("Failed to create child Cargo.toml"); + + // Create a source file in the child crate + let source_file_path = child_src_dir.join("lib.rs"); + File::create(&source_file_path).expect("Failed to create source file"); + + let crate_name = get_crate_for_file(&source_file_path.to_string_lossy()); + + assert_eq!( + crate_name, "child-crate", + "Should identify the closest crate" + ); + } + + #[test] + fn test_get_crate_for_file_no_cargo_toml() { + // Create a temporary directory with no Cargo.toml + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let src_dir = temp_dir.path().join("src"); + fs::create_dir_all(&src_dir).expect("Failed to create src directory"); + + // Create a source file + let source_file_path = src_dir.join("orphan.rs"); + File::create(&source_file_path).expect("Failed to create source file"); + + let crate_name = get_crate_for_file(&source_file_path.to_string_lossy()); + + assert_eq!( + crate_name, "unknown-crate", + "Should return unknown-crate when no Cargo.toml is found" + ); + } + + #[test] + fn test_get_crate_for_file_invalid_cargo_toml() { + // Create a temporary directory with an invalid Cargo.toml + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let crate_dir = temp_dir.path().join("invalid-crate"); + fs::create_dir_all(&crate_dir).expect("Failed to create crate directory"); + + // Create an invalid Cargo.toml (missing package name) + let cargo_toml_path = crate_dir.join("Cargo.toml"); + let mut file = File::create(&cargo_toml_path).expect("Failed to create Cargo.toml"); + writeln!( + file, + r#" +[package] +version = "0.1.0" +edition = "2021" +"# + ) + .expect("Failed to write to Cargo.toml"); + + // Create a source file + let source_file_path = crate_dir.join("lib.rs"); + File::create(&source_file_path).expect("Failed to create source file"); + + let crate_name = get_crate_for_file(&source_file_path.to_string_lossy()); + + assert_eq!( + crate_name, "unknown-crate", + "Should return unknown-crate when Cargo.toml is invalid" + ); + } + + #[test] + fn test_get_crate_for_file_workspace_member() { + // Create a temporary directory with a workspace structure + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let workspace_dir = temp_dir.path().join("workspace"); + fs::create_dir_all(&workspace_dir).expect("Failed to create workspace directory"); + + // Create workspace Cargo.toml + let workspace_cargo_toml = workspace_dir.join("Cargo.toml"); + let mut file = + File::create(&workspace_cargo_toml).expect("Failed to create workspace Cargo.toml"); + writeln!( + file, + r#" +[workspace] +members = ["member1", "member2"] +"# + ) + .expect("Failed to write to workspace Cargo.toml"); + + // Create member1 crate + let member1_dir = workspace_dir.join("member1"); + let member1_src_dir = member1_dir.join("src"); + fs::create_dir_all(&member1_src_dir).expect("Failed to create member1 src directory"); + + // Create member1 Cargo.toml + let member1_cargo_toml = member1_dir.join("Cargo.toml"); + create_cargo_toml(&member1_cargo_toml, "workspace-member1") + .expect("Failed to create member1 Cargo.toml"); + + // Create a source file in member1 + let source_file_path = member1_src_dir.join("lib.rs"); + File::create(&source_file_path).expect("Failed to create source file"); + + let crate_name = get_crate_for_file(&source_file_path.to_string_lossy()); + + assert_eq!( + crate_name, "workspace-member1", + "Should identify the workspace member crate" + ); + } + + #[test] + fn test_get_crate_for_file_non_existent_file() { + // Test with a non-existent file path + let crate_name = get_crate_for_file("/path/to/non/existent/file.rs"); + + assert_eq!( + crate_name, "unknown-crate", + "Should return unknown-crate for non-existent file" + ); + } + + #[test] + fn test_get_crate_for_file_with_special_chars() { + // Create a temporary directory with spaces and special characters + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let crate_dir = temp_dir.path().join("my crate with spaces"); + fs::create_dir_all(&crate_dir).expect("Failed to create crate directory with spaces"); + + // Create Cargo.toml + let cargo_toml_path = crate_dir.join("Cargo.toml"); + create_cargo_toml(&cargo_toml_path, "special-chars-crate") + .expect("Failed to create Cargo.toml"); + + // Create a source file + let source_file_path = crate_dir.join("special file.rs"); + File::create(&source_file_path).expect("Failed to create source file with spaces"); + + let crate_name = get_crate_for_file(&source_file_path.to_string_lossy()); + + assert_eq!( + crate_name, "special-chars-crate", + "Should handle paths with spaces correctly" + ); + } + + #[test] + fn test_get_crate_for_file_with_directory_not_file() { + // Create a temporary directory structure + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let crate_dir = temp_dir.path().join("directory-test"); + let src_dir = crate_dir.join("src"); + fs::create_dir_all(&src_dir).expect("Failed to create directories"); + + // Create Cargo.toml + let cargo_toml_path = crate_dir.join("Cargo.toml"); + create_cargo_toml(&cargo_toml_path, "directory-crate") + .expect("Failed to create Cargo.toml"); + + let crate_name = get_crate_for_file(&src_dir.to_string_lossy()); + + assert_eq!( + crate_name, "directory-crate", + "Should work with directory paths" + ); + } +} diff --git a/.github/actions/clippy-annotation-reporter/src/analyzer/git.rs b/.github/actions/clippy-annotation-reporter/src/analyzer/git.rs new file mode 100644 index 0000000000..ef5aee6b39 --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/analyzer/git.rs @@ -0,0 +1,586 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::{Context as _, Result}; +use log::{debug, info}; +use octocrab::Octocrab; +use std::process::{Command, Output}; + +// Define a trait for command execution +#[cfg_attr(test, mockall::automock)] +pub trait CommandExecutor { + fn execute_command<'a>(&self, command: &str, args: &'a [&'a str]) -> std::io::Result; +} + +pub struct RealCommandExecutor; + +impl CommandExecutor for RealCommandExecutor { + fn execute_command<'a>(&self, command: &str, args: &'a [&'a str]) -> std::io::Result { + Command::new(command).args(args).output() + } +} + +// Default implementation for RealCommandExecutor +impl Default for RealCommandExecutor { + fn default() -> Self { + Self + } +} + +// Git operations struct that takes a CommandExecutor +pub struct GitOperations { + executor: T, +} + +impl GitOperations { + // Constructor with explicit executor + pub fn new(executor: T) -> Self { + Self { executor } + } + + /// Get file content from a specific branch + pub fn get_file_content(&self, file: &str, branch: &str) -> Result { + debug!("Getting content for {} from {}", file, branch); + + let output = self + .executor + .execute_command("git", &["show", &format!("{}:{}", branch, file)]) + .context(format!("Failed to execute git show command for {}", file))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("Git show command failed: {}", stderr); + } + + let content = + String::from_utf8(output.stdout).context("Failed to parse file content as UTF-8")?; + + Ok(content) + } + + /// Get file content from a branch, handling common errors + pub fn get_branch_content(&self, file: &str, branch: &str) -> String { + match self.get_file_content(file, branch) { + Ok(content) => content, + Err(e) => { + // Skip errors for files that might not exist in one branch + if !e.to_string().contains("did not match any file") { + log::warn!("Failed to get {} content from {}: {}", file, branch, e); + } + String::new() + } + } + } + + /// Get all Rust files in the repository + pub fn get_all_rust_files(&self) -> Result> { + info!("Getting all Rust files in the repository..."); + + let output = self + .executor + .execute_command("git", &["ls-files", "*.rs"]) + .context("Failed to execute git ls-files command")?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("Git ls-files command failed: {}", stderr); + } + + let files = + String::from_utf8(output.stdout).context("Failed to parse git ls-files output")?; + + let rust_files: Vec = files.lines().map(|line| line.to_owned()).collect(); + + info!("Found {} Rust files in total", rust_files.len()); + + Ok(rust_files) + } +} + +// Default implementation for GitOperations with RealCommandExecutor +impl Default for GitOperations { + fn default() -> Self { + Self::new(RealCommandExecutor::default()) + } +} + +// Standalone function for getting changed files from GitHub PR +pub async fn get_changed_files( + octocrab: &Octocrab, + owner: &str, + repo: &str, + pr_number: u64, +) -> Result> { + info!("Getting changed files from PR #{}...", pr_number); + + let first_files = octocrab + .pulls(owner, repo) + .list_files(pr_number) + .await + .context("Failed to list PR files")?; + + let all_files = octocrab + .all_pages(first_files) + .await + .context("Failed to fetch all pages of PR files")?; + + // Filter for Rust files only + let rust_files: Vec = all_files + .into_iter() + .filter(|file| file.filename.ends_with(".rs")) + .map(|file| file.filename) + .collect(); + + info!("Found {} changed Rust files", rust_files.len()); + + Ok(rust_files) +} + +#[cfg(test)] +mod tests { + use super::*; + use http::Uri; + use httpmock::prelude::*; + use httpmock::MockServer; + use mockall::predicate::*; + use serde_json::json; + use std::io::{Error as IoError, ErrorKind}; + use std::os::unix::process::ExitStatusExt; + use std::process::{ExitStatus, Output}; + use std::str::FromStr; + + // Helper function to create mock output + fn create_mock_output(status: i32, stdout: &str, stderr: &str) -> std::io::Result { + Ok(Output { + status: ExitStatus::from_raw(status), + stdout: stdout.as_bytes().to_vec(), + stderr: stderr.as_bytes().to_vec(), + }) + } + + // Helper function to create test octocrab + fn create_test_octocrab(server: &MockServer) -> Octocrab { + let uri = Uri::from_str(&server.base_url()).unwrap(); + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() + } + + #[test] + fn test_get_file_content_success() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" + && args.len() == 2 + && args[0] == "show" + && args[1] == "main:src/test.rs" + }) + .times(1) + .returning(|_, _| create_mock_output(0, "fn test() {}\n", "")); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_file_content("src/test.rs", "main"); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "fn test() {}\n"); + } + + #[test] + fn test_get_file_content_command_error() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" + && args.len() == 2 + && args[0] == "show" + && args[1] == "main:src/test.rs" + }) + .times(1) + .returning(|_, _| Err(IoError::new(ErrorKind::NotFound, "git command not found"))); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_file_content("src/test.rs", "main"); + + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Failed to execute git show command")); + } + + #[test] + fn test_get_file_content_git_error() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" + && args.len() == 2 + && args[0] == "show" + && args[1] == "main:src/nonexistent.rs" + }) + .times(1) + .returning(|_, _| { + create_mock_output( + 1, + "", + "fatal: Path 'src/nonexistent.rs' does not exist in 'main'", + ) + }); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_file_content("src/nonexistent.rs", "main"); + + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Git show command failed")); + } + + #[test] + fn test_get_file_content_invalid_utf8() { + let mut mock_executor = MockCommandExecutor::new(); + + // Create invalid UTF-8 output + let invalid_utf8 = vec![0, 159, 146, 150]; // Invalid UTF-8 sequence + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" + && args.len() == 2 + && args[0] == "show" + && args[1] == "main:src/binary.rs" + }) + .times(1) + .returning(move |_, _| { + Ok(Output { + status: ExitStatus::from_raw(0), + stdout: invalid_utf8.clone(), + stderr: Vec::new(), + }) + }); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_file_content("src/binary.rs", "main"); + + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Failed to parse file content as UTF-8")); + } + + #[test] + fn test_get_branch_content_success() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" + && args.len() == 2 + && args[0] == "show" + && args[1] == "main:src/test.rs" + }) + .times(1) + .returning(|_, _| create_mock_output(0, "fn test() {}\n", "")); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_branch_content("src/test.rs", "main"); + + assert_eq!(result, "fn test() {}\n"); + } + + #[test] + fn test_get_branch_content_file_not_found() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" + && args.len() == 2 + && args[0] == "show" + && args[1] == "main:src/nonexistent.rs" + }) + .times(1) + .returning(|_, _| { + create_mock_output( + 1, + "", + "fatal: Path 'src/nonexistent.rs' does not exist in 'main'", + ) + }); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_branch_content("src/nonexistent.rs", "main"); + + assert_eq!(result, ""); // Should return empty string for non-existent file + } + + #[test] + fn test_get_branch_content_other_error() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" + && args.len() == 2 + && args[0] == "show" + && args[1] == "invalid-branch:src/test.rs" + }) + .times(1) + .returning(|_, _| { + create_mock_output(1, "", "fatal: invalid branch name: invalid-branch") + }); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_branch_content("src/test.rs", "invalid-branch"); + + assert_eq!(result, ""); // Should return empty string for any error + } + + #[test] + fn test_get_all_rust_files_success() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" && args.len() == 2 && args[0] == "ls-files" && args[1] == "*.rs" + }) + .times(1) + .returning(|_, _| { + create_mock_output(0, "src/main.rs\nsrc/lib.rs\nsrc/module.rs\n", "") + }); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_all_rust_files(); + + assert!(result.is_ok()); + let files = result.unwrap(); + assert_eq!(files.len(), 3); + assert_eq!(files[0], "src/main.rs"); + assert_eq!(files[1], "src/lib.rs"); + assert_eq!(files[2], "src/module.rs"); + } + + #[test] + fn test_get_all_rust_files_empty_repo() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" && args.len() == 2 && args[0] == "ls-files" && args[1] == "*.rs" + }) + .times(1) + .returning(|_, _| create_mock_output(0, "", "")); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_all_rust_files(); + + assert!(result.is_ok()); + let files = result.unwrap(); + assert_eq!(files.len(), 0); // Should return empty list for repo with no Rust files + } + + #[test] + fn test_get_all_rust_files_command_error() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" && args.len() == 2 && args[0] == "ls-files" && args[1] == "*.rs" + }) + .times(1) + .returning(|_, _| Err(IoError::new(ErrorKind::NotFound, "git command not found"))); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_all_rust_files(); + + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Failed to execute git ls-files command")); + } + + #[test] + fn test_get_all_rust_files_git_error() { + let mut mock_executor = MockCommandExecutor::new(); + + mock_executor + .expect_execute_command() + .withf(|cmd, args| { + cmd == "git" && args.len() == 2 && args[0] == "ls-files" && args[1] == "*.rs" + }) + .times(1) + .returning(|_, _| create_mock_output(1, "", "fatal: not a git repository")); + + let git_ops = GitOperations::new(mock_executor); + + let result = git_ops.get_all_rust_files(); + + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Git ls-files command failed")); + } + + // Tests for get_changed_files + #[tokio::test] + async fn test_get_changed_files_success() { + let server = MockServer::start(); + + let list_files_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/pulls/123/files"); + + then.status(200) + .header("content-type", "application/json") + .json_body(json!([ + { + "sha": "abc123", + "filename": "src/main.rs", + "status": "modified", + "additions": 10, + "deletions": 5, + "changes": 15, + "blob_url": "https://github.com/test-owner/test-repo/blob/abc123/src/main.rs", + "raw_url": "https://github.com/test-owner/test-repo/raw/abc123/src/main.rs", + "contents_url": "https://api.github.com/repos/test-owner/test-repo/contents/src/main.rs?ref=abc123" + }, + { + "sha": "def456", + "filename": "src/lib.rs", + "status": "modified", + "additions": 7, + "deletions": 3, + "changes": 10, + "blob_url": "https://github.com/test-owner/test-repo/blob/def456/src/lib.rs", + "raw_url": "https://github.com/test-owner/test-repo/raw/def456/src/lib.rs", + "contents_url": "https://api.github.com/repos/test-owner/test-repo/contents/src/lib.rs?ref=def456" + }, + { + "sha": "ghi789", + "filename": "README.md", + "status": "modified", + "additions": 2, + "deletions": 0, + "changes": 2, + "blob_url": "https://github.com/test-owner/test-repo/blob/ghi789/README.md", + "raw_url": "https://github.com/test-owner/test-repo/raw/ghi789/README.md", + "contents_url": "https://api.github.com/repos/test-owner/test-repo/contents/README.md?ref=ghi789" + } + ])); + }); + + let octocrab = create_test_octocrab(&server); + + let result = get_changed_files(&octocrab, "test-owner", "test-repo", 123).await; + + assert!(result.is_ok()); + let files = result.unwrap(); + assert_eq!(files.len(), 2); // Should only include the .rs files + assert_eq!(files[0], "src/main.rs"); + assert_eq!(files[1], "src/lib.rs"); + + list_files_mock.assert(); + } + + #[tokio::test] + async fn test_get_changed_files_no_rust_files() { + let server = MockServer::start(); + + let list_files_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/pulls/123/files"); + + then.status(200) + .header("content-type", "application/json") + .json_body(json!([ + { + "sha": "abc123", + "filename": "README.md", + "status": "modified", + "additions": 10, + "deletions": 5, + "changes": 15, + "blob_url": "https://github.com/test-owner/test-repo/blob/abc123/README.md", + "raw_url": "https://github.com/test-owner/test-repo/raw/abc123/README.md", + "contents_url": "https://api.github.com/repos/test-owner/test-repo/contents/README.md?ref=abc123" + }, + { + "sha": "def456", + "filename": "LICENSE", + "status": "modified", + "additions": 7, + "deletions": 3, + "changes": 10, + "blob_url": "https://github.com/test-owner/test-repo/blob/def456/LICENSE", + "raw_url": "https://github.com/test-owner/test-repo/raw/def456/LICENSE", + "contents_url": "https://api.github.com/repos/test-owner/test-repo/contents/LICENSE?ref=def456" + } + ])); + }); + + let octocrab = create_test_octocrab(&server); + + let result = get_changed_files(&octocrab, "test-owner", "test-repo", 123).await; + + assert!(result.is_ok()); + let files = result.unwrap(); + assert_eq!(files.len(), 0); // Should return empty list as no Rust files changed + + list_files_mock.assert(); + } + + #[tokio::test] + async fn test_get_changed_files_error() { + let server = MockServer::start(); + + let list_files_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/pulls/123/files"); + + then.status(404) + .header("content-type", "application/json") + .json_body(json!({ + "message": "Not Found", + "documentation_url": "https://docs.github.com/rest/pulls/pulls#list-pull-requests-files" + })); + }); + + let octocrab = create_test_octocrab(&server); + + let result = get_changed_files(&octocrab, "test-owner", "test-repo", 123).await; + + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Failed to list PR files")); + + list_files_mock.assert(); + } +} diff --git a/.github/actions/clippy-annotation-reporter/src/analyzer/mod.rs b/.github/actions/clippy-annotation-reporter/src/analyzer/mod.rs new file mode 100644 index 0000000000..de82fbe68b --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/analyzer/mod.rs @@ -0,0 +1,203 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Main analyzer module for clippy-annotation-reporter +//! +//! This module is responsible for analyzing clippy annotations +//! in Rust code across different branches. + +use crate::analyzer::annotation::{ + count_annotations_by_crate, count_annotations_by_rule, create_annotation_regex, + find_annotations, +}; +use crate::analyzer::git::{get_changed_files, GitOperations}; +use anyhow::Result; +use log::{debug, info, warn}; +use octocrab::Octocrab; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; + +mod annotation; +mod crate_detection; +mod git; + +/// Represents a clippy annotation in code +#[derive(Debug, Clone)] +pub struct ClippyAnnotation { + pub file: Rc, + pub rule: Rc, +} + +/// Result of annotation analysis +pub struct AnalysisResult { + pub base_annotations: Vec, + pub head_annotations: Vec, + pub base_counts: HashMap, usize>, + pub head_counts: HashMap, usize>, + pub changed_files: HashSet, + pub base_crate_counts: HashMap, usize>, + pub head_crate_counts: HashMap, usize>, +} + +pub async fn run_analysis( + octocrab: &Octocrab, + owner: &str, + repo: &str, + pr_number: u64, + base_branch: &str, + head_branch: &str, + rules: &[String], +) -> Result { + let changed_files = get_changed_files(octocrab, owner, repo, pr_number).await?; + + if changed_files.is_empty() { + return Err(anyhow::anyhow!("No Rust files changed in this PR")); + } + let git_ops = GitOperations::default(); + + let all_files = git_ops.get_all_rust_files()?; + let pr_analysis = analyze_annotations(&changed_files, base_branch, head_branch, rules)?; + let (repo_base_crate_counts, repo_head_crate_counts) = + analyze_all_files_for_crates(&all_files, base_branch, head_branch, rules)?; + + Ok(AnalysisResult { + base_annotations: pr_analysis.base_annotations, + head_annotations: pr_analysis.head_annotations, + base_counts: pr_analysis.base_counts, + head_counts: pr_analysis.head_counts, + changed_files: pr_analysis.changed_files, + base_crate_counts: repo_base_crate_counts, + head_crate_counts: repo_head_crate_counts, + }) +} +/// Analyze clippy annotations in base and head branches +fn analyze_annotations( + files: &[String], + base_branch: &str, + head_branch: &str, + rules: &[String], +) -> Result { + debug!("Analyzing clippy annotations in {} files...", files.len()); + + // Create a regex for matching clippy allow annotations + let annotation_regex = create_annotation_regex(rules)?; + + let mut base_annotations = Vec::new(); + let mut head_annotations = Vec::new(); + let mut changed_files = HashSet::new(); + + // Cache for rule Rc instances to avoid duplicates + let mut rule_cache = HashMap::new(); + let git_ops = GitOperations::default(); + for file in files { + changed_files.insert(file.clone()); + + // most likely reason for errors is the files don't exist in the respective branch. + + let base_content = match git_ops.get_file_content(file, base_branch) { + Ok(content) => content, + Err(e) => { + warn!("Failed to get {} content from {}: {}", file, base_branch, e); + String::new() + } + }; + + let head_content = match git_ops.get_file_content(file, head_branch) { + Ok(content) => content, + Err(e) => { + warn!("Failed to get {} content from {}: {}", file, head_branch, e); + String::new() + } + }; + + // Find annotations in base branch + find_annotations( + &mut base_annotations, + file, + &base_content, + &annotation_regex, + &mut rule_cache, + ); + + // Find annotations in head branch + find_annotations( + &mut head_annotations, + file, + &head_content, + &annotation_regex, + &mut rule_cache, + ); + } + + // Count annotations by rule + let base_counts = count_annotations_by_rule(&base_annotations); + let head_counts = count_annotations_by_rule(&head_annotations); + + // Count annotations by crate + let base_crate_counts = count_annotations_by_crate(&base_annotations); + let head_crate_counts = count_annotations_by_crate(&head_annotations); + + info!( + "Analysis complete. Found {} annotations in base branch and {} in head branch", + base_annotations.len(), + head_annotations.len() + ); + + Ok(AnalysisResult { + base_annotations, + head_annotations, + base_counts, + head_counts, + changed_files, + base_crate_counts, + head_crate_counts, + }) +} + +/// Analyze all files just for crate-level statistics +fn analyze_all_files_for_crates( + files: &[String], + base_branch: &str, + head_branch: &str, + rules: &[String], +) -> Result<(HashMap, usize>, HashMap, usize>)> { + info!( + "Analyzing all {} Rust files for crate-level statistics...", + files.len() + ); + + let annotation_regex = create_annotation_regex(rules)?; + + let mut base_annotations = Vec::new(); + let mut head_annotations = Vec::new(); + let mut rule_cache = HashMap::new(); + + let git_ops = GitOperations::default(); + for file in files { + let base_content = git_ops.get_branch_content(file, base_branch); + let head_content = git_ops.get_branch_content(file, head_branch); + + // Find annotations in base branch + find_annotations( + &mut base_annotations, + file, + &base_content, + &annotation_regex, + &mut rule_cache, + ); + + // Find annotations in head branch + find_annotations( + &mut head_annotations, + file, + &head_content, + &annotation_regex, + &mut rule_cache, + ); + } + + let base_crate_counts = count_annotations_by_crate(&base_annotations); + let head_crate_counts = count_annotations_by_crate(&head_annotations); + + Ok((base_crate_counts, head_crate_counts)) +} From d855eee42e9ef2480aa89e380d8e47f79fcca0a0 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sat, 31 May 2025 22:44:41 -0400 Subject: [PATCH 3/5] add the config module to clippy-annotation-reporter. This module combines env vars and CLI args to get relevant info about the PR being analyzed. --- .../clippy-annotation-reporter/src/config.rs | 469 ++++++++++++++++++ 1 file changed, 469 insertions(+) create mode 100644 .github/actions/clippy-annotation-reporter/src/config.rs diff --git a/.github/actions/clippy-annotation-reporter/src/config.rs b/.github/actions/clippy-annotation-reporter/src/config.rs new file mode 100644 index 0000000000..1dfa02814e --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/config.rs @@ -0,0 +1,469 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Configuration for the clippy-annotation-reporter +//! +//! This module handles all configuration-related logic including +//! command-line arguments, GitHub context, and environment variables. + +use anyhow::{Context as _, Result}; +use clap::Parser; +use log::{debug, warn}; +use serde_json::Value; +use std::env; +use std::fs; + +/// Command-line arguments for the clippy-annotation-reporter +#[derive(Parser, Clone, Debug)] +#[command(name = "clippy-annotation-reporter")] +#[command(about = "Reports changes in clippy allow annotations")] +pub struct Args { + /// GitHub token for API access + #[arg(long)] + pub token: String, + /// Comma-separated list of clippy rules to track + #[arg( + long, + default_value = "unwrap_used,expect_used,todo,unimplemented,panic,unreachable" + )] + pub rules: String, + /// GitHub repository (owner/repo) - defaults to current repository + #[arg(long)] + pub repo: Option, + /// Pull request number - defaults to PR from event context + #[arg(long)] + pub pr: Option, + /// Base branch to compare against (defaults to the PR's base branch) + #[arg(long)] + pub base_branch: Option, +} + +impl Args { + /// Create a new Args instance from command-line arguments + pub fn from_cli() -> Result { + let mut args = Self::parse(); + + if args.token.is_empty() { + args.token = env::var("GITHUB_TOKEN").map_err(|_| { + anyhow::anyhow!("No token provided and GITHUB_TOKEN environment variable not set") + })?; + } + + Ok(args) + } + + /// Parse the rules list from the comma-separated string + pub fn parse_rules(&self) -> Vec { + self.rules.split(',').map(|s| s.trim().to_owned()).collect() + } +} + +/// GitHub event context extracted from environment +#[derive(Clone, Debug)] +pub struct GitHubContext { + pub repository: String, + pub pr_number: u64, + pub base_ref: String, + pub head_ref: String, +} + +impl GitHubContext { + /// Try to extract GitHub context from environment variables and event file + pub fn from_env() -> Result { + // Get repository from env + let repository = env::var("GITHUB_REPOSITORY") + .context("GITHUB_REPOSITORY environment variable not set")?; + + // Get event name (pull_request, push, etc.) + let event_name = env::var("GITHUB_EVENT_NAME") + .context("GITHUB_EVENT_NAME environment variable not set")?; + + // For PR events, get PR number and refs from event payload + let event_path = env::var("GITHUB_EVENT_PATH") + .context("GITHUB_EVENT_PATH environment variable not set")?; + + let event_data = + fs::read_to_string(event_path).context("Failed to read GitHub event file")?; + + let event_json: Value = + serde_json::from_str(&event_data).context("Failed to parse GitHub event JSON")?; + + // Extract values from event JSON + let (pr_number, base_ref, head_ref) = match event_name.as_str() { + "pull_request" | "pull_request_target" => { + let pr_number = event_json["pull_request"]["number"] + .as_u64() + .context("Could not find pull_request.number in event data")?; + + // Direct access to base.ref + let base_ref = match event_json["pull_request"]["base"]["ref"].as_str() { + Some(val) => val.to_owned(), + None => { + warn!( + "Could not extract base.ref as string, Falling back to main as base branch", + ); + + "main".to_owned() + } + }; + + // Direct access to head.ref + let head_ref = match event_json["pull_request"]["head"]["ref"].as_str() { + Some(val) => val.to_owned(), + None => { + warn!("Warning: Could not extract head.ref as string"); + + String::new() + } + }; + + (pr_number, base_ref, head_ref) + } + _ => { + // For other events, default values (will be overridden by args) + (0, "main".to_owned(), "".to_owned()) + } + }; + + debug!( + "Extracted PR: {}, base: {}, head: {}", + pr_number, base_ref, head_ref + ); + + Ok(GitHubContext { + repository, + pr_number, + base_ref, + head_ref, + }) + } +} + +/// Configuration combining command-line arguments and GitHub context +#[derive(Debug)] +pub struct Config { + pub repository: String, + pub pr_number: u64, + pub base_branch: String, + pub head_branch: String, + pub rules: Vec, + pub token: String, + pub owner: String, + pub repo: String, +} + +impl Config { + /// Create a new configuration from command-line arguments and GitHub context + pub fn new(mut args: Args, github_ctx: GitHubContext) -> Result { + // Check for empty token and try to get from environment if needed + if args.token.is_empty() { + args.token = env::var("GITHUB_TOKEN").map_err(|_| { + anyhow::anyhow!("No token provided and GITHUB_TOKEN environment variable not set") + })?; + } + + // Use provided values from args if available, otherwise use context + let repository = args.repo.as_ref().unwrap_or(&github_ctx.repository); + + let pr_number = match args.pr { + Some(pr) => pr, + None => { + if github_ctx.pr_number == 0 { + return Err(anyhow::anyhow!( + "No PR number found in event context. Please provide --pr argument." + )); + } + github_ctx.pr_number + } + }; + + // Set base branch (default to the PR's base branch or 'main') + let base_branch = match args.base_branch.as_ref() { + Some(branch) => { + if !branch.is_empty() { + format!("origin/{}", branch) + } else if !github_ctx.base_ref.is_empty() { + format!("origin/{}", github_ctx.base_ref) + } else { + "origin/main".to_owned() + } + } + _ => "origin/main".to_owned(), + }; + + // Set head branch (PR's head branch) + let head_branch = if !github_ctx.head_ref.is_empty() { + format!("origin/{}", github_ctx.head_ref) + } else { + env::var("GITHUB_HEAD_REF") + .map(|ref_name| format!("origin/{}", ref_name)) + .unwrap_or_else(|_| "HEAD".to_owned()) + }; + + // Parse repository into owner and repo + let parts: Vec<&str> = repository.split('/').collect(); + if parts.len() != 2 { + return Err(anyhow::anyhow!( + "Invalid repository format. Expected 'owner/repo', got '{}'", + repository + )); + } + + let owner = parts[0].to_owned(); + let repo = parts[1].to_owned(); + + let rules = args.parse_rules(); + + Ok(Config { + repository: repository.to_owned(), + pr_number, + base_branch, + head_branch, + rules, + owner, + repo, + token: args.token, + }) + } +} + +/// Builder for creating Config instances +pub struct ConfigBuilder { + args: Option, + github_ctx: Option, +} + +impl ConfigBuilder { + /// Create a new empty builder + pub fn new() -> Self { + Self { + args: None, + github_ctx: None, + } + } + + /// Build the Config instance, using defaults for any unset values + pub fn build(self) -> Result { + // Get command line arguments if not provided + let args = match self.args { + Some(args) => args, + None => Args::from_cli()?, + }; + + // Get GitHub context if not provided + let github_ctx = match self.github_ctx { + Some(ctx) => ctx, + None => GitHubContext::from_env()?, + }; + + Config::new(args, github_ctx) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::env; + use std::fs::File; + use std::io::Write; + use tempfile::tempdir; + + // Helper function to create a mock GitHub event file + fn create_mock_event_file(content: &str) -> (tempfile::TempDir, String) { + let dir = tempdir().unwrap(); + let file_path = dir.path().join("event.json"); + let mut file = File::create(&file_path).unwrap(); + write!(file, "{}", content).unwrap(); + (dir, file_path.to_string_lossy().to_string()) + } + + // Helper to set and reset environment variables safely + struct EnvGuard { + key: String, + original_value: Option, + } + + impl EnvGuard { + fn new(key: &str, value: &str) -> Self { + let original_value = env::var(key).ok(); + env::set_var(key, value); + Self { + key: key.to_string(), + original_value, + } + } + } + + impl Drop for EnvGuard { + fn drop(&mut self) { + match &self.original_value { + Some(val) => env::set_var(&self.key, val), + None => env::remove_var(&self.key), + } + } + } + + // Test that directly creates Args without the builder + #[test] + fn test_args_parse_rules() { + let args = Args { + token: "dummy_token".to_string(), + rules: "unwrap_used,expect_used,panic".to_string(), + repo: None, + pr: None, + base_branch: None, + }; + + let rules = args.parse_rules(); + assert_eq!(rules.len(), 3); + assert_eq!(rules[0], "unwrap_used"); + assert_eq!(rules[1], "expect_used"); + assert_eq!(rules[2], "panic"); + } + + #[test] + fn test_args_parse_rules_with_spaces() { + let args = Args { + token: "dummy_token".to_string(), + rules: "unwrap_used, expect_used , panic".to_string(), + repo: None, + pr: None, + base_branch: None, + }; + + let rules = args.parse_rules(); + assert_eq!(rules.len(), 3); + assert_eq!(rules[0], "unwrap_used"); + assert_eq!(rules[1], "expect_used"); + assert_eq!(rules[2], "panic"); + } + + // Test that uses environment variables for GitHub context + #[test] + fn test_github_context_from_env() { + // Setup mock environment and event file + let event_json = r#" + { + "pull_request": { + "number": 123, + "base": { + "ref": "main" + }, + "head": { + "ref": "feature-branch" + } + } + } + "#; + + let (dir, event_path) = create_mock_event_file(event_json); + + // Set required environment variables + let _guard1 = EnvGuard::new("GITHUB_REPOSITORY", "owner/repo"); + let _guard2 = EnvGuard::new("GITHUB_EVENT_NAME", "pull_request"); + let _guard3 = EnvGuard::new("GITHUB_EVENT_PATH", &event_path); + + let ctx = GitHubContext::from_env().unwrap(); + + assert_eq!(ctx.repository, "owner/repo"); + assert_eq!(ctx.pr_number, 123); + assert_eq!(ctx.base_ref, "main"); + assert_eq!(ctx.head_ref, "feature-branch"); + + drop(dir); + } + + // Test that directly creates Config + #[test] + fn test_config_new() { + let args = Args { + token: "test_token".to_string(), + rules: "unwrap_used,expect_used".to_string(), + repo: Some("custom_owner/custom_repo".to_string()), + pr: Some(456), + base_branch: Some("develop".to_string()), + }; + + let github_ctx = GitHubContext { + repository: "default_owner/default_repo".to_string(), + pr_number: 123, + base_ref: "main".to_string(), + head_ref: "feature".to_string(), + }; + + let config = Config::new(args, github_ctx).unwrap(); + + // Check that args values are used when provided + assert_eq!(config.repository, "custom_owner/custom_repo"); + assert_eq!(config.pr_number, 456); + assert_eq!(config.base_branch, "origin/develop"); + assert_eq!(config.head_branch, "origin/feature"); + assert_eq!(config.owner, "custom_owner"); + assert_eq!(config.repo, "custom_repo"); + assert_eq!(config.rules.len(), 2); + assert_eq!(config.rules[0], "unwrap_used"); + assert_eq!(config.rules[1], "expect_used"); + assert_eq!(config.token, "test_token"); + } + + // Test using mocked environment for token + #[test] + fn test_empty_token_uses_env_token() { + // Create a mock GitHub event file + let event_json = + r#"{"pull_request":{"number":123,"base":{"ref":"main"},"head":{"ref":"feature"}}}"#; + let (dir, event_path) = create_mock_event_file(event_json); + + // Set up GitHub context environment + let _repo_guard = EnvGuard::new("GITHUB_REPOSITORY", "owner/repo"); + let _event_guard = EnvGuard::new("GITHUB_EVENT_NAME", "pull_request"); + let _path_guard = EnvGuard::new("GITHUB_EVENT_PATH", &event_path); + + // Set the token environment variable + let _token_guard = EnvGuard::new("GITHUB_TOKEN", "env_token"); + + // Create args and context directly + let args = Args { + token: "".to_string(), // Empty token should trigger using env var + rules: "unwrap_used".to_string(), + repo: Some("owner/repo".to_string()), + pr: Some(123), + base_branch: None, + }; + + let github_ctx = GitHubContext::from_env().unwrap(); + + // Test Config::new directly + let config = Config::new(args, github_ctx).unwrap(); + + // Should use token from environment + assert_eq!(config.token, "env_token"); + + drop(dir); + } + + #[test] + fn test_config_new_invalid_repo_format() { + let args = Args { + token: "test_token".to_string(), + rules: "unwrap_used".to_string(), + repo: Some("invalid-format".to_string()), + pr: Some(123), + base_branch: None, + }; + + let github_ctx = GitHubContext { + repository: "default_owner/default_repo".to_string(), + pr_number: 0, + base_ref: "".to_string(), + head_ref: "".to_string(), + }; + + let result = Config::new(args, github_ctx); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("Invalid repository format")); + } +} From 5dcd2ad70200622a86b0f6979cdbe23d650f2156 Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sat, 31 May 2025 22:44:53 -0400 Subject: [PATCH 4/5] add the commenter module to clippy-annotation-reporter. This takes the finished report and comments it on the PR. it checks for existing comments and updates them instead of repeatedly posting a new comment for every PR change. --- .../src/commenter.rs | 515 ++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 .github/actions/clippy-annotation-reporter/src/commenter.rs diff --git a/.github/actions/clippy-annotation-reporter/src/commenter.rs b/.github/actions/clippy-annotation-reporter/src/commenter.rs new file mode 100644 index 0000000000..536f229f98 --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/commenter.rs @@ -0,0 +1,515 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Commenter module for clippy-annotation-reporter +//! +//! This module handles interactions with GitHub for commenting on PRs, +//! including finding existing comments and updating or creating comments. + +use anyhow::{Context as _, Result}; +use log::{error, info}; +use octocrab::Octocrab; + +/// Post or update a comment on a PR with the given report +pub async fn post_comment( + octocrab: &Octocrab, + owner: &str, + repo: &str, + pr_number: u64, + report: String, + signature: Option<&str>, +) -> Result<()> { + // Use the provided signature or default + let signature = signature.unwrap_or(""); + + // Add the signature to the report + let report_with_signature = format!("{}\n\n{}", report, signature); + + // Search for existing comment by the bot + info!("Checking for existing comment on PR #{}", pr_number); + let existing_comment = + find_existing_comment(octocrab, owner, repo, pr_number, signature).await?; + + // Update existing comment or create a new one + if let Some(comment_id) = existing_comment { + info!("Updating existing comment #{}", comment_id); + octocrab + .issues(owner, repo) + .update_comment(comment_id.into(), report_with_signature) + .await + .context("Failed to update existing comment")?; + } else { + info!("Creating new comment on PR #{}", pr_number); + octocrab + .issues(owner, repo) + .create_comment(pr_number, report_with_signature) + .await + .context("Failed to post comment to PR")?; + } + + Ok(()) +} + +/// Find existing comment by the bot on a PR +async fn find_existing_comment( + octocrab: &Octocrab, + owner: &str, + repo: &str, + pr_number: u64, + signature: &str, +) -> Result> { + // Get all comments on the PR + let page = octocrab + .issues(owner, repo) + .list_comments(pr_number) + .per_page(100) + .send() + .await + .context("Failed to list PR comments")?; + + let all_pages = octocrab + .all_pages(page) + .await + .context("Failed to fetch all pages of comments")?; + + for comment in all_pages { + if comment + .body + .as_ref() + .is_some_and(|body| body.contains(signature)) + { + return Ok(Some(*comment.id)); + } + } + + // No matching comment found + Ok(None) +} + +#[cfg(test)] +mod tests { + use super::*; + use http::Uri; + use httpmock::prelude::*; + use serde_json::json; + use std::str::FromStr; + + /// Helper function to create an Octocrab instance that uses our mock server + fn create_test_octocrab(server: &MockServer) -> Octocrab { + let uri = Uri::from_str(&server.base_url()).unwrap(); + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() + } + + /// Helper function to create a standard user object for responses + fn standard_user() -> serde_json::Value { + json!({ + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }) + } + + /// Helper function to create a standard comment object for responses + fn create_comment_json(id: u64, body: &str) -> serde_json::Value { + json!({ + "id": id, + "node_id": "MDExOlB1bGxSZXF1ZXN0Q29tbWVudHt9", + "html_url": format!("https://github.com/test-owner/test-repo/pull/123#issuecomment-{}", id), + "body": body, + "user": standard_user(), + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + "url": "https://api.github.com/repos/test-owner/test-repo/issues/comments/123", + "author_association": "COLLABORATOR" + }) + } + + #[tokio::test] + async fn test_post_comment_create_new() { + // Create a mock server + let server = MockServer::start(); + + // Mock the list comments endpoint (used internally by find_existing_comment) + let list_comments_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("per_page", "100"); + + then.status(200) + .header("content-type", "application/json") + .json_body(json!([])); // No existing comments + }); + + // Mock the create comment endpoint + let create_comment_mock = server.mock(|when, then| { + when.method(POST) + .path("/repos/test-owner/test-repo/issues/123/comments") + .json_body(json!({ + "body": "Test report\n\n" + })); + + then.status(201) + .header("content-type", "application/json") + .json_body(create_comment_json( + 456, + "Test report\n\n", + )); + }); + + let octocrab = create_test_octocrab(&server); + + // Call the public function we're testing + let result = post_comment( + &octocrab, + "test-owner", + "test-repo", + 123, + "Test report".to_string(), + Some(""), + ) + .await; + + // Verify the result + assert!(result.is_ok()); + list_comments_mock.assert(); + create_comment_mock.assert(); + } + + #[tokio::test] + async fn test_post_comment_update_existing() { + // Create a mock server + let server = MockServer::start(); + + // Mock the list comments endpoint + let list_comments_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("per_page", "100"); + + then.status(200) + .header("content-type", "application/json") + .json_body(json!([create_comment_json( + 456, + "Old report\n\n" + )])); + }); + + // Mock the update comment endpoint with the exact path and body format + let update_comment_mock = server.mock(|when, then| { + when.method(POST) + .path("/repos/test-owner/test-repo/issues/comments/456") + .json_body(json!({ + "body": "Updated report\n\n" + })); + + then.status(200) + .header("content-type", "application/json") + .json_body(create_comment_json( + 456, + "Updated report\n\n", + )); + }); + + let octocrab = create_test_octocrab(&server); + + // Call the public function we're testing + let result = post_comment( + &octocrab, + "test-owner", + "test-repo", + 123, + "Updated report".to_string(), + Some(""), + ) + .await; + + // Verify the result + assert!(result.is_ok()); + list_comments_mock.assert(); + update_comment_mock.assert(); + } + + #[tokio::test] + async fn test_post_comment_with_pagination() { + // Create a mock server + let server = MockServer::start(); + + // Second page mock - ONLY match requests WITH page=2. Needs to be registered first. + let second_page_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("page", "2"); + + then.status(200) + .header("content-type", "application/json") + .header("Link", format!( + "<{}/repos/test-owner/test-repo/issues/123/comments?page=1&per_page=100>; rel=\"prev\", <{}/repos/test-owner/test-repo/issues/123/comments?per_page=100>; rel=\"first\"", + server.base_url(), server.base_url() + )) + .json_body(json!([ + create_comment_json(200, "Second page comment\n\n") + ])); + }); + + // First page mock + let first_page_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("per_page", "100"); + + then.status(200) + .header("content-type", "application/json") + .header("Link", format!( + "<{}/repos/test-owner/test-repo/issues/123/comments?page=2&per_page=100>; rel=\"next\", <{}/repos/test-owner/test-repo/issues/123/comments?page=2&per_page=100>; rel=\"last\"", + server.base_url(), server.base_url() + )) + .json_body(json!([ + create_comment_json(100, "First page comment") + ])); + }); + + // Update comment mock + let update_comment_mock = server.mock(|when, then| { + when.method(POST) + .path("/repos/test-owner/test-repo/issues/comments/200") + .json_body(json!({ + "body": "Test with pagination\n\n" + })); + + then.status(200) + .header("content-type", "application/json") + .json_body(create_comment_json( + 200, + "Test with pagination\n\n", + )); + }); + + let octocrab = create_test_octocrab(&server); + + let result = post_comment( + &octocrab, + "test-owner", + "test-repo", + 123, + "Test with pagination".to_string(), + Some(""), + ) + .await; + + assert!(result.is_ok()); + assert!(first_page_mock.hits() > 0, "First page should be requested"); + assert!( + second_page_mock.hits() > 0, + "Second page should be requested" + ); + assert!( + update_comment_mock.hits() > 0, + "Update comment endpoint should be called" + ); + } + + #[tokio::test] + async fn test_post_comment_with_error() { + let server = MockServer::start(); + + // Mock the list comments endpoint with a server error + let list_comments_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("per_page", "100"); + + then.status(500) + .header("content-type", "application/json") + .json_body(json!({ + "message": "Internal server error" + })); + }); + + let octocrab = create_test_octocrab(&server); + + let result = post_comment( + &octocrab, + "test-owner", + "test-repo", + 123, + "Test report".to_string(), + Some(""), + ) + .await; + + assert!(result.is_err()); + assert!( + list_comments_mock.hits() >= 1, + "Expected the mock to be called at least once" + ); + } + + #[tokio::test] + async fn test_post_comment_default_signature() { + let server = MockServer::start(); + + // Mock the list comments endpoint + let list_comments_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("per_page", "100"); + + then.status(200) + .header("content-type", "application/json") + .json_body(json!([])); + }); + + // Mock the create comment endpoint - checking for default signature + let create_comment_mock = server.mock(|when, then| { + when.method(POST) + .path("/repos/test-owner/test-repo/issues/123/comments") + .json_body(json!({ + "body": "Test report\n\n" + })); + + then.status(201) + .header("content-type", "application/json") + .json_body(create_comment_json( + 456, + "Test report\n\n", + )); + }); + + let octocrab = create_test_octocrab(&server); + + let result = post_comment( + &octocrab, + "test-owner", + "test-repo", + 123, + "Test report".to_string(), + None, + ) + .await; + + assert!(result.is_ok()); + list_comments_mock.assert(); + create_comment_mock.assert(); + } + + #[tokio::test] + async fn test_post_comment_error_on_create() { + // Create a mock server + let server = MockServer::start(); + + // Mock the list comments endpoint (no existing comments) + let list_comments_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("per_page", "100"); + + then.status(200) + .header("content-type", "application/json") + .json_body(json!([])); + }); + + // Mock the create comment endpoint with an error + let create_comment_mock = server.mock(|when, then| { + when.method(POST) + .path("/repos/test-owner/test-repo/issues/123/comments") + .json_body(json!({ + "body": "Test report\n\n" + })); + + then.status(422) + .header("content-type", "application/json") + .json_body(json!({ + "message": "Validation failed", + "errors": [ + { + "resource": "Issue", + "field": "body", + "code": "invalid" + } + ] + })); + }); + + let octocrab = create_test_octocrab(&server); + + let result = post_comment( + &octocrab, + "test-owner", + "test-repo", + 123, + "Test report".to_string(), + Some(""), + ) + .await; + + assert!(result.is_err()); + list_comments_mock.assert(); + create_comment_mock.assert(); + } + + #[tokio::test] + async fn test_post_comment_error_on_update() { + let server = MockServer::start(); + + // Mock the list comments endpoint + let list_comments_mock = server.mock(|when, then| { + when.method(GET) + .path("/repos/test-owner/test-repo/issues/123/comments") + .query_param("per_page", "100"); + + then.status(200) + .header("content-type", "application/json") + .json_body(json!([create_comment_json( + 456, + "Old report\n\n" + )])); + }); + + // Mock the update comment endpoint with an error + let update_comment_mock = server.mock(|when, then| { + when.method(POST) + .path("/repos/test-owner/test-repo/issues/comments/456") + .json_body(json!({ + "body": "Updated report\n\n" + })); + + then.status(403) + .header("content-type", "application/json") + .json_body(json!({ + "message": "Forbidden", + "documentation_url": "https://docs.github.com/rest/issues/comments" + })); + }); + + let octocrab = create_test_octocrab(&server); + + let result = post_comment( + &octocrab, + "test-owner", + "test-repo", + 123, + "Updated report".to_string(), + Some(""), + ) + .await; + + assert!(result.is_err()); + list_comments_mock.assert(); + update_comment_mock.assert(); + } +} From 0a96a8fba87f8ee3bd9ae3d244973d6efa0763ea Mon Sep 17 00:00:00 2001 From: Edmund Kump Date: Sat, 31 May 2025 22:45:13 -0400 Subject: [PATCH 5/5] add the reporter module to clippy-annotation-reporter. This uses the data from the analyzer and generates the text of the report that will be posted on the PR. --- .../src/report_generator.rs | 593 ++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 .github/actions/clippy-annotation-reporter/src/report_generator.rs diff --git a/.github/actions/clippy-annotation-reporter/src/report_generator.rs b/.github/actions/clippy-annotation-reporter/src/report_generator.rs new file mode 100644 index 0000000000..305fef3304 --- /dev/null +++ b/.github/actions/clippy-annotation-reporter/src/report_generator.rs @@ -0,0 +1,593 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Report generator module for clippy-annotation-reporter +//! +//! This module handles the logic for generating formatted reports +//! based on annotation analysis results. + +use crate::analyzer::AnalysisResult; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; + +/// Generate a detailed report for PR comment +pub fn generate_report( + analysis: &AnalysisResult, + rules: &[String], + repository: &str, + base_branch: &str, + head_branch: &str, +) -> String { + let mut report = String::new(); + + // Add header and branch information + add_header(&mut report, repository, base_branch, head_branch); + + // Add rule summary section + add_rule_summary(&mut report, analysis, rules); + + // Add file-level section + add_file_level_section(&mut report, analysis); + + // Add crate-level section + add_crate_level_section(&mut report, analysis); + + // Add explanation + add_explanation(&mut report); + + report +} + +/// Add report header and branch information +fn add_header(report: &mut String, repository: &str, base_branch: &str, head_branch: &str) { + report.push_str("## Clippy Allow Annotation Report\n\n"); + + // Add branch information with link to base branch + let base_branch_for_url = base_branch.strip_prefix("origin/").unwrap_or(base_branch); + + report.push_str("Comparing clippy allow annotations between branches:\n"); + report.push_str(&format!( + "- **Base Branch**: [{}](https://github.com/{}/tree/{})\n", + base_branch, repository, base_branch_for_url + )); + report.push_str(&format!("- **PR Branch**: {}\n\n", head_branch)); +} + +/// Add summary table by rule +fn add_rule_summary(report: &mut String, analysis: &AnalysisResult, rules: &[String]) { + report.push_str("### Summary by Rule\n\n"); + report.push_str("| Rule | Base Branch | PR Branch | Change |\n"); + report.push_str("|------|------------|-----------|--------|\n"); + + let mut total_base = 0; + let mut total_head = 0; + + // Add row for each rule + for rule in rules { + let base_count = *analysis.base_counts.get(rule).unwrap_or(&0); + let head_count = *analysis.head_counts.get(rule).unwrap_or(&0); + + total_base += base_count; + total_head += head_count; + + add_table_row(report, rule, base_count, head_count); + } + + // Add total row + add_table_row(report, "**Total**", total_base, total_head); + report.push('\n'); +} + +/// Add section showing annotation counts by file +fn add_file_level_section(report: &mut String, analysis: &AnalysisResult) { + if analysis.changed_files.is_empty() { + return; + } + + report.push_str("### Annotation Counts by File\n\n"); + report.push_str("| File | Base Branch | PR Branch | Change |\n"); + report.push_str("|------|------------|-----------|--------|\n"); + + // Count annotations by file + let base_file_counts = count_annotations_by_file(&analysis.base_annotations); + let head_file_counts = count_annotations_by_file(&analysis.head_annotations); + + // Get sorted list of changed files + let mut all_files: Vec = analysis.changed_files.iter().cloned().collect(); + all_files.sort(); + + // Add row for each file + for file in all_files { + let base_count = *base_file_counts.get(&file).unwrap_or(&0); + let head_count = *head_file_counts.get(&file).unwrap_or(&0); + + // Skip files with no annotations in either branch + if base_count == 0 && head_count == 0 { + continue; + } + + add_table_row(report, &format!("`{}`", file), base_count, head_count); + } + + report.push('\n'); +} + +/// Add section showing annotation stats by crate +fn add_crate_level_section(report: &mut String, analysis: &AnalysisResult) { + report.push_str("### Annotation Stats by Crate\n\n"); + report.push_str("| Crate | Base Branch | PR Branch | Change |\n"); + report.push_str("|-------|------------|-----------|--------|\n"); + + // Get all crates from both base and head + let all_crates = get_all_keys(&analysis.base_crate_counts, &analysis.head_crate_counts); + + let mut total_base = 0; + let mut total_head = 0; + + // Add row for each crate + for crate_name in all_crates { + let base_count = *analysis.base_crate_counts.get(&crate_name).unwrap_or(&0); + let head_count = *analysis.head_crate_counts.get(&crate_name).unwrap_or(&0); + + // Skip crates with no annotations in either branch + if base_count == 0 && head_count == 0 { + continue; + } + + total_base += base_count; + total_head += head_count; + + add_table_row(report, &format!("`{}`", crate_name), base_count, head_count); + } + + // Add total row + add_table_row(report, "**Total**", total_base, total_head); + report.push('\n'); +} + +/// Add report explanation footer +fn add_explanation(report: &mut String) { + report.push_str("### About This Report\n\n"); + report.push_str("This report tracks Clippy allow annotations for specific rules, "); + report.push_str("showing how they've changed in this PR. "); + report + .push_str("Decreasing the number of these annotations generally improves code quality.\n"); +} + +/// Add a table row with counts and change +fn add_table_row(report: &mut String, label: &str, base_count: usize, head_count: usize) { + let change = head_count as isize - base_count as isize; + + // Skip rows with no change and no counts + if change == 0 && base_count == 0 && head_count == 0 { + return; + } + + // Calculate percentage change + let percent_change = calculate_percent_change(base_count, change); + + // Format the change string with percentage + let change_str = format_change_string(change, percent_change); + + report.push_str(&format!( + "| {} | {} | {} | {} |\n", + label, base_count, head_count, change_str + )); +} + +/// Calculate percentage change +fn calculate_percent_change(base_count: usize, change: isize) -> f64 { + if base_count > 0 { + (change as f64 / base_count as f64) * 100.0 + } else if change > 0 { + f64::INFINITY + } else { + 0.0 + } +} + +/// Format change string with appropriate icon and percentage +fn format_change_string(change: isize, percent_change: f64) -> String { + if change > 0 { + if percent_change.is_infinite() { + format!("⚠️ +{} (N/A)", change) + } else { + format!("⚠️ +{} (+{:.1}%)", change, percent_change) + } + } else if change < 0 { + format!("✅ {} ({:.1}%)", change, percent_change) + } else { + "No change (0%)".to_owned() + } +} + +/// Count annotations by file +fn count_annotations_by_file( + annotations: &[crate::analyzer::ClippyAnnotation], +) -> HashMap, usize> { + let mut counts = HashMap::new(); + + for anno in annotations { + *counts.entry(anno.file.clone()).or_insert(0) += 1; + } + + counts +} + +/// Get all unique keys from two HashMaps, sorted +fn get_all_keys( + map1: &HashMap, + map2: &HashMap, +) -> Vec { + let mut all_keys = HashSet::new(); + + for key in map1.keys() { + all_keys.insert(key.clone()); + } + + for key in map2.keys() { + all_keys.insert(key.clone()); + } + + let mut keys_vec: Vec = all_keys.into_iter().collect(); + keys_vec.sort(); + + keys_vec +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::analyzer::ClippyAnnotation; + use std::collections::{HashMap, HashSet}; + use std::rc::Rc; + + // Helper function to create a test AnalysisResult + fn create_analysis_result() -> crate::analyzer::AnalysisResult { + let mut base_counts = HashMap::new(); + let rule1 = Rc::new("clippy::unwrap_used".to_owned()); + let rule2 = Rc::new("clippy::match_bool".to_owned()); + let rule3 = Rc::new("clippy::unused_imports".to_owned()); + + base_counts.insert(rule1.clone(), 5); + base_counts.insert(rule2.clone(), 3); + base_counts.insert(rule3.clone(), 10); + + let mut head_counts = HashMap::new(); + head_counts.insert(rule1.clone(), 3); + head_counts.insert(rule2.clone(), 4); + head_counts.insert(rule3.clone(), 5); + + let mut base_crate_counts = HashMap::new(); + let crate1 = Rc::new("crate1".to_owned()); + let crate2 = Rc::new("crate2".to_owned()); + + base_crate_counts.insert(crate1.clone(), 8); + base_crate_counts.insert(crate2.clone(), 10); + + let mut head_crate_counts = HashMap::new(); + head_crate_counts.insert(crate1.clone(), 5); + head_crate_counts.insert(crate2.clone(), 12); + + let mut changed_files = HashSet::new(); + changed_files.insert("src/file1.rs".to_owned()); + changed_files.insert("src/file2.rs".to_owned()); + + let file1 = Rc::new("src/file1.rs".to_owned()); + let file2 = Rc::new("src/file2.rs".to_owned()); + + let base_annotations = vec![ + ClippyAnnotation { + file: file1.clone(), + rule: rule1.clone(), + }, + ClippyAnnotation { + file: file1.clone(), + rule: rule1.clone(), + }, + ClippyAnnotation { + file: file1.clone(), + rule: rule2.clone(), + }, + ClippyAnnotation { + file: file2.clone(), + rule: rule1.clone(), + }, + ClippyAnnotation { + file: file2.clone(), + rule: rule3.clone(), + }, + ]; + + let head_annotations = vec![ + ClippyAnnotation { + file: file1.clone(), + rule: rule1.clone(), + }, + ClippyAnnotation { + file: file1.clone(), + rule: rule2.clone(), + }, + ClippyAnnotation { + file: file1.clone(), + rule: rule2.clone(), + }, + ClippyAnnotation { + file: file2.clone(), + rule: rule3.clone(), + }, + ]; + + crate::analyzer::AnalysisResult { + base_annotations, + head_annotations, + base_counts, + head_counts, + changed_files, + base_crate_counts, + head_crate_counts, + } + } + + #[test] + fn test_generate_report_basic() { + let analysis = create_analysis_result(); + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + ]; + + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "main", + "feature-branch", + ); + + // Verify the report contains expected sections + assert!(report.contains("## Clippy Allow Annotation Report")); + assert!(report.contains("### Summary by Rule")); + assert!(report.contains("### Annotation Counts by File")); + assert!(report.contains("### Annotation Stats by Crate")); + assert!(report.contains("### About This Report")); + + // Verify the report contains repository and branch information + assert!(report.contains("test-owner/test-repo")); + assert!(report.contains("main")); + assert!(report.contains("feature-branch")); + } + + #[test] + fn test_generate_report_rule_summary() { + let analysis = create_analysis_result(); + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + ]; + + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "main", + "feature-branch", + ); + + // Verify rule summary contains all rules + assert!(report.contains("clippy::unwrap_used")); + assert!(report.contains("clippy::match_bool")); + assert!(report.contains("clippy::unused_imports")); + + // Verify counts and changes + assert!(report.contains("5")); // Base count for unwrap_used + assert!(report.contains("3")); // Head count for unwrap_used + assert!(report.contains("-2")); // Change for unwrap_used + + assert!(report.contains("3")); // Base count for match_bool + assert!(report.contains("4")); // Head count for match_bool + assert!(report.contains("+1")); // Change for match_bool + + assert!(report.contains("10")); // Base count for unused_imports + assert!(report.contains("5")); // Head count for unused_imports + assert!(report.contains("-5")); // Change for unused_imports + } + + #[test] + fn test_generate_report_file_section() { + let analysis = create_analysis_result(); + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + ]; + + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "main", + "feature-branch", + ); + + // Verify file section contains the changed files + assert!(report.contains("src/file1.rs")); + assert!(report.contains("src/file2.rs")); + + // Verify file counts for file1.rs + // In the base branch, file1.rs has 3 annotations (2 unwrap_used, 1 match_bool) + // In the head branch, file1.rs has 3 annotations (1 unwrap_used, 2 match_bool) + let file1_pattern = r"`src/file1\.rs`\s*\|\s*3\s*\|\s*3\s*\|\s*No change"; + assert!( + report.contains("| `src/file1.rs` | 3 | 3 |") + || regex::Regex::new(file1_pattern).unwrap().is_match(&report), + "File1 count information not found in report" + ); + + // Verify file counts for file2.rs + // In the base branch, file2.rs has 2 annotations (1 unwrap_used, 1 unused_imports) + // In the head branch, file2.rs has 1 annotation (1 unused_imports) + let file2_pattern = r"`src/file2\.rs`\s*\|\s*2\s*\|\s*1\s*\|\s*.*-1"; + assert!( + report.contains("| `src/file2.rs` | 2 | 1 |") + || regex::Regex::new(file2_pattern).unwrap().is_match(&report), + "File2 count information not found in report" + ); + + // Make sure the change column has the correct indicators + assert!( + report.contains("No change") || report.contains("(0%)"), + "No change indicator missing for file1" + ); + assert!( + report.contains("✅ -1"), + "Decrease indicator missing for file2" + ); + } + + #[test] + fn test_generate_report_crate_section() { + let analysis = create_analysis_result(); + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + ]; + + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "main", + "feature-branch", + ); + + // Verify crate section contains the crates + assert!(report.contains("`crate1`")); + assert!(report.contains("`crate2`")); + + // Verify crate counts + // Base count for crate1: 8, Head count: 5 + assert!(report.contains("8")); + assert!(report.contains("5")); + assert!(report.contains("-3")); // Change + + // Base count for crate2: 10, Head count: 12 + assert!(report.contains("10")); + assert!(report.contains("12")); + assert!(report.contains("+2")); // Change + } + + #[test] + fn test_generate_report_empty_changed_files() { + let mut analysis = create_analysis_result(); + analysis.changed_files.clear(); + + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + ]; + + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "main", + "feature-branch", + ); + + // Verify that the file-level section is not included when there are no changed files + assert!(!report.contains("### Annotation Counts by File")); + + // But other sections should still be present + assert!(report.contains("### Summary by Rule")); + assert!(report.contains("### Annotation Stats by Crate")); + } + + #[test] + fn test_generate_report_formatting() { + let analysis = create_analysis_result(); + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + ]; + + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "main", + "feature-branch", + ); + + // Verify positive changes are formatted with ⚠️ + assert!(report.contains("⚠️ +1")); + + // Verify negative changes are formatted with ✅ + assert!(report.contains("✅ -2")); + + // Verify total row exists + assert!(report.contains("**Total**")); + } + + #[test] + fn test_generate_report_with_origin_prefix() { + let analysis = create_analysis_result(); + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + ]; + + // Test with origin/ prefix in base branch + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "origin/main", + "feature-branch", + ); + + // Verify the origin/ prefix is removed in the link URL + assert!(report.contains("https://github.com/test-owner/test-repo/tree/main")); + assert!(report.contains("origin/main")); + } + + #[test] + fn test_generate_report_new_annotations() { + // Create an analysis where annotations are added in the head branch + let mut analysis = create_analysis_result(); + + // Add a new rule that only appears in the head counts + let new_rule = Rc::new("clippy::new_rule".to_owned()); + analysis.head_counts.insert(new_rule.clone(), 2); + + let rules = vec![ + "clippy::unwrap_used".to_owned(), + "clippy::match_bool".to_owned(), + "clippy::unused_imports".to_owned(), + "clippy::new_rule".to_owned(), + ]; + + let report = generate_report( + &analysis, + &rules, + "test-owner/test-repo", + "main", + "feature-branch", + ); + + // Verify that new rule appears with appropriate formatting + assert!(report.contains("clippy::new_rule")); + assert!(report.contains("0")); // Base count should be 0 + assert!(report.contains("2")); // Head count should be 2 + assert!(report.contains("⚠️ +2")); // Change should be +2 + + // Also verify N/A for the percentage since base count is 0 + assert!(report.contains("N/A")); + } +}