1mod classifier;
30mod parser;
31mod visibility;
32
33pub use classifier::{FrameCategory, FrameClassifier, PerlFrameClassifier};
34pub use parser::{PerlStackParser, StackParseError};
35pub use visibility::{
36 filter_user_visible_frames, is_internal_frame, is_internal_frame_name_and_path,
37};
38
39use serde::{Deserialize, Serialize};
40
41#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
46#[serde(rename_all = "camelCase")]
47pub struct StackFrame {
48 pub id: i64,
50
51 pub name: String,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
56 pub source: Option<Source>,
57
58 pub line: i64,
60
61 pub column: i64,
63
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub end_line: Option<i64>,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
70 pub end_column: Option<i64>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
74 pub can_restart: Option<bool>,
75
76 #[serde(skip_serializing_if = "Option::is_none")]
78 pub presentation_hint: Option<StackFramePresentationHint>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub module_id: Option<String>,
83}
84
85impl StackFrame {
86 #[must_use]
88 pub fn new(id: i64, name: impl Into<String>, source: Option<Source>, line: i64) -> Self {
89 Self {
90 id,
91 name: name.into(),
92 source,
93 line,
94 column: 1,
95 end_line: None,
96 end_column: None,
97 can_restart: None,
98 presentation_hint: None,
99 module_id: None,
100 }
101 }
102
103 #[must_use]
105 pub fn for_subroutine(id: i64, package: &str, sub_name: &str, file: &str, line: i64) -> Self {
106 let name = if package.is_empty() || package == "main" {
107 sub_name.to_string()
108 } else {
109 format!("{}::{}", package, sub_name)
110 };
111
112 Self::new(id, name, Some(Source::new(file)), line)
113 }
114
115 #[must_use]
117 pub fn with_column(mut self, column: i64) -> Self {
118 self.column = column;
119 self
120 }
121
122 #[must_use]
124 pub fn with_end(mut self, end_line: i64, end_column: i64) -> Self {
125 self.end_line = Some(end_line);
126 self.end_column = Some(end_column);
127 self
128 }
129
130 #[must_use]
132 pub fn with_presentation_hint(mut self, hint: StackFramePresentationHint) -> Self {
133 self.presentation_hint = Some(hint);
134 self
135 }
136
137 #[must_use]
139 pub fn with_module(mut self, module_id: impl Into<String>) -> Self {
140 self.module_id = Some(module_id.into());
141 self
142 }
143
144 #[must_use]
146 pub fn qualified_name(&self) -> &str {
147 &self.name
148 }
149
150 #[must_use]
152 pub fn file_path(&self) -> Option<&str> {
153 self.source.as_ref().and_then(|s| s.path.as_deref())
154 }
155
156 #[must_use]
158 pub fn is_user_code(&self) -> bool {
159 self.presentation_hint.as_ref() != Some(&StackFramePresentationHint::Subtle)
160 }
161}
162
163impl Default for StackFrame {
164 fn default() -> Self {
165 Self::new(0, "<unknown>", None, 0)
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub enum StackFramePresentationHint {
173 Normal,
175 Label,
177 Subtle,
179}
180
181#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct Source {
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub name: Option<String>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
191 pub path: Option<String>,
192
193 #[serde(skip_serializing_if = "Option::is_none")]
195 pub source_reference: Option<i64>,
196
197 #[serde(skip_serializing_if = "Option::is_none")]
199 pub origin: Option<String>,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
203 pub presentation_hint: Option<SourcePresentationHint>,
204}
205
206impl Source {
207 #[must_use]
209 pub fn new(path: impl Into<String>) -> Self {
210 let path_str = path.into();
211 let name =
212 std::path::Path::new(&path_str).file_name().and_then(|n| n.to_str()).map(String::from);
213
214 Self {
215 name,
216 path: Some(path_str),
217 source_reference: None,
218 origin: None,
219 presentation_hint: None,
220 }
221 }
222
223 #[must_use]
225 pub fn from_reference(reference: i64, name: impl Into<String>) -> Self {
226 Self {
227 name: Some(name.into()),
228 path: None,
229 source_reference: Some(reference),
230 origin: None,
231 presentation_hint: None,
232 }
233 }
234
235 #[must_use]
237 pub fn with_origin(mut self, origin: impl Into<String>) -> Self {
238 self.origin = Some(origin.into());
239 self
240 }
241
242 #[must_use]
244 pub fn with_presentation_hint(mut self, hint: SourcePresentationHint) -> Self {
245 self.presentation_hint = Some(hint);
246 self
247 }
248
249 #[must_use]
251 pub fn is_eval(&self) -> bool {
252 self.origin.as_deref() == Some("eval")
253 || self.path.as_ref().is_some_and(|p| p.contains("(eval"))
254 }
255
256 #[must_use]
258 pub fn has_file(&self) -> bool {
259 self.path.is_some() && !self.is_eval()
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
265#[serde(rename_all = "camelCase")]
266pub enum SourcePresentationHint {
267 Normal,
269 Emphasize,
271 Deemphasize,
273}
274
275pub trait StackTraceProvider {
280 type Error;
282
283 fn get_stack_trace(
295 &self,
296 thread_id: i64,
297 start_frame: usize,
298 levels: Option<usize>,
299 ) -> Result<Vec<StackFrame>, Self::Error>;
300
301 fn total_frames(&self, thread_id: i64) -> Result<usize, Self::Error>;
307
308 fn get_frame(&self, frame_id: i64) -> Result<Option<StackFrame>, Self::Error>;
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_stack_frame_new() {
322 let frame = StackFrame::new(1, "main::foo", Some(Source::new("/path/to/file.pl")), 42);
323
324 assert_eq!(frame.id, 1);
325 assert_eq!(frame.name, "main::foo");
326 assert_eq!(frame.line, 42);
327 assert_eq!(frame.column, 1);
328 assert!(frame.source.is_some());
329 }
330
331 #[test]
332 fn test_stack_frame_for_subroutine() {
333 let frame =
334 StackFrame::for_subroutine(1, "My::Package", "do_stuff", "/lib/My/Package.pm", 100);
335
336 assert_eq!(frame.name, "My::Package::do_stuff");
337 assert_eq!(frame.line, 100);
338 assert_eq!(frame.file_path(), Some("/lib/My/Package.pm"));
339 }
340
341 #[test]
342 fn test_stack_frame_for_main() {
343 let frame = StackFrame::for_subroutine(1, "main", "run", "/script.pl", 10);
344
345 assert_eq!(frame.name, "run");
346 }
347
348 #[test]
349 fn test_stack_frame_with_presentation_hint() {
350 let frame = StackFrame::new(1, "foo", None, 1)
351 .with_presentation_hint(StackFramePresentationHint::Subtle);
352
353 assert_eq!(frame.presentation_hint, Some(StackFramePresentationHint::Subtle));
354 assert!(!frame.is_user_code());
355 }
356
357 #[test]
358 fn test_source_new() {
359 let source = Source::new("/path/to/file.pm");
360
361 assert_eq!(source.path, Some("/path/to/file.pm".to_string()));
362 assert_eq!(source.name, Some("file.pm".to_string()));
363 }
364
365 #[test]
366 fn test_source_is_eval() {
367 let eval_source = Source::new("(eval 42)");
368 assert!(eval_source.is_eval());
369
370 let file_source = Source::new("/path/to/file.pl");
371 assert!(!file_source.is_eval());
372
373 let origin_eval = Source::new("/path/to/file.pl").with_origin("eval");
374 assert!(origin_eval.is_eval());
375 }
376
377 #[test]
378 fn test_source_has_file() {
379 let file_source = Source::new("/path/to/file.pl");
380 assert!(file_source.has_file());
381
382 let eval_source = Source::new("(eval 42)");
383 assert!(!eval_source.has_file());
384
385 let ref_source = Source::from_reference(1, "dynamic");
386 assert!(!ref_source.has_file());
387 }
388
389 #[test]
390 fn test_source_from_reference() {
391 let source = Source::from_reference(42, "eval code");
392
393 assert_eq!(source.source_reference, Some(42));
394 assert_eq!(source.name, Some("eval code".to_string()));
395 assert!(source.path.is_none());
396 }
397}