Skip to main content

hx_core/
error.rs

1//! Error types for hx.
2
3use std::path::PathBuf;
4
5/// Result type alias using hx Error.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Error codes for categorizing failures.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ErrorCode {
11    /// Tool not found in PATH
12    ToolchainMissing,
13    /// Wrong version of a tool
14    ToolchainMismatch,
15    /// HLS version doesn't match GHC
16    HlsMismatch,
17    /// Native library not found
18    SystemDepMissing,
19    /// Package resolution failed
20    SolverFailure,
21    /// Compilation error
22    BuildFailure,
23    /// Invalid configuration
24    ConfigError,
25    /// I/O error
26    IoError,
27    /// Command execution failed
28    CommandFailed,
29    /// Lock file error
30    LockError,
31}
32
33/// A fix suggestion for an error.
34#[derive(Debug, Clone)]
35pub struct Fix {
36    /// Description of what this fix does
37    pub description: String,
38    /// Command to run, if applicable
39    pub command: Option<String>,
40}
41
42impl Fix {
43    /// Create a fix with just a description.
44    pub fn new(description: impl Into<String>) -> Self {
45        Self {
46            description: description.into(),
47            command: None,
48        }
49    }
50
51    /// Create a fix with a command.
52    pub fn with_command(description: impl Into<String>, command: impl Into<String>) -> Self {
53        Self {
54            description: description.into(),
55            command: Some(command.into()),
56        }
57    }
58}
59
60/// Structured error type for hx.
61#[derive(Debug, thiserror::Error)]
62pub enum Error {
63    #[error("toolchain not found: {tool}")]
64    ToolchainMissing {
65        tool: String,
66        #[source]
67        source: Option<Box<dyn std::error::Error + Send + Sync>>,
68        fixes: Vec<Fix>,
69    },
70
71    #[error("toolchain version mismatch for {tool}: expected {expected}, found {found}")]
72    ToolchainMismatch {
73        tool: String,
74        expected: String,
75        found: String,
76        fixes: Vec<Fix>,
77    },
78
79    #[error("configuration error: {message}")]
80    Config {
81        message: String,
82        path: Option<PathBuf>,
83        #[source]
84        source: Option<Box<dyn std::error::Error + Send + Sync>>,
85        fixes: Vec<Fix>,
86    },
87
88    #[error("I/O error: {message}")]
89    Io {
90        message: String,
91        path: Option<PathBuf>,
92        #[source]
93        source: std::io::Error,
94    },
95
96    #[error("command failed: {command}")]
97    CommandFailed {
98        command: String,
99        exit_code: Option<i32>,
100        stdout: String,
101        stderr: String,
102        fixes: Vec<Fix>,
103    },
104
105    #[error("build failed")]
106    BuildFailed {
107        errors: Vec<String>,
108        fixes: Vec<Fix>,
109    },
110
111    #[error("lock error: {message}")]
112    Lock {
113        message: String,
114        #[source]
115        source: Option<Box<dyn std::error::Error + Send + Sync>>,
116        fixes: Vec<Fix>,
117    },
118
119    #[error("project not found")]
120    ProjectNotFound {
121        searched: Vec<PathBuf>,
122        fixes: Vec<Fix>,
123    },
124
125    #[error("{0}")]
126    Other(#[from] anyhow::Error),
127}
128
129impl Error {
130    /// Get the error code for this error.
131    pub fn code(&self) -> ErrorCode {
132        match self {
133            Error::ToolchainMissing { .. } => ErrorCode::ToolchainMissing,
134            Error::ToolchainMismatch { .. } => ErrorCode::ToolchainMismatch,
135            Error::Config { .. } => ErrorCode::ConfigError,
136            Error::Io { .. } => ErrorCode::IoError,
137            Error::CommandFailed { .. } => ErrorCode::CommandFailed,
138            Error::BuildFailed { .. } => ErrorCode::BuildFailure,
139            Error::Lock { .. } => ErrorCode::LockError,
140            Error::ProjectNotFound { .. } => ErrorCode::ConfigError,
141            Error::Other(_) => ErrorCode::IoError,
142        }
143    }
144
145    /// Get suggested fixes for this error.
146    pub fn fixes(&self) -> &[Fix] {
147        match self {
148            Error::ToolchainMissing { fixes, .. } => fixes,
149            Error::ToolchainMismatch { fixes, .. } => fixes,
150            Error::Config { fixes, .. } => fixes,
151            Error::CommandFailed { fixes, .. } => fixes,
152            Error::BuildFailed { fixes, .. } => fixes,
153            Error::Lock { fixes, .. } => fixes,
154            Error::ProjectNotFound { fixes, .. } => fixes,
155            Error::Io { .. } | Error::Other(_) => &[],
156        }
157    }
158
159    /// Create a config error.
160    pub fn config(message: impl Into<String>) -> Self {
161        Error::Config {
162            message: message.into(),
163            path: None,
164            source: None,
165            fixes: vec![],
166        }
167    }
168
169    /// Create a config error with a path.
170    pub fn config_at(message: impl Into<String>, path: impl Into<PathBuf>) -> Self {
171        Error::Config {
172            message: message.into(),
173            path: Some(path.into()),
174            source: None,
175            fixes: vec![],
176        }
177    }
178
179    /// Create a toolchain missing error with default fix suggestions.
180    pub fn toolchain_missing(tool: impl Into<String>) -> Self {
181        let tool = tool.into();
182        let fixes = match tool.as_str() {
183            "ghc" => vec![
184                Fix::with_command("Install GHC via ghcup", "ghcup install ghc"),
185                Fix::with_command("Or install via hx", "hx toolchain install"),
186            ],
187            "cabal" => vec![
188                Fix::with_command("Install Cabal via ghcup", "ghcup install cabal"),
189                Fix::with_command("Or install via hx", "hx toolchain install"),
190            ],
191            "ghcup" => vec![Fix::new(
192                "Install ghcup from https://www.haskell.org/ghcup/",
193            )],
194            "hls" | "haskell-language-server" => vec![
195                Fix::with_command("Install HLS via ghcup", "ghcup install hls"),
196                Fix::with_command("Or install via hx", "hx toolchain install --hls latest"),
197            ],
198            _ => vec![Fix::with_command(
199                format!("Install {}", tool),
200                "hx toolchain install",
201            )],
202        };
203
204        Error::ToolchainMissing {
205            tool,
206            source: None,
207            fixes,
208        }
209    }
210
211    /// Create a toolchain mismatch error with default fix suggestions.
212    pub fn toolchain_mismatch(
213        tool: impl Into<String>,
214        expected: impl Into<String>,
215        found: impl Into<String>,
216    ) -> Self {
217        let tool = tool.into();
218        let expected = expected.into();
219        let found = found.into();
220
221        let fixes = vec![
222            Fix::with_command(
223                format!("Install {} {}", tool, expected),
224                format!(
225                    "hx toolchain install --{} {}",
226                    tool.to_lowercase(),
227                    expected
228                ),
229            ),
230            Fix::with_command(
231                format!("Or use {} {} for this session", tool, expected),
232                format!("ghcup set {} {}", tool.to_lowercase(), expected),
233            ),
234        ];
235
236        Error::ToolchainMismatch {
237            tool,
238            expected,
239            found,
240            fixes,
241        }
242    }
243
244    /// Create a project not found error with helpful suggestions.
245    pub fn project_not_found(searched: Vec<PathBuf>) -> Self {
246        Error::ProjectNotFound {
247            searched,
248            fixes: vec![
249                Fix::with_command("Initialize a new project", "hx init"),
250                Fix::new("Or navigate to a directory containing hx.toml or *.cabal"),
251            ],
252        }
253    }
254
255    /// Create a lock error with suggestions.
256    pub fn lock_outdated() -> Self {
257        Error::Lock {
258            message: "lockfile is out of date".to_string(),
259            source: None,
260            fixes: vec![
261                Fix::with_command("Update the lockfile", "hx lock"),
262                Fix::with_command("Or force sync with current lock", "hx sync --force"),
263            ],
264        }
265    }
266
267    /// Create a build failed error with common suggestions.
268    pub fn build_failed(errors: Vec<String>) -> Self {
269        let mut fixes = vec![Fix::with_command(
270            "See full compiler output",
271            "hx build --verbose",
272        )];
273
274        // Add specific suggestions based on error content
275        for error in &errors {
276            if error.contains("Could not find module") {
277                fixes.push(Fix::with_command(
278                    "Missing dependency - add it",
279                    "hx add <package-name>",
280                ));
281                break;
282            }
283            if error.contains("parse error") || error.contains("Parse error") {
284                fixes.push(Fix::new("Check syntax near the reported line"));
285                break;
286            }
287        }
288
289        Error::BuildFailed { errors, fixes }
290    }
291}