@@ -5,7 +5,7 @@ use fluent_bundle::concurrent::FluentBundle;
55use miette:: { LabeledSpan , miette} ;
66use pyo3:: exceptions:: { PyFileNotFoundError , PyTypeError , PyValueError } ;
77use pyo3:: prelude:: * ;
8- use pyo3:: types:: { PyDate , PyDict , PyInt , PyString } ;
8+ use pyo3:: types:: { PyDate , PyDict , PyInt , PyList , PyString } ;
99use std:: fs;
1010use std:: path:: PathBuf ;
1111use unic_langid:: LanguageIdentifier ;
@@ -14,13 +14,218 @@ use pyo3::create_exception;
1414
1515create_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]
18214mod 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