1use super::{StackFrame, StackFramePresentationHint};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FrameCategory {
11 User,
13 Library,
15 Core,
17 Eval,
19 Unknown,
21}
22
23impl FrameCategory {
24 #[must_use]
26 pub fn presentation_hint(&self) -> StackFramePresentationHint {
27 match self {
28 FrameCategory::User => StackFramePresentationHint::Normal,
29 FrameCategory::Eval => StackFramePresentationHint::Label,
30 FrameCategory::Library | FrameCategory::Core | FrameCategory::Unknown => {
31 StackFramePresentationHint::Subtle
32 }
33 }
34 }
35
36 #[must_use]
38 pub fn is_user_code(&self) -> bool {
39 matches!(self, FrameCategory::User)
40 }
41
42 #[must_use]
44 pub fn is_external(&self) -> bool {
45 matches!(self, FrameCategory::Library | FrameCategory::Core)
46 }
47}
48
49pub trait FrameClassifier {
54 fn classify(&self, frame: &StackFrame) -> FrameCategory;
64
65 fn apply_classification(&self, frame: StackFrame) -> StackFrame {
75 let category = self.classify(&frame);
76 frame.with_presentation_hint(category.presentation_hint())
77 }
78
79 fn classify_all(&self, frames: Vec<StackFrame>, include_external: bool) -> Vec<StackFrame> {
90 frames
91 .into_iter()
92 .map(|f| self.apply_classification(f))
93 .filter(|f| include_external || f.is_user_code())
94 .collect()
95 }
96}
97
98#[derive(Debug, Default)]
107pub struct PerlFrameClassifier {
108 user_paths: Vec<String>,
110 library_paths: Vec<String>,
112}
113
114impl PerlFrameClassifier {
115 #[must_use]
117 pub fn new() -> Self {
118 Self { user_paths: Vec::new(), library_paths: Vec::new() }
119 }
120
121 #[must_use]
125 pub fn with_user_path(mut self, path: impl Into<String>) -> Self {
126 self.user_paths.push(path.into());
127 self
128 }
129
130 #[must_use]
134 pub fn with_library_path(mut self, path: impl Into<String>) -> Self {
135 self.library_paths.push(path.into());
136 self
137 }
138
139 fn is_under_root(path: &str, root: &str) -> bool {
140 let raw_root = root;
141 let root = root.trim_end_matches(['/', '\\']);
142 if root.is_empty() {
143 return Self::root_matches_filesystem_root(path, raw_root);
144 }
145
146 if path == root {
147 return true;
148 }
149
150 if let Some(remainder) = path.strip_prefix(root) {
151 return remainder.starts_with(['/', '\\']);
152 }
153
154 false
155 }
156
157 fn root_matches_filesystem_root(path: &str, root: &str) -> bool {
158 let uses_unix_root = !root.is_empty() && root.chars().all(|ch| ch == '/');
159 let uses_windows_root = !root.is_empty() && root.chars().all(|ch| ch == '\\');
160 (uses_unix_root && path.starts_with('/')) || (uses_windows_root && path.starts_with('\\'))
161 }
162
163 fn is_under_user_path(&self, path: &str) -> bool {
165 self.user_paths.iter().any(|user_path| Self::is_under_root(path, user_path))
166 }
167
168 fn is_under_library_path(&self, path: &str) -> bool {
170 self.library_paths.iter().any(|lib_path| Self::is_under_root(path, lib_path))
171 }
172
173 fn is_core_path(path: &str) -> bool {
175 let core_patterns = ["/perl/", "/perl5/", "/site_perl/", "/vendor_perl/", "/lib/perl5/"];
177
178 let core_packages = [
180 "strict.pm",
181 "warnings.pm",
182 "vars.pm",
183 "Exporter.pm",
184 "Carp.pm",
185 "constant.pm",
186 "overload.pm",
187 "AutoLoader.pm",
188 "base.pm",
189 "parent.pm",
190 "feature.pm",
191 "utf8.pm",
192 "encoding.pm",
193 "lib.pm",
194 ];
195
196 for pattern in &core_patterns {
198 if path.contains(pattern) {
199 return true;
200 }
201 }
202
203 for module in &core_packages {
205 if path.ends_with(module) {
206 return true;
207 }
208 }
209
210 false
211 }
212
213 fn is_library_path(path: &str) -> bool {
215 let library_patterns =
217 ["/local/lib/", "/vendor/", "/cpan/", "/.cpanm/", "/extlib/", "/fatlib/"];
218
219 for pattern in &library_patterns {
220 if path.contains(pattern) {
221 return true;
222 }
223 }
224
225 false
226 }
227
228 fn is_eval_source(path: &str) -> bool {
230 path.starts_with("(eval") || path.contains("(eval ")
231 }
232}
233
234impl FrameClassifier for PerlFrameClassifier {
235 fn classify(&self, frame: &StackFrame) -> FrameCategory {
236 let path = match frame.file_path() {
238 Some(p) => p,
239 None => return FrameCategory::Unknown,
240 };
241
242 if Self::is_eval_source(path) {
244 return FrameCategory::Eval;
245 }
246
247 if frame.source.as_ref().is_some_and(|s| s.is_eval()) {
249 return FrameCategory::Eval;
250 }
251
252 if self.is_under_user_path(path) {
254 return FrameCategory::User;
255 }
256
257 if self.is_under_library_path(path) {
259 return FrameCategory::Library;
260 }
261
262 if Self::is_core_path(path) {
264 return FrameCategory::Core;
265 }
266
267 if Self::is_library_path(path) {
269 return FrameCategory::Library;
270 }
271
272 FrameCategory::User
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::stack::Source;
282
283 fn frame_with_path(path: &str) -> StackFrame {
284 StackFrame::new(1, "test", Some(Source::new(path)), 1)
285 }
286
287 #[test]
288 fn test_classify_user_code() {
289 let classifier = PerlFrameClassifier::new();
290
291 let frame = frame_with_path("/home/user/project/lib/MyApp/Module.pm");
293 assert_eq!(classifier.classify(&frame), FrameCategory::User);
294
295 let frame = frame_with_path("./script.pl");
296 assert_eq!(classifier.classify(&frame), FrameCategory::User);
297 }
298
299 #[test]
300 fn test_classify_core_modules() {
301 let classifier = PerlFrameClassifier::new();
302
303 let frame = frame_with_path("/usr/lib/perl5/strict.pm");
304 assert_eq!(classifier.classify(&frame), FrameCategory::Core);
305
306 let frame = frame_with_path("/usr/share/perl/5.30/Exporter.pm");
307 assert_eq!(classifier.classify(&frame), FrameCategory::Core);
308
309 let frame = frame_with_path("/usr/lib/perl/5.30/warnings.pm");
310 assert_eq!(classifier.classify(&frame), FrameCategory::Core);
311 }
312
313 #[test]
314 fn test_classify_library_code() {
315 let classifier = PerlFrameClassifier::new();
316
317 let frame = frame_with_path("/home/user/project/extlib/Moose.pm");
319 assert_eq!(classifier.classify(&frame), FrameCategory::Library);
320
321 let frame = frame_with_path("/home/user/.cpanm/work/1234/Foo-1.0/lib/Foo.pm");
322 assert_eq!(classifier.classify(&frame), FrameCategory::Library);
323 }
324
325 #[test]
326 fn test_classify_eval() {
327 let classifier = PerlFrameClassifier::new();
328
329 let frame = frame_with_path("(eval 42)");
330 assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
331
332 let frame = frame_with_path("(eval 10)[script.pl:5]");
333 assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
334
335 let mut frame = frame_with_path("/path/file.pm");
337 frame.source = Some(Source::new("/path/file.pm").with_origin("eval"));
338 assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
339 }
340
341 #[test]
342 fn test_classify_no_source() {
343 let classifier = PerlFrameClassifier::new();
344
345 let frame = StackFrame::new(1, "test", None, 1);
346 assert_eq!(classifier.classify(&frame), FrameCategory::Unknown);
347 }
348
349 #[test]
350 fn test_explicit_user_path() {
351 let classifier = PerlFrameClassifier::new().with_user_path("/my/project/");
352
353 let frame = frame_with_path("/my/project/local/lib/perl5/MyModule.pm");
355 assert_eq!(classifier.classify(&frame), FrameCategory::User);
356 }
357
358 #[test]
359 fn test_explicit_library_path() {
360 let classifier = PerlFrameClassifier::new().with_library_path("/opt/mylibs/");
361
362 let frame = frame_with_path("/opt/mylibs/SomeModule.pm");
363 assert_eq!(classifier.classify(&frame), FrameCategory::Library);
364 }
365
366 #[test]
367 fn test_explicit_path_matching_uses_directory_boundaries() {
368 let classifier = PerlFrameClassifier::new().with_user_path("/workspace/project");
369 let frame = frame_with_path("/workspace/project2/extlib/App.pm");
370 assert_eq!(classifier.classify(&frame), FrameCategory::Library);
371
372 let classifier = PerlFrameClassifier::new().with_library_path("/opt/lib");
373 let frame = frame_with_path("/opt/library/Foo.pm");
374 assert_eq!(classifier.classify(&frame), FrameCategory::User);
375 }
376
377 #[test]
378 fn test_explicit_path_matching_preserves_filesystem_roots() {
379 let classifier = PerlFrameClassifier::new().with_user_path("/");
380 let frame = frame_with_path("/workspace/project/extlib/App.pm");
381 assert_eq!(classifier.classify(&frame), FrameCategory::User);
382
383 let classifier = PerlFrameClassifier::new().with_library_path("\\");
384 let frame = frame_with_path("\\workspace\\project\\App.pm");
385 assert_eq!(classifier.classify(&frame), FrameCategory::Library);
386 }
387
388 #[test]
389 fn test_frame_category_presentation_hint() {
390 assert_eq!(FrameCategory::User.presentation_hint(), StackFramePresentationHint::Normal);
391 assert_eq!(FrameCategory::Library.presentation_hint(), StackFramePresentationHint::Subtle);
392 assert_eq!(FrameCategory::Core.presentation_hint(), StackFramePresentationHint::Subtle);
393 assert_eq!(FrameCategory::Eval.presentation_hint(), StackFramePresentationHint::Label);
394 }
395
396 #[test]
397 fn test_apply_classification() {
398 let classifier = PerlFrameClassifier::new();
399 let frame = frame_with_path("/usr/lib/perl5/strict.pm");
400
401 let classified = classifier.apply_classification(frame);
402 assert_eq!(classified.presentation_hint, Some(StackFramePresentationHint::Subtle));
403 }
404
405 #[test]
406 fn test_classify_all() {
407 let classifier = PerlFrameClassifier::new();
408
409 let frames = vec![
410 frame_with_path("/home/user/project/script.pl"),
411 frame_with_path("/usr/lib/perl5/strict.pm"),
412 frame_with_path("/home/user/project/lib/App.pm"),
413 ];
414
415 let classified = classifier.classify_all(frames.clone(), true);
417 assert_eq!(classified.len(), 3);
418
419 let classified = classifier.classify_all(frames, false);
421 assert_eq!(classified.len(), 2);
422 }
423
424 #[test]
425 fn test_is_user_code() {
426 assert!(FrameCategory::User.is_user_code());
427 assert!(!FrameCategory::Library.is_user_code());
428 assert!(!FrameCategory::Core.is_user_code());
429 assert!(!FrameCategory::Eval.is_user_code());
430 }
431
432 #[test]
433 fn test_is_external() {
434 assert!(!FrameCategory::User.is_external());
435 assert!(FrameCategory::Library.is_external());
436 assert!(FrameCategory::Core.is_external());
437 assert!(!FrameCategory::Eval.is_external());
438 }
439}