tldr_cli/commands/contracts/
error.rs1use std::io;
8use std::path::PathBuf;
9
10use thiserror::Error;
11
12#[derive(Debug, Error)]
17pub enum ContractsError {
18 #[error("file not found: {}", path.display())]
20 FileNotFound {
21 path: PathBuf,
23 },
24
25 #[error("function '{function}' not found in {}", file.display())]
27 FunctionNotFound {
28 function: String,
30 file: PathBuf,
32 },
33
34 #[error("test path not found: {}", path.display())]
36 TestPathNotFound {
37 path: PathBuf,
39 },
40
41 #[error("line {line} is outside function '{function}' (lines {start}-{end})")]
43 LineOutsideFunction {
44 line: u32,
46 function: String,
48 start: u32,
50 end: u32,
52 },
53
54 #[error("parse error in {}: {message}", file.display())]
56 ParseError {
57 file: PathBuf,
59 message: String,
61 },
62
63 #[error("SSA construction failed: {0}")]
65 SsaError(String),
66
67 #[error("analysis did not converge after {iterations} iterations")]
69 DidNotConverge {
70 iterations: u32,
72 },
73
74 #[error("sub-analysis '{name}' failed: {message}")]
76 SubAnalysisFailed {
77 name: String,
79 message: String,
81 },
82
83 #[error("no test directory found in {}", project.display())]
85 NoTestDirectory {
86 project: PathBuf,
88 },
89
90 #[error("operation timed out after {timeout_secs}s")]
92 Timeout {
93 timeout_secs: u64,
95 },
96
97 #[error("file too large: {} ({bytes} bytes, max {max_bytes} bytes)", path.display())]
99 FileTooLarge {
100 path: PathBuf,
102 bytes: u64,
104 max_bytes: u64,
106 },
107
108 #[error("AST too deeply nested in {}: depth {depth} exceeds limit {max_depth}", file.display())]
110 AstTooDeep {
111 file: PathBuf,
113 depth: u32,
115 max_depth: u32,
117 },
118
119 #[error("SSA graph too large: {nodes} nodes exceeds limit {max_nodes}")]
121 SsaTooLarge {
122 nodes: u32,
124 max_nodes: u32,
126 },
127
128 #[error("slice computation exceeded depth limit of {max_depth}")]
130 SliceDepthExceeded {
131 max_depth: u32,
133 },
134
135 #[error("invalid function name: {reason}")]
137 InvalidFunctionName {
138 reason: String,
140 },
141
142 #[error("path traversal blocked: {} attempts to escape project root", path.display())]
144 PathTraversal {
145 path: PathBuf,
147 },
148
149 #[error("IO error: {0}")]
151 Io(#[from] io::Error),
152
153 #[error("JSON error: {0}")]
155 Json(#[from] serde_json::Error),
156}
157
158pub type ContractsResult<T> = Result<T, ContractsError>;
160
161impl ContractsError {
166 pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
168 Self::FileNotFound { path: path.into() }
169 }
170
171 pub fn function_not_found(function: impl Into<String>, file: impl Into<PathBuf>) -> Self {
173 Self::FunctionNotFound {
174 function: function.into(),
175 file: file.into(),
176 }
177 }
178
179 pub fn parse_error(file: impl Into<PathBuf>, message: impl Into<String>) -> Self {
181 Self::ParseError {
182 file: file.into(),
183 message: message.into(),
184 }
185 }
186
187 pub fn ssa_error(message: impl Into<String>) -> Self {
189 Self::SsaError(message.into())
190 }
191
192 pub fn line_outside_function(
194 line: u32,
195 function: impl Into<String>,
196 start: u32,
197 end: u32,
198 ) -> Self {
199 Self::LineOutsideFunction {
200 line,
201 function: function.into(),
202 start,
203 end,
204 }
205 }
206}
207
208#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_error_file_not_found() {
218 let err = ContractsError::file_not_found("/path/to/file.py");
219 let msg = err.to_string();
220 assert!(msg.contains("file not found"));
221 assert!(msg.contains("file.py"));
222 }
223
224 #[test]
225 fn test_error_function_not_found() {
226 let err = ContractsError::function_not_found("my_func", "/path/to/file.py");
227 let msg = err.to_string();
228 assert!(msg.contains("my_func"));
229 assert!(msg.contains("not found"));
230 assert!(msg.contains("file.py"));
231 }
232
233 #[test]
234 fn test_error_parse_error() {
235 let err = ContractsError::parse_error("/path/to/file.py", "unexpected token");
236 let msg = err.to_string();
237 assert!(msg.contains("parse error"));
238 assert!(msg.contains("unexpected token"));
239 }
240
241 #[test]
242 fn test_error_ssa_error() {
243 let err = ContractsError::ssa_error("failed to compute dominators");
244 let msg = err.to_string();
245 assert!(msg.contains("SSA construction failed"));
246 assert!(msg.contains("dominators"));
247 }
248
249 #[test]
250 fn test_error_line_outside_function() {
251 let err = ContractsError::line_outside_function(100, "my_func", 10, 50);
252 let msg = err.to_string();
253 assert!(msg.contains("line 100"));
254 assert!(msg.contains("my_func"));
255 assert!(msg.contains("10-50"));
256 }
257
258 #[test]
259 fn test_error_test_path_not_found() {
260 let err = ContractsError::TestPathNotFound {
261 path: PathBuf::from("/path/to/tests"),
262 };
263 let msg = err.to_string();
264 assert!(msg.contains("test path not found"));
265 }
266
267 #[test]
268 fn test_error_did_not_converge() {
269 let err = ContractsError::DidNotConverge { iterations: 50 };
270 let msg = err.to_string();
271 assert!(msg.contains("did not converge"));
272 assert!(msg.contains("50"));
273 }
274
275 #[test]
276 fn test_error_timeout() {
277 let err = ContractsError::Timeout { timeout_secs: 60 };
278 let msg = err.to_string();
279 assert!(msg.contains("timed out"));
280 assert!(msg.contains("60s"));
281 }
282
283 #[test]
284 fn test_error_file_too_large() {
285 let err = ContractsError::FileTooLarge {
286 path: PathBuf::from("/path/to/large.py"),
287 bytes: 15_000_000,
288 max_bytes: 10_000_000,
289 };
290 let msg = err.to_string();
291 assert!(msg.contains("file too large"));
292 assert!(msg.contains("large.py"));
293 }
294
295 #[test]
296 fn test_error_path_traversal() {
297 let err = ContractsError::PathTraversal {
298 path: PathBuf::from("../../etc/passwd"),
299 };
300 let msg = err.to_string();
301 assert!(msg.contains("path traversal blocked"));
302 }
303
304 #[test]
305 fn test_error_sub_analysis_failed() {
306 let err = ContractsError::SubAnalysisFailed {
307 name: "contracts".to_string(),
308 message: "parse error".to_string(),
309 };
310 let msg = err.to_string();
311 assert!(msg.contains("sub-analysis"));
312 assert!(msg.contains("contracts"));
313 }
314
315 #[test]
316 fn test_error_no_test_directory() {
317 let err = ContractsError::NoTestDirectory {
318 project: PathBuf::from("/path/to/project"),
319 };
320 let msg = err.to_string();
321 assert!(msg.contains("no test directory"));
322 }
323
324 #[test]
325 fn test_error_io_from() {
326 let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
327 let contracts_err: ContractsError = io_err.into();
328 assert!(matches!(contracts_err, ContractsError::Io(_)));
329 }
330
331 #[test]
332 fn test_error_json_from() {
333 let json_str = "{ invalid json }";
334 let json_result: Result<serde_json::Value, _> = serde_json::from_str(json_str);
335 let json_err = json_result.unwrap_err();
336 let contracts_err: ContractsError = json_err.into();
337 assert!(matches!(contracts_err, ContractsError::Json(_)));
338 }
339
340 #[test]
341 fn test_result_type_alias() {
342 fn example_fn() -> ContractsResult<i32> {
343 Ok(42)
344 }
345
346 fn example_err() -> ContractsResult<i32> {
347 Err(ContractsError::file_not_found("/test.py"))
348 }
349
350 assert_eq!(example_fn().unwrap(), 42);
351 assert!(example_err().is_err());
352 }
353}