Skip to main content

thread_services/
error.rs

1// SPDX-FileCopyrightText: 2025 Knitli Inc. <knitli@knit.li>
2// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5//! # Service Layer Error Types
6//!
7//! Comprehensive error handling for the Thread service layer using Tower's BoxError pattern
8//! for unified error handling and improved performance. This design enables composable
9//! service layers while maintaining excellent error context and recovery capabilities.
10
11use std::borrow::Cow;
12use std::fmt;
13use std::path::PathBuf;
14use thiserror::Error;
15
16// Import Tower's BoxError pattern for unified error handling
17#[cfg(feature = "tower-services")]
18pub use tower::BoxError;
19
20#[cfg(not(feature = "tower-services"))]
21pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
22
23// Conditionally import thread dependencies when available
24#[cfg(feature = "ast-grep-backend")]
25use thread_ast_engine::tree_sitter::TSParseError;
26
27#[cfg(feature = "ast-grep-backend")]
28use thread_language::SupportLangErr;
29
30#[cfg(all(feature = "matching", feature = "ast-grep-backend"))]
31use thread_ast_engine::PatternError;
32
33/// Service result type using Tower's BoxError pattern for unified error handling
34pub type ServiceResult<T> = Result<T, BoxError>;
35
36/// Main error type for service layer operations with optimized string handling
37#[derive(Error, Debug)]
38pub enum ServiceError {
39    /// Errors related to parsing source code
40    #[error("Parse error: {0}")]
41    Parse(#[from] ParseError),
42
43    /// Errors related to code analysis and pattern matching
44    #[error("Analysis error: {0}")]
45    Analysis(#[from] AnalysisError),
46
47    /// Errors related to storage operations
48    #[error("Storage error: {0}")]
49    Storage(#[from] StorageError),
50
51    /// Errors related to execution context
52    #[error("Execution error: {message}")]
53    Execution { message: Cow<'static, str> },
54
55    /// I/O related errors
56    #[error("IO error: {0}")]
57    Io(#[from] std::io::Error),
58
59    /// Configuration errors with optimized string storage
60    #[error("Configuration error: {message}")]
61    Config { message: Cow<'static, str> },
62
63    /// Language support errors (when ast-grep-backend available)
64    #[cfg(feature = "ast-grep-backend")]
65    #[error("Language error: {0}")]
66    Language(#[from] SupportLangErr),
67
68    /// Timeout errors with duration context
69    #[error("Operation timed out after {duration:?}: {operation}")]
70    Timeout {
71        operation: Cow<'static, str>,
72        duration: std::time::Duration,
73    },
74
75    /// Concurrency/threading errors
76    #[error("Concurrency error: {message}")]
77    Concurrency { message: Cow<'static, str> },
78
79    /// Generic service errors
80    #[error("Service error: {message}")]
81    Generic { message: Cow<'static, str> },
82}
83
84// Note: ServiceError already implements Error, so it automatically converts to BoxError via alloc's blanket impl
85
86// Helper functions for creating optimized error instances
87impl ServiceError {
88    /// Create execution error with static string (zero allocation)
89    pub fn execution_static(msg: &'static str) -> Self {
90        Self::Execution {
91            message: Cow::Borrowed(msg),
92        }
93    }
94
95    /// Create execution error with dynamic string
96    pub fn execution_dynamic(msg: String) -> Self {
97        Self::Execution {
98            message: Cow::Owned(msg),
99        }
100    }
101
102    /// Create config error with static string (zero allocation)
103    pub fn config_static(msg: &'static str) -> Self {
104        Self::Config {
105            message: Cow::Borrowed(msg),
106        }
107    }
108
109    /// Create config error with dynamic string
110    pub fn config_dynamic(msg: String) -> Self {
111        Self::Config {
112            message: Cow::Owned(msg),
113        }
114    }
115
116    /// Create timeout error with operation context
117    pub fn timeout(operation: impl Into<Cow<'static, str>>, duration: std::time::Duration) -> Self {
118        Self::Timeout {
119            operation: operation.into(),
120            duration,
121        }
122    }
123}
124
125/// Errors related to parsing source code into ASTs with performance optimization
126#[derive(Error, Debug)]
127pub enum ParseError {
128    /// Tree-sitter parsing failed (when ast-grep-backend available)
129    #[cfg(feature = "ast-grep-backend")]
130    #[error("Tree-sitter parse error: {0}")]
131    TreeSitter(#[from] TSParseError),
132
133    /// Language not supported
134    #[error("Language not supported for file: {file_path}")]
135    UnsupportedLanguage { file_path: PathBuf },
136
137    /// Language could not be detected
138    #[error("Could not detect language for file: {file_path}")]
139    LanguageDetectionFailed { file_path: PathBuf },
140
141    /// File could not be read
142    #[error("Could not read file: {file_path}: {source}")]
143    FileRead {
144        file_path: PathBuf,
145        source: BoxError,
146    },
147
148    /// Source code is empty or invalid with optimized string storage
149    #[error("Invalid source code: {message}")]
150    InvalidSource { message: Cow<'static, str> },
151
152    /// Content is too large to process
153    #[error("Content too large: {size} bytes (max: {max_size})")]
154    ContentTooLarge { size: usize, max_size: usize },
155
156    /// Generic parsing error with optimized string storage
157    #[error("Parse error: {message}")]
158    Generic { message: Cow<'static, str> },
159
160    /// Encoding issues
161    #[error("Encoding error in file {file_path}: {message}")]
162    Encoding { file_path: PathBuf, message: String },
163
164    /// Parser configuration errors
165    #[error("Parser configuration error: {message}")]
166    Configuration { message: String },
167}
168
169/// Errors related to code analysis and pattern matching
170#[derive(Error, Debug)]
171pub enum AnalysisError {
172    /// Pattern matching errors
173    #[cfg(feature = "matching")]
174    #[error("Pattern error: {0}")]
175    Pattern(#[from] PatternError),
176
177    /// Pattern compilation failed
178    #[error("Pattern compilation failed: {pattern}: {message}")]
179    PatternCompilation { pattern: String, message: String },
180
181    /// Invalid pattern syntax
182    #[error("Invalid pattern syntax: {pattern}")]
183    InvalidPattern { pattern: String },
184
185    /// Meta-variable errors
186    #[error("Meta-variable error: {variable}: {message}")]
187    MetaVariable { variable: String, message: String },
188
189    /// Cross-file analysis errors
190    #[error("Cross-file analysis error: {message}")]
191    CrossFile { message: String },
192
193    /// Graph construction errors
194    #[error("Graph construction error: {message}")]
195    GraphConstruction { message: String },
196
197    /// Dependency resolution errors
198    #[error("Dependency resolution error: {message}")]
199    DependencyResolution { message: String },
200
201    /// Symbol resolution errors
202    #[error("Symbol resolution error: symbol '{symbol}' in file {file_path}")]
203    SymbolResolution { symbol: String, file_path: PathBuf },
204
205    /// Type analysis errors
206    #[error("Type analysis error: {message}")]
207    TypeAnalysis { message: String },
208
209    /// Scope analysis errors
210    #[error("Scope analysis error: {message}")]
211    ScopeAnalysis { message: String },
212
213    /// Analysis depth limit exceeded
214    #[error("Analysis depth limit exceeded: {current_depth} > {max_depth}")]
215    DepthLimitExceeded {
216        current_depth: usize,
217        max_depth: usize,
218    },
219
220    /// Analysis operation cancelled
221    #[error("Analysis operation cancelled: {reason}")]
222    Cancelled { reason: String },
223
224    /// Resource exhaustion during analysis
225    #[error("Resource exhaustion: {resource}: {message}")]
226    ResourceExhaustion { resource: String, message: String },
227}
228
229/// Errors related to storage operations (commercial boundary)
230#[derive(Error, Debug)]
231pub enum StorageError {
232    /// Database connection errors
233    #[error("Database connection error: {message}")]
234    Connection { message: String },
235
236    /// Database query errors
237    #[error("Database query error: {query}: {message}")]
238    Query { query: String, message: String },
239
240    /// Serialization errors
241    #[error("Serialization error: {message}")]
242    Serialization { message: String },
243
244    /// Deserialization errors
245    #[error("Deserialization error: {message}")]
246    Deserialization { message: String },
247
248    /// Cache errors
249    #[error("Cache error: {operation}: {message}")]
250    Cache { operation: String, message: String },
251
252    /// Transaction errors
253    #[error("Transaction error: {message}")]
254    Transaction { message: String },
255
256    /// Storage quota exceeded
257    #[error("Storage quota exceeded: {used} > {limit}")]
258    QuotaExceeded { used: u64, limit: u64 },
259
260    /// Storage corruption detected
261    #[error("Storage corruption detected: {message}")]
262    Corruption { message: String },
263
264    /// Backup/restore errors
265    #[error("Backup/restore error: {operation}: {message}")]
266    BackupRestore { operation: String, message: String },
267}
268
269/// Context information for errors
270#[derive(Debug, Clone, Default)]
271pub struct ErrorContext {
272    /// File being processed when error occurred
273    pub file_path: Option<PathBuf>,
274
275    /// Line number where error occurred
276    pub line: Option<usize>,
277
278    /// Column where error occurred
279    pub column: Option<usize>,
280
281    /// Operation being performed
282    pub operation: Option<String>,
283
284    /// Additional context data
285    pub context_data: thread_utilities::RapidMap<String, String>,
286}
287
288impl ErrorContext {
289    /// Create new error context
290    pub fn new() -> Self {
291        Self::default()
292    }
293
294    /// Set file path
295    pub fn with_file_path(mut self, file_path: PathBuf) -> Self {
296        self.file_path = Some(file_path);
297        self
298    }
299
300    /// Set line number
301    pub fn with_line(mut self, line: usize) -> Self {
302        self.line = Some(line);
303        self
304    }
305
306    /// Set column number
307    pub fn with_column(mut self, column: usize) -> Self {
308        self.column = Some(column);
309        self
310    }
311
312    /// Set operation name
313    pub fn with_operation(mut self, operation: String) -> Self {
314        self.operation = Some(operation);
315        self
316    }
317
318    /// Add context data
319    pub fn with_context_data(mut self, key: String, value: String) -> Self {
320        self.context_data.insert(key, value);
321        self
322    }
323}
324
325/// Enhanced error type that includes context information
326#[derive(Error, Debug)]
327pub struct ContextualError {
328    /// The underlying error
329    pub error: ServiceError,
330
331    /// Additional context information
332    pub context: ErrorContext,
333}
334
335impl fmt::Display for ContextualError {
336    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337        write!(f, "{}", self.error)?;
338
339        if let Some(ref file_path) = self.context.file_path {
340            write!(f, " (file: {})", file_path.display())?;
341        }
342
343        if let Some(line) = self.context.line {
344            write!(f, " (line: {})", line)?;
345        }
346
347        if let Some(column) = self.context.column {
348            write!(f, " (column: {})", column)?;
349        }
350
351        if let Some(ref operation) = self.context.operation {
352            write!(f, " (operation: {})", operation)?;
353        }
354
355        Ok(())
356    }
357}
358
359impl From<ServiceError> for ContextualError {
360    fn from(error: ServiceError) -> Self {
361        Self {
362            error,
363            context: ErrorContext::default(),
364        }
365    }
366}
367
368/// Compatibility type for legacy ServiceError usage
369pub type LegacyServiceResult<T> = Result<T, ServiceError>;
370
371/// Result type for contextual operations
372pub type ContextualResult<T> = Result<T, ContextualError>;
373
374/// Helper trait for adding context to errors
375pub trait ErrorContextExt {
376    type Output;
377
378    /// Add context to the error
379    fn with_context(self, context: ErrorContext) -> Self::Output;
380
381    /// Add file path context
382    fn with_file(self, file_path: PathBuf) -> Self::Output;
383
384    /// Add line context
385    fn with_line(self, line: usize) -> Self::Output;
386
387    /// Add operation context
388    fn with_operation(self, operation: &str) -> Self::Output;
389}
390
391impl<T> ErrorContextExt for Result<T, ServiceError> {
392    type Output = ContextualResult<T>;
393
394    fn with_context(self, context: ErrorContext) -> Self::Output {
395        self.map_err(|error| ContextualError { error, context })
396    }
397
398    fn with_file(self, file_path: PathBuf) -> Self::Output {
399        self.with_context(ErrorContext::new().with_file_path(file_path))
400    }
401
402    fn with_line(self, line: usize) -> Self::Output {
403        self.with_context(ErrorContext::new().with_line(line))
404    }
405
406    fn with_operation(self, operation: &str) -> Self::Output {
407        self.with_context(ErrorContext::new().with_operation(operation.to_string()))
408    }
409}
410
411/// Error recovery strategies
412#[derive(Debug, Clone)]
413pub enum RecoveryStrategy {
414    /// Retry the operation
415    Retry { max_attempts: usize },
416
417    /// Skip the current item and continue
418    Skip,
419
420    /// Use a fallback approach
421    Fallback { strategy: String },
422
423    /// Abort the entire operation
424    Abort,
425
426    /// Continue with partial results
427    Partial,
428}
429
430/// Error recovery information
431#[derive(Debug, Clone)]
432pub struct ErrorRecovery {
433    /// Suggested recovery strategy
434    pub strategy: RecoveryStrategy,
435
436    /// Human-readable recovery instructions
437    pub instructions: String,
438
439    /// Whether automatic recovery is possible
440    pub auto_recoverable: bool,
441}
442
443/// Trait for errors that support recovery
444pub trait RecoverableError {
445    /// Get recovery information for this error
446    fn recovery_info(&self) -> Option<ErrorRecovery>;
447
448    /// Check if this error is retryable
449    fn is_retryable(&self) -> bool {
450        matches!(
451            self.recovery_info(),
452            Some(ErrorRecovery {
453                strategy: RecoveryStrategy::Retry { .. },
454                ..
455            })
456        )
457    }
458
459    /// Check if this error allows partial continuation
460    fn allows_partial(&self) -> bool {
461        matches!(
462            self.recovery_info(),
463            Some(ErrorRecovery {
464                strategy: RecoveryStrategy::Partial | RecoveryStrategy::Skip,
465                ..
466            })
467        )
468    }
469}
470
471impl RecoverableError for ServiceError {
472    fn recovery_info(&self) -> Option<ErrorRecovery> {
473        match self {
474            #[cfg(feature = "ast-grep-backend")]
475            ServiceError::Parse(ParseError::TreeSitter(_)) => Some(ErrorRecovery {
476                strategy: RecoveryStrategy::Retry { max_attempts: 3 },
477                instructions: "Tree-sitter parsing failed. Retry with error recovery enabled."
478                    .to_string(),
479                auto_recoverable: true,
480            }),
481
482            #[cfg(all(feature = "matching", feature = "ast-grep-backend"))]
483            ServiceError::Analysis(AnalysisError::PatternCompilation { .. }) => {
484                Some(ErrorRecovery {
485                    strategy: RecoveryStrategy::Skip,
486                    instructions: "Pattern compilation failed. Skip this pattern and continue."
487                        .to_string(),
488                    auto_recoverable: true,
489                })
490            }
491
492            ServiceError::Io(_) => Some(ErrorRecovery {
493                strategy: RecoveryStrategy::Retry { max_attempts: 3 },
494                instructions: "I/O operation failed. Retry with exponential backoff.".to_string(),
495                auto_recoverable: true,
496            }),
497
498            ServiceError::Timeout { .. } => Some(ErrorRecovery {
499                strategy: RecoveryStrategy::Retry { max_attempts: 2 },
500                instructions: "Operation timed out. Retry with increased timeout.".to_string(),
501                auto_recoverable: true,
502            }),
503
504            ServiceError::Storage(StorageError::Connection { .. }) => Some(ErrorRecovery {
505                strategy: RecoveryStrategy::Retry { max_attempts: 5 },
506                instructions: "Storage connection failed. Retry with exponential backoff."
507                    .to_string(),
508                auto_recoverable: true,
509            }),
510
511            _ => None,
512        }
513    }
514}
515
516/// Macro for creating parse errors with context
517#[macro_export]
518macro_rules! parse_error {
519    ($variant:ident, $($field:ident: $value:expr),* $(,)?) => {
520        $crate::error::ServiceError::Parse(
521            $crate::error::ParseError::$variant {
522                $($field: $value,)*
523            }
524        )
525    };
526}
527
528/// Macro for creating analysis errors with context
529#[macro_export]
530macro_rules! analysis_error {
531    ($variant:ident, $($field:ident: $value:expr),* $(,)?) => {
532        $crate::error::ServiceError::Analysis(
533            $crate::error::AnalysisError::$variant {
534                $($field: $value,)*
535            }
536        )
537    };
538}
539
540/// Macro for creating storage errors with context
541#[macro_export]
542macro_rules! storage_error {
543    ($variant:ident, $($field:ident: $value:expr),* $(,)?) => {
544        $crate::error::ServiceError::Storage(
545            $crate::error::StorageError::$variant {
546                $($field: $value,)*
547            }
548        )
549    };
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555    use std::path::PathBuf;
556
557    #[test]
558    fn test_error_context() {
559        let context = ErrorContext::new()
560            .with_file_path(PathBuf::from("test.rs"))
561            .with_line(42)
562            .with_operation("pattern_matching".to_string());
563
564        assert_eq!(context.file_path, Some(PathBuf::from("test.rs")));
565        assert_eq!(context.line, Some(42));
566        assert_eq!(context.operation, Some("pattern_matching".to_string()));
567    }
568
569    #[test]
570    fn test_contextual_error_display() {
571        let error = ServiceError::config_dynamic("test error".to_string());
572        let contextual = ContextualError {
573            error,
574            context: ErrorContext::new()
575                .with_file_path(PathBuf::from("test.rs"))
576                .with_line(42),
577        };
578
579        let display = format!("{}", contextual);
580        assert!(display.contains("test error"));
581        assert!(display.contains("test.rs"));
582        assert!(display.contains("42"));
583    }
584
585    #[test]
586    fn test_recovery_info() {
587        let error = ServiceError::timeout("test timeout", std::time::Duration::from_secs(1));
588        let recovery = error.recovery_info().unwrap();
589
590        assert!(matches!(
591            recovery.strategy,
592            RecoveryStrategy::Retry { max_attempts: 2 }
593        ));
594        assert!(recovery.auto_recoverable);
595    }
596}