foundry_cli/
diagnostic.rs1use std::{fmt, sync::OnceLock};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct InvalidDiagnosticCode {
24 code: String,
25}
26
27impl fmt::Display for InvalidDiagnosticCode {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 write!(
30 f,
31 "invalid diagnostic code `{}`: must match `^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$`",
32 self.code
33 )
34 }
35}
36
37impl std::error::Error for InvalidDiagnosticCode {}
38
39pub fn validate(s: &str) -> Result<(), InvalidDiagnosticCode> {
41 static RE: OnceLock<regex::Regex> = OnceLock::new();
42 let re = RE.get_or_init(|| {
43 regex::Regex::new(r"^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$")
44 .expect("diagnostic-code regex compiles")
45 });
46 if re.is_match(s) { Ok(()) } else { Err(InvalidDiagnosticCode { code: s.to_string() }) }
47}
48
49#[derive(Clone, Debug, PartialEq, Eq, Hash)]
51pub struct DiagnosticCode(String);
52
53impl DiagnosticCode {
54 pub fn new(code: impl Into<String>) -> Result<Self, InvalidDiagnosticCode> {
56 let code = code.into();
57 validate(&code)?;
58 Ok(Self(code))
59 }
60
61 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65
66 pub fn into_string(self) -> String {
68 self.0
69 }
70}
71
72impl fmt::Display for DiagnosticCode {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.write_str(&self.0)
75 }
76}
77
78impl AsRef<str> for DiagnosticCode {
79 fn as_ref(&self) -> &str {
80 &self.0
81 }
82}
83
84impl From<DiagnosticCode> for String {
85 fn from(code: DiagnosticCode) -> Self {
86 code.0
87 }
88}
89
90impl std::str::FromStr for DiagnosticCode {
91 type Err = InvalidDiagnosticCode;
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
93 Self::new(s)
94 }
95}
96
97pub mod cli {
99 pub const USAGE_INVALID: &str = "cli.usage.invalid";
102 pub const HELP: &str = "cli.help";
104 pub const VERSION: &str = "cli.version";
106 pub const INTERRUPTED: &str = "cli.interrupted";
108 pub const UNKNOWN: &str = "cli.unknown";
110
111 pub(crate) const ALL: &[&str] = &[USAGE_INVALID, HELP, VERSION, INTERRUPTED, UNKNOWN];
112}
113
114pub mod config {
116 pub const INVALID: &str = "config.invalid";
117 pub const MISSING_FIELD: &str = "config.missing_field";
118
119 pub(crate) const ALL: &[&str] = &[INVALID, MISSING_FIELD];
120}
121
122pub mod compiler {
124 pub const SOLC_ERROR: &str = "compiler.solc.error";
125 pub const VYPER_ERROR: &str = "compiler.vyper.error";
126
127 pub(crate) const ALL: &[&str] = &[SOLC_ERROR, VYPER_ERROR];
128}
129
130pub mod network {
132 pub const RPC_ERROR: &str = "network.rpc.error";
134 pub const RPC_TIMEOUT: &str = "network.rpc.timeout";
135 pub const RPC_UNAUTHORIZED: &str = "network.rpc.unauthorized";
136
137 pub(crate) const ALL: &[&str] = &[RPC_ERROR, RPC_TIMEOUT, RPC_UNAUTHORIZED];
138}
139
140pub mod wallet {
142 pub const KEY_MISSING: &str = "wallet.key.missing";
143 pub const SIGNATURE_REJECTED: &str = "wallet.signature.rejected";
144
145 pub(crate) const ALL: &[&str] = &[KEY_MISSING, SIGNATURE_REJECTED];
146}
147
148pub mod test {
150 pub const FAILED: &str = "test.failed";
151 pub const SETUP_FAILED: &str = "test.setup_failed";
152 pub const WARNING: &str = "test.warning";
154
155 pub(crate) const ALL: &[&str] = &[FAILED, SETUP_FAILED, WARNING];
156}
157
158pub mod script {
160 pub const BROADCAST_FAILED: &str = "script.broadcast_failed";
161
162 pub(crate) const ALL: &[&str] = &[BROADCAST_FAILED];
163}
164
165pub mod cast {
167 pub const TX_NOT_FOUND: &str = "cast.tx.not_found";
168
169 pub(crate) const ALL: &[&str] = &[TX_NOT_FOUND];
170}
171
172pub mod anvil {
174 pub const FORK_UNREACHABLE: &str = "anvil.fork.unreachable";
175
176 pub(crate) const ALL: &[&str] = &[FORK_UNREACHABLE];
177}
178
179pub mod chisel {
181 pub const SESSION_INVALID: &str = "chisel.session.invalid";
182
183 pub(crate) const ALL: &[&str] = &[SESSION_INVALID];
184}
185
186pub fn known_codes() -> Vec<&'static str> {
190 let groups: &[&[&str]] = &[
191 cli::ALL,
192 config::ALL,
193 compiler::ALL,
194 network::ALL,
195 wallet::ALL,
196 test::ALL,
197 script::ALL,
198 cast::ALL,
199 anvil::ALL,
200 chisel::ALL,
201 ];
202 groups.iter().flat_map(|g| g.iter().copied()).collect()
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn valid_codes_round_trip() {
211 for c in &["cli.usage.invalid", "config.invalid", "network.rpc.timeout", "a.b.c.d.e"] {
212 let code = DiagnosticCode::new(*c).unwrap();
213 assert_eq!(code.as_str(), *c);
214 }
215 }
216
217 #[test]
218 fn invalid_codes_rejected() {
219 for c in &[
220 "",
221 "no_dot",
222 "Cli.usage", ".cli.usage", "cli.usage.", "cli..usage", "1cli.usage", "cli.1usage", "cli.usage-invalid", "cli.usage invalid", ] {
231 assert!(DiagnosticCode::new(*c).is_err(), "expected `{c}` to be rejected");
232 }
233 }
234
235 #[test]
236 fn every_known_code_validates() {
237 for c in known_codes() {
238 assert!(validate(c).is_ok(), "registered code `{c}` failed validation");
239 }
240 }
241
242 #[test]
243 fn known_codes_are_unique() {
244 let mut seen = std::collections::BTreeSet::new();
245 let mut dups = Vec::new();
246 for c in known_codes() {
247 if !seen.insert(c) {
248 dups.push(c);
249 }
250 }
251 assert!(dups.is_empty(), "duplicate diagnostic codes: {dups:?}");
252 }
253}