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    ///
151    /// med-low-schema-cleanup-v1 (N9): standardized the
152    /// `tldr definition` failure codes:
153    /// - `FileNotFound` → 5 (filesystem-class error, mirrors the rest
154    ///   of the CLI where missing input files map to the 2-9 band).
155    /// - `SymbolNotFound` → 20 (analysis-class error, mirrors
156    ///   `tldr_core::TldrError::FunctionNotFound` exit 20 used by
157    ///   `tldr impact`).
158    ///
159    /// Pre-fix all `definition` failures collapsed onto exit 1
160    /// (generic), so callers had no way to distinguish "I gave a bad
161    /// path" from "the symbol genuinely isn't there".
162    pub fn exit_code(&self) -> i32 {
163        match self {
164            // Filesystem class (N9): missing input file.
165            Self::FileNotFound { .. } => 5,
166            // Analysis class (N9): the symbol genuinely doesn't exist
167            // in the file. Matches the `impact` exit-20 convention.
168            Self::SymbolNotFound { .. } => 20,
169            // Special exit code for findings (scan ran, had results)
170            Self::FindingsDetected { .. } => 2,
171            // Special exit code for "scan not attempted because
172            // autodetected language is outside the supported set".
173            // Distinct from exit 1 (general failure) so tooling can
174            // tell the difference between "ran and errored" and
175            // "didn't run at all".
176            Self::AutodetectUnsupported { .. } => 2,
177            _ => 1, // General error
178        }
179    }
180}
181
182/// Result type alias for remaining commands
183pub type RemainingResult<T> = Result<T, RemainingError>;
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_file_not_found_error() {
191        let err = RemainingError::file_not_found("/path/to/file.py");
192        assert!(err.to_string().contains("file not found"));
193        assert!(err.to_string().contains("file.py"));
194    }
195
196    #[test]
197    fn test_symbol_not_found_error() {
198        let err = RemainingError::symbol_not_found("my_function", "/path/to/file.py");
199        assert!(err.to_string().contains("my_function"));
200        assert!(err.to_string().contains("not found"));
201    }
202
203    #[test]
204    fn test_exit_codes() {
205        // med-low-schema-cleanup-v1 (N9): file_not_found is now 5
206        // (filesystem-class) and symbol_not_found is now 20
207        // (analysis-class, matches `tldr impact` convention).
208        assert_eq!(RemainingError::file_not_found("/foo").exit_code(), 5);
209        assert_eq!(
210            RemainingError::symbol_not_found("foo", "/bar.py").exit_code(),
211            20
212        );
213        assert_eq!(RemainingError::findings_detected(5).exit_code(), 2);
214        assert_eq!(
215            RemainingError::autodetect_unsupported("nope").exit_code(),
216            2
217        );
218    }
219}