1use std::path::PathBuf;
4
5pub type Result<T> = std::result::Result<T, Error>;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ErrorCode {
11 ToolchainMissing,
13 ToolchainMismatch,
15 HlsMismatch,
17 SystemDepMissing,
19 SolverFailure,
21 BuildFailure,
23 ConfigError,
25 IoError,
27 CommandFailed,
29 LockError,
31}
32
33#[derive(Debug, Clone)]
35pub struct Fix {
36 pub description: String,
38 pub command: Option<String>,
40}
41
42impl Fix {
43 pub fn new(description: impl Into<String>) -> Self {
45 Self {
46 description: description.into(),
47 command: None,
48 }
49 }
50
51 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#[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 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 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 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 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 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 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 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 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 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 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}