mcpls_core/
error.rs

1//! Error types for mcpls-core.
2//!
3//! This module defines the canonical error type for the library,
4//! following the Microsoft Rust Guidelines for error handling.
5
6use std::path::PathBuf;
7
8/// Details of a single server spawn failure.
9#[derive(Debug, Clone)]
10pub struct ServerSpawnFailure {
11    /// Language ID of the failed server.
12    pub language_id: String,
13    /// Command that was attempted.
14    pub command: String,
15    /// Error message describing the failure.
16    pub message: String,
17}
18
19impl std::fmt::Display for ServerSpawnFailure {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        write!(
22            f,
23            "{} ({}): {}",
24            self.language_id, self.command, self.message
25        )
26    }
27}
28
29/// The main error type for mcpls-core operations.
30#[derive(Debug, thiserror::Error)]
31pub enum Error {
32    /// LSP server failed to initialize.
33    #[error("LSP server initialization failed: {message}")]
34    LspInitFailed {
35        /// Description of the initialization failure.
36        message: String,
37    },
38
39    /// LSP server returned an error response.
40    #[error("LSP server error: {code} - {message}")]
41    LspServerError {
42        /// JSON-RPC error code.
43        code: i32,
44        /// Error message from the server.
45        message: String,
46    },
47
48    /// MCP server error.
49    #[error("MCP server error: {0}")]
50    McpServer(String),
51
52    /// Document was not found or could not be opened.
53    #[error("document not found: {0}")]
54    DocumentNotFound(PathBuf),
55
56    /// No LSP server configured for the given language.
57    #[error("no LSP server configured for language: {0}")]
58    NoServerForLanguage(String),
59
60    /// No LSP server is currently configured.
61    #[error("no LSP server configured")]
62    NoServerConfigured,
63
64    /// Configuration error.
65    #[error("configuration error: {0}")]
66    Config(String),
67
68    /// Configuration file not found.
69    #[error("configuration file not found: {0}")]
70    ConfigNotFound(PathBuf),
71
72    /// Invalid configuration format.
73    #[error("invalid configuration: {0}")]
74    InvalidConfig(String),
75
76    /// I/O error.
77    #[error("I/O error: {0}")]
78    Io(#[from] std::io::Error),
79
80    /// JSON serialization/deserialization error.
81    #[error("JSON error: {0}")]
82    Json(#[from] serde_json::Error),
83
84    /// TOML deserialization error.
85    #[error("TOML parsing error: {0}")]
86    TomlDe(#[from] toml::de::Error),
87
88    /// TOML serialization error.
89    #[error("TOML serialization error: {0}")]
90    TomlSer(#[from] toml::ser::Error),
91
92    /// LSP client transport error.
93    #[error("transport error: {0}")]
94    Transport(String),
95
96    /// Request timeout.
97    #[error("request timed out after {0} seconds")]
98    Timeout(u64),
99
100    /// Server shutdown requested.
101    #[error("server shutdown requested")]
102    Shutdown,
103
104    /// LSP server failed to spawn.
105    #[error("failed to spawn LSP server '{command}': {source}")]
106    ServerSpawnFailed {
107        /// Command that failed to spawn.
108        command: String,
109        /// Underlying IO error.
110        #[source]
111        source: std::io::Error,
112    },
113
114    /// LSP protocol error during message parsing.
115    #[error("LSP protocol error: {0}")]
116    LspProtocolError(String),
117
118    /// Invalid URI format.
119    #[error("invalid URI: {0}")]
120    InvalidUri(String),
121
122    /// Position encoding error.
123    #[error("position encoding error: {0}")]
124    EncodingError(String),
125
126    /// Server process terminated unexpectedly.
127    #[error("LSP server process terminated unexpectedly")]
128    ServerTerminated,
129
130    /// Invalid tool parameters provided.
131    #[error("invalid tool parameters: {0}")]
132    InvalidToolParams(String),
133
134    /// File I/O error occurred.
135    #[error("file I/O error for {path:?}: {source}")]
136    FileIo {
137        /// Path to the file.
138        path: PathBuf,
139        /// Underlying I/O error.
140        #[source]
141        source: std::io::Error,
142    },
143
144    /// Path is outside allowed workspace boundaries.
145    #[error("path outside workspace: {0}")]
146    PathOutsideWorkspace(PathBuf),
147
148    /// Document limit exceeded.
149    #[error("document limit exceeded: {current}/{max}")]
150    DocumentLimitExceeded {
151        /// Current number of documents.
152        current: usize,
153        /// Maximum allowed documents.
154        max: usize,
155    },
156
157    /// File size limit exceeded.
158    #[error("file size limit exceeded: {size} bytes (max: {max} bytes)")]
159    FileSizeLimitExceeded {
160        /// Actual file size.
161        size: u64,
162        /// Maximum allowed size.
163        max: u64,
164    },
165
166    /// Partial server initialization - some servers failed but at least one succeeded.
167    #[error("some LSP servers failed to initialize: {failed_count}/{total_count} servers")]
168    PartialServerInit {
169        /// Number of servers that failed.
170        failed_count: usize,
171        /// Total number of configured servers.
172        total_count: usize,
173        /// Details of each failure.
174        failures: Vec<ServerSpawnFailure>,
175    },
176
177    /// All configured LSP servers failed to initialize.
178    #[error("all LSP servers failed to initialize ({count} configured)")]
179    AllServersFailedToInit {
180        /// Number of servers that were configured.
181        count: usize,
182        /// Details of each failure.
183        failures: Vec<ServerSpawnFailure>,
184    },
185
186    /// No LSP servers available (none configured or all failed).
187    #[error("{0}")]
188    NoServersAvailable(String),
189}
190
191/// A specialized Result type for mcpls-core operations.
192pub type Result<T> = std::result::Result<T, Error>;
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_error_display_lsp_init_failed() {
200        let err = Error::LspInitFailed {
201            message: "server not found".to_string(),
202        };
203        assert_eq!(
204            err.to_string(),
205            "LSP server initialization failed: server not found"
206        );
207    }
208
209    #[test]
210    fn test_error_display_lsp_server_error() {
211        let err = Error::LspServerError {
212            code: -32600,
213            message: "Invalid request".to_string(),
214        };
215        assert_eq!(
216            err.to_string(),
217            "LSP server error: -32600 - Invalid request"
218        );
219    }
220
221    #[test]
222    fn test_error_display_document_not_found() {
223        let err = Error::DocumentNotFound(PathBuf::from("/path/to/file.rs"));
224        assert!(err.to_string().contains("document not found"));
225        assert!(err.to_string().contains("file.rs"));
226    }
227
228    #[test]
229    fn test_error_display_no_server_for_language() {
230        let err = Error::NoServerForLanguage("rust".to_string());
231        assert_eq!(
232            err.to_string(),
233            "no LSP server configured for language: rust"
234        );
235    }
236
237    #[test]
238    fn test_error_display_timeout() {
239        let err = Error::Timeout(30);
240        assert_eq!(err.to_string(), "request timed out after 30 seconds");
241    }
242
243    #[test]
244    fn test_error_display_document_limit() {
245        let err = Error::DocumentLimitExceeded {
246            current: 150,
247            max: 100,
248        };
249        assert_eq!(err.to_string(), "document limit exceeded: 150/100");
250    }
251
252    #[test]
253    fn test_error_display_file_size_limit() {
254        let err = Error::FileSizeLimitExceeded {
255            size: 20_000_000,
256            max: 10_000_000,
257        };
258        assert!(err.to_string().contains("file size limit exceeded"));
259    }
260
261    #[test]
262    fn test_error_from_io() {
263        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
264        let err: Error = io_err.into();
265        assert!(matches!(err, Error::Io(_)));
266    }
267
268    #[test]
269    #[allow(clippy::unwrap_used)]
270    fn test_error_from_json() {
271        let json_str = "{invalid json}";
272        let json_err = serde_json::from_str::<serde_json::Value>(json_str).unwrap_err();
273        let err: Error = json_err.into();
274        assert!(matches!(err, Error::Json(_)));
275    }
276
277    #[test]
278    #[allow(clippy::unwrap_used)]
279    fn test_error_from_toml_de() {
280        let toml_str = "[invalid toml";
281        let toml_err = toml::from_str::<toml::Value>(toml_str).unwrap_err();
282        let err: Error = toml_err.into();
283        assert!(matches!(err, Error::TomlDe(_)));
284    }
285
286    #[test]
287    fn test_result_type_alias() {
288        fn _returns_error() -> Result<i32> {
289            Err(Error::Config("test error".to_string()))
290        }
291
292        let result: Result<i32> = Ok(42);
293        assert!(result.is_ok());
294        if let Ok(value) = result {
295            assert_eq!(value, 42);
296        }
297    }
298
299    #[test]
300    fn test_error_source_chain() {
301        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
302        let err = Error::ServerSpawnFailed {
303            command: "rust-analyzer".to_string(),
304            source: io_err,
305        };
306
307        let source = std::error::Error::source(&err);
308        assert!(source.is_some());
309    }
310
311    #[test]
312    fn test_server_spawn_failure_display() {
313        let failure = ServerSpawnFailure {
314            language_id: "rust".to_string(),
315            command: "rust-analyzer".to_string(),
316            message: "No such file or directory".to_string(),
317        };
318        assert_eq!(
319            failure.to_string(),
320            "rust (rust-analyzer): No such file or directory"
321        );
322    }
323
324    #[test]
325    fn test_server_spawn_failure_debug() {
326        let failure = ServerSpawnFailure {
327            language_id: "python".to_string(),
328            command: "pyright".to_string(),
329            message: "command not found".to_string(),
330        };
331        let debug_str = format!("{failure:?}");
332        assert!(debug_str.contains("python"));
333        assert!(debug_str.contains("pyright"));
334        assert!(debug_str.contains("command not found"));
335    }
336
337    #[test]
338    fn test_server_spawn_failure_clone() {
339        let failure = ServerSpawnFailure {
340            language_id: "typescript".to_string(),
341            command: "tsserver".to_string(),
342            message: "failed to start".to_string(),
343        };
344        let cloned = failure.clone();
345        assert_eq!(failure.language_id, cloned.language_id);
346        assert_eq!(failure.command, cloned.command);
347        assert_eq!(failure.message, cloned.message);
348    }
349
350    #[test]
351    fn test_error_display_partial_server_init() {
352        let err = Error::PartialServerInit {
353            failed_count: 2,
354            total_count: 3,
355            failures: vec![],
356        };
357        assert_eq!(
358            err.to_string(),
359            "some LSP servers failed to initialize: 2/3 servers"
360        );
361    }
362
363    #[test]
364    fn test_error_display_all_servers_failed_to_init() {
365        let err = Error::AllServersFailedToInit {
366            count: 2,
367            failures: vec![],
368        };
369        assert_eq!(
370            err.to_string(),
371            "all LSP servers failed to initialize (2 configured)"
372        );
373    }
374
375    #[test]
376    fn test_error_all_servers_failed_with_failures() {
377        let failures = vec![
378            ServerSpawnFailure {
379                language_id: "rust".to_string(),
380                command: "rust-analyzer".to_string(),
381                message: "not found".to_string(),
382            },
383            ServerSpawnFailure {
384                language_id: "python".to_string(),
385                command: "pyright".to_string(),
386                message: "permission denied".to_string(),
387            },
388        ];
389
390        let err = Error::AllServersFailedToInit { count: 2, failures };
391
392        assert!(err.to_string().contains("all LSP servers failed"));
393        assert!(err.to_string().contains("2 configured"));
394    }
395
396    #[test]
397    fn test_error_partial_server_init_with_failures() {
398        let failures = vec![ServerSpawnFailure {
399            language_id: "python".to_string(),
400            command: "pyright".to_string(),
401            message: "not found".to_string(),
402        }];
403
404        let err = Error::PartialServerInit {
405            failed_count: 1,
406            total_count: 2,
407            failures,
408        };
409
410        assert!(err.to_string().contains("some LSP servers failed"));
411        assert!(err.to_string().contains("1/2"));
412    }
413
414    #[test]
415    fn test_error_display_no_servers_available() {
416        let err =
417            Error::NoServersAvailable("none configured or all failed to initialize".to_string());
418        assert_eq!(
419            err.to_string(),
420            "none configured or all failed to initialize"
421        );
422    }
423
424    #[test]
425    fn test_error_no_servers_available_with_custom_message() {
426        let custom_msg = "none configured or all failed to initialize";
427        let err = Error::NoServersAvailable(custom_msg.to_string());
428        assert_eq!(err.to_string(), custom_msg);
429    }
430}