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