1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
use crate::parser::Rule; // Import Rule from the parser module
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CelParserError {
#[error("Pest parsing error: {0}")]
PestError(#[from] Box<pest::error::Error<Rule>>), // Boxed to avoid large enum variant
#[error("Invalid integer literal: '{0}' ({1})")]
InvalidIntegerLiteral(String, std::num::ParseIntError),
#[error("Invalid unsigned integer literal: '{0}' ({1})")]
InvalidUintLiteral(String, std::num::ParseIntError),
#[error("Invalid float literal: '{0}' ({1})")]
InvalidFloatLiteral(String, std::num::ParseFloatError),
#[error("Invalid escape sequence in string/bytes literal: '\\{0}'")]
InvalidEscapeSequence(String),
#[error("Incomplete escape sequence: '{0}'")]
IncompleteEscapeSequence(String),
#[error(
"Invalid Unicode escape sequence (must be U+0000 to U+10FFFF, excluding surrogates): '{0}'"
)]
InvalidUnicodeEscape(String),
#[error("Invalid byte sequence in string literal (not valid UTF-8)")]
InvalidUtf8String(#[from] std::string::FromUtf8Error),
#[error("Internal parser error: {0}")]
InternalError(String), // For unexpected states
}
// Helper to convert Pest errors, wrapping in Box
impl From<pest::error::Error<Rule>> for CelParserError {
fn from(err: pest::error::Error<Rule>) -> Self {
CelParserError::PestError(Box::new(err))
}
}
impl CelParserError {
/// Returns the line and column number where the error occurred, if available.
///
/// This is useful for providing detailed feedback to users about syntax errors.
/// The location is 1-indexed (line 1, column 1).
///
/// # Example
/// ```
/// use rust_cel_parser::parse_cel_program;
///
/// let result = parse_cel_program("1 + & 2"); // Invalid operator
/// if let Err(e) = result {
/// if let Some((line, col)) = e.location() {
/// println!("Error at line {}, column {}: {}", line, col, e);
/// assert_eq!(line, 1);
/// assert_eq!(col, 5);
/// }
/// }
/// ```
pub fn location(&self) -> Option<(usize, usize)> {
match self {
CelParserError::PestError(e) => match e.line_col {
pest::error::LineColLocation::Pos(pos) => Some(pos),
pest::error::LineColLocation::Span(start, _) => Some(start),
},
_ => None, // Location information is only available for Pest-level errors.
}
}
}
#[cfg(test)]
mod tests {
// We need to import the parser function to generate an error
use crate::parser::parse_cel_program;
#[test]
fn test_error_location_invalid_operator() {
// This expression has a syntax error. The parser should fail
// when it sees the '&' which is not a valid operator.
// Input: "a && b & c"
// Col: 123456789
let invalid_expr = "a && b & c";
let result = parse_cel_program(invalid_expr);
// Ensure it failed
assert!(result.is_err());
// Get the error
let err = result.unwrap_err();
// Check the location using the new helper
let location = err.location();
assert!(
location.is_some(),
"Expected location information for a parse error"
);
let (line, col) = location.unwrap();
assert_eq!(line, 1, "Error should be on line 1");
// The error is *on* the invalid token here, because it's unexpected.
assert_eq!(col, 8, "Error should be at column 8 (the '&')");
}
#[test]
fn test_error_location_on_incomplete_expr() {
// The error is at the end of the input because the expression is incomplete.
// Pest reports this at the EOI (End of Input) position.
// Input: "1 + " (length 4)
// Col: 12345
let invalid_expr = "1 + ";
let result = parse_cel_program(invalid_expr);
assert!(result.is_err());
let err = result.unwrap_err();
let (line, col) = err.location().expect("Should have location info");
assert_eq!(line, 1);
// --- FIX ---
// The error is reported at the EOI, which is one column *after* the last character.
assert_eq!(
col, 5,
"Error should be at the end of the expression (col 5)"
);
}
#[test]
fn test_error_location_multiline() {
// The error is on a different line. Pest correctly tracks newlines.
// The `!` is on line 3. The last line is " : !" (length 4).
// The error is reported at the EOI on that line.
let invalid_expr = "a ||\n b ? c \n : !";
let result = parse_cel_program(invalid_expr);
assert!(result.is_err());
let err = result.unwrap_err();
let (line, col) = err.location().expect("Should have location info");
assert_eq!(line, 3, "Error should be on line 3");
// --- FIX ---
// The error is reported at the EOI for the incomplete unary op '!', which is column 5 on line 3.
assert_eq!(col, 5, "Error should be at EOI on line 3 (col 5)");
}
}