Skip to main content

perl_lexer/
error.rs

1//! Error types for the Perl lexer
2
3use thiserror::Error;
4
5/// Result type for lexer operations
6pub type Result<T> = std::result::Result<T, LexerError>;
7
8/// Errors that can occur during lexing
9#[derive(Debug, Clone, Error)]
10pub enum LexerError {
11    /// Unterminated string literal
12    #[error("Unterminated string literal starting at position {position}")]
13    UnterminatedString {
14        /// Byte offset where the string literal started
15        position: usize,
16    },
17
18    /// Unterminated regex
19    #[error("Unterminated regex starting at position {position}")]
20    UnterminatedRegex {
21        /// Byte offset where the regex started
22        position: usize,
23    },
24
25    /// Invalid escape sequence
26    #[error("Invalid escape sequence '\\{char}' at position {position}")]
27    InvalidEscape {
28        /// The character following the backslash
29        char: char,
30        /// Byte offset of the backslash
31        position: usize,
32    },
33
34    /// Invalid numeric literal
35    #[error("Invalid numeric literal at position {position}: {reason}")]
36    InvalidNumber {
37        /// Byte offset of the number token
38        position: usize,
39        /// Human-readable description of why the number is invalid
40        reason: String,
41    },
42
43    /// Unexpected character
44    #[error("Unexpected character '{char}' at position {position}")]
45    UnexpectedChar {
46        /// The character that was not expected
47        char: char,
48        /// Byte offset of the unexpected character
49        position: usize,
50    },
51
52    /// Invalid UTF-8
53    #[error("Invalid UTF-8 at position {position}")]
54    InvalidUtf8 {
55        /// Byte offset of the invalid byte sequence
56        position: usize,
57    },
58
59    /// Heredoc error
60    #[error("Heredoc error at position {position}: {reason}")]
61    HeredocError {
62        /// Byte offset of the heredoc marker
63        position: usize,
64        /// Human-readable description of the heredoc problem
65        reason: String,
66    },
67
68    /// Generic error
69    #[error("{0}")]
70    Other(String),
71}
72
73impl LexerError {
74    /// Get the position where the error occurred
75    pub fn position(&self) -> Option<usize> {
76        match self {
77            LexerError::UnterminatedString { position }
78            | LexerError::UnterminatedRegex { position }
79            | LexerError::InvalidEscape { position, .. }
80            | LexerError::InvalidNumber { position, .. }
81            | LexerError::UnexpectedChar { position, .. }
82            | LexerError::InvalidUtf8 { position }
83            | LexerError::HeredocError { position, .. } => Some(*position),
84            LexerError::Other(_) => None,
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::LexerError;
92
93    // --- variants with positions ---
94
95    #[test]
96    fn lexer_error_unterminated_string_returns_position() -> Result<(), Box<dyn std::error::Error>>
97    {
98        let err = LexerError::UnterminatedString { position: 7 };
99        assert_eq!(err.position(), Some(7));
100        Ok(())
101    }
102
103    #[test]
104    fn lexer_error_unterminated_regex_returns_position() -> Result<(), Box<dyn std::error::Error>> {
105        let err = LexerError::UnterminatedRegex { position: 42 };
106        assert_eq!(err.position(), Some(42));
107        Ok(())
108    }
109
110    #[test]
111    fn lexer_error_invalid_escape_returns_position() -> Result<(), Box<dyn std::error::Error>> {
112        let err = LexerError::InvalidEscape { char: 'z', position: 15 };
113        assert_eq!(err.position(), Some(15));
114        Ok(())
115    }
116
117    #[test]
118    fn lexer_error_invalid_number_returns_position() -> Result<(), Box<dyn std::error::Error>> {
119        let err = LexerError::InvalidNumber { position: 3, reason: "bad digit".into() };
120        assert_eq!(err.position(), Some(3));
121        Ok(())
122    }
123
124    #[test]
125    fn lexer_error_unexpected_char_returns_position() -> Result<(), Box<dyn std::error::Error>> {
126        let err = LexerError::UnexpectedChar { char: '@', position: 99 };
127        assert_eq!(err.position(), Some(99));
128        Ok(())
129    }
130
131    #[test]
132    fn lexer_error_invalid_utf8_returns_position() -> Result<(), Box<dyn std::error::Error>> {
133        let err = LexerError::InvalidUtf8 { position: 0 };
134        assert_eq!(err.position(), Some(0));
135        Ok(())
136    }
137
138    #[test]
139    fn lexer_error_heredoc_error_returns_position() -> Result<(), Box<dyn std::error::Error>> {
140        let err = LexerError::HeredocError { position: 256, reason: "no terminator".into() };
141        assert_eq!(err.position(), Some(256));
142        Ok(())
143    }
144
145    // --- variant without a position ---
146
147    #[test]
148    fn lexer_error_other_returns_none() -> Result<(), Box<dyn std::error::Error>> {
149        let err = LexerError::Other("something went wrong".into());
150        assert_eq!(err.position(), None);
151        Ok(())
152    }
153
154    // --- edge values ---
155
156    #[test]
157    fn lexer_error_position_zero_is_returned_correctly() -> Result<(), Box<dyn std::error::Error>> {
158        let err = LexerError::UnterminatedString { position: 0 };
159        assert_eq!(err.position(), Some(0));
160        Ok(())
161    }
162
163    #[test]
164    fn lexer_error_position_usize_max_is_returned_correctly()
165    -> Result<(), Box<dyn std::error::Error>> {
166        let err = LexerError::UnterminatedString { position: usize::MAX };
167        assert_eq!(err.position(), Some(usize::MAX));
168        Ok(())
169    }
170}