Skip to main content

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