Initial working version of new error parser and display

This commit is contained in:
Filip Tibell 2024-04-22 15:42:24 +02:00
parent 7c702e4d34
commit 6dad31df14
No known key found for this signature in database
6 changed files with 415 additions and 85 deletions

View file

@ -1,85 +0,0 @@
use mlua::prelude::*;
/**
Source of a stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Clone, Copy)]
pub enum StackTraceSource {
/// Error originated from a C function.
C,
/// Error originated from a Rust function.
Rust,
/// Error originated from [`mlua`].
Mlua,
/// Error originated from a Lua (user) function.
User,
}
/**
Stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Clone)]
pub struct StackTraceLine {
source: StackTraceSource,
path: Option<String>,
line_number: Option<usize>,
function_name: Option<String>,
}
impl StackTraceLine {
#[must_use]
pub fn source(&self) -> StackTraceSource {
self.source
}
#[must_use]
pub fn path(&self) -> Option<&str> {
self.path.as_deref()
}
#[must_use]
pub fn line_number(&self) -> Option<usize> {
self.line_number
}
#[must_use]
pub fn function_name(&self) -> Option<&str> {
self.function_name.as_deref()
}
}
/**
Stack trace parsed from a [`LuaError`].
*/
#[derive(Debug, Clone)]
pub struct StackTrace {
lines: Vec<StackTraceLine>,
}
impl StackTrace {
#[must_use]
pub fn lines(&self) -> &[StackTraceLine] {
&self.lines
}
}
/**
Error components parsed from a [`LuaError`].
*/
#[derive(Debug, Clone)]
pub struct ErrorComponents {
message: String,
trace: StackTrace,
}
impl ErrorComponents {
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn trace(&self) -> &StackTrace {
&self.trace
}
}

View file

@ -0,0 +1,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 }
}
}

View file

@ -0,0 +1,5 @@
mod components;
mod stack_trace;
pub use self::components::ErrorComponents;
pub use self::stack_trace::{StackTrace, StackTraceLine, StackTraceSource};

View file

@ -0,0 +1,170 @@
use std::fmt;
use std::str::FromStr;
fn parse_path(s: &str) -> Option<(&str, &str)> {
let path = s.strip_prefix("[string \"")?;
let (path, after) = path.split_once("\"]:")?;
// Remove line number after any found colon, this may
// exist if the source path is from a rust source file
let path = match path.split_once(':') {
Some((before, _)) => before,
None => path,
};
Some((path, after))
}
fn parse_function_name(s: &str) -> Option<&str> {
s.strip_prefix("in function '")
.and_then(|s| s.strip_suffix('\''))
}
fn parse_line_number(s: &str) -> (Option<usize>, &str) {
match s.split_once(':') {
Some((before, after)) => (before.parse::<usize>().ok(), after),
None => (None, s),
}
}
/**
Source of a stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone, Copy)]
pub enum StackTraceSource {
/// Error originated from a C / Rust function.
C,
/// Error originated from a Lua (user) function.
#[default]
Lua,
}
/**
Stack trace line parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone)]
pub struct StackTraceLine {
source: StackTraceSource,
path: Option<String>,
line_number: Option<usize>,
function_name: Option<String>,
}
impl StackTraceLine {
/**
Returns the source of the stack trace line.
*/
#[must_use]
pub fn source(&self) -> StackTraceSource {
self.source
}
/**
Returns the path, if it exists.
*/
#[must_use]
pub fn path(&self) -> Option<&str> {
self.path.as_deref()
}
/**
Returns the line number, if it exists.
*/
#[must_use]
pub fn line_number(&self) -> Option<usize> {
self.line_number
}
/**
Returns the function name, if it exists.
*/
#[must_use]
pub fn function_name(&self) -> Option<&str> {
self.function_name.as_deref()
}
}
impl FromStr for StackTraceLine {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(after) = s.strip_prefix("[C]: ") {
let function_name = parse_function_name(after).map(ToString::to_string);
Ok(Self {
source: StackTraceSource::C,
path: None,
line_number: None,
function_name,
})
} else if let Some((path, after)) = parse_path(s) {
let (line_number, after) = parse_line_number(after);
let function_name = parse_function_name(after).map(ToString::to_string);
Ok(Self {
source: StackTraceSource::Lua,
path: Some(path.to_string()),
line_number,
function_name,
})
} else {
Err(String::from("unknown format"))
}
}
}
impl fmt::Display for StackTraceLine {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if matches!(self.source, StackTraceSource::C) {
write!(f, "Script '[C]'")?;
} else {
write!(f, "Script '{}'", self.path.as_deref().unwrap_or("[?]"))?;
if let Some(line_number) = self.line_number {
write!(f, ", Line {line_number}")?;
}
}
if let Some(function_name) = self.function_name.as_deref() {
write!(f, " - function '{function_name}'")?;
}
Ok(())
}
}
/**
Stack trace parsed from a [`LuaError`].
*/
#[derive(Debug, Default, Clone)]
pub struct StackTrace {
lines: Vec<StackTraceLine>,
}
impl StackTrace {
/**
Returns the individual stack trace lines.
*/
#[must_use]
pub fn lines(&self) -> &[StackTraceLine] {
&self.lines
}
}
impl FromStr for StackTrace {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (_, after) = s
.split_once("stack traceback:")
.ok_or_else(|| String::from("missing 'stack traceback:' prefix"))?;
let lines = after
.trim()
.lines()
.filter_map(|line| {
let line = line.trim();
if line.is_empty() {
None
} else {
Some(line.parse())
}
})
.collect::<Result<Vec<_>, _>>()?;
Ok(StackTrace { lines })
}
}

View file

@ -1,5 +1,8 @@
mod error;
mod label;
#[cfg(test)]
mod tests;
pub use self::error::{ErrorComponents, StackTrace, StackTraceLine, StackTraceSource};
pub use self::label::Label;

View file

@ -0,0 +1,85 @@
use mlua::prelude::*;
use crate::fmt::ErrorComponents;
fn new_lua_result() -> LuaResult<()> {
let lua = Lua::new();
lua.globals()
.set(
"f",
LuaFunction::wrap(|_, (): ()| {
Err::<(), _>(LuaError::runtime("oh no, a runtime error"))
}),
)
.unwrap();
lua.load("f()").set_name("chunk_name").eval()
}
// Tests for error context stack
mod context {
use super::*;
#[test]
fn preserves_original() {
let lua_error = new_lua_result().context("additional context").unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "additional context");
assert_eq!(components.messages()[1], "oh no, a runtime error");
}
#[test]
fn preserves_levels() {
// NOTE: The behavior in mlua is to preserve a single level of context
// and not all levels (context gets replaced on each call to `context`)
let lua_error = new_lua_result()
.context("level 1")
.context("level 2")
.context("level 3")
.unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(
components.messages(),
&["level 3", "oh no, a runtime error"]
);
}
}
// Tests for error components struct: separated messages + stack trace
mod error_components {
use super::*;
#[test]
fn message() {
let lua_error = new_lua_result().unwrap_err();
let components = ErrorComponents::from(lua_error);
assert_eq!(components.messages()[0], "oh no, a runtime error");
}
#[test]
fn stack_begin_end() {
let lua_error = new_lua_result().unwrap_err();
let formatted = format!("{}", ErrorComponents::from(lua_error));
assert!(formatted.contains("Stack Begin"));
assert!(formatted.contains("Stack End"));
}
#[test]
fn stack_lines() {
let lua_error = new_lua_result().unwrap_err();
let components = ErrorComponents::from(lua_error);
let mut lines = components.trace().unwrap().lines().iter();
let line_1 = lines.next().unwrap().to_string();
let line_2 = lines.next().unwrap().to_string();
assert!(lines.next().is_none());
assert_eq!(line_1, "Script '[C]' - function 'f'");
assert_eq!(line_2, "Script 'chunk_name', Line 1");
}
}