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