Skip to main content

use_php_error/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7macro_rules! diagnostic_text_newtype {
8    ($name:ident) => {
9        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10        pub struct $name(String);
11
12        impl $name {
13            pub fn new(input: &str) -> Result<Self, PhpDiagnosticError> {
14                let trimmed = input.trim();
15                if trimmed.is_empty() {
16                    Err(PhpDiagnosticError::Empty)
17                } else {
18                    Ok(Self(trimmed.to_string()))
19                }
20            }
21
22            pub fn as_str(&self) -> &str {
23                &self.0
24            }
25        }
26
27        impl fmt::Display for $name {
28            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
29                formatter.write_str(self.as_str())
30            }
31        }
32    };
33}
34
35diagnostic_text_newtype!(DiagnosticMessage);
36diagnostic_text_newtype!(DiagnosticSource);
37
38/// PHP error level metadata.
39#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub enum PhpErrorLevel {
41    Error,
42    Warning,
43    Parse,
44    Notice,
45    CoreError,
46    CoreWarning,
47    CompileError,
48    CompileWarning,
49    UserError,
50    UserWarning,
51    UserNotice,
52    Deprecated,
53    UserDeprecated,
54    RecoverableError,
55}
56
57impl PhpErrorLevel {
58    pub const fn as_str(self) -> &'static str {
59        match self {
60            Self::Error => "E_ERROR",
61            Self::Warning => "E_WARNING",
62            Self::Parse => "E_PARSE",
63            Self::Notice => "E_NOTICE",
64            Self::CoreError => "E_CORE_ERROR",
65            Self::CoreWarning => "E_CORE_WARNING",
66            Self::CompileError => "E_COMPILE_ERROR",
67            Self::CompileWarning => "E_COMPILE_WARNING",
68            Self::UserError => "E_USER_ERROR",
69            Self::UserWarning => "E_USER_WARNING",
70            Self::UserNotice => "E_USER_NOTICE",
71            Self::Deprecated => "E_DEPRECATED",
72            Self::UserDeprecated => "E_USER_DEPRECATED",
73            Self::RecoverableError => "E_RECOVERABLE_ERROR",
74        }
75    }
76
77    pub const fn severity(self) -> PhpSeverity {
78        match self {
79            Self::Error
80            | Self::Parse
81            | Self::CoreError
82            | Self::CompileError
83            | Self::UserError
84            | Self::RecoverableError => PhpSeverity::Error,
85            Self::Warning | Self::CoreWarning | Self::CompileWarning | Self::UserWarning => {
86                PhpSeverity::Warning
87            },
88            Self::Deprecated | Self::UserDeprecated => PhpSeverity::Deprecated,
89            Self::Notice | Self::UserNotice => PhpSeverity::Info,
90        }
91    }
92}
93
94impl fmt::Display for PhpErrorLevel {
95    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96        formatter.write_str(self.as_str())
97    }
98}
99
100/// Severity metadata for PHP diagnostics.
101#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
102pub enum PhpSeverity {
103    Error,
104    Warning,
105    Info,
106    Deprecated,
107}
108
109impl PhpSeverity {
110    pub const fn as_str(self) -> &'static str {
111        match self {
112            Self::Error => "error",
113            Self::Warning => "warning",
114            Self::Info => "info",
115            Self::Deprecated => "deprecated",
116        }
117    }
118}
119
120impl fmt::Display for PhpSeverity {
121    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122        formatter.write_str(self.as_str())
123    }
124}
125
126/// Broad PHP diagnostic category metadata.
127#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
128pub enum PhpErrorKind {
129    Runtime,
130    Parse,
131    Compile,
132    Core,
133    User,
134    Deprecation,
135    Unknown,
136}
137
138impl PhpErrorKind {
139    pub const fn as_str(self) -> &'static str {
140        match self {
141            Self::Runtime => "runtime",
142            Self::Parse => "parse",
143            Self::Compile => "compile",
144            Self::Core => "core",
145            Self::User => "user",
146            Self::Deprecation => "deprecation",
147            Self::Unknown => "unknown",
148        }
149    }
150}
151
152impl fmt::Display for PhpErrorKind {
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        formatter.write_str(self.as_str())
155    }
156}
157
158impl FromStr for PhpErrorKind {
159    type Err = PhpDiagnosticError;
160
161    fn from_str(input: &str) -> Result<Self, Self::Err> {
162        match input.trim().to_ascii_lowercase().as_str() {
163            "runtime" => Ok(Self::Runtime),
164            "parse" => Ok(Self::Parse),
165            "compile" => Ok(Self::Compile),
166            "core" => Ok(Self::Core),
167            "user" => Ok(Self::User),
168            "deprecation" | "deprecated" => Ok(Self::Deprecation),
169            "unknown" => Ok(Self::Unknown),
170            "" => Err(PhpDiagnosticError::Empty),
171            _ => Err(PhpDiagnosticError::UnknownLabel),
172        }
173    }
174}
175
176/// PHP diagnostic metadata.
177#[derive(Clone, Debug, Eq, PartialEq)]
178pub struct PhpDiagnostic {
179    kind: PhpErrorKind,
180    severity: PhpSeverity,
181    message: DiagnosticMessage,
182    source: Option<DiagnosticSource>,
183}
184
185impl PhpDiagnostic {
186    pub const fn new(
187        kind: PhpErrorKind,
188        severity: PhpSeverity,
189        message: DiagnosticMessage,
190    ) -> Self {
191        Self {
192            kind,
193            severity,
194            message,
195            source: None,
196        }
197    }
198
199    pub fn with_source(mut self, source: DiagnosticSource) -> Self {
200        self.source = Some(source);
201        self
202    }
203
204    pub const fn kind(&self) -> PhpErrorKind {
205        self.kind
206    }
207
208    pub const fn severity(&self) -> PhpSeverity {
209        self.severity
210    }
211
212    pub const fn message(&self) -> &DiagnosticMessage {
213        &self.message
214    }
215
216    pub const fn source(&self) -> Option<&DiagnosticSource> {
217        self.source.as_ref()
218    }
219}
220
221/// Error returned when PHP diagnostic metadata is invalid.
222#[derive(Clone, Copy, Debug, Eq, PartialEq)]
223pub enum PhpDiagnosticError {
224    Empty,
225    UnknownLabel,
226}
227
228impl fmt::Display for PhpDiagnosticError {
229    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
230        match self {
231            Self::Empty => formatter.write_str("PHP diagnostic metadata cannot be empty"),
232            Self::UnknownLabel => formatter.write_str("unknown PHP diagnostic metadata label"),
233        }
234    }
235}
236
237impl Error for PhpDiagnosticError {}
238
239#[cfg(test)]
240mod tests {
241    use super::{
242        DiagnosticMessage, DiagnosticSource, PhpDiagnostic, PhpDiagnosticError, PhpErrorKind,
243        PhpErrorLevel, PhpSeverity,
244    };
245
246    #[test]
247    fn maps_error_level_to_severity() {
248        assert_eq!(PhpErrorLevel::UserWarning.severity(), PhpSeverity::Warning);
249        assert_eq!(
250            PhpErrorLevel::Deprecated.severity(),
251            PhpSeverity::Deprecated
252        );
253    }
254
255    #[test]
256    fn builds_diagnostic_metadata() -> Result<(), PhpDiagnosticError> {
257        let diagnostic = PhpDiagnostic::new(
258            PhpErrorKind::Runtime,
259            PhpSeverity::Error,
260            DiagnosticMessage::new("Undefined variable")?,
261        )
262        .with_source(DiagnosticSource::new("example.php:10")?);
263
264        assert_eq!(diagnostic.severity(), PhpSeverity::Error);
265        assert_eq!(
266            diagnostic.source().expect("source").as_str(),
267            "example.php:10"
268        );
269        Ok(())
270    }
271}