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 { message: String, operation: String },
107
108    /// Parse errors (AST parsing, tree-sitter failures)
109    #[error("Parse error: {message} (file: {file:?})")]
110    Parse {
111        message: String,
112        file: Option<PathBuf>,
113        #[source]
114        source: Option<Box<dyn std::error::Error + Send + Sync>>,
115    },
116
117    /// Tokenization errors (tiktoken integration, encoding issues)
118    #[error("Tokenization error: {message}")]
119    Tokenization {
120        message: String,
121        #[source]
122        source: Option<Box<dyn std::error::Error + Send + Sync>>,
123    },
124
125    /// Scaling optimization errors (when scaling feature is enabled)
126    #[cfg(feature = "scaling")]
127    #[error("Scaling error: {message}")]
128    Scaling {
129        message: String,
130        #[source]
131        source: Option<Box<dyn std::error::Error + Send + Sync>>,
132    },
133
134    /// General internal errors (should not occur in normal operation)
135    #[error("Internal error: {message}")]
136    Internal {
137        message: String,
138        location: Option<String>,
139    },
140}
141
142impl ScribeError {
143    /// Create a new I/O error with context
144    pub fn io<S: Into<String>>(message: S, source: io::Error) -> Self {
145        Self::Io {
146            message: message.into(),
147            source,
148        }
149    }
150
151    /// Create a new path error with context
152    pub fn path<S: Into<String>, P: Into<PathBuf>>(message: S, path: P) -> Self {
153        Self::Path {
154            message: message.into(),
155            path: path.into(),
156            source: None,
157        }
158    }
159
160    /// Create a new path error with source error
161    pub fn path_with_source<S: Into<String>, P: Into<PathBuf>>(
162        message: S,
163        path: P,
164        source: io::Error,
165    ) -> Self {
166        Self::Path {
167            message: message.into(),
168            path: path.into(),
169            source: Some(source),
170        }
171    }
172
173    /// Create a new git error
174    pub fn git<S: Into<String>>(message: S) -> Self {
175        Self::Git {
176            message: message.into(),
177            source: None,
178        }
179    }
180
181    /// Create a new configuration error
182    pub fn config<S: Into<String>>(message: S) -> Self {
183        Self::Config {
184            message: message.into(),
185            field: None,
186        }
187    }
188
189    /// Create a new configuration error with field context
190    pub fn config_field<S: Into<String>, F: Into<String>>(message: S, field: F) -> Self {
191        Self::Config {
192            message: message.into(),
193            field: Some(field.into()),
194        }
195    }
196
197    /// Create a new analysis error
198    pub fn analysis<S: Into<String>, P: Into<PathBuf>>(message: S, file: P) -> Self {
199        Self::Analysis {
200            message: message.into(),
201            file: file.into(),
202            source: None,
203        }
204    }
205
206    /// Create a new scoring error
207    pub fn scoring<S: Into<String>>(message: S) -> Self {
208        Self::Scoring {
209            message: message.into(),
210            context: None,
211        }
212    }
213
214    /// Create a new scoring error with context
215    pub fn scoring_with_context<S: Into<String>, C: Into<String>>(message: S, context: C) -> Self {
216        Self::Scoring {
217            message: message.into(),
218            context: Some(context.into()),
219        }
220    }
221
222    /// Create a new graph computation error
223    pub fn graph<S: Into<String>>(message: S) -> Self {
224        Self::Graph {
225            message: message.into(),
226            details: None,
227        }
228    }
229
230    /// Create a new pattern error
231    pub fn pattern<S: Into<String>, P: Into<String>>(message: S, pattern: P) -> Self {
232        Self::Pattern {
233            message: message.into(),
234            pattern: pattern.into(),
235            source: None,
236        }
237    }
238
239    /// Create a new resource limit error
240    pub fn resource_limit<S: Into<String>>(message: S, limit: u64, actual: u64) -> Self {
241        Self::ResourceLimit {
242            message: message.into(),
243            limit,
244            actual,
245        }
246    }
247
248    /// Create a new invalid operation error
249    pub fn invalid_operation<S: Into<String>, O: Into<String>>(message: S, operation: O) -> Self {
250        Self::InvalidOperation {
251            message: message.into(),
252            operation: operation.into(),
253        }
254    }
255
256    /// Create a new internal error
257    pub fn internal<S: Into<String>>(message: S) -> Self {
258        Self::Internal {
259            message: message.into(),
260            location: None,
261        }
262    }
263
264    /// Create a new parse error
265    pub fn parse<S: Into<String>>(message: S) -> Self {
266        Self::Parse {
267            message: message.into(),
268            file: None,
269            source: None,
270        }
271    }
272
273    /// Create a new parse error with file context
274    pub fn parse_file<S: Into<String>, P: Into<PathBuf>>(message: S, file: P) -> Self {
275        Self::Parse {
276            message: message.into(),
277            file: Some(file.into()),
278            source: None,
279        }
280    }
281
282    /// Create a new parse error with source error
283    pub fn parse_with_source<S: Into<String>>(
284        message: S,
285        source: Box<dyn std::error::Error + Send + Sync>,
286    ) -> Self {
287        Self::Parse {
288            message: message.into(),
289            file: None,
290            source: Some(source),
291        }
292    }
293
294    /// Create a new tokenization error
295    pub fn tokenization<S: Into<String>>(message: S) -> Self {
296        Self::Tokenization {
297            message: message.into(),
298            source: None,
299        }
300    }
301
302    /// Create a new tokenization error with source
303    pub fn tokenization_with_source<S: Into<String>>(
304        message: S,
305        source: Box<dyn std::error::Error + Send + Sync>,
306    ) -> Self {
307        Self::Tokenization {
308            message: message.into(),
309            source: Some(source),
310        }
311    }
312
313    /// Create a new scaling error (when scaling feature is enabled)
314    #[cfg(feature = "scaling")]
315    pub fn scaling<S: Into<String>>(message: S) -> Self {
316        Self::Scaling {
317            message: message.into(),
318            source: None,
319        }
320    }
321
322    /// Create a new scaling error with source (when scaling feature is enabled)
323    #[cfg(feature = "scaling")]
324    pub fn scaling_with_source<S: Into<String>>(
325        message: S,
326        source: Box<dyn std::error::Error + Send + Sync>,
327    ) -> Self {
328        Self::Scaling {
329            message: message.into(),
330            source: Some(source),
331        }
332    }
333
334    /// Create a new internal error with location
335    pub fn internal_with_location<S: Into<String>, L: Into<String>>(
336        message: S,
337        location: L,
338    ) -> 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}