1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
use std::fmt;
use std::str::FromStr;
use std::sync::Arc;
use console::style;
use mlua::prelude::*;
use once_cell::sync::Lazy;
use super::StackTrace;
static STYLED_STACK_BEGIN: Lazy<String> = Lazy::new(|| {
format!(
"{}{}{}",
style("[").dim(),
style("Stack Begin").blue(),
style("]").dim()
)
});
static STYLED_STACK_END: Lazy<String> = Lazy::new(|| {
format!(
"{}{}{}",
style("[").dim(),
style("Stack End").blue(),
style("]").dim()
)
});
/**
Error components parsed from a [`LuaError`].
Can be used to display a human-friendly error message
and stack trace, in the following Roblox-inspired format:
```plaintext
Error message
[Stack Begin]
Stack trace line
Stack trace line
Stack trace line
[Stack End]
```
*/
#[derive(Debug, Default, Clone)]
pub struct ErrorComponents {
messages: Vec<String>,
trace: Option<StackTrace>,
}
impl ErrorComponents {
/**
Returns the error messages.
*/
#[must_use]
pub fn messages(&self) -> &[String] {
&self.messages
}
/**
Returns the stack trace, if it exists.
*/
#[must_use]
pub fn trace(&self) -> Option<&StackTrace> {
self.trace.as_ref()
}
/**
Returns `true` if the error has a non-empty stack trace.
Note that a trace may still *exist*, but it may be empty.
*/
#[must_use]
pub fn has_trace(&self) -> bool {
self.trace
.as_ref()
.is_some_and(|trace| !trace.lines().is_empty())
}
}
impl fmt::Display for ErrorComponents {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for message in self.messages() {
writeln!(f, "{message}")?;
}
if self.has_trace() {
let trace = self.trace.as_ref().unwrap();
writeln!(f, "{}", *STYLED_STACK_BEGIN)?;
for line in trace.lines() {
writeln!(f, "\t{line}")?;
}
writeln!(f, "{}", *STYLED_STACK_END)?;
}
Ok(())
}
}
impl From<LuaError> for ErrorComponents {
fn from(error: LuaError) -> Self {
fn lua_error_message(e: &LuaError) -> String {
if let LuaError::RuntimeError(s) = e {
s.to_string()
} else {
e.to_string()
}
}
fn lua_stack_trace(source: &str) -> Option<StackTrace> {
// FUTURE: Preserve a parsing error here somehow?
// Maybe we can emit parsing errors using tracing?
StackTrace::from_str(source).ok()
}
// Extract any additional "context" messages before the actual error(s)
// The Arc is necessary here because mlua wraps all inner errors in an Arc
let mut error = Arc::new(error);
let mut messages = Vec::new();
while let LuaError::WithContext {
ref context,
ref cause,
} = *error
{
messages.push(context.to_string());
error = cause.clone();
}
// We will then try to extract any stack trace
let trace = if let LuaError::CallbackError {
ref traceback,
ref cause,
} = *error
{
messages.push(lua_error_message(cause));
lua_stack_trace(traceback)
} else if let LuaError::RuntimeError(ref s) = *error {
// NOTE: Runtime errors may include tracebacks, but they're
// joined with error messages, so we need to split them out
if let Some(pos) = s.find("stack traceback:") {
let (message, traceback) = s.split_at(pos);
messages.push(message.trim().to_string());
lua_stack_trace(traceback)
} else {
messages.push(s.to_string());
None
}
} else {
messages.push(lua_error_message(&error));
None
};
ErrorComponents { messages, trace }
}
}