Skip to main content

tldr_cli/commands/contracts/
error.rs

1//! Error types for Contracts & Flow commands
2//!
3//! Provides specific error types for all failure modes in the contracts
4//! and flow analysis commands. Errors include actionable information like
5//! file paths and line numbers.
6
7use std::io;
8use std::path::PathBuf;
9
10use thiserror::Error;
11
12/// Errors specific to contracts and flow analysis commands.
13///
14/// Each variant includes contextual information to help users understand
15/// and fix the issue.
16#[derive(Debug, Error)]
17pub enum ContractsError {
18    /// Source file not found.
19    #[error("file not found: {}", path.display())]
20    FileNotFound {
21        /// Path that was not found
22        path: PathBuf,
23    },
24
25    /// Function not found in source file.
26    #[error("function '{function}' not found in {}", file.display())]
27    FunctionNotFound {
28        /// Function name that was searched for
29        function: String,
30        /// File that was searched
31        file: PathBuf,
32    },
33
34    /// Test path not found.
35    #[error("test path not found: {}", path.display())]
36    TestPathNotFound {
37        /// Path that was not found
38        path: PathBuf,
39    },
40
41    /// Line number is outside function range.
42    #[error("line {line} is outside function '{function}' (lines {start}-{end})")]
43    LineOutsideFunction {
44        /// Line number that was requested
45        line: u32,
46        /// Function name
47        function: String,
48        /// Start line of function
49        start: u32,
50        /// End line of function
51        end: u32,
52    },
53
54    /// Parse error in source file.
55    #[error("parse error in {}: {message}", file.display())]
56    ParseError {
57        /// File that failed to parse
58        file: PathBuf,
59        /// Parser error message
60        message: String,
61    },
62
63    /// SSA construction failed.
64    #[error("SSA construction failed: {0}")]
65    SsaError(String),
66
67    /// Analysis did not converge within iteration limit.
68    #[error("analysis did not converge after {iterations} iterations")]
69    DidNotConverge {
70        /// Number of iterations attempted
71        iterations: u32,
72    },
73
74    /// Sub-analysis failed in verify command.
75    #[error("sub-analysis '{name}' failed: {message}")]
76    SubAnalysisFailed {
77        /// Name of the sub-analysis that failed
78        name: String,
79        /// Error message from the sub-analysis
80        message: String,
81    },
82
83    /// No test directory found in project.
84    #[error("no test directory found in {}", project.display())]
85    NoTestDirectory {
86        /// Project directory that was searched
87        project: PathBuf,
88    },
89
90    /// Operation timed out.
91    #[error("operation timed out after {timeout_secs}s")]
92    Timeout {
93        /// Timeout duration in seconds
94        timeout_secs: u64,
95    },
96
97    /// File too large to analyze.
98    #[error("file too large: {} ({bytes} bytes, max {max_bytes} bytes)", path.display())]
99    FileTooLarge {
100        /// Path to the file
101        path: PathBuf,
102        /// Actual file size
103        bytes: u64,
104        /// Maximum allowed size
105        max_bytes: u64,
106    },
107
108    /// AST too deeply nested.
109    #[error("AST too deeply nested in {}: depth {depth} exceeds limit {max_depth}", file.display())]
110    AstTooDeep {
111        /// File with deeply nested AST
112        file: PathBuf,
113        /// Actual depth
114        depth: u32,
115        /// Maximum allowed depth
116        max_depth: u32,
117    },
118
119    /// SSA graph has too many nodes.
120    #[error("SSA graph too large: {nodes} nodes exceeds limit {max_nodes}")]
121    SsaTooLarge {
122        /// Actual number of nodes
123        nodes: u32,
124        /// Maximum allowed nodes
125        max_nodes: u32,
126    },
127
128    /// Slice computation exceeded depth limit.
129    #[error("slice computation exceeded depth limit of {max_depth}")]
130    SliceDepthExceeded {
131        /// Maximum allowed depth
132        max_depth: u32,
133    },
134
135    /// Invalid function name.
136    #[error("invalid function name: {reason}")]
137    InvalidFunctionName {
138        /// Why the name is invalid
139        reason: String,
140    },
141
142    /// Path traversal attempt detected.
143    #[error("path traversal blocked: {} attempts to escape project root", path.display())]
144    PathTraversal {
145        /// Suspicious path
146        path: PathBuf,
147    },
148
149    /// Generic IO error.
150    #[error("IO error: {0}")]
151    Io(#[from] io::Error),
152
153    /// JSON serialization/deserialization error.
154    #[error("JSON error: {0}")]
155    Json(#[from] serde_json::Error),
156}
157
158/// Result type for contracts commands.
159pub type ContractsResult<T> = Result<T, ContractsError>;
160
161// =============================================================================
162// Error Construction Helpers
163// =============================================================================
164
165impl ContractsError {
166    /// Create a FileNotFound error.
167    pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
168        Self::FileNotFound { path: path.into() }
169    }
170
171    /// Create a FunctionNotFound error.
172    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    /// Create a ParseError.
180    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    /// Create an SsaError.
188    pub fn ssa_error(message: impl Into<String>) -> Self {
189        Self::SsaError(message.into())
190    }
191
192    /// Create a LineOutsideFunction error.
193    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// =============================================================================
209// Tests
210// =============================================================================
211
212#[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}