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 }
    }
}