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