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