use std::borrow::Cow;
use std::fmt;
use std::path::PathBuf;
use thiserror::Error;
#[cfg(feature = "tower-services")]
pub use tower::BoxError;
#[cfg(not(feature = "tower-services"))]
pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
#[cfg(feature = "ast-grep-backend")]
use thread_ast_engine::tree_sitter::TSParseError;
#[cfg(feature = "ast-grep-backend")]
use thread_language::SupportLangErr;
#[cfg(all(feature = "matching", feature = "ast-grep-backend"))]
use thread_ast_engine::PatternError;
pub type ServiceResult<T> = Result<T, BoxError>;
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("Parse error: {0}")]
Parse(#[from] ParseError),
#[error("Analysis error: {0}")]
Analysis(#[from] AnalysisError),
#[error("Storage error: {0}")]
Storage(#[from] StorageError),
#[error("Execution error: {message}")]
Execution { message: Cow<'static, str> },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Configuration error: {message}")]
Config { message: Cow<'static, str> },
#[cfg(feature = "ast-grep-backend")]
#[error("Language error: {0}")]
Language(#[from] SupportLangErr),
#[error("Operation timed out after {duration:?}: {operation}")]
Timeout {
operation: Cow<'static, str>,
duration: std::time::Duration,
},
#[error("Concurrency error: {message}")]
Concurrency { message: Cow<'static, str> },
#[error("Service error: {message}")]
Generic { message: Cow<'static, str> },
}
impl ServiceError {
pub fn execution_static(msg: &'static str) -> Self {
Self::Execution {
message: Cow::Borrowed(msg),
}
}
pub fn execution_dynamic(msg: String) -> Self {
Self::Execution {
message: Cow::Owned(msg),
}
}
pub fn config_static(msg: &'static str) -> Self {
Self::Config {
message: Cow::Borrowed(msg),
}
}
pub fn config_dynamic(msg: String) -> Self {
Self::Config {
message: Cow::Owned(msg),
}
}
pub fn timeout(operation: impl Into<Cow<'static, str>>, duration: std::time::Duration) -> Self {
Self::Timeout {
operation: operation.into(),
duration,
}
}
}
#[derive(Error, Debug)]
pub enum ParseError {
#[cfg(feature = "ast-grep-backend")]
#[error("Tree-sitter parse error: {0}")]
TreeSitter(#[from] TSParseError),
#[error("Language not supported for file: {file_path}")]
UnsupportedLanguage { file_path: PathBuf },
#[error("Could not detect language for file: {file_path}")]
LanguageDetectionFailed { file_path: PathBuf },
#[error("Could not read file: {file_path}: {source}")]
FileRead {
file_path: PathBuf,
source: BoxError,
},
#[error("Invalid source code: {message}")]
InvalidSource { message: Cow<'static, str> },
#[error("Content too large: {size} bytes (max: {max_size})")]
ContentTooLarge { size: usize, max_size: usize },
#[error("Parse error: {message}")]
Generic { message: Cow<'static, str> },
#[error("Encoding error in file {file_path}: {message}")]
Encoding { file_path: PathBuf, message: String },
#[error("Parser configuration error: {message}")]
Configuration { message: String },
}
#[derive(Error, Debug)]
pub enum AnalysisError {
#[cfg(feature = "matching")]
#[error("Pattern error: {0}")]
Pattern(#[from] PatternError),
#[error("Pattern compilation failed: {pattern}: {message}")]
PatternCompilation { pattern: String, message: String },
#[error("Invalid pattern syntax: {pattern}")]
InvalidPattern { pattern: String },
#[error("Meta-variable error: {variable}: {message}")]
MetaVariable { variable: String, message: String },
#[error("Cross-file analysis error: {message}")]
CrossFile { message: String },
#[error("Graph construction error: {message}")]
GraphConstruction { message: String },
#[error("Dependency resolution error: {message}")]
DependencyResolution { message: String },
#[error("Symbol resolution error: symbol '{symbol}' in file {file_path}")]
SymbolResolution { symbol: String, file_path: PathBuf },
#[error("Type analysis error: {message}")]
TypeAnalysis { message: String },
#[error("Scope analysis error: {message}")]
ScopeAnalysis { message: String },
#[error("Analysis depth limit exceeded: {current_depth} > {max_depth}")]
DepthLimitExceeded {
current_depth: usize,
max_depth: usize,
},
#[error("Analysis operation cancelled: {reason}")]
Cancelled { reason: String },
#[error("Resource exhaustion: {resource}: {message}")]
ResourceExhaustion { resource: String, message: String },
}
#[derive(Error, Debug)]
pub enum StorageError {
#[error("Database connection error: {message}")]
Connection { message: String },
#[error("Database query error: {query}: {message}")]
Query { query: String, message: String },
#[error("Serialization error: {message}")]
Serialization { message: String },
#[error("Deserialization error: {message}")]
Deserialization { message: String },
#[error("Cache error: {operation}: {message}")]
Cache { operation: String, message: String },
#[error("Transaction error: {message}")]
Transaction { message: String },
#[error("Storage quota exceeded: {used} > {limit}")]
QuotaExceeded { used: u64, limit: u64 },
#[error("Storage corruption detected: {message}")]
Corruption { message: String },
#[error("Backup/restore error: {operation}: {message}")]
BackupRestore { operation: String, message: String },
}
#[derive(Debug, Clone, Default)]
pub struct ErrorContext {
pub file_path: Option<PathBuf>,
pub line: Option<usize>,
pub column: Option<usize>,
pub operation: Option<String>,
pub context_data: thread_utilities::RapidMap<String, String>,
}
impl ErrorContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_file_path(mut self, file_path: PathBuf) -> Self {
self.file_path = Some(file_path);
self
}
pub fn with_line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
pub fn with_column(mut self, column: usize) -> Self {
self.column = Some(column);
self
}
pub fn with_operation(mut self, operation: String) -> Self {
self.operation = Some(operation);
self
}
pub fn with_context_data(mut self, key: String, value: String) -> Self {
self.context_data.insert(key, value);
self
}
}
#[derive(Error, Debug)]
pub struct ContextualError {
pub error: ServiceError,
pub context: ErrorContext,
}
impl fmt::Display for ContextualError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.error)?;
if let Some(ref file_path) = self.context.file_path {
write!(f, " (file: {})", file_path.display())?;
}
if let Some(line) = self.context.line {
write!(f, " (line: {})", line)?;
}
if let Some(column) = self.context.column {
write!(f, " (column: {})", column)?;
}
if let Some(ref operation) = self.context.operation {
write!(f, " (operation: {})", operation)?;
}
Ok(())
}
}
impl From<ServiceError> for ContextualError {
fn from(error: ServiceError) -> Self {
Self {
error,
context: ErrorContext::default(),
}
}
}
pub type LegacyServiceResult<T> = Result<T, ServiceError>;
pub type ContextualResult<T> = Result<T, ContextualError>;
pub trait ErrorContextExt {
type Output;
fn with_context(self, context: ErrorContext) -> Self::Output;
fn with_file(self, file_path: PathBuf) -> Self::Output;
fn with_line(self, line: usize) -> Self::Output;
fn with_operation(self, operation: &str) -> Self::Output;
}
impl<T> ErrorContextExt for Result<T, ServiceError> {
type Output = ContextualResult<T>;
fn with_context(self, context: ErrorContext) -> Self::Output {
self.map_err(|error| ContextualError { error, context })
}
fn with_file(self, file_path: PathBuf) -> Self::Output {
self.with_context(ErrorContext::new().with_file_path(file_path))
}
fn with_line(self, line: usize) -> Self::Output {
self.with_context(ErrorContext::new().with_line(line))
}
fn with_operation(self, operation: &str) -> Self::Output {
self.with_context(ErrorContext::new().with_operation(operation.to_string()))
}
}
#[derive(Debug, Clone)]
pub enum RecoveryStrategy {
Retry { max_attempts: usize },
Skip,
Fallback { strategy: String },
Abort,
Partial,
}
#[derive(Debug, Clone)]
pub struct ErrorRecovery {
pub strategy: RecoveryStrategy,
pub instructions: String,
pub auto_recoverable: bool,
}
pub trait RecoverableError {
fn recovery_info(&self) -> Option<ErrorRecovery>;
fn is_retryable(&self) -> bool {
matches!(
self.recovery_info(),
Some(ErrorRecovery {
strategy: RecoveryStrategy::Retry { .. },
..
})
)
}
fn allows_partial(&self) -> bool {
matches!(
self.recovery_info(),
Some(ErrorRecovery {
strategy: RecoveryStrategy::Partial | RecoveryStrategy::Skip,
..
})
)
}
}
impl RecoverableError for ServiceError {
fn recovery_info(&self) -> Option<ErrorRecovery> {
match self {
#[cfg(feature = "ast-grep-backend")]
ServiceError::Parse(ParseError::TreeSitter(_)) => Some(ErrorRecovery {
strategy: RecoveryStrategy::Retry { max_attempts: 3 },
instructions: "Tree-sitter parsing failed. Retry with error recovery enabled."
.to_string(),
auto_recoverable: true,
}),
#[cfg(all(feature = "matching", feature = "ast-grep-backend"))]
ServiceError::Analysis(AnalysisError::PatternCompilation { .. }) => {
Some(ErrorRecovery {
strategy: RecoveryStrategy::Skip,
instructions: "Pattern compilation failed. Skip this pattern and continue."
.to_string(),
auto_recoverable: true,
})
}
ServiceError::Io(_) => Some(ErrorRecovery {
strategy: RecoveryStrategy::Retry { max_attempts: 3 },
instructions: "I/O operation failed. Retry with exponential backoff.".to_string(),
auto_recoverable: true,
}),
ServiceError::Timeout { .. } => Some(ErrorRecovery {
strategy: RecoveryStrategy::Retry { max_attempts: 2 },
instructions: "Operation timed out. Retry with increased timeout.".to_string(),
auto_recoverable: true,
}),
ServiceError::Storage(StorageError::Connection { .. }) => Some(ErrorRecovery {
strategy: RecoveryStrategy::Retry { max_attempts: 5 },
instructions: "Storage connection failed. Retry with exponential backoff."
.to_string(),
auto_recoverable: true,
}),
_ => None,
}
}
}
#[macro_export]
macro_rules! parse_error {
($variant:ident, $($field:ident: $value:expr),* $(,)?) => {
$crate::error::ServiceError::Parse(
$crate::error::ParseError::$variant {
$($field: $value,)*
}
)
};
}
#[macro_export]
macro_rules! analysis_error {
($variant:ident, $($field:ident: $value:expr),* $(,)?) => {
$crate::error::ServiceError::Analysis(
$crate::error::AnalysisError::$variant {
$($field: $value,)*
}
)
};
}
#[macro_export]
macro_rules! storage_error {
($variant:ident, $($field:ident: $value:expr),* $(,)?) => {
$crate::error::ServiceError::Storage(
$crate::error::StorageError::$variant {
$($field: $value,)*
}
)
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_error_context() {
let context = ErrorContext::new()
.with_file_path(PathBuf::from("test.rs"))
.with_line(42)
.with_operation("pattern_matching".to_string());
assert_eq!(context.file_path, Some(PathBuf::from("test.rs")));
assert_eq!(context.line, Some(42));
assert_eq!(context.operation, Some("pattern_matching".to_string()));
}
#[test]
fn test_contextual_error_display() {
let error = ServiceError::config_dynamic("test error".to_string());
let contextual = ContextualError {
error,
context: ErrorContext::new()
.with_file_path(PathBuf::from("test.rs"))
.with_line(42),
};
let display = format!("{}", contextual);
assert!(display.contains("test error"));
assert!(display.contains("test.rs"));
assert!(display.contains("42"));
}
#[test]
fn test_recovery_info() {
let error = ServiceError::timeout("test timeout", std::time::Duration::from_secs(1));
let recovery = error.recovery_info().unwrap();
assert!(matches!(
recovery.strategy,
RecoveryStrategy::Retry { max_attempts: 2 }
));
assert!(recovery.auto_recoverable);
}
}