diff --git a/CHANGELOG.md b/CHANGELOG.md index dfea01763..e0a51fe00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +- Retired the remaining 17 production `.expect()` sites in `lading_payload` + and dropped the crate-root `#![allow(clippy::expect_used)]` quarantine. + 13 sites became `.unwrap_or_else(... unreachable!("..."))` (structural + infallibles: serde_json on simple structs, RFC-3339 of a known-valid + `OffsetDateTime`, `NonZeroI64` after a documented positivity invariant, + `parts.last()` after `str::split`, `Vec::pop` after a known push, etc.). + 4 sites in `dogstatsd/{common,metric,service_check}.rs` are annotated + with fn-level `#[expect(clippy::expect_used, reason = "...")]` because + the panic is the documented contract when upstream config validation + is too loose (empty `templates`/`names` vec, `ConfRange::Inclusive` + with `min > max`). `lading_payload` is now fully covered by the + workspace-level `clippy::expect_used = "deny"`. - Fixed a latent panic in `lading_payload::block::Block::arbitrary` (`#[cfg(feature = "arbitrary")]`). `u32::arbitrary` can legitimately produce 0, but `Block::total_bytes` is `NonZeroU32`; the previous code diff --git a/lading_payload/src/common/strings/random_string_pool.rs b/lading_payload/src/common/strings/random_string_pool.rs index 211df2de8..0a5d04d6b 100644 --- a/lading_payload/src/common/strings/random_string_pool.rs +++ b/lading_payload/src/common/strings/random_string_pool.rs @@ -186,10 +186,9 @@ where let mut buf = Vec::with_capacity(total); for _ in 0..total { let sz = length_range.sample(&mut rng) as usize; - buf.push(String::from( - pool.of_size(&mut rng, sz) - .expect("failed to generate string"), - )); + buf.push(String::from(pool.of_size(&mut rng, sz).unwrap_or_else( + || unreachable!("pool is sized larger than length_range by caller convention"), + ))); } buf } diff --git a/lading_payload/src/datadog_logs.rs b/lading_payload/src/datadog_logs.rs index 10107b00b..953a21f65 100644 --- a/lading_payload/src/datadog_logs.rs +++ b/lading_payload/src/datadog_logs.rs @@ -54,13 +54,13 @@ where R: rand::Rng + ?Sized, { match rng.random_range(0..2) { - 0 => Message::Unstructured( - str_pool - .of_size_range(rng, 1_u8..16) - .expect("failed to generate string"), - ), + 0 => Message::Unstructured(str_pool.of_size_range(rng, 1_u8..16).unwrap_or_else(|| { + unreachable!("str_pool is sized 1_000_000 by construction; 1..16 always fits") + })), 1 => Message::Structured( - serde_json::to_string(&rng.random::()).expect("failed to generate string"), + serde_json::to_string(&rng.random::()).unwrap_or_else(|_| { + unreachable!("Structured is a struct of primitive fields with derived Serialize") + }), ), _ => unreachable!(), } diff --git a/lading_payload/src/dogstatsd/common.rs b/lading_payload/src/dogstatsd/common.rs index 3ea307946..9f219672c 100644 --- a/lading_payload/src/dogstatsd/common.rs +++ b/lading_payload/src/dogstatsd/common.rs @@ -34,6 +34,10 @@ pub(crate) enum NumValueGenerator { impl NumValueGenerator { #[allow(clippy::cast_possible_truncation)] + #[expect( + clippy::expect_used, + reason = "ConfRange::Inclusive { min, max } does not validate min <= max at deserialization; a misconfigured config with min > max will panic here with the existing diagnostic message" + )] pub(crate) fn new(conf: ValueConf) -> Self { match conf.range { ConfRange::Constant(c) => Self::Constant { diff --git a/lading_payload/src/dogstatsd/metric.rs b/lading_payload/src/dogstatsd/metric.rs index e78470fe3..008fe649b 100644 --- a/lading_payload/src/dogstatsd/metric.rs +++ b/lading_payload/src/dogstatsd/metric.rs @@ -127,6 +127,10 @@ impl<'a> Generator<'a> for MetricGenerator { type Error = Error; #[expect(clippy::too_many_lines)] + #[expect( + clippy::expect_used, + reason = "self.templates is validated non-empty at MetricGenerator construction; an empty templates vector here is a serious upstream logic bug" + )] fn generate(&'a self, mut rng: &mut R) -> Result where R: rand::Rng + ?Sized, @@ -147,7 +151,7 @@ impl<'a> Generator<'a> for MetricGenerator { let sample_rate = if prob < self.sampling_probability { let sample_rate = self.sampling.sample(&mut rng).clamp(0.0, 1.0); let sample_rate = common::ZeroToOne::try_from(sample_rate) - .expect("failed to convert sample rate to ZeroToOne"); + .unwrap_or_else(|_| unreachable!("clamp(0.0, 1.0) guarantees ZeroToOne range")); Some(sample_rate) } else { None @@ -260,7 +264,9 @@ impl<'a> Generator<'a> for MetricGenerator { .ok_or(Error::StringGenerate)?; Ok(Metric::Set(Set { name, - value: values.pop().expect("failed to pop value from Vec"), + value: values + .pop() + .unwrap_or_else(|| unreachable!("values has at least one push above")), tags, pools: &self.pools, container_id, diff --git a/lading_payload/src/dogstatsd/service_check.rs b/lading_payload/src/dogstatsd/service_check.rs index 7df116bdd..f31a19945 100644 --- a/lading_payload/src/dogstatsd/service_check.rs +++ b/lading_payload/src/dogstatsd/service_check.rs @@ -26,6 +26,10 @@ impl<'a> Generator<'a> for ServiceCheckGenerator { type Output = ServiceCheck<'a>; type Error = Error; + #[expect( + clippy::expect_used, + reason = "self.names is validated non-empty at ServiceCheckGenerator construction; an empty names vector here is a serious upstream logic bug" + )] fn generate(&'a self, mut rng: &mut R) -> Result where R: rand::Rng + ?Sized, diff --git a/lading_payload/src/fluent.rs b/lading_payload/src/fluent.rs index 00356972e..670f0587a 100644 --- a/lading_payload/src/fluent.rs +++ b/lading_payload/src/fluent.rs @@ -126,17 +126,15 @@ where R: rand::Rng + ?Sized, { match rng.random_range(0..2) { - 0 => RecordValue::String( - str_pool - .of_size_range(rng, 1_u8..16) - .expect("failed to generate string"), - ), + 0 => RecordValue::String(str_pool.of_size_range(rng, 1_u8..16).unwrap_or_else(|| { + unreachable!("str_pool is sized by construction; 1..16 always fits") + })), 1 => { let mut obj = BTreeMap::new(); for _ in 0..rng.random_range(0..128) { - let key = str_pool - .of_size_range(rng, 1_u8..16) - .expect("failed to generate string"); + let key = str_pool.of_size_range(rng, 1_u8..16).unwrap_or_else(|| { + unreachable!("str_pool is sized by construction; 1..16 always fits") + }); let val = rng.random(); obj.insert(key, val); diff --git a/lading_payload/src/lib.rs b/lading_payload/src/lib.rs index 3f412a84f..b6ef8fbe6 100644 --- a/lading_payload/src/lib.rs +++ b/lading_payload/src/lib.rs @@ -5,9 +5,6 @@ #![deny(clippy::cargo)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::multiple_crate_versions)] -// Quarantine: workspace denies `clippy::expect_used`, but this crate still has -// production `.expect()` sites awaiting cleanup. Remove once cleaned up. -#![allow(clippy::expect_used)] use std::{ io::{self, Write}, diff --git a/lading_payload/src/syslog.rs b/lading_payload/src/syslog.rs index 0381c00d4..535b5798f 100644 --- a/lading_payload/src/syslog.rs +++ b/lading_payload/src/syslog.rs @@ -73,8 +73,10 @@ impl Distribution for StandardUniform { let range: i64 = 315_360_000; // ~10 years in seconds let offset: i64 = rng.random_range(0..range); let ts = OffsetDateTime::from_unix_timestamp(base_ts + offset) - .expect("timestamp in valid range"); - ts.format(&Rfc3339).expect("failed to format timestamp") + .unwrap_or_else(|_| unreachable!("base_ts + offset stays within [2020, 2030] which is well inside the valid OffsetDateTime range")); + ts.format(&Rfc3339).unwrap_or_else(|_| { + unreachable!("RFC-3339 formatting of a valid OffsetDateTime cannot fail") + }) }, hostname: HOSTNAMES .choose(rng) @@ -84,7 +86,9 @@ impl Distribution for StandardUniform { .unwrap_or_else(|| unreachable!("APP_NAMES is a non-empty const array")), procid: rng.random_range(100..=9999), msgid: rng.random_range(1..=999), - message: serde_json::to_string(&rng.random::()).expect("failed to serialize"), + message: serde_json::to_string(&rng.random::()).unwrap_or_else(|_| { + unreachable!("Message is a struct of primitive fields with derived Serialize") + }), } } } diff --git a/lading_payload/src/templated_json/config.rs b/lading_payload/src/templated_json/config.rs index 7bf0871e7..223d9ebdc 100644 --- a/lading_payload/src/templated_json/config.rs +++ b/lading_payload/src/templated_json/config.rs @@ -285,7 +285,8 @@ pub(super) struct ArraySpec { /// between the outer quotation marks if `s` were serialized as a JSON string. /// Called once at parse time when pre-compiling a `!format` template. fn json_string_escape_content(s: &str) -> String { - let encoded = serde_json::to_string(s).expect("string serialization cannot fail"); + let encoded = serde_json::to_string(s) + .unwrap_or_else(|_| unreachable!("serde_json::to_string on a &str cannot fail")); encoded[1..encoded.len() - 1].to_string() } @@ -439,8 +440,11 @@ impl<'de> Deserialize<'de> for FormatSpec { ))); } - let trailing_escaped = - json_string_escape_content(parts.last().expect("split yields at least one part")); + let trailing_escaped = json_string_escape_content( + parts + .last() + .unwrap_or_else(|| unreachable!("str::split always yields at least one part")), + ); let segments = parts[..parts.len() - 1] .iter() diff --git a/lading_payload/src/templated_json/generator.rs b/lading_payload/src/templated_json/generator.rs index f0959664c..a8de88140 100644 --- a/lading_payload/src/templated_json/generator.rs +++ b/lading_payload/src/templated_json/generator.rs @@ -183,7 +183,9 @@ impl Timestamp { let new_ms = base_ms.saturating_add(delta_ms); // base_ms >= TIMESTAMP_BASE_SECS_MIN * MS_PER_SEC and delta_ms >= 1, // so new_ms is always nonzero; the error branch is unreachable in practice. - let new_nz = NonZeroI64::new(new_ms).expect("operations above guarantee nonzero"); + let new_nz = NonZeroI64::new(new_ms).unwrap_or_else(|| { + unreachable!("base_ms >= TIMESTAMP_BASE_SECS_MIN * MS_PER_SEC and delta_ms >= 1") + }); self.0.set(Some(new_nz)); // Emit only the whole-second part as RFC-3339. let dt = OffsetDateTime::from_unix_timestamp(new_ms / MS_PER_SEC)