1use std::borrow::Cow;
12use std::fmt;
13use std::path::PathBuf;
14use thiserror::Error;
15
16#[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#[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
33pub type ServiceResult<T> = Result<T, BoxError>;
35
36#[derive(Error, Debug)]
38pub enum ServiceError {
39 #[error("Parse error: {0}")]
41 Parse(#[from] ParseError),
42
43 #[error("Analysis error: {0}")]
45 Analysis(#[from] AnalysisError),
46
47 #[error("Storage error: {0}")]
49 Storage(#[from] StorageError),
50
51 #[error("Execution error: {message}")]
53 Execution { message: Cow<'static, str> },
54
55 #[error("IO error: {0}")]
57 Io(#[from] std::io::Error),
58
59 #[error("Configuration error: {message}")]
61 Config { message: Cow<'static, str> },
62
63 #[cfg(feature = "ast-grep-backend")]
65 #[error("Language error: {0}")]
66 Language(#[from] SupportLangErr),
67
68 #[error("Operation timed out after {duration:?}: {operation}")]
70 Timeout {
71 operation: Cow<'static, str>,
72 duration: std::time::Duration,
73 },
74
75 #[error("Concurrency error: {message}")]
77 Concurrency { message: Cow<'static, str> },
78
79 #[error("Service error: {message}")]
81 Generic { message: Cow<'static, str> },
82}
83
84impl ServiceError {
88 pub fn execution_static(msg: &'static str) -> Self {
90 Self::Execution {
91 message: Cow::Borrowed(msg),
92 }
93 }
94
95 pub fn execution_dynamic(msg: String) -> Self {
97 Self::Execution {
98 message: Cow::Owned(msg),
99 }
100 }
101
102 pub fn config_static(msg: &'static str) -> Self {
104 Self::Config {
105 message: Cow::Borrowed(msg),
106 }
107 }
108
109 pub fn config_dynamic(msg: String) -> Self {
111 Self::Config {
112 message: Cow::Owned(msg),
113 }
114 }
115
116 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#[derive(Error, Debug)]
127pub enum ParseError {
128 #[cfg(feature = "ast-grep-backend")]
130 #[error("Tree-sitter parse error: {0}")]
131 TreeSitter(#[from] TSParseError),
132
133 #[error("Language not supported for file: {file_path}")]
135 UnsupportedLanguage { file_path: PathBuf },
136
137 #[error("Could not detect language for file: {file_path}")]
139 LanguageDetectionFailed { file_path: PathBuf },
140
141 #[error("Could not read file: {file_path}: {source}")]
143 FileRead {
144 file_path: PathBuf,
145 source: BoxError,
146 },
147
148 #[error("Invalid source code: {message}")]
150 InvalidSource { message: Cow<'static, str> },
151
152 #[error("Content too large: {size} bytes (max: {max_size})")]
154 ContentTooLarge { size: usize, max_size: usize },
155
156 #[error("Parse error: {message}")]
158 Generic { message: Cow<'static, str> },
159
160 #[error("Encoding error in file {file_path}: {message}")]
162 Encoding { file_path: PathBuf, message: String },
163
164 #[error("Parser configuration error: {message}")]
166 Configuration { message: String },
167}
168
169#[derive(Error, Debug)]
171pub enum AnalysisError {
172 #[cfg(feature = "matching")]
174 #[error("Pattern error: {0}")]
175 Pattern(#[from] PatternError),
176
177 #[error("Pattern compilation failed: {pattern}: {message}")]
179 PatternCompilation { pattern: String, message: String },
180
181 #[error("Invalid pattern syntax: {pattern}")]
183 InvalidPattern { pattern: String },
184
185 #[error("Meta-variable error: {variable}: {message}")]
187 MetaVariable { variable: String, message: String },
188
189 #[error("Cross-file analysis error: {message}")]
191 CrossFile { message: String },
192
193 #[error("Graph construction error: {message}")]
195 GraphConstruction { message: String },
196
197 #[error("Dependency resolution error: {message}")]
199 DependencyResolution { message: String },
200
201 #[error("Symbol resolution error: symbol '{symbol}' in file {file_path}")]
203 SymbolResolution { symbol: String, file_path: PathBuf },
204
205 #[error("Type analysis error: {message}")]
207 TypeAnalysis { message: String },
208
209 #[error("Scope analysis error: {message}")]
211 ScopeAnalysis { message: String },
212
213 #[error("Analysis depth limit exceeded: {current_depth} > {max_depth}")]
215 DepthLimitExceeded {
216 current_depth: usize,
217 max_depth: usize,
218 },
219
220 #[error("Analysis operation cancelled: {reason}")]
222 Cancelled { reason: String },
223
224 #[error("Resource exhaustion: {resource}: {message}")]
226 ResourceExhaustion { resource: String, message: String },
227}
228
229#[derive(Error, Debug)]
231pub enum StorageError {
232 #[error("Database connection error: {message}")]
234 Connection { message: String },
235
236 #[error("Database query error: {query}: {message}")]
238 Query { query: String, message: String },
239
240 #[error("Serialization error: {message}")]
242 Serialization { message: String },
243
244 #[error("Deserialization error: {message}")]
246 Deserialization { message: String },
247
248 #[error("Cache error: {operation}: {message}")]
250 Cache { operation: String, message: String },
251
252 #[error("Transaction error: {message}")]
254 Transaction { message: String },
255
256 #[error("Storage quota exceeded: {used} > {limit}")]
258 QuotaExceeded { used: u64, limit: u64 },
259
260 #[error("Storage corruption detected: {message}")]
262 Corruption { message: String },
263
264 #[error("Backup/restore error: {operation}: {message}")]
266 BackupRestore { operation: String, message: String },
267}
268
269#[derive(Debug, Clone, Default)]
271pub struct ErrorContext {
272 pub file_path: Option<PathBuf>,
274
275 pub line: Option<usize>,
277
278 pub column: Option<usize>,
280
281 pub operation: Option<String>,
283
284 pub context_data: thread_utilities::RapidMap<String, String>,
286}
287
288impl ErrorContext {
289 pub fn new() -> Self {
291 Self::default()
292 }
293
294 pub fn with_file_path(mut self, file_path: PathBuf) -> Self {
296 self.file_path = Some(file_path);
297 self
298 }
299
300 pub fn with_line(mut self, line: usize) -> Self {
302 self.line = Some(line);
303 self
304 }
305
306 pub fn with_column(mut self, column: usize) -> Self {
308 self.column = Some(column);
309 self
310 }
311
312 pub fn with_operation(mut self, operation: String) -> Self {
314 self.operation = Some(operation);
315 self
316 }
317
318 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#[derive(Error, Debug)]
327pub struct ContextualError {
328 pub error: ServiceError,
330
331 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
368pub type LegacyServiceResult<T> = Result<T, ServiceError>;
370
371pub type ContextualResult<T> = Result<T, ContextualError>;
373
374pub trait ErrorContextExt {
376 type Output;
377
378 fn with_context(self, context: ErrorContext) -> Self::Output;
380
381 fn with_file(self, file_path: PathBuf) -> Self::Output;
383
384 fn with_line(self, line: usize) -> Self::Output;
386
387 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#[derive(Debug, Clone)]
413pub enum RecoveryStrategy {
414 Retry { max_attempts: usize },
416
417 Skip,
419
420 Fallback { strategy: String },
422
423 Abort,
425
426 Partial,
428}
429
430#[derive(Debug, Clone)]
432pub struct ErrorRecovery {
433 pub strategy: RecoveryStrategy,
435
436 pub instructions: String,
438
439 pub auto_recoverable: bool,
441}
442
443pub trait RecoverableError {
445 fn recovery_info(&self) -> Option<ErrorRecovery>;
447
448 fn is_retryable(&self) -> bool {
450 matches!(
451 self.recovery_info(),
452 Some(ErrorRecovery {
453 strategy: RecoveryStrategy::Retry { .. },
454 ..
455 })
456 )
457 }
458
459 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_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_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_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}