diff --git a/src/boot.rs b/src/boot.rs index 629ff2f..abc005e 100644 --- a/src/boot.rs +++ b/src/boot.rs @@ -34,18 +34,21 @@ const DEFAULT_RUST_CHAT_ID: i64 = -1001228598755; pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1/chat/completions"; // Compile each mention regex exactly once. The patterns are compile-time -// constants, so `expect` here can only fire on a developer typo — never at -// runtime — which is why it is an accepted panic site. -static RUST_RE: LazyLock = - LazyLock::new(|| Regex::new(RUST_REGEX).expect("RUST_REGEX must compile")); -static BLAZING_FAST_RE: LazyLock = - LazyLock::new(|| Regex::new(BLAZING_FAST_REGEX).expect("BLAZING_FAST_REGEX must compile")); -static GAYNESS_RE: LazyLock = - LazyLock::new(|| Regex::new(GAYNESS_REGEX).expect("GAYNESS_REGEX must compile")); -static CHAT_GPT_RE: LazyLock = - LazyLock::new(|| Regex::new(CHAT_GPT_REGEX).expect("CHAT_GPT_REGEX must compile")); -static URL_RE: LazyLock = - LazyLock::new(|| Regex::new(URL_REGEX).expect("URL_REGEX must compile")); +// constants, so a failure can only be a developer typo, never a runtime error. +static RUST_RE: LazyLock = LazyLock::new(|| compile_regex(RUST_REGEX)); +static BLAZING_FAST_RE: LazyLock = LazyLock::new(|| compile_regex(BLAZING_FAST_REGEX)); +static GAYNESS_RE: LazyLock = LazyLock::new(|| compile_regex(GAYNESS_REGEX)); +static CHAT_GPT_RE: LazyLock = LazyLock::new(|| compile_regex(CHAT_GPT_REGEX)); +static URL_RE: LazyLock = LazyLock::new(|| compile_regex(URL_REGEX)); + +/// Compile a compile-time-constant regex pattern. The `expect` is the one +/// sanctioned panic path (guarded by the crate-level `unwrap_used`/`expect_used` +/// deny): a bad pattern is a source bug caught immediately by the tests, not a +/// runtime failure on user input. +#[allow(clippy::expect_used)] +pub(crate) fn compile_regex(pattern: &str) -> Regex { + Regex::new(pattern).expect("regex pattern must compile") +} #[derive(Clone)] pub struct GptParameters { diff --git a/src/chat_gpt_handler.rs b/src/chat_gpt_handler.rs index a4e5e52..d10a52a 100644 --- a/src/chat_gpt_handler.rs +++ b/src/chat_gpt_handler.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use std::sync::LazyLock; +use crate::boot::compile_regex; use crate::chat_gpt_handler::BotProfile::{Fedor, Felix, Ferris}; use crate::chat_gpt_handler::ChatMessageRole::{System, User}; use crate::gpt_service::{ChatMessage, ChatMessageRole}; @@ -43,28 +44,24 @@ static BOT_PROFILES: LazyLock>> = LazyLock::new(|| vec![ BotConfiguration { profile: Fedor, - mention_regex: Regex::new(r"(?i)(fedor|ф[её]дор|федя)") - .expect("Fedor mention regex must compile"), + mention_regex: compile_regex(r"(?i)(fedor|ф[её]дор|федя)"), gpt_system_context: FEDOR_CHAT_GPT_SYSTEM_CONTEXT, }, BotConfiguration { profile: Felix, - mention_regex: Regex::new(r"(?i)(felix|феликс)") - .expect("Felix mention regex must compile"), + mention_regex: compile_regex(r"(?i)(felix|феликс)"), gpt_system_context: FELIX_CHAT_GPT_SYSTEM_CONTEXT, }, BotConfiguration { profile: Ferris, - mention_regex: Regex::new(r"(?i)(feris|ferris|ферис|феррис)") - .expect("Ferris mention regex must compile"), + mention_regex: compile_regex(r"(?i)(feris|ferris|ферис|феррис)"), gpt_system_context: FERRIS_CHAT_GPT_SYSTEM_CONTEXT, }, ] }); const SUMMARY_REQUEST_REGEX: &str = r"(?i)([чш].о?\b.*\bпроисходит)"; -static CHAT_SUMMARY_REQUEST_REGEX: LazyLock = LazyLock::new(|| { - Regex::new(SUMMARY_REQUEST_REGEX).expect("SUMMARY_REQUEST_REGEX must compile") -}); +static CHAT_SUMMARY_REQUEST_REGEX: LazyLock = + LazyLock::new(|| compile_regex(SUMMARY_REQUEST_REGEX)); pub async fn handle_chat_gpt_question( bot: Bot, diff --git a/src/chat_repository.rs b/src/chat_repository.rs index b9b0b0d..d0a46a4 100644 --- a/src/chat_repository.rs +++ b/src/chat_repository.rs @@ -17,12 +17,21 @@ pub async fn get_bot_context( timeout_cmd(connection_manager.lrange(key, 0, 11)).await } +/// Serialize a value for storage in Redis. Serialization of these plain, +/// owned structs is infallible in practice, and `ToRedisArgs::write_redis_args` +/// has no channel to report an error, so the `expect` is a sanctioned panic +/// site (guarded by the crate-level `expect_used` deny). +#[allow(clippy::expect_used)] +fn to_redis_json(value: &T) -> String { + serde_json::to_string(value).expect("value must serialize to JSON for Redis") +} + impl ToRedisArgs for ChatMessage { fn write_redis_args(&self, out: &mut W) where W: ?Sized + RedisWrite, { - out.write_arg_fmt(serde_json::to_string(self).expect("Can't serialize Context as string")) + out.write_arg_fmt(to_redis_json(self)) } } @@ -38,7 +47,7 @@ impl ToRedisArgs for BotProfile { where W: ?Sized + RedisWrite, { - out.write_arg_fmt(serde_json::to_string(self).expect("Can't serialize Context as string")) + out.write_arg_fmt(to_redis_json(self)) } } diff --git a/src/lib.rs b/src/lib.rs index dcf5e9c..bd0861f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,10 @@ +// Forbid panic-on-unexpected in production code: runtime failures must be +// modelled as errors (AppError / Result), not `unwrap`/`expect`/`panic!`. The +// few sanctioned panic sites (compile-time-constant regexes, infallible serde) +// carry a narrow `#[allow(clippy::expect_used)]`. Test code is exempt. +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] +#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))] + pub mod bf_mention_handler; pub mod boot; pub mod chat_gpt_handler; diff --git a/src/main.rs b/src/main.rs index 5949ffa..de56480 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + use std::env; use std::sync::Arc;