1pub mod runtime;
2pub mod syntax;
3
4use miette::{Diagnostic, NamedSource, SourceOffset, SourceSpan};
5use std::borrow::Cow;
6
7use crate::{
8 ModuleLoader, ModuleResolver, Token, TokenKind,
9 error::{runtime::RuntimeError, syntax::SyntaxError},
10 module::{self, error::ModuleError},
11};
12
13#[allow(clippy::useless_conversion)]
14#[derive(Debug, thiserror::Error, PartialEq)]
15pub enum InnerError {
16 #[error(transparent)]
17 Runtime(#[from] RuntimeError),
18 #[error(transparent)]
19 Syntax(#[from] SyntaxError),
20 #[error(transparent)]
21 Module(#[from] ModuleError),
22}
23
24impl InnerError {
25 #[cold]
26 pub fn token(&self) -> Option<&Token> {
27 match self {
28 InnerError::Syntax(err) => err.token(),
29 InnerError::Runtime(err) => err.token(),
30 InnerError::Module(err) => err.token(),
31 }
32 }
33
34 #[cold]
36 pub fn secondary_token(&self) -> Option<&Token> {
37 match self {
38 InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, opening))
39 | InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, opening))
40 | InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, opening)) => {
41 opening.as_ref().map(|v| v.as_ref())
42 }
43 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, opening)))
44 | InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, opening)))
45 | InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, opening))) => {
46 opening.as_ref().map(|v| v.as_ref())
47 }
48 _ => None,
49 }
50 }
51}
52
53#[derive(PartialEq, Debug, thiserror::Error)]
55#[error("{cause}")]
56pub struct Error {
57 pub cause: InnerError,
59 pub source_code: NamedSource<String>,
61 pub location: SourceSpan,
63 pub secondary_location: Option<SourceSpan>,
65}
66
67impl Error {
68 #[cold]
69 pub fn from_error(
70 top_level_source_code: impl Into<String>,
71 cause: InnerError,
72 module_loader: ModuleLoader<impl ModuleResolver>,
73 ) -> Self {
74 let source_code = top_level_source_code.into();
75 let token = cause.token();
76
77 match token {
78 Some(token) => {
79 let source_str = module_loader
80 .get_source_code(token.module_id, source_code)
81 .unwrap_or_default();
82 let source_name = module_loader.module_file_name(token.module_id);
83
84 let span_for = |t: &Token| {
85 SourceSpan::new(
86 SourceOffset::from_location(&source_str, t.range.start.line as usize, t.range.start.column),
87 std::cmp::max(
88 SourceOffset::from_location(&source_str, t.range.end.line as usize, t.range.end.column)
89 .offset()
90 .saturating_sub(
91 SourceOffset::from_location(
92 &source_str,
93 t.range.start.line as usize,
94 t.range.start.column,
95 )
96 .offset(),
97 ),
98 1,
99 ),
100 )
101 };
102
103 let location = span_for(token);
104 let secondary_location = cause.secondary_token().map(span_for);
105
106 Self {
107 cause,
108 source_code: NamedSource::new(source_name, source_str),
109 location,
110 secondary_location,
111 }
112 }
113 None => {
114 let (module_id, is_eof) = match &cause {
115 InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(module_id)) => (Some(module_id), true),
116 InnerError::Runtime(_) => (None, false),
117 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(module_id))) => {
118 (Some(module_id), true)
119 }
120 _ => (None, false),
121 };
122
123 let source_name = module_id
124 .map(|id| module_loader.module_file_name(*id))
125 .unwrap_or_default();
126
127 let source_str = module_id
128 .map(|id| {
129 module_loader
130 .get_source_code(*id, source_code.clone())
131 .unwrap_or_default()
132 })
133 .unwrap_or(source_code);
134
135 let location = if is_eof {
136 let lines = source_str.lines();
137 let loc_line = lines.clone().count().saturating_sub(1);
138 let loc_col = lines.last().map(|lines| lines.len()).unwrap_or(0);
139 SourceSpan::new(SourceOffset::from_location(&source_str, loc_line, loc_col), 1)
140 } else {
141 SourceSpan::new(SourceOffset::from_location(&source_str, 0, 0), 1)
142 };
143
144 Self {
145 cause,
146 source_code: NamedSource::new(source_name, source_str),
147 location,
148 secondary_location: None,
149 }
150 }
151 }
152 }
153}
154
155impl Diagnostic for Error {
156 #[cold]
157 fn code<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
158 let code = match &self.cause {
159 InnerError::Runtime(_) => "mq::runtime",
160 InnerError::Syntax(_) => "mq::syntax",
161 InnerError::Module(_) => "mq::module",
162 };
163 Some(Box::new(code) as Box<dyn std::fmt::Display>)
164 }
165
166 #[cold]
167 fn url<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
168 match &self.cause {
169 InnerError::Runtime(RuntimeError::InvalidDefinition(_, _))
170 | InnerError::Runtime(RuntimeError::InvalidTypes { .. }) => {
171 Some(Box::new("https://mqlang.org/book") as Box<dyn std::fmt::Display>)
172 }
173 _ => None,
174 }
175 }
176
177 #[cold]
178 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
179 let msg: Option<Cow<'static, str>> = match &self.cause {
180 InnerError::Syntax(SyntaxError::EnvNotFound(_, env)) => Some(Cow::Owned(format!(
181 "Environment variable '{env}' not found. Did you forget to set it?"
182 ))),
183 InnerError::Syntax(SyntaxError::UnexpectedToken(token)) if token.kind == TokenKind::Eof => {
184 Some(Cow::Borrowed(
185 "The source could not be fully parsed from this position. Check for unsupported escape sequences (use \\u{XXXX} for Unicode), invalid characters, or unterminated string literals.",
186 ))
187 }
188 InnerError::Syntax(SyntaxError::UnexpectedToken(_)) => Some(Cow::Borrowed(
189 "This token is not valid here. Check for typos, missing operators, or misplaced punctuation.",
190 )),
191 InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(_)) => Some(Cow::Borrowed(
192 "Unexpected end of input. Check for missing closing brackets, parentheses, or incomplete expressions.",
193 )),
194 InnerError::Syntax(SyntaxError::InsufficientTokens(_)) => Some(Cow::Borrowed(
195 "Parsing could not continue here. Check for missing arguments, operators, or mismatched delimiters.",
196 )),
197 InnerError::Syntax(SyntaxError::UnknownSelector(_)) => Some(Cow::Borrowed(
198 "Unknown selector. Valid selectors include node types (e.g. .h1, .p, .code) and bracket access (e.g. .[0], .[n][m]).",
199 )),
200 InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, _)) => Some(Cow::Borrowed(
201 "Expected a closing parenthesis ')'. Check your parentheses for balance.",
202 )),
203 InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, _)) => Some(Cow::Borrowed(
204 "Expected a closing brace '}'. Check your braces for balance.",
205 )),
206 InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, _)) => Some(Cow::Borrowed(
207 "Expected a closing bracket ']'. Check your brackets for balance.",
208 )),
209 InnerError::Syntax(SyntaxError::InvalidAssignmentTarget(_)) => Some(Cow::Borrowed(
210 "Invalid assignment target. Ensure you're assigning to a valid variable or property.",
211 )),
212 InnerError::Syntax(SyntaxError::ParameterWithoutDefaultAfterDefault(_)) => Some(Cow::Borrowed(
213 "Move this parameter before any parameters that have default values, or give it a default value.",
214 )),
215 InnerError::Syntax(SyntaxError::MacroParametersCannotHaveDefaults(_)) => {
216 Some(Cow::Borrowed("Macro parameters cannot have default values."))
217 }
218 InnerError::Syntax(SyntaxError::VariadicParameterMustBeLast(_)) => Some(Cow::Borrowed(
219 "Variadic parameter (*) must be the last parameter in the parameter list.",
220 )),
221 InnerError::Syntax(SyntaxError::MultipleVariadicParameters(_)) => Some(Cow::Borrowed(
222 "Only one variadic parameter (*) is allowed per function.",
223 )),
224 InnerError::Syntax(SyntaxError::MacroParametersCannotBeVariadic(_)) => {
225 Some(Cow::Borrowed("Macro parameters cannot be variadic."))
226 }
227 InnerError::Syntax(SyntaxError::UnexpectedEOFAfterToken(_)) => Some(Cow::Borrowed(
228 "An expression was expected here. Check for incomplete expressions after operators or keywords.",
229 )),
230 InnerError::Syntax(SyntaxError::UnmatchedEnd(_)) => Some(Cow::Borrowed(
231 "This `end` keyword does not match any open block. \
232 Note: single-line `if` expressions do not require `end`. \
233 Check that each `end` closes a `def`, `fn`, `do`, `while`, `loop`, or `foreach` block.",
234 )),
235 InnerError::Runtime(RuntimeError::UserDefined { .. }) => {
236 Some(Cow::Borrowed("A user-defined error occurred during evaluation."))
237 }
238 InnerError::Runtime(RuntimeError::InvalidBase64String(_, _)) => Some(Cow::Borrowed(
239 "The provided string is not valid Base64. Check your input.",
240 )),
241 InnerError::Runtime(RuntimeError::NotDefined(_, name)) => Some(Cow::Owned(format!(
242 "'{name}' is not defined. Did you forget to declare it?"
243 ))),
244 InnerError::Runtime(RuntimeError::DateTimeFormatError(_, _)) => Some(Cow::Borrowed(
245 "Invalid date/time format. Please check your format string.",
246 )),
247 InnerError::Runtime(RuntimeError::IndexOutOfBounds(_, _)) => Some(Cow::Borrowed(
248 "Index out of bounds. Check your array or string indices.",
249 )),
250 InnerError::Runtime(RuntimeError::InvalidDefinition(_, _)) => Some(Cow::Borrowed(
251 "Invalid definition. Please check your function or variable declaration.",
252 )),
253 InnerError::Runtime(RuntimeError::AssignToImmutable(_, name)) => Some(Cow::Owned(format!(
254 "Cannot assign to immutable variable '{name}'. Consider declaring it as mutable."
255 ))),
256 InnerError::Runtime(RuntimeError::UndefinedVariable(_, name)) => Some(Cow::Owned(format!(
257 "Variable '{name}' is undefined. Did you forget to declare it?"
258 ))),
259 InnerError::Runtime(RuntimeError::InvalidTypes { .. }) => {
260 Some(Cow::Borrowed("Type mismatch. Check the types of your operands."))
261 }
262 InnerError::Runtime(RuntimeError::InvalidNumberOfArguments {
263 token: _,
264 name: _,
265 expected,
266 actual,
267 }) => Some(Cow::Owned(format!(
268 "Invalid number of arguments: expected {expected}, got {actual}."
269 ))),
270 InnerError::Runtime(RuntimeError::InvalidRegularExpression(_, _)) => Some(Cow::Borrowed(
271 "Invalid regular expression. Please check your regex syntax.",
272 )),
273 InnerError::Runtime(RuntimeError::InternalError(_)) => Some(Cow::Borrowed(
274 "An internal error occurred. Please report this if it persists.",
275 )),
276 InnerError::Runtime(RuntimeError::Runtime(_, _)) => {
277 Some(Cow::Borrowed("A runtime error occurred during evaluation."))
278 }
279 InnerError::Runtime(RuntimeError::ZeroDivision(_)) => {
280 Some(Cow::Borrowed("Division by zero is not allowed."))
281 }
282 InnerError::Runtime(RuntimeError::RecursionError(_)) => {
283 Some(Cow::Borrowed("Maximum recursion depth exceeded."))
284 }
285 InnerError::Runtime(RuntimeError::ModuleLoadError(_)) => {
286 Some(Cow::Borrowed("Failed to load module. Check module paths and names."))
287 }
288 InnerError::Runtime(RuntimeError::UnexpectedBreak(_)) => {
289 Some(Cow::Borrowed("'break' can only be used inside a loop."))
290 }
291 InnerError::Runtime(RuntimeError::UnexpectedContinue(_)) => {
292 Some(Cow::Borrowed("'continue' can only be used inside a loop."))
293 }
294 InnerError::Runtime(RuntimeError::EnvNotFound(_, env)) => Some(Cow::Owned(format!(
295 "Environment variable '{env}' not found. Did you forget to set it?"
296 ))),
297 InnerError::Runtime(RuntimeError::QuoteNotAllowedInRuntimeContext(_)) => Some(Cow::Borrowed(
298 "quote() is not allowed in runtime context. It should only appear inside macros.",
299 )),
300 InnerError::Runtime(RuntimeError::UnquoteNotAllowedOutsideQuote(_)) => {
301 Some(Cow::Borrowed("unquote() can only be used inside quote()."))
302 }
303 InnerError::Runtime(RuntimeError::InvalidConvert(_, msg)) => Some(Cow::Owned(format!(
304 "Invalid conversion: {msg}. Check that the conversion is supported and value types match."
305 ))),
306 InnerError::Module(ModuleError::NotFound(name)) => Some(Cow::Owned(format!(
307 "Module '{name}' not found. Check the module name or path."
308 ))),
309 InnerError::Module(ModuleError::AlreadyLoaded(name)) => {
310 Some(Cow::Owned(format!("Module '{name}' is already loaded.")))
311 }
312 InnerError::Module(ModuleError::IOError(_)) => Some(Cow::Borrowed(
313 "An I/O error occurred while loading a module. Check file permissions and paths.",
314 )),
315 InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(_, env))) => {
316 Some(Cow::Owned(format!("Environment variable '{env}' not found in module.")))
317 }
318 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(token)))
319 if token.kind == TokenKind::Eof =>
320 {
321 Some(Cow::Borrowed(
322 "The source could not be fully parsed from this position. Check for unsupported escape sequences (use \\u{XXXX} for Unicode), invalid characters, or unterminated string literals.",
323 ))
324 }
325 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(_))) => Some(Cow::Borrowed(
326 "This token is not valid here. Check for typos, missing operators, or misplaced punctuation.",
327 )),
328 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(_))) => Some(Cow::Borrowed(
329 "Unexpected end of input. Check for missing closing brackets, parentheses, or incomplete expressions.",
330 )),
331 InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(_))) => Some(Cow::Borrowed(
332 "Parsing could not continue here. Check for missing arguments, operators, or mismatched delimiters.",
333 )),
334 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, _))) => Some(
335 Cow::Borrowed("Expected a closing bracket ']'. Check your brackets for balance."),
336 ),
337 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, _))) => Some(
338 Cow::Borrowed("Expected a closing brace '}'. Check your braces for balance."),
339 ),
340 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, _))) => Some(
341 Cow::Borrowed("Expected a closing parenthesis ')'. Check your parentheses for balance."),
342 ),
343 InnerError::Module(ModuleError::SyntaxError(SyntaxError::InvalidAssignmentTarget(_))) => Some(
344 Cow::Borrowed("Invalid assignment target. Ensure you're assigning to a valid variable or property."),
345 ),
346 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnknownSelector(_))) => Some(Cow::Borrowed(
347 "Unknown selector. Valid selectors include node types (e.g. .h1, .p, .code) and bracket access (e.g. .[0], .[n][m]).",
348 )),
349 InnerError::Module(ModuleError::InvalidModule) => Some(Cow::Borrowed("Invalid module format or content.")),
350 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ParameterWithoutDefaultAfterDefault(_))) => {
351 Some(Cow::Borrowed(
352 "Move this parameter before any parameters that have default values, or give it a default value.",
353 ))
354 }
355 InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotHaveDefaults(_))) => {
356 Some(Cow::Borrowed("Macro parameters cannot have default values."))
357 }
358 InnerError::Module(ModuleError::SyntaxError(SyntaxError::VariadicParameterMustBeLast(_))) => Some(
359 Cow::Borrowed("Variadic parameter (*) must be the last parameter in the parameter list."),
360 ),
361 InnerError::Module(ModuleError::SyntaxError(SyntaxError::MultipleVariadicParameters(_))) => Some(
362 Cow::Borrowed("Only one variadic parameter (*) is allowed per function."),
363 ),
364 InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotBeVariadic(_))) => {
365 Some(Cow::Borrowed("Macro parameters cannot be variadic."))
366 }
367 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFAfterToken(_))) => {
368 Some(Cow::Borrowed(
369 "An expression was expected here. Check for incomplete expressions after operators or keywords.",
370 ))
371 }
372 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnmatchedEnd(_))) => Some(Cow::Borrowed(
373 "This `end` keyword does not match any open block. \
374 Note: single-line `if` expressions do not require `end`. \
375 Check that each `end` closes a `def`, `fn`, `do`, `while`, `loop`, or `foreach` block.",
376 )),
377 InnerError::Runtime(RuntimeError::UndefinedMacro(_)) => {
378 Some(Cow::Borrowed("Macro expansion error: undefined macro used."))
379 }
380 InnerError::Runtime(RuntimeError::ArityMismatch { .. }) => {
381 Some(Cow::Borrowed("Macro expansion error: macro arity mismatch."))
382 }
383 InnerError::Runtime(RuntimeError::RecursionLimit) => {
384 Some(Cow::Borrowed("Macro expansion error: recursion limit exceeded."))
385 }
386 InnerError::Runtime(RuntimeError::InvalidMacroResultAst(_)) => {
387 Some(Cow::Borrowed("Invalid macro result AST during macro expansion."))
388 }
389 InnerError::Runtime(RuntimeError::InvalidMacroResult(_)) => Some(Cow::Borrowed(
390 "Invalid macro result: expected AST value during macro body evaluation.",
391 )),
392 InnerError::Runtime(RuntimeError::DestructuringFailed(_)) => Some(Cow::Borrowed(
393 "Destructuring pattern did not match the value. Check that the pattern structure matches the value.",
394 )),
395 #[cfg(feature = "http-import")]
396 InnerError::Module(ModuleError::HttpImportNotAllowed(_)) => Some(Cow::Borrowed(
397 "HTTP imports are only allowed at the top level. \
398 Move the HTTP import to the top-level script instead of inside an imported module.",
399 )),
400 };
401
402 msg.map(|m| Box::new(m) as Box<dyn std::fmt::Display>)
403 }
404
405 #[cold]
406 fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
407 let label = match &self.cause {
408 InnerError::Syntax(SyntaxError::UnexpectedToken(_)) => "unexpected token",
409 InnerError::Syntax(SyntaxError::InsufficientTokens(_)) => "expression incomplete here",
410 InnerError::Syntax(SyntaxError::ExpectedClosingParen(_, _)) => "expected `)` here",
411 InnerError::Syntax(SyntaxError::ExpectedClosingBrace(_, _)) => "expected `}` here",
412 InnerError::Syntax(SyntaxError::ExpectedClosingBracket(_, _)) => "expected `]` here",
413 InnerError::Syntax(SyntaxError::InvalidAssignmentTarget(_)) => "invalid assignment target",
414 InnerError::Syntax(SyntaxError::UnknownSelector(_)) => "unknown selector",
415 InnerError::Syntax(SyntaxError::EnvNotFound(_, _)) => "environment variable not found",
416 InnerError::Syntax(SyntaxError::ParameterWithoutDefaultAfterDefault(_)) => "parameter without default",
417 InnerError::Syntax(SyntaxError::MacroParametersCannotHaveDefaults(_)) => "parameter with default value",
418 InnerError::Syntax(SyntaxError::VariadicParameterMustBeLast(_)) => "misplaced variadic parameter",
419 InnerError::Syntax(SyntaxError::MultipleVariadicParameters(_)) => "duplicate variadic parameter",
420 InnerError::Syntax(SyntaxError::MacroParametersCannotBeVariadic(_)) => "variadic macro parameter",
421 InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(_)) => "unexpected end of input",
422 InnerError::Syntax(SyntaxError::UnexpectedEOFAfterToken(_)) => "expected expression here",
423 InnerError::Syntax(SyntaxError::UnmatchedEnd(_)) => "unmatched `end` keyword",
424 InnerError::Runtime(_) => "error occurred here",
425 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(_))) => "unexpected token",
426 InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(_))) => {
427 "expression incomplete here"
428 }
429 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(_, _))) => {
430 "expected `)` here"
431 }
432 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(_, _))) => {
433 "expected `}` here"
434 }
435 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(_, _))) => {
436 "expected `]` here"
437 }
438 InnerError::Module(ModuleError::SyntaxError(SyntaxError::InvalidAssignmentTarget(_))) => {
439 "invalid assignment target"
440 }
441 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnknownSelector(_))) => "unknown selector",
442 InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(_, _))) => {
443 "environment variable not found"
444 }
445 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ParameterWithoutDefaultAfterDefault(_))) => {
446 "parameter without default"
447 }
448 InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotHaveDefaults(_))) => {
449 "parameter with default value"
450 }
451 InnerError::Module(ModuleError::SyntaxError(SyntaxError::VariadicParameterMustBeLast(_))) => {
452 "misplaced variadic parameter"
453 }
454 InnerError::Module(ModuleError::SyntaxError(SyntaxError::MultipleVariadicParameters(_))) => {
455 "duplicate variadic parameter"
456 }
457 InnerError::Module(ModuleError::SyntaxError(SyntaxError::MacroParametersCannotBeVariadic(_))) => {
458 "variadic macro parameter"
459 }
460 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(_))) => {
461 "unexpected end of input"
462 }
463 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFAfterToken(_))) => {
464 "expected expression here"
465 }
466 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnmatchedEnd(_))) => "unmatched `end` keyword",
467 InnerError::Module(_) => "module error here",
468 };
469
470 let primary = miette::LabeledSpan::new_with_span(Some(label.to_string()), self.location);
471
472 if let Some(secondary_span) = self.secondary_location {
473 Some(Box::new(
474 [
475 miette::LabeledSpan::new_with_span(Some("opened here".to_string()), secondary_span),
476 primary,
477 ]
478 .into_iter(),
479 ) as Box<dyn Iterator<Item = miette::LabeledSpan>>)
480 } else {
481 Some(Box::new(std::iter::once(primary)))
482 }
483 }
484
485 #[cold]
486 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
487 Some(&self.source_code as &dyn miette::SourceCode)
488 }
489}
490
491#[cfg(test)]
492mod test {
493 use rstest::{fixture, rstest};
494 use scopeguard::defer;
495 use std::io::Write;
496 use std::{fs::File, path::PathBuf};
497
498 use super::*;
499 use crate::module::resolver::DefaultModuleResolver;
500 use crate::{Arena, Range, Shared, SharedCell, Token, TokenKind, arena::ArenaId};
501
502 type TempDir = PathBuf;
503 type TempFile = PathBuf;
504
505 fn create_file(name: &str, content: &str) -> (TempDir, TempFile) {
506 let temp_dir = std::env::temp_dir();
507 let temp_file_path = temp_dir.join(name);
508 let mut file = File::create(&temp_file_path).expect("Failed to create temp file");
509 file.write_all(content.as_bytes())
510 .expect("Failed to write to temp file");
511
512 (temp_dir, temp_file_path)
513 }
514
515 #[fixture]
516 fn module_loader() -> ModuleLoader {
517 ModuleLoader::default()
518 }
519
520 #[test]
521 fn test_from_error_with_eof_error() {
522 let cause = InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)));
523 let module_loader: ModuleLoader = ModuleLoader::default();
524 let error = Error::from_error("line 1\nline 2", cause, module_loader);
525
526 assert_eq!(error.source_code.inner(), "line 1\nline 2");
527 }
528
529 #[rstest]
530 #[case::parse_unexpected_token(
531 InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
532 range: Range::default(),
533 kind: TokenKind::Eof,
534 module_id: ArenaId::new(0),
535 })),
536 "source code"
537 )]
538 #[case::parse_unexpected_eof_detected(
539 InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))),
540 "source code"
541 )]
542 #[case::parse_env_not_found(
543 InnerError::Syntax(SyntaxError::EnvNotFound(Token {
544 range: Range::default(),
545 kind: TokenKind::Eof,
546 module_id: ArenaId::new(0),
547 }, "ENV_VAR".into())),
548 "source code"
549 )]
550 #[case::parse_env_not_found(
551 InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
552 range: Range::default(),
553 kind: TokenKind::Eof,
554 module_id: ArenaId::new(0),
555 })),
556 "source code"
557 )]
558 #[case::parse_env_not_found(
559 InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
560 range: Range::default(),
561 kind: TokenKind::Eof,
562 module_id: ArenaId::new(0),
563 })),
564 "source code"
565 )]
566 #[case::eval_zero_division(
567 InnerError::Runtime(RuntimeError::ZeroDivision(Token {
568 range: Range::default(),
569 kind: TokenKind::Eof,
570 module_id: ArenaId::new(0),
571 })),
572 "source code"
573 )]
574 #[case::eval_invalid_base64_string(
575 InnerError::Runtime(RuntimeError::InvalidBase64String(Token {
576 range: Range::default(),
577 kind: TokenKind::Eof,
578 module_id: ArenaId::new(0),
579 }, "".to_string())),
580 "source code"
581 )]
582 #[case::eval_not_defined(
583 InnerError::Runtime(RuntimeError::NotDefined(Token {
584 range: Range::default(),
585 kind: TokenKind::Eof,
586 module_id: ArenaId::new(0),
587 }, "".to_string())),
588 "source code"
589 )]
590 #[case::eval_index_out_of_bounds(
591 InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token {
592 range: Range::default(),
593 kind: TokenKind::Eof,
594 module_id: ArenaId::new(0),
595 }, 1.into())),
596 "source code"
597 )]
598 #[case::eval_invalid_definition(
599 InnerError::Runtime(RuntimeError::InvalidDefinition(Token {
600 range: Range::default(),
601 kind: TokenKind::Eof,
602 module_id: ArenaId::new(0),
603 }, "".to_string())),
604 "source code"
605 )]
606 #[case::eval_invalid_number_of_arguments(
607 InnerError::Runtime(RuntimeError::InvalidNumberOfArguments{token: Token {
608 range: Range::default(),
609 kind: TokenKind::Eof,
610 module_id: ArenaId::new(0),
611 }, name: "".to_string(), expected: 1, actual:1}),
612 "source code"
613 )]
614 #[case::eval_invalid_regular_expression(
615 InnerError::Runtime(RuntimeError::InvalidRegularExpression(Token {
616 range: Range::default(),
617 kind: TokenKind::Eof,
618 module_id: ArenaId::new(0),
619 }, "".to_string())),
620 "source code"
621 )]
622 #[case::eval_internal_error(
623 InnerError::Runtime(RuntimeError::InternalError(Token {
624 range: Range::default(),
625 kind: TokenKind::Eof,
626 module_id: ArenaId::new(0),
627 })),
628 "source code"
629 )]
630 #[case::eval_internal_error(
631 InnerError::Runtime(RuntimeError::Runtime(Token {
632 range: Range::default(),
633 kind: TokenKind::Eof,
634 module_id: ArenaId::new(0),
635 }, "".to_string())),
636 "source code"
637 )]
638 #[case::module_not_found(InnerError::Module(ModuleError::NotFound(Cow::Borrowed("test"))), "source code")]
639 #[case::module_io_error(InnerError::Module(ModuleError::IOError(Cow::Borrowed("test"))), "source code")]
640 #[case::module_parse_error(
641 InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(Token {
642 range: Range::default(),
643 kind: TokenKind::Eof,
644 module_id: ArenaId::new(0),
645 }, "test".into()))),
646 "source code"
647 )]
648 #[case::module_parse_error(
649 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(Token {
650 range: Range::default(),
651 kind: TokenKind::Eof,
652 module_id: ArenaId::new(0),
653 }))),
654 "source code"
655 )]
656 #[case::module_parse_error(
657 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0)))),
658 "source code"
659 )]
660 #[case::module_parse_error(
661 InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(Token {
662 range: Range::default(),
663 kind: TokenKind::Eof,
664 module_id: ArenaId::new(0),
665 }
666 ))),
667 "source code"
668 )]
669 fn test_from_error(
670 module_loader: module::ModuleLoader<impl ModuleResolver>,
671 #[case] cause: InnerError,
672 #[case] source_code: &str,
673 ) {
674 let error = Error::from_error(source_code, cause, module_loader);
675 assert_eq!(error.source_code.inner(), source_code);
676 }
677
678 #[test]
679 fn test_from_error_with_module_source() {
680 let (temp_dir, temp_file_path) = create_file(
681 "test_from_error_with_module_source.mq",
682 "def func1(): 42; | let val1 = 1",
683 );
684
685 defer! {
686 if temp_file_path.exists() {
687 std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file");
688 }
689 }
690
691 let token_arena = Shared::new(SharedCell::new(Arena::new(10)));
692 let mut loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()]));
693
694 loader
695 .load_from_file("test_from_error_with_module_source", token_arena)
696 .unwrap();
697
698 let token = Token {
699 range: Range::default(),
700 kind: TokenKind::Eof,
701 module_id: ArenaId::new(1),
702 };
703
704 let cause = InnerError::Runtime(RuntimeError::ZeroDivision(token));
705 let error = Error::from_error("top level source", cause, loader);
706
707 assert_eq!(error.source_code.inner(), "def func1(): 42; | let val1 = 1");
708 }
709
710 #[test]
711 fn test_from_error_with_builtin_module() {
712 let token_arena = Shared::new(SharedCell::new(Arena::new(10)));
713 let mut loader: ModuleLoader = ModuleLoader::default();
714 loader.load_builtin(token_arena).unwrap();
715 let token = Token {
716 range: Range::default(),
717 kind: TokenKind::Eof,
718 module_id: ArenaId::new(1),
719 };
720
721 let cause = InnerError::Runtime(RuntimeError::ZeroDivision(token));
722 let error = Error::from_error("top level source", cause, loader);
723
724 assert_eq!(error.source_code.inner(), module::BUILTIN_FILE);
725 }
726
727 #[rstest]
728 #[case::parse_env_not_found(
729 InnerError::Syntax(SyntaxError::EnvNotFound(Token {
730 range: Range::default(),
731 kind: TokenKind::Eof,
732 module_id: ArenaId::new(0),
733 }, "ENV_VAR".into()))
734 )]
735 #[case::parse_unexpected_token(
736 InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
737 range: Range::default(),
738 kind: TokenKind::Eof,
739 module_id: ArenaId::new(0),
740 }))
741 )]
742 #[case::parse_unexpected_eof_detected(InnerError::Syntax(SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))))]
743 #[case::parse_insufficient_tokens(
744 InnerError::Syntax(SyntaxError::InsufficientTokens(Token {
745 range: Range::default(),
746 kind: TokenKind::Eof,
747 module_id: ArenaId::new(0),
748 }))
749 )]
750 #[case::parse_expected_closing_paren(
751 InnerError::Syntax(SyntaxError::ExpectedClosingParen(Token {
752 range: Range::default(),
753 kind: TokenKind::Eof,
754 module_id: ArenaId::new(0),
755 }, None))
756 )]
757 #[case::parse_expected_closing_brace(
758 InnerError::Syntax(SyntaxError::ExpectedClosingBrace(Token {
759 range: Range::default(),
760 kind: TokenKind::Eof,
761 module_id: ArenaId::new(0),
762 }, None))
763 )]
764 #[case::parse_expected_closing_bracket(
765 InnerError::Syntax(SyntaxError::ExpectedClosingBracket(Token {
766 range: Range::default(),
767 kind: TokenKind::Eof,
768 module_id: ArenaId::new(0),
769 }, None))
770 )]
771 #[case::eval_recursion_error(InnerError::Runtime(RuntimeError::RecursionError(0)))]
772 #[case::eval_module_load_error(
773 InnerError::Runtime(RuntimeError::ModuleLoadError(ModuleError::NotFound("mod".into())))
774 )]
775 #[case::eval_user_defined(
776 InnerError::Runtime(RuntimeError::UserDefined {
777 token: Token {
778 range: Range::default(),
779 kind: TokenKind::Eof,
780 module_id: ArenaId::new(0),
781 },
782 message: "msg".to_string(),
783 })
784 )]
785 #[case::eval_invalid_base64_string(
786 InnerError::Runtime(RuntimeError::InvalidBase64String(Token {
787 range: Range::default(),
788 kind: TokenKind::Eof,
789 module_id: ArenaId::new(0),
790 }, "bad".to_string()))
791 )]
792 #[case::eval_not_defined(
793 InnerError::Runtime(RuntimeError::NotDefined(Token {
794 range: Range::default(),
795 kind: TokenKind::Eof,
796 module_id: ArenaId::new(0),
797 }, "name".to_string()))
798 )]
799 #[case::eval_datetime_format_error(
800 InnerError::Runtime(RuntimeError::DateTimeFormatError(Token {
801 range: Range::default(),
802 kind: TokenKind::Eof,
803 module_id: ArenaId::new(0),
804 }, "fmt".to_string()))
805 )]
806 #[case::eval_index_out_of_bounds(
807 InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token {
808 range: Range::default(),
809 kind: TokenKind::Eof,
810 module_id: ArenaId::new(0),
811 }, 1.into()))
812 )]
813 #[case::eval_invalid_definition(
814 InnerError::Runtime(RuntimeError::InvalidDefinition(Token {
815 range: Range::default(),
816 kind: TokenKind::Eof,
817 module_id: ArenaId::new(0),
818 }, "bad".into()))
819 )]
820 #[case::eval_invalid_types(
821 InnerError::Runtime(RuntimeError::InvalidTypes {
822 token: Token {
823 range: Range::default(),
824 kind: TokenKind::Eof,
825 module_id: ArenaId::new(0),
826 },
827 name: "int".into(),
828 args: vec!["str".into()],
829 })
830 )]
831 #[case::eval_invalid_number_of_arguments(
832 InnerError::Runtime(RuntimeError::InvalidNumberOfArguments{token: Token {
833 range: Range::default(),
834 kind: TokenKind::Eof,
835 module_id: ArenaId::new(0),
836 }, name: "func".to_string(), expected: 2, actual: 1})
837 )]
838 #[case::eval_invalid_regular_expression(
839 InnerError::Runtime(RuntimeError::InvalidRegularExpression(Token {
840 range: Range::default(),
841 kind: TokenKind::Eof,
842 module_id: ArenaId::new(0),
843 }, "bad".to_string()))
844 )]
845 #[case::eval_internal_error(
846 InnerError::Runtime(RuntimeError::InternalError(Token {
847 range: Range::default(),
848 kind: TokenKind::Eof,
849 module_id: ArenaId::new(0),
850 }))
851 )]
852 #[case::eval_runtime_error(
853 InnerError::Runtime(RuntimeError::Runtime(Token {
854 range: Range::default(),
855 kind: TokenKind::Eof,
856 module_id: ArenaId::new(0),
857 }, "err".to_string()))
858 )]
859 #[case::eval_zero_division(
860 InnerError::Runtime(RuntimeError::ZeroDivision(Token {
861 range: Range::default(),
862 kind: TokenKind::Eof,
863 module_id: ArenaId::new(0),
864 }))
865 )]
866 #[case::eval_unexpected_break(InnerError::Runtime(RuntimeError::UnexpectedBreak(Token {
867 range: Range::default(),
868 kind: TokenKind::Eof,
869 module_id: ArenaId::new(0),
870 })))]
871 #[case::eval_unexpected_continue(InnerError::Runtime(RuntimeError::UnexpectedContinue(Token {
872 range: Range::default(),
873 kind: TokenKind::Eof,
874 module_id: ArenaId::new(0),
875 })))]
876 #[case::eval_env_not_found(
877 InnerError::Runtime(RuntimeError::EnvNotFound(Token {
878 range: Range::default(),
879 kind: TokenKind::Eof,
880 module_id: ArenaId::new(0),
881 }, "ENV".into()))
882 )]
883 #[case::module_not_found(InnerError::Module(ModuleError::NotFound(Cow::Borrowed("mod"))))]
884 #[case::module_io_error(InnerError::Module(ModuleError::IOError(Cow::Borrowed("io"))))]
885 #[case::module_parse_error_env_not_found(
886 InnerError::Module(ModuleError::SyntaxError(SyntaxError::EnvNotFound(Token {
887 range: Range::default(),
888 kind: TokenKind::Eof,
889 module_id: ArenaId::new(0),
890 }, "ENV".into())))
891 )]
892 #[case::module_parse_error_unexpected_token(
893 InnerError::Module(ModuleError::SyntaxError(SyntaxError::UnexpectedToken(Token {
894 range: Range::default(),
895 kind: TokenKind::Eof,
896 module_id: ArenaId::new(0),
897 })))
898 )]
899 #[case::module_parse_error_unexpected_eof(InnerError::Module(ModuleError::SyntaxError(
900 SyntaxError::UnexpectedEOFDetected(ArenaId::new(0))
901 )))]
902 #[case::module_parse_error_insufficient_tokens(
903 InnerError::Module(ModuleError::SyntaxError(SyntaxError::InsufficientTokens(Token {
904 range: Range::default(),
905 kind: TokenKind::Eof,
906 module_id: ArenaId::new(0),
907 })))
908 )]
909 #[case::module_invalid_module(InnerError::Module(ModuleError::InvalidModule))]
910 #[case::module_parse_error_expected_closing_paren(
911 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingParen(Token {
912 range: Range::default(),
913 kind: TokenKind::Eof,
914 module_id: ArenaId::new(0),
915 }, None)))
916 )]
917 #[case::module_parse_error_expected_closing_brace(
918 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBrace(Token {
919 range: Range::default(),
920 kind: TokenKind::Eof,
921 module_id: ArenaId::new(0),
922 }, None)))
923 )]
924 #[case::module_parse_error_expected_closing_bracket(
925 InnerError::Module(ModuleError::SyntaxError(SyntaxError::ExpectedClosingBracket(Token {
926 range: Range::default(),
927 kind: TokenKind::Eof,
928 module_id: ArenaId::new(0),
929 }, None)))
930 )]
931 fn test_diagnostic_code_and_help(module_loader: ModuleLoader<impl ModuleResolver>, #[case] cause: InnerError) {
932 let error = Error::from_error("source code", cause, module_loader);
933 let _ = error.code();
935 let _ = error.help();
936 }
937
938 #[test]
939 fn test_code_shows_category() {
940 let module_loader: ModuleLoader = ModuleLoader::default();
941 let runtime_error = Error::from_error(
942 "source code",
943 InnerError::Runtime(RuntimeError::ZeroDivision(Token {
944 range: Range::default(),
945 kind: TokenKind::Eof,
946 module_id: ArenaId::new(0),
947 })),
948 module_loader.clone(),
949 );
950 assert_eq!(
951 runtime_error.code().map(|c| c.to_string()),
952 Some("mq::runtime".to_string())
953 );
954
955 let syntax_error = Error::from_error(
956 "source code",
957 InnerError::Syntax(SyntaxError::UnexpectedToken(Token {
958 range: Range::default(),
959 kind: TokenKind::Eof,
960 module_id: ArenaId::new(0),
961 })),
962 module_loader,
963 );
964 assert_eq!(
965 syntax_error.code().map(|c| c.to_string()),
966 Some("mq::syntax".to_string())
967 );
968 }
969
970 #[test]
971 fn test_env_not_found_help_includes_name() {
972 let module_loader: ModuleLoader = ModuleLoader::default();
973 let cause = InnerError::Runtime(RuntimeError::EnvNotFound(
974 Token {
975 range: Range::default(),
976 kind: TokenKind::Eof,
977 module_id: ArenaId::new(0),
978 },
979 "MY_VAR".into(),
980 ));
981 let error = Error::from_error("source code", cause, module_loader);
982 let help = error.help().map(|h| h.to_string());
983 assert_eq!(
984 help,
985 Some("Environment variable 'MY_VAR' not found. Did you forget to set it?".to_string())
986 );
987 }
988
989 #[test]
990 fn test_invalid_convert_help_shows_message() {
991 let module_loader: ModuleLoader = ModuleLoader::default();
992 let cause = InnerError::Runtime(RuntimeError::InvalidConvert(
993 Token {
994 range: Range::default(),
995 kind: TokenKind::Eof,
996 module_id: ArenaId::new(0),
997 },
998 "cannot convert array to string".to_string(),
999 ));
1000 let error = Error::from_error("source code", cause, module_loader);
1001 let help = error.help().map(|h| h.to_string());
1002 assert!(
1003 help.as_deref().unwrap_or("").contains("cannot convert array to string"),
1004 "help text should contain the conversion message, got: {help:?}"
1005 );
1006 }
1007}