lune_utils/fmt/error/
stack_trace.rs

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