mirror of
https://github.com/lune-org/lune.git
synced 2025-04-13 23:10:52 +01:00
Initial working version of new error parser and display
This commit is contained in:
parent
7c702e4d34
commit
6dad31df14
6 changed files with 415 additions and 85 deletions
crates/lune-utils/src/fmt
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
152
crates/lune-utils/src/fmt/error/components.rs
Normal file
152
crates/lune-utils/src/fmt/error/components.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
5
crates/lune-utils/src/fmt/error/mod.rs
Normal file
5
crates/lune-utils/src/fmt/error/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod components;
|
||||||
|
mod stack_trace;
|
||||||
|
|
||||||
|
pub use self::components::ErrorComponents;
|
||||||
|
pub use self::stack_trace::{StackTrace, StackTraceLine, StackTraceSource};
|
170
crates/lune-utils/src/fmt/error/stack_trace.rs
Normal file
170
crates/lune-utils/src/fmt/error/stack_trace.rs
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
mod error;
|
mod error;
|
||||||
mod label;
|
mod label;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
pub use self::error::{ErrorComponents, StackTrace, StackTraceLine, StackTraceSource};
|
pub use self::error::{ErrorComponents, StackTrace, StackTraceLine, StackTraceSource};
|
||||||
pub use self::label::Label;
|
pub use self::label::Label;
|
||||||
|
|
85
crates/lune-utils/src/fmt/tests.rs
Normal file
85
crates/lune-utils/src/fmt/tests.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue