awk_rs/
error.rs

1use std::fmt;
2use thiserror::Error;
3
4/// Location in source code for error reporting
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct SourceLocation {
7    pub line: usize,
8    pub column: usize,
9}
10
11impl SourceLocation {
12    pub fn new(line: usize, column: usize) -> Self {
13        Self { line, column }
14    }
15}
16
17impl fmt::Display for SourceLocation {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        write!(f, "line {}, column {}", self.line, self.column)
20    }
21}
22
23/// All error types for awk-rs
24#[derive(Error, Debug)]
25pub enum Error {
26    #[error("lexer error at {location}: {message}")]
27    Lexer {
28        message: String,
29        location: SourceLocation,
30    },
31
32    #[error("parser error at {location}: {message}")]
33    Parser {
34        message: String,
35        location: SourceLocation,
36    },
37
38    #[error("runtime error: {message}")]
39    Runtime { message: String },
40
41    #[error("runtime error at {location}: {message}")]
42    RuntimeWithLocation {
43        message: String,
44        location: SourceLocation,
45    },
46
47    #[error("I/O error: {0}")]
48    Io(#[from] std::io::Error),
49
50    #[error("regex error: {0}")]
51    Regex(#[from] regex::Error),
52}
53
54impl Error {
55    pub fn lexer(message: impl Into<String>, line: usize, column: usize) -> Self {
56        Self::Lexer {
57            message: message.into(),
58            location: SourceLocation::new(line, column),
59        }
60    }
61
62    pub fn parser(message: impl Into<String>, line: usize, column: usize) -> Self {
63        Self::Parser {
64            message: message.into(),
65            location: SourceLocation::new(line, column),
66        }
67    }
68
69    pub fn runtime(message: impl Into<String>) -> Self {
70        Self::Runtime {
71            message: message.into(),
72        }
73    }
74
75    pub fn runtime_at(message: impl Into<String>, line: usize, column: usize) -> Self {
76        Self::RuntimeWithLocation {
77            message: message.into(),
78            location: SourceLocation::new(line, column),
79        }
80    }
81}
82
83/// Result type alias for awk-rs operations
84pub type Result<T> = std::result::Result<T, Error>;
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_source_location() {
92        let loc = SourceLocation::new(10, 5);
93        assert_eq!(loc.line, 10);
94        assert_eq!(loc.column, 5);
95        assert_eq!(format!("{}", loc), "line 10, column 5");
96    }
97
98    #[test]
99    fn test_lexer_error() {
100        let err = Error::lexer("unexpected character", 1, 5);
101        assert!(matches!(err, Error::Lexer { .. }));
102        let msg = format!("{}", err);
103        assert!(msg.contains("lexer error"));
104        assert!(msg.contains("unexpected character"));
105    }
106
107    #[test]
108    fn test_parser_error() {
109        let err = Error::parser("expected expression", 2, 10);
110        assert!(matches!(err, Error::Parser { .. }));
111        let msg = format!("{}", err);
112        assert!(msg.contains("parser error"));
113    }
114
115    #[test]
116    fn test_runtime_error() {
117        let err = Error::runtime("division by zero");
118        assert!(matches!(err, Error::Runtime { .. }));
119        let msg = format!("{}", err);
120        assert!(msg.contains("runtime error"));
121        assert!(msg.contains("division by zero"));
122    }
123
124    #[test]
125    fn test_runtime_error_with_location() {
126        let err = Error::runtime_at("undefined variable", 5, 3);
127        assert!(matches!(err, Error::RuntimeWithLocation { .. }));
128        let msg = format!("{}", err);
129        assert!(msg.contains("runtime error"));
130        assert!(msg.contains("line 5"));
131    }
132
133    #[test]
134    fn test_io_error() {
135        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
136        let err: Error = io_err.into();
137        assert!(matches!(err, Error::Io(_)));
138        let msg = format!("{}", err);
139        assert!(msg.contains("I/O error"));
140    }
141
142    #[test]
143    #[allow(clippy::invalid_regex)]
144    fn test_regex_error() {
145        let re_err = regex::Regex::new("[invalid").unwrap_err();
146        let err: Error = re_err.into();
147        assert!(matches!(err, Error::Regex(_)));
148        let msg = format!("{}", err);
149        assert!(msg.contains("regex error"));
150    }
151}