Skip to main content

dissolve_python/
error.rs

1// Copyright (C) 2024 Jelmer Vernooij <jelmer@samba.org>
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//    http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Comprehensive error handling for the dissolve library
16
17use crate::domain_types::{ModuleName, SourcePath};
18use std::io;
19use thiserror::Error;
20
21/// The main error type for all dissolve operations
22#[derive(Debug, Error)]
23pub enum DissolveError {
24    #[error("IO error: {0}")]
25    Io(#[from] io::Error),
26
27    #[error("Parse error in {file}: {message}")]
28    Parse {
29        file: SourcePath,
30        message: String,
31        #[source]
32        source: Option<Box<dyn std::error::Error + Send + Sync>>,
33    },
34
35    #[error("Type introspection error: {0}")]
36    TypeIntrospection(#[from] TypeIntrospectionError),
37
38    #[error("Migration error in {module}: {message}")]
39    Migration { module: ModuleName, message: String },
40
41    #[error("Configuration error: {0}")]
42    Config(String),
43
44    #[error("Invalid input: {0}")]
45    InvalidInput(String),
46
47    #[error("Feature not implemented: {0}")]
48    NotImplemented(String),
49
50    #[error("Internal error: {0}")]
51    Internal(String),
52}
53
54#[derive(Debug, Error)]
55pub enum TypeIntrospectionError {
56    #[error("Pyright query failed: {0}")]
57    PyrightError(String),
58
59    #[error("Mypy query failed: {0}")]
60    MypyError(String),
61
62    #[error("Failed to determine position in source: {0}")]
63    PositionError(String),
64
65    #[error("No type introspection client available")]
66    NoClientAvailable,
67
68    #[error("LSP communication error: {0}")]
69    LspError(String),
70
71    #[error("Timeout waiting for LSP response after {seconds}s")]
72    Timeout { seconds: u64 },
73}
74
75#[derive(Debug, Error)]
76pub enum CollectorError {
77    #[error("Failed to parse decorator in {file} at line {line}: {message}")]
78    DecoratorParse {
79        file: SourcePath,
80        line: usize,
81        message: String,
82    },
83
84    #[error("Invalid replacement expression: {expression}")]
85    InvalidReplacement { expression: String },
86
87    #[error("Missing required parameter: {parameter}")]
88    MissingParameter { parameter: String },
89}
90
91/// Result type alias for convenience
92pub type Result<T, E = DissolveError> = std::result::Result<T, E>;
93
94/// Result type for type introspection operations
95pub type TypeResult<T> = std::result::Result<T, TypeIntrospectionError>;
96
97/// Result type for collection operations
98pub type CollectorResult<T> = std::result::Result<T, CollectorError>;
99
100impl DissolveError {
101    /// Create a parse error
102    pub fn parse_error(
103        file: SourcePath,
104        message: impl Into<String>,
105        source: Option<Box<dyn std::error::Error + Send + Sync>>,
106    ) -> Self {
107        Self::Parse {
108            file,
109            message: message.into(),
110            source,
111        }
112    }
113
114    /// Create a migration error
115    pub fn migration_error(module: ModuleName, message: impl Into<String>) -> Self {
116        Self::Migration {
117            module,
118            message: message.into(),
119        }
120    }
121
122    /// Create a configuration error
123    pub fn config_error(message: impl Into<String>) -> Self {
124        Self::Config(message.into())
125    }
126
127    /// Create an invalid input error
128    pub fn invalid_input(message: impl Into<String>) -> Self {
129        Self::InvalidInput(message.into())
130    }
131
132    /// Create a not implemented error
133    pub fn not_implemented(feature: impl Into<String>) -> Self {
134        Self::NotImplemented(feature.into())
135    }
136
137    /// Create an internal error
138    pub fn internal(message: impl Into<String>) -> Self {
139        Self::Internal(message.into())
140    }
141}
142
143impl TypeIntrospectionError {
144    /// Create a timeout error
145    pub fn timeout(seconds: u64) -> Self {
146        Self::Timeout { seconds }
147    }
148
149    /// Create an LSP error
150    pub fn lsp_error(message: impl Into<String>) -> Self {
151        Self::LspError(message.into())
152    }
153
154    /// Create a position error
155    pub fn position_error(message: impl Into<String>) -> Self {
156        Self::PositionError(message.into())
157    }
158}
159
160impl CollectorError {
161    /// Create a decorator parse error
162    pub fn decorator_parse(file: SourcePath, line: usize, message: impl Into<String>) -> Self {
163        Self::DecoratorParse {
164            file,
165            line,
166            message: message.into(),
167        }
168    }
169
170    /// Create an invalid replacement error
171    pub fn invalid_replacement(expression: impl Into<String>) -> Self {
172        Self::InvalidReplacement {
173            expression: expression.into(),
174        }
175    }
176
177    /// Create a missing parameter error
178    pub fn missing_parameter(parameter: impl Into<String>) -> Self {
179        Self::MissingParameter {
180            parameter: parameter.into(),
181        }
182    }
183}
184
185/// Extension trait to convert anyhow errors to DissolveError
186pub trait AnyhowExt<T> {
187    fn with_context(self, f: impl FnOnce() -> DissolveError) -> Result<T>;
188}
189
190impl<T> AnyhowExt<T> for anyhow::Result<T> {
191    fn with_context(self, f: impl FnOnce() -> DissolveError) -> Result<T> {
192        self.map_err(|_| f())
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::*;
199
200    #[test]
201    fn test_error_creation() {
202        let file = SourcePath::new("test.py");
203        let module = ModuleName::new("test_module");
204
205        let parse_err = DissolveError::parse_error(file.clone(), "syntax error", None);
206        assert!(matches!(parse_err, DissolveError::Parse { .. }));
207
208        let migration_err = DissolveError::migration_error(module, "failed to migrate");
209        assert!(matches!(migration_err, DissolveError::Migration { .. }));
210    }
211
212    #[test]
213    fn test_error_display() {
214        let err = TypeIntrospectionError::timeout(30);
215        assert_eq!(
216            err.to_string(),
217            "Timeout waiting for LSP response after 30s"
218        );
219    }
220}