1use std::path::PathBuf;
7use thiserror::Error;
8
9#[derive(Debug, Error)]
15pub enum TldrError {
16 #[error("Path not found: {0}")]
21 PathNotFound(PathBuf),
22
23 #[error("Path traversal detected: {0}")]
25 PathTraversal(PathBuf),
26
27 #[error("Symlink cycle detected: {0}")]
29 SymlinkCycle(PathBuf),
30
31 #[error("Permission denied: {0}")]
33 PermissionDenied(PathBuf),
34
35 #[error("File too large: {path} is {size_mb}MB (max {max_mb}MB)")]
40 FileTooLarge {
41 path: PathBuf,
43 size_mb: usize,
45 max_mb: usize,
47 },
48
49 #[error("Encoding error in {path}: {detail}")]
51 EncodingError {
52 path: PathBuf,
54 detail: String,
56 },
57
58 #[error("Coverage parse error ({format}): {detail}")]
60 CoverageParseError {
61 format: String,
63 detail: String,
65 },
66
67 #[error("Not a git repository: {0}")]
69 NotGitRepository(PathBuf),
70
71 #[error("Git operation in progress: {0}. Complete or abort the operation first.")]
73 GitOperationInProgress(String),
74
75 #[error("Git error: {0}")]
77 GitError(String),
78
79 #[error("Parse error in {file}{}: {message}",
86 line.map(|l| format!(" at line {}", l)).unwrap_or_default())]
87 ParseError {
88 file: PathBuf,
90 line: Option<u32>,
92 message: String,
94 },
95
96 #[error("Unsupported language: {0}")]
98 UnsupportedLanguage(String),
99
100 #[error("Function not found: {name}{}{}",
107 file.as_ref().map(|f| format!(" in {}", f.display())).unwrap_or_default(),
108 if suggestions.is_empty() { String::new() }
109 else { format!("\n\nDid you mean:\n{}", suggestions.iter().map(|s| format!(" - {}", s)).collect::<Vec<_>>().join("\n")) }
110 )]
111 FunctionNotFound {
112 name: String,
114 file: Option<PathBuf>,
116 suggestions: Vec<String>,
118 },
119
120 #[error("Invalid direction: {0}. Expected 'backward' or 'forward'")]
122 InvalidDirection(String),
123
124 #[error("Line {0} is not within the specified function")]
126 LineNotInFunction(u32),
127
128 #[error("No supported files found in {0}")]
130 NoSupportedFiles(PathBuf),
131
132 #[error("{entity} not found: {name}{}",
134 suggestion.as_ref().map(|s| format!("\n\n{}", s)).unwrap_or_default())]
135 NotFound {
136 entity: String,
138 name: String,
140 suggestion: Option<String>,
142 },
143
144 #[error("Invalid argument {arg}: {message}{}",
146 suggestion.as_ref().map(|s| format!("\n\nHint: {}", s)).unwrap_or_default())]
147 InvalidArgs {
148 arg: String,
150 message: String,
152 suggestion: Option<String>,
154 },
155
156 #[error("Daemon error: {0}")]
161 DaemonError(String),
162
163 #[error("Connection failed: {0}")]
165 ConnectionFailed(String),
166
167 #[error("Timeout: {0}")]
169 Timeout(String),
170
171 #[error("MCP error: {0}")]
176 McpError(String),
177
178 #[error("Serialization error: {0}")]
183 SerializationError(String),
184
185 #[error("Embedding error: {0}")]
190 Embedding(String),
191
192 #[error("Failed to load embedding model '{model}': {detail}")]
194 ModelLoadError {
195 model: String,
197 detail: String,
199 },
200
201 #[error("Index too large: {count} chunks exceeds maximum of {max}. Filter by language or directory.")]
203 IndexTooLarge {
204 count: usize,
206 max: usize,
208 },
209
210 #[error("Memory limit exceeded: estimated {estimated_mb}MB exceeds maximum of {max_mb}MB")]
212 MemoryLimitExceeded {
213 estimated_mb: usize,
215 max_mb: usize,
217 },
218
219 #[error("Chunk not found: {file}{}", function.as_ref().map(|f| format!("::{}", f)).unwrap_or_default())]
221 ChunkNotFound {
222 file: String,
224 function: Option<String>,
226 },
227
228 #[error(transparent)]
233 IoError(#[from] std::io::Error),
234}
235
236impl TldrError {
237 pub fn function_not_found(name: impl Into<String>) -> Self {
239 TldrError::FunctionNotFound {
240 name: name.into(),
241 file: None,
242 suggestions: Vec::new(),
243 }
244 }
245
246 pub fn function_not_found_in_file(name: impl Into<String>, file: PathBuf) -> Self {
248 TldrError::FunctionNotFound {
249 name: name.into(),
250 file: Some(file),
251 suggestions: Vec::new(),
252 }
253 }
254
255 pub fn function_not_found_with_suggestions(
257 name: impl Into<String>,
258 file: Option<PathBuf>,
259 suggestions: Vec<String>,
260 ) -> Self {
261 TldrError::FunctionNotFound {
262 name: name.into(),
263 file,
264 suggestions,
265 }
266 }
267
268 pub fn parse_error(file: PathBuf, line: Option<u32>, message: impl Into<String>) -> Self {
270 TldrError::ParseError {
271 file,
272 line,
273 message: message.into(),
274 }
275 }
276
277 pub fn is_recoverable(&self) -> bool {
279 matches!(
280 self,
281 TldrError::ParseError { .. }
282 | TldrError::PermissionDenied(_)
283 | TldrError::FunctionNotFound { .. }
284 )
285 }
286
287 pub fn exit_code(&self) -> i32 {
297 match self {
298 TldrError::PathNotFound(_) => 2,
300 TldrError::PathTraversal(_) => 3,
301 TldrError::SymlinkCycle(_) => 4,
302 TldrError::PermissionDenied(_) => 5,
303 TldrError::FileTooLarge { .. } => 6,
304 TldrError::EncodingError { .. } => 7,
305 TldrError::NotGitRepository(_) => 8,
306 TldrError::GitOperationInProgress(_) => 9,
307 TldrError::GitError(_) => 9,
308
309 TldrError::ParseError { .. } => 10,
311 TldrError::UnsupportedLanguage(_) => 11,
312 TldrError::CoverageParseError { .. } => 12,
313
314 TldrError::FunctionNotFound { .. } => 20,
316 TldrError::InvalidDirection(_) => 21,
317 TldrError::LineNotInFunction(_) => 22,
318 TldrError::NoSupportedFiles(_) => 23,
319 TldrError::NotFound { .. } => 24,
320 TldrError::InvalidArgs { .. } => 25,
321
322 TldrError::DaemonError(_) => 30,
324 TldrError::ConnectionFailed(_) => 31,
325 TldrError::Timeout(_) => 32,
326 TldrError::McpError(_) => 33,
327
328 TldrError::SerializationError(_) => 40,
330
331 TldrError::Embedding(_) => 50,
333 TldrError::ModelLoadError { .. } => 51,
334 TldrError::IndexTooLarge { .. } => 52,
335 TldrError::MemoryLimitExceeded { .. } => 53,
336 TldrError::ChunkNotFound { .. } => 54,
337
338 TldrError::IoError(_) => 1,
340 }
341 }
342
343 pub fn category(&self) -> &'static str {
345 match self {
346 TldrError::PathNotFound(_)
347 | TldrError::PathTraversal(_)
348 | TldrError::SymlinkCycle(_)
349 | TldrError::PermissionDenied(_)
350 | TldrError::FileTooLarge { .. }
351 | TldrError::EncodingError { .. } => "filesystem",
352
353 TldrError::NotGitRepository(_)
354 | TldrError::GitOperationInProgress(_)
355 | TldrError::GitError(_) => "git",
356
357 TldrError::ParseError { .. }
358 | TldrError::UnsupportedLanguage(_)
359 | TldrError::CoverageParseError { .. } => "parse",
360
361 TldrError::FunctionNotFound { .. }
362 | TldrError::InvalidDirection(_)
363 | TldrError::LineNotInFunction(_)
364 | TldrError::NoSupportedFiles(_)
365 | TldrError::NotFound { .. }
366 | TldrError::InvalidArgs { .. } => "analysis",
367
368 TldrError::DaemonError(_) | TldrError::ConnectionFailed(_) | TldrError::Timeout(_) => {
369 "daemon"
370 }
371
372 TldrError::McpError(_) => "mcp",
373
374 TldrError::SerializationError(_) => "serialization",
375
376 TldrError::Embedding(_)
377 | TldrError::ModelLoadError { .. }
378 | TldrError::IndexTooLarge { .. }
379 | TldrError::MemoryLimitExceeded { .. }
380 | TldrError::ChunkNotFound { .. } => "semantic",
381
382 TldrError::IoError(_) => "io",
383 }
384 }
385}
386
387impl From<serde_json::Error> for TldrError {
389 fn from(err: serde_json::Error) -> Self {
390 TldrError::SerializationError(err.to_string())
391 }
392}
393
394impl From<regex::Error> for TldrError {
396 fn from(err: regex::Error) -> Self {
397 TldrError::ParseError {
398 file: std::path::PathBuf::new(),
399 line: None,
400 message: format!("Invalid regex pattern: {}", err),
401 }
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408
409 #[test]
410 fn test_error_display_path_not_found() {
411 let err = TldrError::PathNotFound(PathBuf::from("/some/path"));
412 assert_eq!(err.to_string(), "Path not found: /some/path");
413 }
414
415 #[test]
416 fn test_error_display_parse_error_with_line() {
417 let err = TldrError::parse_error(PathBuf::from("test.py"), Some(42), "unexpected token");
418 assert_eq!(
419 err.to_string(),
420 "Parse error in test.py at line 42: unexpected token"
421 );
422 }
423
424 #[test]
425 fn test_error_display_parse_error_without_line() {
426 let err = TldrError::parse_error(PathBuf::from("test.py"), None, "file is binary");
427 assert_eq!(err.to_string(), "Parse error in test.py: file is binary");
428 }
429
430 #[test]
431 fn test_error_display_function_not_found_simple() {
432 let err = TldrError::function_not_found("process_data");
433 assert_eq!(err.to_string(), "Function not found: process_data");
434 }
435
436 #[test]
437 fn test_error_display_function_not_found_with_file() {
438 let err =
439 TldrError::function_not_found_in_file("process_data", PathBuf::from("src/main.py"));
440 assert_eq!(
441 err.to_string(),
442 "Function not found: process_data in src/main.py"
443 );
444 }
445
446 #[test]
447 fn test_error_display_function_not_found_with_suggestions() {
448 let err = TldrError::function_not_found_with_suggestions(
449 "proces_data",
450 Some(PathBuf::from("src/main.py")),
451 vec!["process_data".to_string(), "process_data_v2".to_string()],
452 );
453 let msg = err.to_string();
454 assert!(msg.contains("proces_data"));
455 assert!(msg.contains("src/main.py"));
456 assert!(msg.contains("Did you mean:"));
457 assert!(msg.contains("process_data"));
458 assert!(msg.contains("process_data_v2"));
459 }
460
461 #[test]
462 fn test_error_is_recoverable() {
463 assert!(TldrError::function_not_found("foo").is_recoverable());
464 assert!(TldrError::parse_error(PathBuf::from("x"), None, "e").is_recoverable());
465 assert!(TldrError::PermissionDenied(PathBuf::from("/")).is_recoverable());
466
467 assert!(!TldrError::PathNotFound(PathBuf::from("/")).is_recoverable());
468 assert!(!TldrError::PathTraversal(PathBuf::from("/")).is_recoverable());
469 }
470
471 #[test]
472 fn test_error_exit_codes() {
473 assert_eq!(TldrError::PathNotFound(PathBuf::from("/")).exit_code(), 2);
474 assert_eq!(TldrError::PathTraversal(PathBuf::from("/")).exit_code(), 3);
475 assert_eq!(TldrError::function_not_found("foo").exit_code(), 20);
476 assert_eq!(TldrError::DaemonError("test".to_string()).exit_code(), 30);
477 assert_eq!(TldrError::McpError("test".to_string()).exit_code(), 33);
478 assert_eq!(
479 TldrError::SerializationError("test".to_string()).exit_code(),
480 40
481 );
482 }
483
484 #[test]
485 fn test_error_categories() {
486 assert_eq!(
487 TldrError::PathNotFound(PathBuf::from("/")).category(),
488 "filesystem"
489 );
490 assert_eq!(
491 TldrError::parse_error(PathBuf::from("x"), None, "e").category(),
492 "parse"
493 );
494 assert_eq!(TldrError::function_not_found("foo").category(), "analysis");
495 assert_eq!(TldrError::DaemonError("x".to_string()).category(), "daemon");
496 assert_eq!(TldrError::McpError("x".to_string()).category(), "mcp");
497 }
498}