Skip to content

Commit aad1399

Browse files
Add structured error types for validation and formatting
Introduces three new PyClass types that provide detailed error information: - ParseErrorDetail: Captures parse errors with line/column information - ValidationError: Represents semantic validation errors (broken refs, cycles, etc.) - FormatError: Enhanced format error with context about variables and types Also adds byte_pos_to_line_col helper for converting byte positions to line/column coordinates for better error reporting. These types are not yet integrated into Bundle but are exported for future use.
1 parent 1c9ffd6 commit aad1399

File tree

1 file changed

+206
-1
lines changed

1 file changed

+206
-1
lines changed

src/lib.rs

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use fluent_bundle::concurrent::FluentBundle;
55
use miette::{LabeledSpan, miette};
66
use pyo3::exceptions::{PyFileNotFoundError, PyTypeError, PyValueError};
77
use pyo3::prelude::*;
8-
use pyo3::types::{PyDate, PyDict, PyInt, PyString};
8+
use pyo3::types::{PyDate, PyDict, PyInt, PyList, PyString};
99
use std::fs;
1010
use std::path::PathBuf;
1111
use unic_langid::LanguageIdentifier;
@@ -14,13 +14,218 @@ use pyo3::create_exception;
1414

1515
create_exception!(rustfluent, ParserError, pyo3::exceptions::PyException);
1616

17+
/// Helper function to convert byte position to line and column numbers
18+
fn byte_pos_to_line_col(source: &str, byte_pos: usize) -> (usize, usize) {
19+
let relevant = &source[..byte_pos.min(source.len())];
20+
let line = relevant.chars().filter(|&c| c == '\n').count() + 1;
21+
let col = relevant.len() - relevant.rfind('\n').map_or(0, |pos| pos + 1) + 1;
22+
(line, col)
23+
}
24+
25+
/// Represents a single parsing error with detailed location information
26+
#[pyclass]
27+
#[derive(Clone)]
28+
struct ParseErrorDetail {
29+
/// Human-readable error message
30+
#[pyo3(get)]
31+
message: String,
32+
33+
/// Line number where the error occurred (1-indexed)
34+
#[pyo3(get)]
35+
line: usize,
36+
37+
/// Column number where the error occurred (1-indexed)
38+
#[pyo3(get)]
39+
column: usize,
40+
41+
/// Byte position where the error starts (0-indexed)
42+
#[pyo3(get)]
43+
byte_start: usize,
44+
45+
/// Byte position where the error ends (0-indexed)
46+
#[pyo3(get)]
47+
byte_end: usize,
48+
49+
/// Optional file path where the error occurred
50+
#[pyo3(get)]
51+
filename: Option<String>,
52+
}
53+
54+
#[pymethods]
55+
impl ParseErrorDetail {
56+
fn __repr__(&self) -> String {
57+
format!(
58+
"ParseErrorDetail(message={:?}, line={}, column={}, byte_start={}, byte_end={})",
59+
self.message, self.line, self.column, self.byte_start, self.byte_end
60+
)
61+
}
62+
63+
fn __str__(&self) -> String {
64+
if let Some(ref filename) = self.filename {
65+
format!(
66+
"{}:{}:{}: {}",
67+
filename, self.line, self.column, self.message
68+
)
69+
} else {
70+
format!("{}:{}: {}", self.line, self.column, self.message)
71+
}
72+
}
73+
}
74+
75+
impl ParseErrorDetail {
76+
fn from_parser_error(
77+
error: fluent_syntax::parser::ParserError,
78+
source: &str,
79+
filename: Option<String>,
80+
) -> Self {
81+
let (line, column) = byte_pos_to_line_col(source, error.pos.start);
82+
Self {
83+
message: error.kind.to_string(),
84+
line,
85+
column,
86+
byte_start: error.pos.start,
87+
byte_end: error.pos.end,
88+
filename,
89+
}
90+
}
91+
}
92+
93+
/// Represents a validation error found during compile-time checking
94+
#[pyclass]
95+
#[derive(Clone)]
96+
struct ValidationError {
97+
#[pyo3(get)]
98+
error_type: String,
99+
#[pyo3(get)]
100+
message: String,
101+
#[pyo3(get)]
102+
message_id: Option<String>,
103+
#[pyo3(get)]
104+
reference: Option<String>,
105+
}
106+
107+
#[pymethods]
108+
impl ValidationError {
109+
fn __repr__(&self) -> String {
110+
format!(
111+
"ValidationError(type={:?}, message={:?}, message_id={:?})",
112+
self.error_type, self.message, self.message_id
113+
)
114+
}
115+
116+
fn __str__(&self) -> String {
117+
if let Some(ref msg_id) = self.message_id {
118+
format!("{} in '{}': {}", self.error_type, msg_id, self.message)
119+
} else {
120+
format!("{}: {}", self.error_type, self.message)
121+
}
122+
}
123+
}
124+
125+
/// Represents a format error during message formatting
126+
#[pyclass]
127+
#[derive(Clone)]
128+
struct FormatError {
129+
#[pyo3(get)]
130+
error_type: String,
131+
#[pyo3(get)]
132+
message: String,
133+
134+
// Enhanced context fields
135+
#[pyo3(get)]
136+
message_id: Option<String>, // Which message had the error
137+
138+
#[pyo3(get)]
139+
variable_name: Option<String>, // Which variable (if applicable)
140+
141+
#[pyo3(get)]
142+
expected_type: Option<String>, // What type was expected
143+
144+
#[pyo3(get)]
145+
actual_type: Option<String>, // What type was provided
146+
}
147+
148+
#[pymethods]
149+
impl FormatError {
150+
fn __repr__(&self) -> String {
151+
let mut parts = vec![
152+
format!("error_type={:?}", self.error_type),
153+
format!("message={:?}", self.message),
154+
];
155+
156+
if let Some(ref msg_id) = self.message_id {
157+
parts.push(format!("message_id={:?}", msg_id));
158+
}
159+
if let Some(ref var) = self.variable_name {
160+
parts.push(format!("variable_name={:?}", var));
161+
}
162+
if let Some(ref expected) = self.expected_type {
163+
parts.push(format!("expected_type={:?}", expected));
164+
}
165+
if let Some(ref actual) = self.actual_type {
166+
parts.push(format!("actual_type={:?}", actual));
167+
}
168+
169+
format!("FormatError({})", parts.join(", "))
170+
}
171+
172+
fn __str__(&self) -> String {
173+
let mut result = format!("{}: {}", self.error_type, self.message);
174+
175+
if let Some(ref msg_id) = self.message_id {
176+
result = format!("{} in '{}'", result, msg_id);
177+
}
178+
if let Some(ref var) = self.variable_name {
179+
result = format!("{} (variable: {})", result, var);
180+
}
181+
if self.expected_type.is_some() && self.actual_type.is_some() {
182+
result = format!(
183+
"{} (expected {}, got {})",
184+
result,
185+
self.expected_type.as_ref().unwrap(),
186+
self.actual_type.as_ref().unwrap()
187+
);
188+
}
189+
190+
result
191+
}
192+
}
193+
194+
impl FormatError {
195+
fn from_fluent_error(error: &fluent_bundle::FluentError) -> Self {
196+
use fluent_bundle::FluentError as BundleFluentError;
197+
let error_type = match error {
198+
BundleFluentError::Overriding { .. } => "Overriding",
199+
BundleFluentError::ParserError(_) => "ParserError",
200+
BundleFluentError::ResolverError(_) => "ResolverError",
201+
};
202+
Self {
203+
error_type: error_type.to_string(),
204+
message: error.to_string(),
205+
message_id: None,
206+
variable_name: None,
207+
expected_type: None,
208+
actual_type: None,
209+
}
210+
}
211+
}
212+
17213
#[pymodule]
18214
mod rustfluent {
19215
use super::*;
20216

21217
#[pymodule_export]
22218
use super::ParserError;
23219

220+
#[pymodule_export]
221+
use super::ParseErrorDetail;
222+
223+
#[pymodule_export]
224+
use super::ValidationError;
225+
226+
#[pymodule_export]
227+
use super::FormatError;
228+
24229
#[pyclass]
25230
struct Bundle {
26231
bundle: FluentBundle<FluentResource>,

0 commit comments

Comments
 (0)