lune_utils/fmt/error/
stack_trace.rs

1use std::fmt;
2use std::str::FromStr;
3
4fn unwrap_braced_path(s: &str) -> &str {
5    s.strip_prefix("[string \"")
6        .and_then(|s2| s2.strip_suffix("\"]"))
7        .unwrap_or(s)
8}
9
10fn parse_path(s: &str) -> Option<(&str, &str)> {
11    let (path, after) = unwrap_braced_path(s).split_once(':')?;
12    let path = unwrap_braced_path(path);
13
14    // Remove line number after any found colon, this may
15    // exist if the source path is from a rust source file
16    let path = match path.split_once(':') {
17        Some((before, _)) => before,
18        None => path,
19    };
20
21    Some((path, after))
22}
23
24fn parse_function_name(s: &str) -> Option<&str> {
25    s.strip_prefix("in function '")
26        .and_then(|s| s.strip_suffix('\''))
27}
28
29fn parse_line_number(s: &str) -> (Option<usize>, &str) {
30    match s.split_once(':') {
31        Some((before, after)) => (before.parse::<usize>().ok(), after),
32        None => (None, s),
33    }
34}
35
36/**
37    Source of a stack trace line parsed from a [`LuaError`].
38*/
39#[derive(Debug, Default, Clone, Copy)]
40pub enum StackTraceSource {
41    /// Error originated from a C / Rust function.
42    C,
43    /// Error originated from a Lua (user) function.
44    #[default]
45    Lua,
46}
47
48impl StackTraceSource {
49    /**
50        Returns `true` if the error originated from a C / Rust function, `false` otherwise.
51    */
52    #[must_use]
53    pub const fn is_c(self) -> bool {
54        matches!(self, Self::C)
55    }
56
57    /**
58        Returns `true` if the error originated from a Lua (user) function, `false` otherwise.
59    */
60    #[must_use]
61    pub const fn is_lua(self) -> bool {
62        matches!(self, Self::Lua)
63    }
64}
65
66/**
67    Stack trace line parsed from a [`LuaError`].
68*/
69#[derive(Debug, Default, Clone)]
70pub struct StackTraceLine {
71    source: StackTraceSource,
72    path: Option<String>,
73    line_number: Option<usize>,
74    function_name: Option<String>,
75}
76
77impl StackTraceLine {
78    /**
79        Returns the source of the stack trace line.
80    */
81    #[must_use]
82    pub fn source(&self) -> StackTraceSource {
83        self.source
84    }
85
86    /**
87        Returns the path, if it exists.
88    */
89    #[must_use]
90    pub fn path(&self) -> Option<&str> {
91        self.path.as_deref()
92    }
93
94    /**
95        Returns the line number, if it exists.
96    */
97    #[must_use]
98    pub fn line_number(&self) -> Option<usize> {
99        self.line_number
100    }
101
102    /**
103        Returns the function name, if it exists.
104    */
105    #[must_use]
106    pub fn function_name(&self) -> Option<&str> {
107        self.function_name.as_deref()
108    }
109
110    /**
111        Returns `true` if the stack trace line contains no "useful" information, `false` otherwise.
112
113        Useful information is determined as one of:
114
115        - A path
116        - A line number
117        - A function name
118    */
119    #[must_use]
120    pub const fn is_empty(&self) -> bool {
121        self.path.is_none() && self.line_number.is_none() && self.function_name.is_none()
122    }
123}
124
125impl FromStr for StackTraceLine {
126    type Err = String;
127    fn from_str(s: &str) -> Result<Self, Self::Err> {
128        if let Some(after) = s.strip_prefix("[C]: ") {
129            let function_name = parse_function_name(after).map(ToString::to_string);
130
131            Ok(Self {
132                source: StackTraceSource::C,
133                path: None,
134                line_number: None,
135                function_name,
136            })
137        } else if let Some((path, after)) = parse_path(s) {
138            let (line_number, after) = parse_line_number(after);
139            let function_name = parse_function_name(after).map(ToString::to_string);
140
141            Ok(Self {
142                source: StackTraceSource::Lua,
143                path: Some(path.to_string()),
144                line_number,
145                function_name,
146            })
147        } else {
148            Err(String::from("unknown format"))
149        }
150    }
151}
152
153impl fmt::Display for StackTraceLine {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        if matches!(self.source, StackTraceSource::C) {
156            write!(f, "Script '[C]'")?;
157        } else {
158            write!(f, "Script '{}'", self.path.as_deref().unwrap_or("[?]"))?;
159            if let Some(line_number) = self.line_number {
160                write!(f, ", Line {line_number}")?;
161            }
162        }
163        if let Some(function_name) = self.function_name.as_deref() {
164            write!(f, " - function '{function_name}'")?;
165        }
166        Ok(())
167    }
168}
169
170/**
171    Stack trace parsed from a [`LuaError`].
172*/
173#[derive(Debug, Default, Clone)]
174pub struct StackTrace {
175    lines: Vec<StackTraceLine>,
176}
177
178impl StackTrace {
179    /**
180        Returns the individual stack trace lines.
181    */
182    #[must_use]
183    pub fn lines(&self) -> &[StackTraceLine] {
184        &self.lines
185    }
186
187    /**
188        Returns the individual stack trace lines, mutably.
189    */
190    #[must_use]
191    pub fn lines_mut(&mut self) -> &mut Vec<StackTraceLine> {
192        &mut self.lines
193    }
194}
195
196impl FromStr for StackTrace {
197    type Err = String;
198    fn from_str(s: &str) -> Result<Self, Self::Err> {
199        let (_, after) = s
200            .split_once("stack traceback:")
201            .ok_or_else(|| String::from("missing 'stack traceback:' prefix"))?;
202        let lines = after
203            .trim()
204            .lines()
205            .filter_map(|line| {
206                let line = line.trim();
207                if line.is_empty() {
208                    None
209                } else {
210                    Some(line.parse())
211                }
212            })
213            .collect::<Result<Vec<_>, _>>()?;
214        Ok(StackTrace { lines })
215    }
216}