diff --git a/crates/app/src/eth2wrap/version.rs b/crates/app/src/eth2wrap/version.rs index fd55a821..5d36599b 100644 --- a/crates/app/src/eth2wrap/version.rs +++ b/crates/app/src/eth2wrap/version.rs @@ -21,7 +21,7 @@ enum BeaconNodeVersionError { static MINIMUM_BEACON_NODE_VERSIONS: LazyLock> = LazyLock::new(|| { - #[allow(clippy::unwrap_used, reason = "literals should be valid semver")] + #[expect(clippy::unwrap_used, reason = "literals should be valid semver")] std::collections::HashMap::from([ ("lighthouse", version::SemVer::parse("v8.0.0-rc.0").unwrap()), ("teku", version::SemVer::parse("v25.9.3").unwrap()), diff --git a/crates/app/src/health/checks.rs b/crates/app/src/health/checks.rs index 78f47d94..b6228951 100644 --- a/crates/app/src/health/checks.rs +++ b/crates/app/src/health/checks.rs @@ -48,7 +48,7 @@ pub(crate) struct Check { pub(crate) name: &'static str, /// Human-readable description. Not yet surfaced anywhere; retained for /// completeness. - #[allow( + #[expect( dead_code, reason = "retained for completeness; surfaced by future tooling" )] @@ -68,7 +68,7 @@ fn label(name: &str, value: &str) -> LabelPair { } /// Lossy `i64` → `f64` conversion used only for threshold comparisons. -#[allow( +#[expect( clippy::cast_precision_loss, reason = "validator/peer counts are small; threshold comparison does not require exactness" )] diff --git a/crates/app/src/node/mod.rs b/crates/app/src/node/mod.rs index 9c134c84..e85b2f82 100644 --- a/crates/app/src/node/mod.rs +++ b/crates/app/src/node/mod.rs @@ -685,7 +685,7 @@ fn production_parsigex_seam(handles: &CoreHandles) -> ParSigExSeam { /// Spawns and supervises the node's long-lived tasks, then performs an ordered /// shutdown on cancellation or first-task failure. -#[allow( +#[expect( clippy::too_many_arguments, reason = "aggregates independent long-lived inputs (swarm, consensus, wired components, monitoring); a single config struct would just move the coupling" )] diff --git a/crates/app/src/retry.rs b/crates/app/src/retry.rs index d95aa2df..23a524f7 100644 --- a/crates/app/src/retry.rs +++ b/crates/app/src/retry.rs @@ -128,7 +128,7 @@ pub async fn do_async< let deadline = (options.deadline_fn)(t); let now = options.clock.now(); - #[allow( + #[expect( clippy::arithmetic_side_effects, reason = "chrono to std conversion is safe for negative values" )] diff --git a/crates/cli/src/commands/run.rs b/crates/cli/src/commands/run.rs index 500e71fa..6ce2a4f6 100644 --- a/crates/cli/src/commands/run.rs +++ b/crates/cli/src/commands/run.rs @@ -648,14 +648,16 @@ pub struct RunConfig { /// Beacon node submission request timeout. pub beacon_node_submit_timeout: StdDuration, /// \[DISABLED\] Jaeger tracing address. - // Accepted for Charon flag parity; `RunConfig::try_from` already warns - // when set, and the field is never read again. - #[allow(dead_code)] + #[expect( + dead_code, + reason = "accepted for Charon flag parity; `RunConfig::try_from` already warns when set, and the field is never read again" + )] pub jaeger_addr: String, /// \[DISABLED\] Jaeger tracing service name. - // Accepted for Charon flag parity; `RunConfig::try_from` already warns - // when set, and the field is never read again. - #[allow(dead_code)] + #[expect( + dead_code, + reason = "accepted for Charon flag parity; `RunConfig::try_from` already warns when set, and the field is never read again" + )] pub jaeger_service: String, /// OTLP gRPC tracing backend address. pub otlp_address: String, diff --git a/crates/cli/src/commands/test/beacon.rs b/crates/cli/src/commands/test/beacon.rs index 2faf166f..6e7f813c 100644 --- a/crates/cli/src/commands/test/beacon.rs +++ b/crates/cli/src/commands/test/beacon.rs @@ -969,17 +969,29 @@ async fn single_cluster_simulation(cancel: CancellationToken, target: &str) -> S let mut slot = get_current_slot(target).await.unwrap_or(1); let now = Instant::now(); - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small fixed interval to a fresh Instant cannot overflow in practice" + )] let mut slot_interval = interval_at(now + SLOT_TIME, SLOT_TIME); - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small fixed interval to a fresh Instant cannot overflow in practice" + )] let mut interval_12_slots = interval_at( now + SLOT_TIME.saturating_mul(12), SLOT_TIME.saturating_mul(12), ); - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small fixed interval to a fresh Instant cannot overflow in practice" + )] let mut interval_10_sec = interval_at(now + StdDuration::from_secs(10), StdDuration::from_secs(10)); - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small fixed interval to a fresh Instant cannot overflow in practice" + )] let mut interval_minute = interval_at(now + StdDuration::from_secs(60), StdDuration::from_secs(60)); @@ -1293,7 +1305,10 @@ async fn attestation_duty( { return Default::default(); } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small tick interval to a fresh Instant cannot overflow in practice" + )] let mut interval = interval_at(Instant::now() + tick_time, tick_time); let mut slot = cancel .run_until_cancelled(get_current_slot(target)) @@ -1346,7 +1361,10 @@ async fn aggregation_duty( { return Default::default(); } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small tick interval to a fresh Instant cannot overflow in practice" + )] let mut interval = interval_at(Instant::now() + tick_time, tick_time); loop { @@ -1392,7 +1410,10 @@ async fn proposal_duty( { return Default::default(); } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small tick interval to a fresh Instant cannot overflow in practice" + )] let mut interval = interval_at(Instant::now() + tick_time, tick_time); let mut slot = cancel .run_until_cancelled(get_current_slot(target)) @@ -1426,7 +1447,10 @@ async fn proposal_duty( (produce_all, publish_all) } -#[allow(clippy::too_many_arguments)] +#[expect( + clippy::too_many_arguments, + reason = "orchestrates several independent sync-committee duty streams; splitting the arguments into a struct would not improve clarity" +)] async fn sync_committee_duties( cancel: CancellationToken, target: &str, @@ -1459,7 +1483,10 @@ async fn sync_committee_duties( { return; } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small tick interval to a fresh Instant cannot overflow in practice" + )] let mut interval = interval_at(Instant::now() + tick_time_subscribe, tick_time_subscribe); loop { @@ -1491,7 +1518,10 @@ async fn sync_committee_contribution_duty( { return; } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small tick interval to a fresh Instant cannot overflow in practice" + )] let mut interval = interval_at(Instant::now() + tick_time, tick_time); let mut slot = cancel .run_until_cancelled(get_current_slot(target)) @@ -1543,7 +1573,10 @@ async fn sync_committee_message_duty( { return; } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a small tick interval to a fresh Instant cannot overflow in practice" + )] let mut interval = interval_at(Instant::now() + tick_time, tick_time); loop { @@ -1634,7 +1667,7 @@ fn generate_simulation_values(durations: &[StdDuration], endpoint: &str) -> Simu tracing::warn!("Failed to convert duration length to u32"); u32::MAX }); - #[allow( + #[expect( clippy::arithmetic_side_effects, reason = "count is non-zero (early return above)" )] diff --git a/crates/cli/src/commands/test/infra.rs b/crates/cli/src/commands/test/infra.rs index e04d64f7..f165ce83 100644 --- a/crates/cli/src/commands/test/infra.rs +++ b/crates/cli/src/commands/test/infra.rs @@ -61,6 +61,10 @@ struct FioResultSingle { bw: f64, } +// `expect` cannot be used here: the lint does not fire under the current +// toolchain, so it would be flagged as an unfulfilled expectation. Kept as +// `allow` for forward compatibility. Reason: internal trait not part of the +// public API; no need for the `Send` bound the lint guards against. #[allow(async_fn_in_trait)] trait DiskTestTool { async fn check_availability(&self) -> Result<()>; diff --git a/crates/cli/src/commands/test/mod.rs b/crates/cli/src/commands/test/mod.rs index d95efb17..2f32bb3b 100644 --- a/crates/cli/src/commands/test/mod.rs +++ b/crates/cli/src/commands/test/mod.rs @@ -4,8 +4,10 @@ //! cluster setup, including tests for peers, beacon nodes, validator clients, //! MEV relays, and infrastructure. -// TODO: Foundation for the test command, the detail will be implemented later -#![allow(dead_code)] +#![expect( + dead_code, + reason = "foundation for the test command; the detail will be implemented later" +)] pub mod all; pub mod beacon; diff --git a/crates/cli/src/commands/test/peers.rs b/crates/cli/src/commands/test/peers.rs index 1eb982ae..3d742bcf 100644 --- a/crates/cli/src/commands/test/peers.rs +++ b/crates/cli/src/commands/test/peers.rs @@ -57,7 +57,13 @@ struct TestBehaviour { #[derive(Debug)] enum TestBehaviourEvent { Relay(relay::client::Event), - RelayManager(#[allow(dead_code)] pluto_p2p::relay::RelayManagerEvent), + RelayManager( + #[expect( + dead_code, + reason = "event payload is never read; only the variant tag matters" + )] + pluto_p2p::relay::RelayManagerEvent, + ), } impl From for TestBehaviourEvent { @@ -98,9 +104,10 @@ pub struct TestPeersArgs { pub test_config: TestConfigArgs, /// [REQUIRED] Comma-separated list of each peer ENR address. - // Doc comment doubles as clap help text, so the brackets must stay - // literal rather than becoming a rustdoc link. - #[allow(rustdoc::broken_intra_doc_links)] + #[expect( + rustdoc::broken_intra_doc_links, + reason = "doc comment doubles as clap help text, so the brackets must stay literal rather than becoming a rustdoc link" + )] #[arg(long = "enrs", value_delimiter = ',')] pub enrs: Option>, @@ -657,7 +664,10 @@ struct PeerState { identify_received: bool, } -#[allow(clippy::too_many_arguments)] +#[expect( + clippy::too_many_arguments, + reason = "drives the peer test event loop from many independent inputs; grouping them into a struct would not improve clarity" +)] async fn run_peer_event_loop( mut node: Node, cluster_peers: &[Peer], @@ -1045,7 +1055,10 @@ async fn keep_node_alive( ct: CancellationToken, ) { tracing::info!("Keeping TCP node alive until keep-alive time is reached..."); - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "adding a bounded keep-alive interval to a fresh Instant cannot overflow in practice" + )] let deadline = tokio::time::Instant::now() + keep_alive; loop { let remaining = deadline.saturating_duration_since(tokio::time::Instant::now()); diff --git a/crates/cli/src/commands/test/speedtest.rs b/crates/cli/src/commands/test/speedtest.rs index 6f135f4d..9c979962 100644 --- a/crates/cli/src/commands/test/speedtest.rs +++ b/crates/cli/src/commands/test/speedtest.rs @@ -325,10 +325,14 @@ pub(super) fn bytes_to_mbps(bytes: usize, elapsed: Duration) -> f64 { return 0.0; } - #[allow( + // `arithmetic_side_effects` cannot be an `expect`: the arithmetic below is + // all floating-point, which the lint never fires on, so it would be an + // unfulfilled expectation. Kept as `allow`. Reason: arithmetic overflow is + // impossible for realistic network speeds. + #[allow(clippy::arithmetic_side_effects)] + #[expect( clippy::cast_precision_loss, - clippy::arithmetic_side_effects, - reason = "precision loss requires >8PB transferred; arithmetic overflow is impossible for realistic network speeds" + reason = "precision loss requires >8PB transferred" )] let bytes: f64 = bytes as f64; bytes * 8.0 / secs / 1_000_000.0 diff --git a/crates/cli/src/duration.rs b/crates/cli/src/duration.rs index 3501491f..0bd501c9 100644 --- a/crates/cli/src/duration.rs +++ b/crates/cli/src/duration.rs @@ -28,7 +28,11 @@ impl Duration { } /// Rounds the duration based on its magnitude - #[allow(clippy::cast_possible_truncation, clippy::arithmetic_side_effects)] + #[expect( + clippy::cast_possible_truncation, + clippy::arithmetic_side_effects, + reason = "rounding arithmetic on bounded millisecond/microsecond values cannot overflow, and the u128->u64 casts fit the rounded magnitudes" + )] pub fn round(self) -> Self { let rounded = if self.inner > StdDuration::from_secs(1) { // Round to 10ms @@ -183,10 +187,11 @@ pub fn parse_go_duration(s: &str) -> Result { if frac > 0 { // Match Go: float64 is nanosecond-accurate for fractions of the // largest unit (hours). - #[allow( + #[expect( clippy::cast_precision_loss, clippy::cast_possible_truncation, - clippy::cast_sign_loss + clippy::cast_sign_loss, + reason = "matches Go: float64 is nanosecond-accurate for fractions of the largest unit (hours), and the result is bounded by GO_MAX_DURATION_NANOS" )] let frac_nanos = (frac as f64 * (unit as f64 / scale)) as u64; nanos = nanos @@ -274,7 +279,10 @@ impl fmt::Display for Duration { } /// Formats a duration like Go's `time.Duration.String()`. -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "buffer index and unit arithmetic operate on a fixed 32-byte buffer sized for the maximum u64 duration, so it cannot overflow or underflow" +)] fn format_go_duration(duration: StdDuration) -> String { let nanos_u128 = duration.as_nanos(); let mut u: u64 = u64::try_from(nanos_u128).unwrap_or(u64::MAX); @@ -354,7 +362,10 @@ fn format_go_duration(duration: StdDuration) -> String { /// Formats the fraction of `v / 10**prec` into the tail of `buf`, omitting /// trailing zeros. Returns the new start index and `v / 10**prec`. -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "buffer index arithmetic stays within the caller's fixed buffer and prec is bounded, so it cannot overflow or underflow" +)] fn fmt_frac(buf: &mut [u8], mut v: u64, prec: usize) -> (usize, u64) { // Omit trailing zeros up to and including decimal point. let mut w = buf.len(); @@ -380,7 +391,10 @@ fn fmt_frac(buf: &mut [u8], mut v: u64, prec: usize) -> (usize, u64) { /// Formats `v` into the tail of `buf`. Returns the index where the output /// begins. -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "buffer index arithmetic stays within the caller's fixed buffer sized for the maximum u64, so it cannot overflow or underflow" +)] fn fmt_int(buf: &mut [u8], mut v: u64) -> usize { let mut w = buf.len(); if v == 0 { diff --git a/crates/cluster/src/definition.rs b/crates/cluster/src/definition.rs index fe26f65d..5028d025 100644 --- a/crates/cluster/src/definition.rs +++ b/crates/cluster/src/definition.rs @@ -419,7 +419,10 @@ pub enum InvalidGasLimitError { impl Definition { /// Create a new cluster definition. - #[allow(clippy::too_many_arguments)] + #[expect( + clippy::too_many_arguments, + reason = "constructor mirrors the full cluster definition field set" + )] pub fn new( name: String, num_validators: u64, diff --git a/crates/cluster/src/test_cluster.rs b/crates/cluster/src/test_cluster.rs index 9664ce85..93b9223a 100644 --- a/crates/cluster/src/test_cluster.rs +++ b/crates/cluster/src/test_cluster.rs @@ -80,11 +80,11 @@ pub fn new_for_test( for i in 0..n { // Generate ENR - #[allow( + #[expect( clippy::arithmetic_side_effects, reason = "matches the original implementation, test code only" )] - #[allow( + #[expect( clippy::cast_possible_truncation, reason = "intentional truncation for testing purposes" )] diff --git a/crates/core/src/bcast/metrics.rs b/crates/core/src/bcast/metrics.rs index 05f7c364..f68a44ed 100644 --- a/crates/core/src/bcast/metrics.rs +++ b/crates/core/src/bcast/metrics.rs @@ -42,8 +42,10 @@ pub(crate) fn instrument_duty(duty: &Duty, delay: Option) { BCAST_METRICS.broadcast_total[&duty_type].inc(); if let Some(delay) = delay { - // Delays never approach f64's 2^53 ms exact range, so the cast is exact. - #[allow(clippy::cast_precision_loss)] + #[expect( + clippy::cast_precision_loss, + reason = "delays never approach f64's 2^53 ms exact range, so the cast is exact" + )] let seconds = delay.num_milliseconds() as f64 / 1_000.0; BCAST_METRICS.broadcast_delay_seconds[&duty_type].observe(seconds); } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 2e07940d..e1fa5358 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -64,10 +64,10 @@ pub mod fetcher; mod parsigex_codec; -// SSZ codec operates on compile-time-constant byte sizes and offsets. -// Arithmetic is bounded and casts from `usize` to `u32` are safe because all -// sizes are well below `u32::MAX`. -#[allow(clippy::arithmetic_side_effects, clippy::cast_possible_truncation)] +#[expect( + clippy::arithmetic_side_effects, + reason = "SSZ codec arithmetic is bounded by compile-time-constant byte sizes and offsets" +)] pub(crate) mod ssz_codec; pub use parsigex_codec::ParSigExCodecError; diff --git a/crates/core/src/qbft/fake_clock.rs b/crates/core/src/qbft/fake_clock.rs index c1891fb5..4d5c19c5 100644 --- a/crates/core/src/qbft/fake_clock.rs +++ b/crates/core/src/qbft/fake_clock.rs @@ -1,4 +1,7 @@ -#![allow(clippy::arithmetic_side_effects)] +#![expect( + clippy::arithmetic_side_effects, + reason = "test-only fake clock over Instant/Duration with bounded, non-overflowing arithmetic" +)] use crossbeam::channel as mpmc; use std::{ diff --git a/crates/core/src/qbft/internal_test.rs b/crates/core/src/qbft/internal_test.rs index a3113927..54abaf25 100644 --- a/crates/core/src/qbft/internal_test.rs +++ b/crates/core/src/qbft/internal_test.rs @@ -1,10 +1,11 @@ -#![allow( +#![expect( clippy::arithmetic_side_effects, clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_precision_loss, clippy::cast_sign_loss, - clippy::collapsible_if + clippy::collapsible_if, + reason = "QBFT test harness uses bounded integer arithmetic and casts over small test values" )] use crate::qbft::{ @@ -758,7 +759,10 @@ fn make_is_leader(n: i64) -> impl for<'a> Fn(LeaderRequest<'a, TestQbft>) -> boo } /// Returns a new message to be broadcast. -#[allow(clippy::too_many_arguments)] +#[expect( + clippy::too_many_arguments, + reason = "test helper mirrors the full QBFT message field set" +)] fn new_msg( type_: MessageType, instance: i64, diff --git a/crates/core/src/tracker/inclusion.rs b/crates/core/src/tracker/inclusion.rs index 7bb94c33..53b969a5 100644 --- a/crates/core/src/tracker/inclusion.rs +++ b/crates/core/src/tracker/inclusion.rs @@ -10,11 +10,6 @@ //! directly from tests. The networked driver that polls the beacon node and //! builds the `Block` inputs is layered on top separately. -// TODO: The networked `InclusionChecker` that wires the default reporters and drives -// this core is added in a follow-up; until then some core items (default -// reporters, committee plumbing) have no in-crate caller. -#![allow(dead_code)] - use std::{ any::Any, collections::HashMap, diff --git a/crates/core/src/tracker/mod.rs b/crates/core/src/tracker/mod.rs index fb027213..9082fd35 100644 --- a/crates/core/src/tracker/mod.rs +++ b/crates/core/src/tracker/mod.rs @@ -174,7 +174,6 @@ const EVENT_BUFFER: usize = 1024; /// /// `par_sig` is only set by `ParSigDBInternal`, `ParSigEx`, and /// `ParSigDBExternal` events, matching Go's `event.parSig`. -#[allow(dead_code)] #[derive(Clone)] pub(crate) struct Event { pub duty: Duty, @@ -204,6 +203,8 @@ pub struct TrackerHandle { input_tx: mpsc::Sender, /// Kept so callers can detect task completion or panics by awaiting it. /// Dropping the handle detaches the task; call `.abort()` to cancel it. + // Read only from tests, so `expect(dead_code)` would be unfulfilled under + // `--all-targets`; use `allow` to keep the field in non-test builds. #[allow(dead_code)] pub(crate) task: tokio::task::JoinHandle<()>, } @@ -356,7 +357,10 @@ impl TrackerService { /// Both `analyser` and `deleter` must have been started with the same /// `cancel` token as passed here, so that all three components shut down /// together. - #[allow(clippy::too_many_arguments)] + #[expect( + clippy::too_many_arguments, + reason = "tracker startup wires all deadliner handles, receivers, and config in one call" + )] pub fn start( cancel: CancellationToken, analyser: DeadlinerHandle, @@ -381,7 +385,10 @@ impl TrackerService { ) } - #[allow(clippy::too_many_arguments)] + #[expect( + clippy::too_many_arguments, + reason = "internal tracker startup wires all deadliner handles, receivers, sinks, and config in one call" + )] fn start_with_buffer_and_sinks( cancel: CancellationToken, analyser: DeadlinerHandle, diff --git a/crates/core/src/tracker/reason.rs b/crates/core/src/tracker/reason.rs index 65e7d5bf..14be9f6d 100644 --- a/crates/core/src/tracker/reason.rs +++ b/crates/core/src/tracker/reason.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - /// A reason for a duty failing, matching Go's `tracker.reason`. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Reason { diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index 7965950b..54869d98 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -819,25 +819,37 @@ pub struct Slot { impl Slot { /// Get the epoch of the slot pub fn epoch(&self) -> u64 { - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "saturating division cannot overflow or panic" + )] self.slot.inner().saturating_div(self.slots_per_epoch) } /// Returns true if this is the last slot in the epoch. - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "comparison uses wrapping and saturating operations that cannot panic" + )] pub fn last_in_epoch(&self) -> bool { self.slot.inner().wrapping_rem(self.slots_per_epoch) == self.slots_per_epoch.saturating_sub(1) } /// Returns true if this is the first slot in the epoch. - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "wrapping remainder cannot panic" + )] pub fn first_in_epoch(&self) -> bool { self.slot.inner().wrapping_rem(self.slots_per_epoch) == 0 } /// Returns the next slot - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "time addition of slot_duration is bounded and cannot realistically overflow" + )] pub fn next_slot(&self) -> Slot { Slot { slot: self.slot.next(), diff --git a/crates/core/src/validatorapi/component.rs b/crates/core/src/validatorapi/component.rs index 748aa40b..eaeeff10 100644 --- a/crates/core/src/validatorapi/component.rs +++ b/crates/core/src/validatorapi/component.rs @@ -187,7 +187,6 @@ pub struct Component { /// user-provided callback. subs: Vec, /// Looks up an unsigned beacon proposal for a slot. - #[allow(dead_code, reason = "consumed by proposal handler in later PRs")] await_proposal_fn: Option, /// Looks up an aggregated attestation by `(slot, attestation_root)`. await_agg_attestation_fn: Option, @@ -2777,7 +2776,6 @@ mod tests { } /// A cache pre-populated with `validators`. - #[allow(dead_code, reason = "consumed by submit_* handler tests in later PRs")] pub(super) fn arc( validators: HashMap, ) -> Arc { diff --git a/crates/core/src/version.rs b/crates/core/src/version.rs index ce6dc851..288caba7 100644 --- a/crates/core/src/version.rs +++ b/crates/core/src/version.rs @@ -337,7 +337,9 @@ mod tests { } #[test] - #[allow(clippy::const_is_empty, reason = "SUPPORTED should never be empty")] + // SUPPORTED should never be empty; lint does not fire on all toolchains so + // `allow` (not `expect`) is used to avoid an unfulfilled-expectation error. + #[allow(clippy::const_is_empty)] fn multi_supported() { assert!(!SUPPORTED.is_empty()); } diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index 8d9f136d..1e0d727e 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -15,11 +15,15 @@ rand_core.workspace = true thiserror.workspace = true zeroize.workspace = true +# Cargo cannot inherit the workspace lint set (`[lints] workspace = true`) while +# overriding a single lint, and this crate must relax `unsafe_code` for the blst +# C bindings. The lints are therefore listed explicitly and MUST be kept in sync +# with `[workspace.lints]` in the root `Cargo.toml`. [lints.rust] +missing_docs = "deny" # `deny` rather than the workspace `forbid` so `tbls::math` — the sole module # wrapping the blst C bindings — can opt back in with `#![allow(unsafe_code)]`. unsafe_code = "deny" -missing_docs = "deny" [lints.clippy] arithmetic_side_effects = "deny" @@ -30,6 +34,7 @@ cast_precision_loss = "deny" cast_sign_loss = "deny" needless_return = "deny" panicking_overflow_checks = "deny" +redundant_test_prefix = "deny" unwrap_used = "deny" [dev-dependencies] diff --git a/crates/dkg/src/dkg.rs b/crates/dkg/src/dkg.rs index d5bc9d91..21f36195 100644 --- a/crates/dkg/src/dkg.rs +++ b/crates/dkg/src/dkg.rs @@ -624,7 +624,7 @@ async fn run_inner(conf: Config, ct: CancellationToken) -> Result<(), DkgError> result } -#[allow(clippy::too_many_arguments, reason = "mirrors the Go DKG run flow")] +#[expect(clippy::too_many_arguments, reason = "mirrors the Go DKG run flow")] async fn run_ceremony( conf: &Config, eth1: &EthClient, diff --git a/crates/dkg/src/frostp2p/event.rs b/crates/dkg/src/frostp2p/event.rs index 8a2abf34..42e673c3 100644 --- a/crates/dkg/src/frostp2p/event.rs +++ b/crates/dkg/src/frostp2p/event.rs @@ -4,7 +4,7 @@ use libp2p::PeerId; /// Event emitted while the FROST P2P transport progresses through its rounds. #[derive(Debug)] -#[allow(dead_code)] +#[expect(dead_code, reason = "observation event fields not yet consumed")] pub(crate) enum FrostP2PEvent { /// A FROST transport round started. RoundStarted { diff --git a/crates/dkg/src/frostp2p/mod.rs b/crates/dkg/src/frostp2p/mod.rs index 43a0c29e..05fdfcf0 100644 --- a/crates/dkg/src/frostp2p/mod.rs +++ b/crates/dkg/src/frostp2p/mod.rs @@ -107,6 +107,8 @@ mod transport; pub(crate) use behaviour::{FrostP2PBehaviour, FrostP2PHandle, FrostP2PSender}; pub(crate) use event::FrostP2PEvent; +// `FrostP2P` is only referenced from the `#[cfg(test)]` integration test, so it +// reads as unused in a non-test build; `#[expect]` would be unfulfilled there. #[allow(unused_imports)] pub(crate) use transport::{FrostP2P, new_frost_p2p}; diff --git a/crates/dkg/src/signing.rs b/crates/dkg/src/signing.rs index ab489a94..653ee2a4 100644 --- a/crates/dkg/src/signing.rs +++ b/crates/dkg/src/signing.rs @@ -241,7 +241,6 @@ pub(crate) async fn sign_and_agg_deposit_data( } /// Signs, exchanges, and aggregates validator registrations. -#[allow(dead_code, reason = "will be used in dkg later ")] pub(crate) async fn sign_and_agg_validator_registrations( exchanger: &Exchanger, shares: &[Share], @@ -283,7 +282,7 @@ pub(crate) async fn sign_and_agg_validator_registrations( /// into the existing lock and the definition is re-hashed; signing happens over /// the union of `existing_shares` and `new_shares` unless the append is /// unverified, in which case signing is skipped. -#[allow(clippy::too_many_arguments, reason = "mirrors Go signAndAggLockHash")] +#[expect(clippy::too_many_arguments, reason = "mirrors Go signAndAggLockHash")] pub(crate) async fn sign_and_aggregate_lock_hash( existing_shares: &[Share], new_shares: &[Share], diff --git a/crates/eth2api/src/test_fixtures.rs b/crates/eth2api/src/test_fixtures.rs index ec23f336..52732be3 100644 --- a/crates/eth2api/src/test_fixtures.rs +++ b/crates/eth2api/src/test_fixtures.rs @@ -1,3 +1,4 @@ +// reason: test fixture items need no doc comments #![allow(missing_docs)] use crate::spec::{altair, bellatrix, capella, deneb, electra, phase0}; diff --git a/crates/eth2util/src/deposit/mod.rs b/crates/eth2util/src/deposit/mod.rs index 1f49278d..72f4d0a8 100644 --- a/crates/eth2util/src/deposit/mod.rs +++ b/crates/eth2util/src/deposit/mod.rs @@ -226,7 +226,10 @@ pub fn get_deposit_file_path(data_dir: impl AsRef, amount: Gwei) -> PathBu "deposit-data.json".to_string() } else { // Convert Gwei to ETH and format - #[allow(clippy::cast_precision_loss)] + #[expect( + clippy::cast_precision_loss, + reason = "gwei amount fits precisely in f64 for filename formatting" + )] let eth = amount as f64 / ONE_ETH_IN_GWEI as f64; format!("deposit-data-{}eth.json", eth) }; diff --git a/crates/frost/Cargo.toml b/crates/frost/Cargo.toml index 554c30f7..5b010481 100644 --- a/crates/frost/Cargo.toml +++ b/crates/frost/Cargo.toml @@ -20,6 +20,10 @@ rand.workspace = true serde.workspace = true serde_json.workspace = true +# Cargo cannot inherit the workspace lint set (`[lints] workspace = true`) while +# overriding a single lint, and this crate must relax `unsafe_code` for the blst +# C bindings. The lints are therefore listed explicitly and MUST be kept in sync +# with `[workspace.lints]` in the root `Cargo.toml`. [lints.rust] missing_docs = "deny" # Allow unsafe code for blst C bindings (overrides workspace forbid) @@ -34,4 +38,5 @@ cast_precision_loss = "deny" cast_sign_loss = "deny" needless_return = "deny" panicking_overflow_checks = "deny" +redundant_test_prefix = "deny" unwrap_used = "deny" diff --git a/crates/frost/src/frost_core.rs b/crates/frost/src/frost_core.rs index d15d4c3a..a85010c5 100644 --- a/crates/frost/src/frost_core.rs +++ b/crates/frost/src/frost_core.rs @@ -264,7 +264,10 @@ impl SecretShare { /// Checks that `G * signing_share == evaluate_vss(identifier, commitment)`. /// /// See: - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "BLS12-381 group/scalar ops have no meaningful overflow semantics" + )] pub fn verify(&self) -> Result<(), FrostCoreError> { let f_result = G1Projective::generator() * self.signing_share.to_scalar(); let result = evaluate_vss(self.identifier, &self.commitment); @@ -419,7 +422,10 @@ impl PublicKeyPackage { /// `a_0 + a_1 * x + a_2 * x^2 + ... + a_{t-1} * x^{t-1}`. /// /// See: -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "BLS12-381 scalar field arithmetic wraps modulo the field order; no integer overflow" +)] fn evaluate_polynomial( identifier: Identifier, coefficients: &[Scalar], @@ -442,7 +448,10 @@ fn evaluate_polynomial( /// Computes `sum_{k=0}^{t-1} commitment[k] * identifier^k`. /// /// See: -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "BLS12-381 group/scalar arithmetic has no integer overflow semantics" +)] fn evaluate_vss( identifier: Identifier, commitment: &VerifiableSecretSharingCommitment, @@ -465,7 +474,10 @@ fn evaluate_vss( /// elements across all participants. /// /// See: -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "BLS12-381 group element addition has no integer overflow semantics" +)] fn sum_commitments( commitments: &[&VerifiableSecretSharingCommitment], ) -> Result { diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index a838b47a..6a30b282 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -188,7 +188,10 @@ pub fn scalar_from_be(bytes: &[u8; 32]) -> Result { } /// RFC 9380 Section 5.3.1 using SHA-256 -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "loop bounds (ell, i) are asserted <= 255 and sizes are RFC 9380 bounded, so index arithmetic cannot overflow" +)] fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { const B_IN_BYTES: usize = 32; // SHA-256 output const S_IN_BYTES: usize = 64; // SHA-256 block size @@ -317,7 +320,10 @@ fn deserialize_commitment( /// - `max_signers`: Total number of signers (n). /// - `ctx`: DKG context byte (typically 0). /// - `rng`: Cryptographic RNG. -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "BLS12-381 group/scalar arithmetic has no integer overflow semantics" +)] pub fn round1( id: u32, threshold: u16, @@ -414,7 +420,10 @@ pub fn round1( /// [`Round1Bcast`]. /// - `received_shares`: Map from source participant ID to the [`ShamirShare`] /// they sent us. -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "threshold/max_signers bounds are validated in round1 so -1 cannot underflow, and BLS12-381 group/scalar arithmetic has no integer overflow semantics" +)] pub fn round2( secret: Round1Secret, received_bcasts: &BTreeMap, @@ -632,7 +641,10 @@ impl BlsSignature { /// /// Returns [`KryptologyError::InsufficientSigners`] if `min_signers < 2` or /// fewer than `min_signers` partial signatures are provided. - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "BLS12-381 scalar Lagrange arithmetic wraps modulo the field order; no integer overflow" + )] pub fn from_partial_signatures( min_signers: u16, partial_sigs: &[BlsPartialSignature], diff --git a/crates/frost/src/kryptology_interop_tests.rs b/crates/frost/src/kryptology_interop_tests.rs index d62a30a3..449d61dd 100644 --- a/crates/frost/src/kryptology_interop_tests.rs +++ b/crates/frost/src/kryptology_interop_tests.rs @@ -1,3 +1,5 @@ +// `missing_docs` does not fire in this test module, so `#![expect]` would be +// unfulfilled; kept as `#![allow]`: test-only fixtures/helpers need no docs. #![allow(missing_docs)] use std::collections::BTreeMap; diff --git a/crates/frost/src/kryptology_round_trip_tests.rs b/crates/frost/src/kryptology_round_trip_tests.rs index c59bc0df..c5337bf5 100644 --- a/crates/frost/src/kryptology_round_trip_tests.rs +++ b/crates/frost/src/kryptology_round_trip_tests.rs @@ -1,3 +1,5 @@ +// `missing_docs` does not fire in this test module, so `#![expect]` would be +// unfulfilled; kept as `#![allow]`: test-only fixtures/helpers need no docs. #![allow(missing_docs)] use std::collections::BTreeMap; diff --git a/crates/infosync/tests/infosync_integration.rs b/crates/infosync/tests/infosync_integration.rs index e78a09ec..ac12a13a 100644 --- a/crates/infosync/tests/infosync_integration.rs +++ b/crates/infosync/tests/infosync_integration.rs @@ -138,7 +138,10 @@ struct Host { /// in-process [`MemoryTransport`], wrapped by an infosync [`InfoSync`] /// component. A capture subscriber is registered *after* infosync's own, so /// receiving a capture message guarantees infosync's store is already updated. -#[allow(clippy::too_many_arguments)] +#[expect( + clippy::too_many_arguments, + reason = "test helper wires the full infosync host setup" +)] fn build_host( seed: u8, idx: usize, diff --git a/crates/k1util/benches/k1util.rs b/crates/k1util/benches/k1util.rs index 49ccf04d..6cdaa6e7 100644 --- a/crates/k1util/benches/k1util.rs +++ b/crates/k1util/benches/k1util.rs @@ -1,6 +1,7 @@ //! # k1util benchmarks //! //! Benchmarks for the k1util module. +// reason: benchmark harness items need no doc comments #![allow(missing_docs)] use std::hint::black_box; diff --git a/crates/p2p/examples/bootnode.rs b/crates/p2p/examples/bootnode.rs index 5db64c8c..e1ec9d97 100644 --- a/crates/p2p/examples/bootnode.rs +++ b/crates/p2p/examples/bootnode.rs @@ -1,4 +1,7 @@ -#![allow(missing_docs)] +#![expect( + missing_docs, + reason = "example binary; public items are self-explanatory" +)] //! Bootnode example demonstrating relay-based P2P connectivity. //! //! This example shows how to: diff --git a/crates/p2p/examples/p2p.rs b/crates/p2p/examples/p2p.rs index d49c4672..26609206 100644 --- a/crates/p2p/examples/p2p.rs +++ b/crates/p2p/examples/p2p.rs @@ -35,7 +35,7 @@ pub struct CombinedBehaviour { } /// Events emitted by the combined behaviour. -#[allow(missing_docs)] +#[expect(missing_docs, reason = "example enum variants are self-explanatory")] #[derive(Debug)] pub enum CombinedBehaviourEvent { Relay(relay::client::Event), diff --git a/crates/p2p/src/bandwidth.rs b/crates/p2p/src/bandwidth.rs index 12230fee..9a8456d3 100644 --- a/crates/p2p/src/bandwidth.rs +++ b/crates/p2p/src/bandwidth.rs @@ -242,7 +242,10 @@ impl AsyncWrite for PeerInstrumentedStream { } #[cfg(test)] -#[allow(clippy::arithmetic_side_effects)] +#[expect( + clippy::arithmetic_side_effects, + reason = "test code uses simple arithmetic on small known values" +)] mod tests { use std::task::Waker; diff --git a/crates/p2p/src/behaviours/mod.rs b/crates/p2p/src/behaviours/mod.rs index a187a2fe..fe9ba894 100644 --- a/crates/p2p/src/behaviours/mod.rs +++ b/crates/p2p/src/behaviours/mod.rs @@ -3,7 +3,10 @@ //! This module provides pre-configured network behaviours that combine multiple //! libp2p protocols for use in Charon nodes. -#![allow(missing_docs)] // we need to allow missing docs for the derive macro +#![expect( + missing_docs, + reason = "the NetworkBehaviour derive macro generates undocumented items" +)] /// Pluto behaviour. pub mod pluto; diff --git a/crates/p2p/src/name.rs b/crates/p2p/src/name.rs index ac2b0ca4..164acb86 100644 --- a/crates/p2p/src/name.rs +++ b/crates/p2p/src/name.rs @@ -372,13 +372,19 @@ pub fn peer_name(id: &PeerId) -> String { p_pow = (p_pow.wrapping_mul(P)) % M; } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "wrapping_rem never overflows and cannot divide by zero (NOUNS is non-empty)" + )] let noun_idx = usize::try_from(hash_value.wrapping_rem( u64::try_from(NOUNS.len()).expect("NOUNS.len() is always less than u64::MAX"), )) .expect("hash_value.wrapping_rem(u64::try_from(NOUNS.len())) is always less than usize::MAX"); - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "wrapping_rem never overflows and cannot divide by zero (ADJECTIVES is non-empty)" + )] let adj_idx = usize::try_from(hash_value.wrapping_rem( u64::try_from(ADJECTIVES.len()).expect("ADJECTIVES.len() is always less than u64::MAX"), )) diff --git a/crates/parsigex/examples/parsigex.rs b/crates/parsigex/examples/parsigex.rs index 0d6ff913..7fee3b68 100644 --- a/crates/parsigex/examples/parsigex.rs +++ b/crates/parsigex/examples/parsigex.rs @@ -1,3 +1,4 @@ +// reason: example binary items need no doc comments #![allow(missing_docs)] //! Partial-signature exchange example. //! @@ -100,7 +101,9 @@ struct CombinedBehaviour { enum CombinedBehaviourEvent { ParSigEx(Event), Relay(relay::client::Event), - RelayManager(#[allow(dead_code)] RelayManagerEvent), + RelayManager( + #[expect(dead_code, reason = "variant payload unused in example")] RelayManagerEvent, + ), } impl From for CombinedBehaviourEvent { diff --git a/crates/peerinfo/examples/peerinfo.rs b/crates/peerinfo/examples/peerinfo.rs index a96e1d68..c374ce73 100644 --- a/crates/peerinfo/examples/peerinfo.rs +++ b/crates/peerinfo/examples/peerinfo.rs @@ -1,7 +1,10 @@ //! Peerinfo example //! //! See the [README](./README.md) for usage instructions. -#![allow(missing_docs)] +#![expect( + missing_docs, + reason = "example binary; public items are self-explanatory" +)] use std::{ collections::HashMap, fs, @@ -98,7 +101,7 @@ pub struct CombinedBehaviour { } /// Events from the combined behaviour. -#[allow(missing_docs)] +#[expect(missing_docs, reason = "example enum variants are self-explanatory")] #[derive(Debug)] pub enum CombinedBehaviourEvent { PeerInfo(Event), diff --git a/crates/peerinfo/src/protocol.rs b/crates/peerinfo/src/protocol.rs index fe485f09..8384f0db 100644 --- a/crates/peerinfo/src/protocol.rs +++ b/crates/peerinfo/src/protocol.rs @@ -149,8 +149,7 @@ impl ProtocolState { return; } - #[allow( - clippy::cast_precision_loss, + #[expect( clippy::arithmetic_side_effects, reason = "RTT/2 subtraction from current time cannot underflow" )] @@ -212,7 +211,10 @@ impl ProtocolState { } } - #[allow(clippy::too_many_arguments)] + #[expect( + clippy::too_many_arguments, + reason = "metrics submission needs all peer-info fields as distinct arguments" + )] fn metrics_submitter( &self, clock_offset: chrono::Duration, @@ -239,7 +241,10 @@ impl ProtocolState { // Clamp clock offset to [-1 hour, 1 hour] let one_hour = chrono::Duration::hours(1); - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "negating a fixed one-hour Duration cannot overflow" + )] let clamped_offset = if clock_offset < -one_hour { -one_hour } else if clock_offset > one_hour { diff --git a/crates/priority/src/calculate.rs b/crates/priority/src/calculate.rs index a5465886..973be958 100644 --- a/crates/priority/src/calculate.rs +++ b/crates/priority/src/calculate.rs @@ -20,7 +20,14 @@ const MAX_PRIORITIES: usize = 1000; /// Equals [`MAX_PRIORITIES`] so that one extra supporting peer always outweighs /// any relative-priority difference (which is bounded by `MAX_PRIORITIES`). /// `MAX_PRIORITIES` is a small compile-time constant that fits an `i64`. -#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] +// `cast_possible_truncation` does not fire on this usize->i64 cast, so it stays +// `#[allow]` (an `#[expect]` would be unfulfilled): MAX_PRIORITIES is a small +// compile-time constant that fits an i64. +#[allow(clippy::cast_possible_truncation)] +#[expect( + clippy::cast_possible_wrap, + reason = "MAX_PRIORITIES is a small compile-time constant that fits an i64" +)] const COUNT_WEIGHT: i64 = MAX_PRIORITIES as i64; /// Returns the SSZ hash root of an `Any` envelope's deterministic protobuf diff --git a/crates/priority/src/component.rs b/crates/priority/src/component.rs index 13585edd..2a8398c7 100644 --- a/crates/priority/src/component.rs +++ b/crates/priority/src/component.rs @@ -255,7 +255,10 @@ pub struct Component { /// [`Error::PeerNotInContext`]. (Without this check such a peer would be gated /// to a no-op handler, its exchange silently skipped, and the instance could /// reach consensus on a partial message set after the exchange timeout.) -#[allow(clippy::too_many_arguments)] +#[expect( + clippy::too_many_arguments, + reason = "constructor wires together the full priority component; each argument is a distinct collaborator" +)] pub fn new_component( peers: Vec, min_required: i64, diff --git a/crates/priority/src/prioritiser.rs b/crates/priority/src/prioritiser.rs index e0864881..6e3f0636 100644 --- a/crates/priority/src/prioritiser.rs +++ b/crates/priority/src/prioritiser.rs @@ -241,7 +241,10 @@ impl Prioritiser { /// handler and its exchange silently skipped, so the instance could /// otherwise reach consensus on a partial message set. Callers using this /// seam directly must uphold that invariant. - #[allow(clippy::too_many_arguments)] + #[expect( + clippy::too_many_arguments, + reason = "internal constructor wires the full engine; each argument is a distinct collaborator" + )] pub fn new_internal( local_id: PeerId, peers: Vec, diff --git a/crates/relay-server/examples/relay_server.rs b/crates/relay-server/examples/relay_server.rs index a38f7d05..6700eac1 100644 --- a/crates/relay-server/examples/relay_server.rs +++ b/crates/relay-server/examples/relay_server.rs @@ -1,3 +1,4 @@ +// reason: example binary items need no doc comments #![allow(missing_docs)] //! Relay server example demonstrating a standalone libp2p relay node. //! diff --git a/crates/ssz/src/hasher.rs b/crates/ssz/src/hasher.rs index 3977daef..79552c11 100644 --- a/crates/ssz/src/hasher.rs +++ b/crates/ssz/src/hasher.rs @@ -156,12 +156,18 @@ impl Hasher { fn pad_to_32(buf: &mut Vec) { let rest = buf.len() % 32; if rest != 0 { - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "rest is buf.len() % 32 in 1..=31, so 32 - rest cannot underflow" + )] buf.extend_from_slice(&ZERO_BYTES[..32 - rest]); } } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "bit-twiddling round-up; v -= 1 guarded by callers passing v >= 1 and v += 1 cannot overflow reachable chunk counts" + )] fn next_power_of_two(mut v: usize) -> usize { v -= 1; v |= v >> 1; @@ -173,7 +179,10 @@ impl Hasher { v } - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "d > 1 here so next_power_of_two >= 2 and 64 - leading_zeros - 1 cannot underflow" + )] fn get_depth(d: usize) -> usize { if d <= 1 { return 0; @@ -249,7 +258,10 @@ impl HashWalker for Hasher { return Err(HasherError::InvalidBufferLength); } let mut result = [0; 32]; - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "buf.len() >= 32 checked above, so buf.len() - 32 cannot underflow" + )] result.copy_from_slice(&self.buf[self.buf.len() - 32..]); Ok(result) } @@ -415,10 +427,16 @@ pub fn calculate_limit(max_capacity: usize, num_items: usize, size: usize) -> us num_items } -#[allow( - clippy::cast_lossless, +// `#[allow]` (not `#[expect]`): the u8->usize widening does not currently +// trip `cast_lossless` on all targets, so an expectation would be unfulfilled. +#[allow(clippy::cast_lossless)] +#[expect( clippy::arithmetic_side_effects, - clippy::cast_possible_truncation + reason = "size arithmetic is bounded by the buffer length and msb in 0..=7" +)] +#[expect( + clippy::cast_possible_truncation, + reason = "leading_zeros() is 0..=7 for a non-zero byte, so the u32->u8 cast cannot truncate" )] fn parse_bitlist(tmp: &mut Vec, buf: &[u8]) -> Result { if buf.is_empty() { diff --git a/crates/ssz/src/types.rs b/crates/ssz/src/types.rs index 2e614e78..bd5b3338 100644 --- a/crates/ssz/src/types.rs +++ b/crates/ssz/src/types.rs @@ -188,7 +188,10 @@ impl Encode for SszVector { fn ssz_fixed_len() -> usize { if T::is_ssz_fixed_len() { - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "fixed element length times a small const SIZE cannot overflow a usize for real SSZ types" + )] { T::ssz_fixed_len() * SIZE } @@ -213,7 +216,10 @@ impl Decode for SszVector { fn ssz_fixed_len() -> usize { if T::is_ssz_fixed_len() { - #[allow(clippy::arithmetic_side_effects)] + #[expect( + clippy::arithmetic_side_effects, + reason = "fixed element length times a small const SIZE cannot overflow a usize for real SSZ types" + )] { T::ssz_fixed_len() * SIZE } @@ -572,12 +578,7 @@ impl BitVector { // `rem` is in 1..=7, so the shift never overflows. A valid final byte // has every bit at position >= rem (the padding region) cleared. match bytes.last() { - Some(&last) => { - #[allow(clippy::arithmetic_side_effects)] - { - last >> rem == 0 - } - } + Some(&last) => last >> rem == 0, None => true, } } diff --git a/crates/testutil/src/beaconmock/gorand.rs b/crates/testutil/src/beaconmock/gorand.rs index 245bf15e..f4882cd2 100644 --- a/crates/testutil/src/beaconmock/gorand.rs +++ b/crates/testutil/src/beaconmock/gorand.rs @@ -23,11 +23,12 @@ // casts) would obscure the contract rather than protect it. All inputs are // bounded by the algorithm (Schrage's method keeps seedrand within i32; Read // consumes 7 low bytes per draw). -#![allow( +#![expect( clippy::arithmetic_side_effects, clippy::cast_possible_wrap, clippy::cast_possible_truncation, - clippy::cast_sign_loss + clippy::cast_sign_loss, + reason = "fixed-width wrapping arithmetic and two's-complement casts are the Go PRNG specification; inputs are algorithm-bounded" )] const RNG_LEN: usize = 607; diff --git a/crates/testutil/src/beaconmock/mod.rs b/crates/testutil/src/beaconmock/mod.rs index d4bbec96..e6230a9d 100644 --- a/crates/testutil/src/beaconmock/mod.rs +++ b/crates/testutil/src/beaconmock/mod.rs @@ -65,7 +65,6 @@ impl Drop for BeaconMock { impl BeaconMock { /// Builds a beacon mock with charon-compatible defaults, overriding any /// provided fields. - #[allow(clippy::too_many_arguments)] #[builder] pub async fn new( validator_set: Option, diff --git a/crates/testutil/src/validatormock/attest.rs b/crates/testutil/src/validatormock/attest.rs index ac63cebc..c4ac387e 100644 --- a/crates/testutil/src/validatormock/attest.rs +++ b/crates/testutil/src/validatormock/attest.rs @@ -105,7 +105,10 @@ pub struct BeaconCommitteeSelection { pub struct SlotAttester { eth2_cl: Arc, slot: Slot, - #[allow(dead_code)] // matched against duties via the active-validator map + #[expect( + dead_code, + reason = "matched against duties via the active-validator map" + )] pubkeys: Vec, sign_func: SignFunc, diff --git a/crates/testutil/src/validatormock/synccomm.rs b/crates/testutil/src/validatormock/synccomm.rs index 85d5b733..122813b4 100644 --- a/crates/testutil/src/validatormock/synccomm.rs +++ b/crates/testutil/src/validatormock/synccomm.rs @@ -94,7 +94,7 @@ pub struct SyncCommMember { // Immutable state. eth2_cl: EthBeaconNodeApiClient, epoch: Epoch, - #[allow(dead_code)] + #[expect(dead_code, reason = "reserved for duty matching against pubkeys")] pubkeys: Vec, sign_func: SignFunc, @@ -749,7 +749,10 @@ mod tests { /// subnet contains 128 indices, and indices [75, 133, 289, 491] map to /// subcommittees [0, 1, 2, 3]. #[tokio::test] - #[allow(clippy::redundant_test_prefix)] + #[expect( + clippy::redundant_test_prefix, + reason = "test name mirrors the Go test identifier" + )] async fn test_get_subcommittees() { let mock = BeaconMock::builder() .sync_committee_size(512)