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    /// Scaling optimization errors (when scaling feature is enabled)
121    #[cfg(feature = "scaling")]
122    #[error("Scaling error: {message}")]
123    Scaling {
124        message: String,
125        #[source]
126        source: Option<Box<dyn std::error::Error + Send + Sync>>,
127    },
128
129    /// General internal errors (should not occur in normal operation)
130    #[error("Internal error: {message}")]
131    Internal {
132        message: String,
133        location: Option<String>,
134    },
135}
136
137impl ScribeError {
138    /// Create a new I/O error with context
139    pub fn io<S: Into<String>>(message: S, source: io::Error) -> Self {
140        Self::Io {
141            message: message.into(),
142            source,
143        }
144    }
145
146    /// Create a new path error with context
147    pub fn path<S: Into<String>, P: Into<PathBuf>>(message: S, path: P) -> Self {
148        Self::Path {
149            message: message.into(),
150            path: path.into(),
151            source: None,
152        }
153    }
154
155    /// Create a new path error with source error
156    pub fn path_with_source<S: Into<String>, P: Into<PathBuf>>(
157        message: S,
158        path: P,
159        source: io::Error,
160    ) -> Self {
161        Self::Path {
162            message: message.into(),
163            path: path.into(),
164            source: Some(source),
165        }
166    }
167
168    /// Create a new git error
169    pub fn git<S: Into<String>>(message: S) -> Self {
170        Self::Git {
171            message: message.into(),
172            source: None,
173        }
174    }
175
176    /// Create a new configuration error
177    pub fn config<S: Into<String>>(message: S) -> Self {
178        Self::Config {
179            message: message.into(),
180            field: None,
181        }
182    }
183
184    /// Create a new configuration error with field context
185    pub fn config_field<S: Into<String>, F: Into<String>>(message: S, field: F) -> Self {
186        Self::Config {
187            message: message.into(),
188            field: Some(field.into()),
189        }
190    }
191
192    /// Create a new analysis error
193    pub fn analysis<S: Into<String>, P: Into<PathBuf>>(message: S, file: P) -> Self {
194        Self::Analysis {
195            message: message.into(),
196            file: file.into(),
197            source: None,
198        }
199    }
200
201    /// Create a new scoring error
202    pub fn scoring<S: Into<String>>(message: S) -> Self {
203        Self::Scoring {
204            message: message.into(),
205            context: None,
206        }
207    }
208
209    /// Create a new scoring error with context
210    pub fn scoring_with_context<S: Into<String>, C: Into<String>>(message: S, context: C) -> Self {
211        Self::Scoring {
212            message: message.into(),
213            context: Some(context.into()),
214        }
215    }
216
217    /// Create a new graph computation error
218    pub fn graph<S: Into<String>>(message: S) -> Self {
219        Self::Graph {
220            message: message.into(),
221            details: None,
222        }
223    }
224
225    /// Create a new pattern error
226    pub fn pattern<S: Into<String>, P: Into<String>>(message: S, pattern: P) -> Self {
227        Self::Pattern {
228            message: message.into(),
229            pattern: pattern.into(),
230            source: None,
231        }
232    }
233
234    /// Create a new resource limit error
235    pub fn resource_limit<S: Into<String>>(message: S, limit: u64, actual: u64) -> Self {
236        Self::ResourceLimit {
237            message: message.into(),
238            limit,
239            actual,
240        }
241    }
242
243    /// Create a new invalid operation error
244    pub fn invalid_operation<S: Into<String>, O: Into<String>>(message: S, operation: O) -> Self {
245        Self::InvalidOperation {
246            message: message.into(),
247            operation: operation.into(),
248        }
249    }
250
251    /// Create a new internal error
252    pub fn internal<S: Into<String>>(message: S) -> Self {
253        Self::Internal {
254            message: message.into(),
255            location: None,
256        }
257    }
258
259    /// Create a new parse error
260    pub fn parse<S: Into<String>>(message: S) -> Self {
261        Self::Parse {
262            message: message.into(),
263            file: None,
264            source: None,
265        }
266    }
267
268    /// Create a new parse error with file context
269    pub fn parse_file<S: Into<String>, P: Into<PathBuf>>(message: S, file: P) -> Self {
270        Self::Parse {
271            message: message.into(),
272            file: Some(file.into()),
273            source: None,
274        }
275    }
276
277    /// Create a new parse error with source error
278    pub fn parse_with_source<S: Into<String>>(
279        message: S,
280        source: Box<dyn std::error::Error + Send + Sync>,
281    ) -> Self {
282        Self::Parse {
283            message: message.into(),
284            file: None,
285            source: Some(source),
286        }
287    }
288
289    /// Create a new scaling error (when scaling feature is enabled)
290    #[cfg(feature = "scaling")]
291    pub fn scaling<S: Into<String>>(message: S) -> Self {
292        Self::Scaling {
293            message: message.into(),
294            source: None,
295        }
296    }
297
298    /// Create a new scaling error with source (when scaling feature is enabled)
299    #[cfg(feature = "scaling")]
300    pub fn scaling_with_source<S: Into<String>>(
301        message: S,
302        source: Box<dyn std::error::Error + Send + Sync>,
303    ) -> Self {
304        Self::Scaling {
305            message: message.into(),
306            source: Some(source),
307        }
308    }
309
310    /// Create a new internal error with location
311    pub fn internal_with_location<S: Into<String>, L: Into<String>>(message: S, location: L) -> Self {
312        Self::Internal {
313            message: message.into(),
314            location: Some(location.into()),
315        }
316    }
317}
318
319impl From<io::Error> for ScribeError {
320    fn from(error: io::Error) -> Self {
321        Self::io("I/O operation failed", error)
322    }
323}
324
325impl From<serde_json::Error> for ScribeError {
326    fn from(error: serde_json::Error) -> Self {
327        Self::Serialization {
328            message: "JSON serialization failed".to_string(),
329            source: Some(Box::new(error)),
330        }
331    }
332}
333
334impl From<globset::Error> for ScribeError {
335    fn from(error: globset::Error) -> Self {
336        Self::Pattern {
337            message: "Glob pattern compilation failed".to_string(),
338            pattern: "unknown".to_string(),
339            source: Some(Box::new(error)),
340        }
341    }
342}
343
344impl From<ignore::Error> for ScribeError {
345    fn from(error: ignore::Error) -> Self {
346        Self::Pattern {
347            message: "Ignore pattern error".to_string(),
348            pattern: "unknown".to_string(),
349            source: Some(Box::new(error)),
350        }
351    }
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357    use std::path::Path;
358
359    #[test]
360    fn test_error_creation() {
361        let err = ScribeError::path("Test path error", Path::new("/test/path"));
362        assert!(err.to_string().contains("Test path error"));
363        assert!(err.to_string().contains("/test/path"));
364    }
365
366    #[test]
367    fn test_io_error_conversion() {
368        let io_err = io::Error::new(io::ErrorKind::NotFound, "File not found");
369        let scribe_err = ScribeError::from(io_err);
370        match scribe_err {
371            ScribeError::Io { message, .. } => {
372                assert_eq!(message, "I/O operation failed");
373            }
374            _ => panic!("Expected Io error variant"),
375        }
376    }
377
378    #[test]
379    fn test_resource_limit_error() {
380        let err = ScribeError::resource_limit("File too large", 1000, 2000);
381        let msg = err.to_string();
382        assert!(msg.contains("limit: 1000"));
383        assert!(msg.contains("actual: 2000"));
384    }
385}