Skip to main content

tldr_cli/commands/remaining/
error.rs

1//! Error types for remaining commands
2//!
3//! This module defines the error types used across all remaining analysis
4//! commands (todo, explain, secure, definition, diff, diff_impact, api_check,
5//! equivalence, vuln).
6
7use std::path::PathBuf;
8use thiserror::Error;
9
10/// Errors for remaining commands.
11#[derive(Debug, Error)]
12pub enum RemainingError {
13    /// File not found.
14    #[error("file not found: {}", path.display())]
15    FileNotFound { path: PathBuf },
16
17    /// Function/symbol not found.
18    #[error("symbol '{}' not found in {}", symbol, file.display())]
19    SymbolNotFound { symbol: String, file: PathBuf },
20
21    /// Parse error.
22    #[error("parse error in {}: {message}", file.display())]
23    ParseError { file: PathBuf, message: String },
24
25    /// Invalid arguments.
26    #[error("invalid argument: {message}")]
27    InvalidArgument { message: String },
28
29    /// File too large.
30    #[error("file too large: {} ({bytes} bytes)", path.display())]
31    FileTooLarge { path: PathBuf, bytes: u64 },
32
33    /// Path traversal blocked.
34    #[error("path traversal blocked: {}", path.display())]
35    PathTraversal { path: PathBuf },
36
37    /// Unsupported language.
38    #[error("unsupported language: {language}")]
39    UnsupportedLanguage { language: String },
40
41    /// Analysis error.
42    #[error("analysis error: {message}")]
43    AnalysisError { message: String },
44
45    /// Findings detected (for vuln/api-check - special exit code).
46    #[error("{count} findings detected")]
47    FindingsDetected { count: u32 },
48
49    /// Autodetected language is not in the command's supported set.
50    ///
51    /// Distinct from [`Self::UnsupportedLanguage`]: that variant fires
52    /// on `--lang <L>` explicitly passed where the command cannot
53    /// handle L. This variant fires when no `--lang` was given, the
54    /// autodetector identified L, and L is outside the command's
55    /// supported set. Emitted with exit code 2 so tooling can
56    /// distinguish "analysis not attempted" from "analysis attempted
57    /// and failed" (exit 1).
58    #[error("{message}")]
59    AutodetectUnsupported { message: String },
60
61    /// Timeout.
62    #[error("analysis timed out after {seconds}s")]
63    Timeout { seconds: u64 },
64
65    /// IO error.
66    #[error("IO error: {0}")]
67    Io(#[from] std::io::Error),
68
69    /// JSON error.
70    #[error("JSON error: {0}")]
71    Json(#[from] serde_json::Error),
72}
73
74impl RemainingError {
75    /// Create a FileNotFound error
76    pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
77        Self::FileNotFound { path: path.into() }
78    }
79
80    /// Create a SymbolNotFound error
81    pub fn symbol_not_found(symbol: impl Into<String>, file: impl Into<PathBuf>) -> Self {
82        Self::SymbolNotFound {
83            symbol: symbol.into(),
84            file: file.into(),
85        }
86    }
87
88    /// Create a ParseError
89    pub fn parse_error(file: impl Into<PathBuf>, message: impl Into<String>) -> Self {
90        Self::ParseError {
91            file: file.into(),
92            message: message.into(),
93        }
94    }
95
96    /// Create an InvalidArgument error
97    pub fn invalid_argument(message: impl Into<String>) -> Self {
98        Self::InvalidArgument {
99            message: message.into(),
100        }
101    }
102
103    /// Create a FileTooLarge error
104    pub fn file_too_large(path: impl Into<PathBuf>, bytes: u64) -> Self {
105        Self::FileTooLarge {
106            path: path.into(),
107            bytes,
108        }
109    }
110
111    /// Create a PathTraversal error
112    pub fn path_traversal(path: impl Into<PathBuf>) -> Self {
113        Self::PathTraversal { path: path.into() }
114    }
115
116    /// Create an UnsupportedLanguage error
117    pub fn unsupported_language(language: impl Into<String>) -> Self {
118        Self::UnsupportedLanguage {
119            language: language.into(),
120        }
121    }
122
123    /// Create an AnalysisError
124    pub fn analysis_error(message: impl Into<String>) -> Self {
125        Self::AnalysisError {
126            message: message.into(),
127        }
128    }
129
130    /// Create a FindingsDetected error
131    pub fn findings_detected(count: u32) -> Self {
132        Self::FindingsDetected { count }
133    }
134
135    /// Create an AutodetectUnsupported error with a full user-facing
136    /// message. The message must describe the detected language and
137    /// point the user at explicit `--lang` flags they can pass.
138    pub fn autodetect_unsupported(message: impl Into<String>) -> Self {
139        Self::AutodetectUnsupported {
140            message: message.into(),
141        }
142    }
143
144    /// Create a Timeout error
145    pub fn timeout(seconds: u64) -> Self {
146        Self::Timeout { seconds }
147    }
148
149    /// Get the appropriate exit code for this error
150    pub fn exit_code(&self) -> i32 {
151        match self {
152            // Special exit code for findings (scan ran, had results)
153            Self::FindingsDetected { .. } => 2,
154            // Special exit code for "scan not attempted because
155            // autodetected language is outside the supported set".
156            // Distinct from exit 1 (general failure) so tooling can
157            // tell the difference between "ran and errored" and
158            // "didn't run at all".
159            Self::AutodetectUnsupported { .. } => 2,
160            _ => 1, // General error
161        }
162    }
163}
164
165/// Result type alias for remaining commands
166pub type RemainingResult<T> = Result<T, RemainingError>;
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_file_not_found_error() {
174        let err = RemainingError::file_not_found("/path/to/file.py");
175        assert!(err.to_string().contains("file not found"));
176        assert!(err.to_string().contains("file.py"));
177    }
178
179    #[test]
180    fn test_symbol_not_found_error() {
181        let err = RemainingError::symbol_not_found("my_function", "/path/to/file.py");
182        assert!(err.to_string().contains("my_function"));
183        assert!(err.to_string().contains("not found"));
184    }
185
186    #[test]
187    fn test_exit_codes() {
188        assert_eq!(RemainingError::file_not_found("/foo").exit_code(), 1);
189        assert_eq!(RemainingError::findings_detected(5).exit_code(), 2);
190        assert_eq!(
191            RemainingError::autodetect_unsupported("nope").exit_code(),
192            2
193        );
194    }
195}