Skip to content

Commit 55c5d6c

Browse files
nolikclaude
andauthored
lint: enforce no-panic in production code (capstone) (#609)
Adds crate-level `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]` to the lib and binary, making the CI clippy gate reject any new panic-on-unexpected in production code. Test code is exempt via `#![cfg_attr(test, allow(...))]` (lib) and the separate integration-test crates. The handful of sanctioned panic sites are collapsed behind two narrowly `#[allow(clippy::expect_used)]`-annotated helpers: - `boot::compile_regex` — compiles the compile-time-constant mention and profile regexes (used by boot.rs and chat_gpt_handler.rs). - `chat_repository::to_redis_json` — infallible serde serialization for the ToRedisArgs impls, which have no channel to report an error. This locks in the error-handling work from iterations 2-5: every runtime failure is now modelled as a Result/AppError, and the lint prevents regressions. Verified: a probe `.unwrap()` in production fails clippy; local gate green (fmt, clippy --all-targets -D warnings, 11 unit + 4 pure tests, all tests compile). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 59ad17b commit 55c5d6c

5 files changed

Lines changed: 41 additions & 23 deletions

File tree

src/boot.rs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,21 @@ const DEFAULT_RUST_CHAT_ID: i64 = -1001228598755;
3434
pub const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1/chat/completions";
3535

3636
// Compile each mention regex exactly once. The patterns are compile-time
37-
// constants, so `expect` here can only fire on a developer typo — never at
38-
// runtime — which is why it is an accepted panic site.
39-
static RUST_RE: LazyLock<Regex> =
40-
LazyLock::new(|| Regex::new(RUST_REGEX).expect("RUST_REGEX must compile"));
41-
static BLAZING_FAST_RE: LazyLock<Regex> =
42-
LazyLock::new(|| Regex::new(BLAZING_FAST_REGEX).expect("BLAZING_FAST_REGEX must compile"));
43-
static GAYNESS_RE: LazyLock<Regex> =
44-
LazyLock::new(|| Regex::new(GAYNESS_REGEX).expect("GAYNESS_REGEX must compile"));
45-
static CHAT_GPT_RE: LazyLock<Regex> =
46-
LazyLock::new(|| Regex::new(CHAT_GPT_REGEX).expect("CHAT_GPT_REGEX must compile"));
47-
static URL_RE: LazyLock<Regex> =
48-
LazyLock::new(|| Regex::new(URL_REGEX).expect("URL_REGEX must compile"));
37+
// constants, so a failure can only be a developer typo, never a runtime error.
38+
static RUST_RE: LazyLock<Regex> = LazyLock::new(|| compile_regex(RUST_REGEX));
39+
static BLAZING_FAST_RE: LazyLock<Regex> = LazyLock::new(|| compile_regex(BLAZING_FAST_REGEX));
40+
static GAYNESS_RE: LazyLock<Regex> = LazyLock::new(|| compile_regex(GAYNESS_REGEX));
41+
static CHAT_GPT_RE: LazyLock<Regex> = LazyLock::new(|| compile_regex(CHAT_GPT_REGEX));
42+
static URL_RE: LazyLock<Regex> = LazyLock::new(|| compile_regex(URL_REGEX));
43+
44+
/// Compile a compile-time-constant regex pattern. The `expect` is the one
45+
/// sanctioned panic path (guarded by the crate-level `unwrap_used`/`expect_used`
46+
/// deny): a bad pattern is a source bug caught immediately by the tests, not a
47+
/// runtime failure on user input.
48+
#[allow(clippy::expect_used)]
49+
pub(crate) fn compile_regex(pattern: &str) -> Regex {
50+
Regex::new(pattern).expect("regex pattern must compile")
51+
}
4952

5053
#[derive(Clone)]
5154
pub struct GptParameters {

src/chat_gpt_handler.rs

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::fmt::Debug;
22
use std::sync::LazyLock;
33

4+
use crate::boot::compile_regex;
45
use crate::chat_gpt_handler::BotProfile::{Fedor, Felix, Ferris};
56
use crate::chat_gpt_handler::ChatMessageRole::{System, User};
67
use crate::gpt_service::{ChatMessage, ChatMessageRole};
@@ -43,28 +44,24 @@ static BOT_PROFILES: LazyLock<Vec<BotConfiguration<'static>>> = LazyLock::new(||
4344
vec![
4445
BotConfiguration {
4546
profile: Fedor,
46-
mention_regex: Regex::new(r"(?i)(fedor|ф[её]дор|федя)")
47-
.expect("Fedor mention regex must compile"),
47+
mention_regex: compile_regex(r"(?i)(fedor|ф[её]дор|федя)"),
4848
gpt_system_context: FEDOR_CHAT_GPT_SYSTEM_CONTEXT,
4949
},
5050
BotConfiguration {
5151
profile: Felix,
52-
mention_regex: Regex::new(r"(?i)(felix|феликс)")
53-
.expect("Felix mention regex must compile"),
52+
mention_regex: compile_regex(r"(?i)(felix|феликс)"),
5453
gpt_system_context: FELIX_CHAT_GPT_SYSTEM_CONTEXT,
5554
},
5655
BotConfiguration {
5756
profile: Ferris,
58-
mention_regex: Regex::new(r"(?i)(feris|ferris|ферис|феррис)")
59-
.expect("Ferris mention regex must compile"),
57+
mention_regex: compile_regex(r"(?i)(feris|ferris|ферис|феррис)"),
6058
gpt_system_context: FERRIS_CHAT_GPT_SYSTEM_CONTEXT,
6159
},
6260
]
6361
});
6462
const SUMMARY_REQUEST_REGEX: &str = r"(?i)([чш].о?\b.*\bпроисходит)";
65-
static CHAT_SUMMARY_REQUEST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
66-
Regex::new(SUMMARY_REQUEST_REGEX).expect("SUMMARY_REQUEST_REGEX must compile")
67-
});
63+
static CHAT_SUMMARY_REQUEST_REGEX: LazyLock<Regex> =
64+
LazyLock::new(|| compile_regex(SUMMARY_REQUEST_REGEX));
6865

6966
pub async fn handle_chat_gpt_question(
7067
bot: Bot,

src/chat_repository.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,21 @@ pub async fn get_bot_context(
1717
timeout_cmd(connection_manager.lrange(key, 0, 11)).await
1818
}
1919

20+
/// Serialize a value for storage in Redis. Serialization of these plain,
21+
/// owned structs is infallible in practice, and `ToRedisArgs::write_redis_args`
22+
/// has no channel to report an error, so the `expect` is a sanctioned panic
23+
/// site (guarded by the crate-level `expect_used` deny).
24+
#[allow(clippy::expect_used)]
25+
fn to_redis_json<T: serde::Serialize>(value: &T) -> String {
26+
serde_json::to_string(value).expect("value must serialize to JSON for Redis")
27+
}
28+
2029
impl ToRedisArgs for ChatMessage {
2130
fn write_redis_args<W>(&self, out: &mut W)
2231
where
2332
W: ?Sized + RedisWrite,
2433
{
25-
out.write_arg_fmt(serde_json::to_string(self).expect("Can't serialize Context as string"))
34+
out.write_arg_fmt(to_redis_json(self))
2635
}
2736
}
2837

@@ -38,7 +47,7 @@ impl ToRedisArgs for BotProfile {
3847
where
3948
W: ?Sized + RedisWrite,
4049
{
41-
out.write_arg_fmt(serde_json::to_string(self).expect("Can't serialize Context as string"))
50+
out.write_arg_fmt(to_redis_json(self))
4251
}
4352
}
4453

src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
// Forbid panic-on-unexpected in production code: runtime failures must be
2+
// modelled as errors (AppError / Result), not `unwrap`/`expect`/`panic!`. The
3+
// few sanctioned panic sites (compile-time-constant regexes, infallible serde)
4+
// carry a narrow `#[allow(clippy::expect_used)]`. Test code is exempt.
5+
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
6+
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used, clippy::panic))]
7+
18
pub mod bf_mention_handler;
29
pub mod boot;
310
pub mod chat_gpt_handler;

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2+
13
use std::env;
24
use std::sync::Arc;
35

0 commit comments

Comments
 (0)