scribe_core/
error.rs

1//! Error handling for the Scribe library.
2//! 
3//! Provides comprehensive error types with proper context and error chaining
4//! for all Scribe operations.
5
6use std::io;
7use std::path::PathBuf;
8use thiserror::Error;
9
10/// Type alias for Results using ScribeError
11pub type Result<T> = std::result::Result<T, ScribeError>;
12
13/// Comprehensive error type for all Scribe operations
14#[derive(Error, Debug)]
15pub enum ScribeError {
16    /// I/O related errors (file system operations)
17    #[error("I/O error: {message}")]
18    Io {
19        message: String,
20        #[source]
21        source: io::Error,
22    },
23
24    /// Path-related errors (invalid paths, path resolution issues)
25    #[error("Path error: {message} (path: {path:?})")]
26    Path {
27        message: String,
28        path: PathBuf,
29        #[source]
30        source: Option<io::Error>,
31    },
32
33    /// Git repository errors
34    #[error("Git error: {message}")]
35    Git {
36        message: String,
37        #[source]
38        source: Option<Box<dyn std::error::Error + Send + Sync>>,
39    },
40
41    /// Configuration errors (invalid settings, missing required config)
42    #[error("Configuration error: {message}")]
43    Config {
44        message: String,
45        field: Option<String>,
46    },
47
48    /// File analysis errors (parsing, language detection, etc.)
49    #[error("Analysis error: {message} (file: {file:?})")]
50    Analysis {
51        message: String,
52        file: PathBuf,
53        #[source]
54        source: Option<Box<dyn std::error::Error + Send + Sync>>,
55    },
56
57    /// Scoring and heuristic computation errors
58    #[error("Scoring error: {message}")]
59    Scoring {
60        message: String,
61        context: Option<String>,
62    },
63
64    /// Graph computation errors (centrality, dependency analysis)
65    #[error("Graph error: {message}")]
66    Graph {
67        message: String,
68        details: Option<String>,
69    },
70
71    /// Pattern matching errors (glob patterns, regex, etc.)
72    #[error("Pattern error: {message} (pattern: {pattern})")]
73    Pattern {
74        message: String,
75        pattern: String,
76        #[source]
77        source: Option<Box<dyn std::error::Error + Send + Sync>>,
78    },
79
80    /// Serialization/deserialization errors
81    #[error("Serialization error: {message}")]
82    Serialization {
83        message: String,
84        #[source]
85        source: Option<Box<dyn std::error::Error + Send + Sync>>,
86    },
87
88    /// Thread pool or concurrency errors
89    #[error("Concurrency error: {message}")]
90    Concurrency {
91        message: String,
92        #[source]
93        source: Option<Box<dyn std::error::Error + Send + Sync>>,
94    },
95
96    /// Resource limit exceeded (memory, time, file size)
97    #[error("Resource limit exceeded: {message} (limit: {limit}, actual: {actual})")]
98    ResourceLimit {
99        message: String,
100        limit: u64,
101        actual: u64,
102    },
103
104    /// Invalid input or operation
105    #[error("Invalid operation: {message}")]
106    InvalidOperation {
107        message: String,
108        operation: String,
109    },
110
111    /// Parse errors (AST parsing, tree-sitter failures)
112    #[error("Parse error: {message} (file: {file:?})")]
113    Parse {
114        message: String,
115        file: Option<PathBuf>,
116        #[source]
117        source: Option<Box<dyn std::error::Error + Send + Sync>>,
118    },
119
120    /// Tokenization errors (tiktoken integration, encoding issues)
121    #[error("Tokenization error: {message}")]
122    Tokenization {
123        message: String,
124        #[source]
125        source: Option<Box<dyn std::error::Error + Send + Sync>>,
126    },
127
128    /// Scaling optimization errors (when scaling feature is enabled)
129    #[cfg(feature = "scaling")]
130    #[error("Scaling error: {message}")]
131    Scaling {
132        message: String,
133        #[source]
134        source: Option<Box<dyn std::error::Error + Send + Sync>>,
135    },
136
137    /// General internal errors (should not occur in normal operation)
138    #[error("Internal error: {message}")]
139    Internal {
140        message: String,
141        location: Option<String>,
142    },
143}
144
145impl ScribeError {
146    /// Create a new I/O error with context
147    pub fn io<S: Into<String>>(message: S, source: io::Error) -> Self {
148        Self::Io {
149            message: message.into(),
150            source,
151        }
152    }
153
154    /// Create a new path error with context
155    pub fn path<S: Into<String>, P: Into<PathBuf>>(message: S, path: P) -> Self {
156        Self::Path {
157            message: message.into(),
158            path: path.into(),
159            source: None,
160        }
161    }
162
163    /// Create a new path error with source error
164    pub fn path_with_source<S: Into<String>, P: Into<PathBuf>>(
165        message: S,
166        path: P,
167        source: io::Error,
168    ) -> Self {
169        Self::Path {
170            message: message.into(),
171            path: path.into(),
172            source: Some(source),
173        }
174    }
175
176    /// Create a new git error
177    pub fn git<S: Into<String>>(message: S) -> Self {
178        Self::Git {
179            message: message.into(),
180            source: None,
181        }
182    }
183
184    /// Create a new configuration error
185    pub fn config<S: Into<String>>(message: S) -> Self {
186        Self::Config {
187            message: message.into(),
188            field: None,
189        }
190    }
191
192    /// Create a new configuration error with field context
193    pub fn config_field<S: Into<String>, F: Into<String>>(message: S, field: F) -> Self {
194        Self::Config {
195            message: message.into(),
196            field: Some(field.into()),
197        }
198    }
199
200    /// Create a new analysis error
201    pub fn analysis<S: Into<String>, P: Into<PathBuf>>(message: S, file: P) -> Self {
202        Self::Analysis {
203            message: message.into(),
204            file: file.into(),
205            source: None,
206        }
207    }
208
209    /// Create a new scoring error
210    pub fn scoring<S: Into<String>>(message: S) -> Self {
211        Self::Scoring {
212            message: message.into(),
213            context: None,
214        }
215    }
216
217    /// Create a new scoring error with context
218    pub fn scoring_with_context<S: Into<String>, C: Into<String>>(message: S, context: C) -> Self {
219        Self::Scoring {
220            message: message.into(),
221            context: Some(context.into()),
222        }
223    }
224
225    /// Create a new graph computation error
226    pub fn graph<S: Into<String>>(message: S) -> Self {
227        Self::Graph {
228            message: message.into(),
229            details: None,
230        }
231    }
232
233    /// Create a new pattern error
234    pub fn pattern<S: Into<String>, P: Into<String>>(message: S, pattern: P) -> Self {
235        Self::Pattern {
236            message: message.into(),
237            pattern: pattern.into(),
238            source: None,
239        }
240    }
241
242    /// Create a new resource limit error
243    pub fn resource_limit<S: Into<String>>(message: S, limit: u64, actual: u64) -> Self {
244        Self::ResourceLimit {
245            message: message.into(),
246            limit,
247            actual,
248        }
249    }
250
251    /// Create a new invalid operation error
252    pub fn invalid_operation<S: Into<String>, O: Into<String>>(message: S, operation: O) -> Self {
253        Self::InvalidOperation {
254            message: message.into(),
255            operation: operation.into(),
256        }
257    }
258
259    /// Create a new internal error
260    pub fn internal<S: Into<String>>(message: S) -> Self {
261        Self::Internal {
262            message: message.into(),
263            location: None,
264        }
265    }
266
267    /// Create a new parse error
268    pub fn parse<S: Into<String>>(message: S) -> Self {
269        Self::Parse {
270            message: message.into(),
271            file: None,
272            source: None,
273        }
274    }
275
276    /// Create a new parse error with file context
277    pub fn parse_file<S: Into<String>, P: Into<PathBuf>>(message: S, file: P) -> Self {
278        Self::Parse {
279            message: message.into(),
280            file: Some(file.into()),
281            source: None,
282        }
283    }
284
285    /// Create a new parse error with source error
286    pub fn parse_with_source<S: Into<String>>(
287        message: S,
288        source: Box<dyn std::error::Error + Send + Sync>,
289    ) -> Self {
290        Self::Parse {
291            message: message.into(),
292            file: None,
293            source: Some(source),
294        }
295    }
296
297    /// Create a new tokenization error
298    pub fn tokenization<S: Into<String>>(message: S) -> Self {
299        Self::Tokenization {
300            message: message.into(),
301            source: None,
302        }
303    }
304
305    /// Create a new tokenization error with source
306    pub fn tokenization_with_source<S: Into<String>>(
307        message: S,
308        source: Box<dyn std::error::Error + Send + Sync>,
309    ) -> Self {
310        Self::Tokenization {
311            message: message.into(),
312            source: Some(source),
313        }
314    }
315
316    /// Create a new scaling error (when scaling feature is enabled)
317    #[cfg(feature = "scaling")]
318    pub fn scaling<S: Into<String>>(message: S) -> Self {
319        Self::Scaling {
320            message: message.into(),
321            source: None,
322        }
323    }
324
325    /// Create a new scaling error with source (when scaling feature is enabled)
326    #[cfg(feature = "scaling")]
327    pub fn scaling_with_source<S: Into<String>>(
328        message: S,
329        source: Box<dyn std::error::Error + Send + Sync>,
330    ) -> Self {
331        Self::Scaling {
332            message: message.into(),
333            source: Some(source),
334        }
335    }
336
337    /// Create a new internal error with location
338    pub fn internal_with_location<S: Into<String>, L: Into<String>>(message: S, location: L) -> Self {
339        Self::Internal {
340            message: message.into(),
341            location: Some(location.into()),
342        }
343    }
344}
345
346impl From<io::Error> for ScribeError {
347    fn from(error: io::Error) -> Self {
348        Self::io("I/O operation failed", error)
349    }
350}
351
352impl From<serde_json::Error> for ScribeError {
353    fn from(error: serde_json::Error) -> Self {
354        Self::Serialization {
355            message: "JSON serialization failed".to_string(),
356            source: Some(Box::new(error)),
357        }
358    }
359}
360
361impl From<globset::Error> for ScribeError {
362    fn from(error: globset::Error) -> Self {
363        Self::Pattern {
364            message: "Glob pattern compilation failed".to_string(),
365            pattern: "unknown".to_string(),
366            source: Some(Box::new(error)),
367        }
368    }
369}
370
371impl From<ignore::Error> for ScribeError {
372    fn from(error: ignore::Error) -> Self {
373        Self::Pattern {
374            message: "Ignore pattern error".to_string(),
375            pattern: "unknown".to_string(),
376            source: Some(Box::new(error)),
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    use super::*;
384    use std::path::Path;
385
386    #[test]
387    fn test_error_creation() {
388        let err = ScribeError::path("Test path error", Path::new("/test/path"));
389        assert!(err.to_string().contains("Test path error"));
390        assert!(err.to_string().contains("/test/path"));
391    }
392
393    #[test]
394    fn test_io_error_conversion() {
395        let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
396        let scribe_err = ScribeError::from(io_err);
397        match scribe_err {
398            ScribeError::Io { message, .. } => {
399                assert_eq!(message, "I/O operation failed");
400            }
401            _ => panic!("Expected Io error variant"),
402        }
403    }
404
405    #[test]
406    fn test_resource_limit_error() {
407        let err = ScribeError::resource_limit("File too large", 1000, 2000);
408        let msg = err.to_string();
409        assert!(msg.contains("limit: 1000"));
410        assert!(msg.contains("actual: 2000"));
411    }
412}