Skip to main content

foundry_cli/
diagnostic.rs

1//! Stable, machine-readable diagnostic codes.
2//!
3//! Codes attach to [`JsonMessage`](crate::json::JsonMessage) entries inside
4//! [`JsonEnvelope`](crate::json::JsonEnvelope) `errors[]` and `warnings[]`.
5//!
6//! See [`docs/agents/diagnostics.md`](../../../docs/agents/diagnostics.md) for
7//! the format and registry rules. Codes are namespaced strings of the form
8//! `^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$`.
9//!
10//! # Implementation choice
11//!
12//! Codes are exposed as `&'static str` constants, organised in per-domain
13//! modules colocated with this crate (or, for adoption PRs, with the owning
14//! crate). [`JsonMessage::error`](crate::json::JsonMessage::error) accepts
15//! `impl Into<String>`, so call sites pass the constant directly. The
16//! [`DiagnosticCode`] newtype is available when callers want a parsed,
17//! validated value.
18
19use std::{fmt, sync::OnceLock};
20
21/// Validation error returned by [`DiagnosticCode::new`] / [`validate`].
22#[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
39/// Validate that `s` matches the diagnostic-code grammar.
40pub 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/// Stable, machine-readable diagnostic code attached to a structured message.
50#[derive(Clone, Debug, PartialEq, Eq, Hash)]
51pub struct DiagnosticCode(String);
52
53impl DiagnosticCode {
54    /// Create a new code, validating against the registry grammar.
55    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    /// Borrow the underlying code as a `&str`.
62    pub fn as_str(&self) -> &str {
63        &self.0
64    }
65
66    /// Consume and return the underlying owned `String`.
67    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
97/// CLI-layer diagnostic codes (argument parsing, global flags).
98pub mod cli {
99    /// Command-line usage was invalid (parse error, missing subcommand,
100    /// invalid flag combination).
101    pub const USAGE_INVALID: &str = "cli.usage.invalid";
102    /// `--help` was requested.
103    pub const HELP: &str = "cli.help";
104    /// `--version` was requested.
105    pub const VERSION: &str = "cli.version";
106    /// Process was interrupted (`SIGINT` / `SIGTERM`).
107    pub const INTERRUPTED: &str = "cli.interrupted";
108    /// Catch-all for failures that do not fit a more specific domain.
109    pub const UNKNOWN: &str = "cli.unknown";
110
111    pub(crate) const ALL: &[&str] = &[USAGE_INVALID, HELP, VERSION, INTERRUPTED, UNKNOWN];
112}
113
114/// `foundry-config` diagnostic codes.
115pub 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
122/// Compiler diagnostic codes (`foundry-compilers`, `forge`).
123pub 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
130/// Network / RPC diagnostic codes.
131pub mod network {
132    /// Generic RPC failure (transport, JSON-RPC error, connectivity).
133    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
140/// `foundry-wallets` diagnostic codes.
141pub 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
148/// `forge test` diagnostic codes.
149pub mod test {
150    pub const FAILED: &str = "test.failed";
151    pub const SETUP_FAILED: &str = "test.setup_failed";
152    /// Non-fatal advisory surfaced by a test suite.
153    pub const WARNING: &str = "test.warning";
154
155    pub(crate) const ALL: &[&str] = &[FAILED, SETUP_FAILED, WARNING];
156}
157
158/// `forge script` diagnostic codes.
159pub mod script {
160    pub const BROADCAST_FAILED: &str = "script.broadcast_failed";
161
162    pub(crate) const ALL: &[&str] = &[BROADCAST_FAILED];
163}
164
165/// `cast` diagnostic codes.
166pub 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
172/// `anvil` diagnostic codes.
173pub mod anvil {
174    pub const FORK_UNREACHABLE: &str = "anvil.fork.unreachable";
175
176    pub(crate) const ALL: &[&str] = &[FORK_UNREACHABLE];
177}
178
179/// `chisel` diagnostic codes.
180pub mod chisel {
181    pub const SESSION_INVALID: &str = "chisel.session.invalid";
182
183    pub(crate) const ALL: &[&str] = &[SESSION_INVALID];
184}
185
186/// All diagnostic codes declared in this crate.
187///
188/// Useful for repo-wide validation tests.
189pub 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",         // uppercase
223            ".cli.usage",        // leading dot
224            "cli.usage.",        // trailing dot
225            "cli..usage",        // empty segment
226            "1cli.usage",        // segment must start with [a-z]
227            "cli.1usage",        // segment must start with [a-z]
228            "cli.usage-invalid", // dash not allowed
229            "cli.usage invalid", // space not allowed
230        ] {
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}