Skip to main content

tldr_cli/commands/patterns/
error.rs

1//! Error types for Pattern Analysis commands.
2//!
3//! This module provides the `PatternsError` enum and `PatternsResult<T>` type alias
4//! for all pattern analysis operations.
5
6use std::path::PathBuf;
7use thiserror::Error;
8
9/// Errors specific to pattern analysis commands.
10#[derive(Debug, Error)]
11pub enum PatternsError {
12    /// Source file not found.
13    #[error("file not found: {}", path.display())]
14    FileNotFound { path: PathBuf },
15
16    /// Function not found in source file.
17    #[error("function '{function}' not found in {}", file.display())]
18    FunctionNotFound { function: String, file: PathBuf },
19
20    /// Class not found in source file.
21    #[error("class '{class_name}' not found in {}", file.display())]
22    ClassNotFound { class_name: String, file: PathBuf },
23
24    /// Parse error in source file.
25    #[error("parse error in {}: {message}", file.display())]
26    ParseError { file: PathBuf, message: String },
27
28    /// File too large to analyze.
29    #[error("file too large: {} ({bytes} bytes, max {max_bytes} bytes)", path.display())]
30    FileTooLarge {
31        path: PathBuf,
32        bytes: u64,
33        max_bytes: u64,
34    },
35
36    /// Directory scan limit exceeded.
37    #[error("directory scan limit exceeded: {count} files found, max {max_files}")]
38    TooManyFiles { count: u32, max_files: u32 },
39
40    /// Analysis depth limit exceeded.
41    #[error("analysis depth limit exceeded: depth {depth}, max {max_depth}")]
42    DepthLimitExceeded { depth: u32, max_depth: u32 },
43
44    /// Analysis timed out.
45    #[error("analysis timed out after {timeout_secs}s")]
46    Timeout { timeout_secs: u64 },
47
48    /// Invalid parameter value.
49    #[error("invalid parameter: {message}")]
50    InvalidParameter { message: String },
51
52    /// Path traversal attempt detected.
53    #[error("path traversal blocked: {} attempts to escape project root", path.display())]
54    PathTraversal { path: PathBuf },
55
56    /// Path is not a directory.
57    #[error("path is not a directory: {}", path.display())]
58    NotADirectory { path: PathBuf },
59
60    /// Unsupported language.
61    #[error("unsupported language: {language}")]
62    UnsupportedLanguage { language: String },
63
64    /// No constraints found (not an error, but special exit code).
65    #[error("no constraints found matching criteria")]
66    NoConstraintsFound,
67
68    /// Issues found (for resources command).
69    #[error("resource issues found: {leaks} leaks, {double_closes} double-closes, {use_after_closes} use-after-close")]
70    IssuesFound {
71        leaks: u32,
72        double_closes: u32,
73        use_after_closes: u32,
74    },
75
76    /// Generic IO error.
77    #[error("IO error: {0}")]
78    Io(#[from] std::io::Error),
79
80    /// JSON serialization error.
81    #[error("JSON error: {0}")]
82    Json(#[from] serde_json::Error),
83}
84
85/// Result type for pattern analysis commands.
86pub type PatternsResult<T> = Result<T, PatternsError>;
87
88impl PatternsError {
89    /// Create a FileNotFound error.
90    pub fn file_not_found(path: impl Into<PathBuf>) -> Self {
91        Self::FileNotFound { path: path.into() }
92    }
93
94    /// Create a FunctionNotFound error.
95    pub fn function_not_found(function: impl Into<String>, file: impl Into<PathBuf>) -> Self {
96        Self::FunctionNotFound {
97            function: function.into(),
98            file: file.into(),
99        }
100    }
101
102    /// Create a ClassNotFound error.
103    pub fn class_not_found(class_name: impl Into<String>, file: impl Into<PathBuf>) -> Self {
104        Self::ClassNotFound {
105            class_name: class_name.into(),
106            file: file.into(),
107        }
108    }
109
110    /// Create a ParseError.
111    pub fn parse_error(file: impl Into<PathBuf>, message: impl Into<String>) -> Self {
112        Self::ParseError {
113            file: file.into(),
114            message: message.into(),
115        }
116    }
117
118    /// Create an InvalidParameter error.
119    pub fn invalid_parameter(message: impl Into<String>) -> Self {
120        Self::InvalidParameter {
121            message: message.into(),
122        }
123    }
124
125    /// Create a PathTraversal error.
126    pub fn path_traversal(path: impl Into<PathBuf>) -> Self {
127        Self::PathTraversal { path: path.into() }
128    }
129
130    /// Create a FileTooLarge error.
131    pub fn file_too_large(path: impl Into<PathBuf>, bytes: u64, max_bytes: u64) -> Self {
132        Self::FileTooLarge {
133            path: path.into(),
134            bytes,
135            max_bytes,
136        }
137    }
138
139    /// Create a DepthLimitExceeded error.
140    pub fn depth_exceeded(depth: u32, max_depth: u32) -> Self {
141        Self::DepthLimitExceeded { depth, max_depth }
142    }
143
144    /// Create a TooManyFiles error.
145    pub fn too_many_files(count: u32, max_files: u32) -> Self {
146        Self::TooManyFiles { count, max_files }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_error_file_not_found_message() {
156        let err = PatternsError::file_not_found("/path/to/file.py");
157        let msg = err.to_string();
158        assert!(msg.contains("file not found"));
159        assert!(msg.contains("file.py"));
160    }
161
162    #[test]
163    fn test_error_function_not_found_message() {
164        let err = PatternsError::function_not_found("my_func", "/path/to/file.py");
165        let msg = err.to_string();
166        assert!(msg.contains("my_func"));
167        assert!(msg.contains("not found"));
168    }
169
170    #[test]
171    fn test_error_path_traversal_message() {
172        let err = PatternsError::path_traversal("../etc/passwd");
173        let msg = err.to_string();
174        assert!(msg.contains("path traversal"));
175        assert!(msg.contains("etc/passwd"));
176    }
177
178    #[test]
179    fn test_error_file_too_large_message() {
180        let err = PatternsError::file_too_large("/big/file.py", 20_000_000, 10_000_000);
181        let msg = err.to_string();
182        assert!(msg.contains("too large"));
183        assert!(msg.contains("20000000"));
184    }
185
186    #[test]
187    fn test_error_depth_exceeded_message() {
188        let err = PatternsError::depth_exceeded(150, 100);
189        let msg = err.to_string();
190        assert!(msg.contains("depth"));
191        assert!(msg.contains("150"));
192        assert!(msg.contains("100"));
193    }
194
195    #[test]
196    fn test_error_too_many_files_message() {
197        let err = PatternsError::too_many_files(1500, 1000);
198        let msg = err.to_string();
199        assert!(msg.contains("1500"));
200        assert!(msg.contains("1000"));
201    }
202
203    #[test]
204    fn test_error_parse_error_message() {
205        let err = PatternsError::parse_error("/file.py", "unexpected token");
206        let msg = err.to_string();
207        assert!(msg.contains("parse error"));
208        assert!(msg.contains("unexpected token"));
209    }
210
211    #[test]
212    fn test_error_invalid_parameter_message() {
213        let err = PatternsError::invalid_parameter("value must be positive");
214        let msg = err.to_string();
215        assert!(msg.contains("invalid parameter"));
216        assert!(msg.contains("positive"));
217    }
218}