use super::{StackFrame, StackFramePresentationHint};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameCategory {
User,
Library,
Core,
Eval,
Unknown,
}
impl FrameCategory {
#[must_use]
pub fn presentation_hint(&self) -> StackFramePresentationHint {
match self {
FrameCategory::User => StackFramePresentationHint::Normal,
FrameCategory::Eval => StackFramePresentationHint::Label,
FrameCategory::Library | FrameCategory::Core | FrameCategory::Unknown => {
StackFramePresentationHint::Subtle
}
}
}
#[must_use]
pub fn is_user_code(&self) -> bool {
matches!(self, FrameCategory::User)
}
#[must_use]
pub fn is_external(&self) -> bool {
matches!(self, FrameCategory::Library | FrameCategory::Core)
}
}
pub trait FrameClassifier {
fn classify(&self, frame: &StackFrame) -> FrameCategory;
fn apply_classification(&self, frame: StackFrame) -> StackFrame {
let category = self.classify(&frame);
frame.with_presentation_hint(category.presentation_hint())
}
fn classify_all(&self, frames: Vec<StackFrame>, include_external: bool) -> Vec<StackFrame> {
frames
.into_iter()
.map(|f| self.apply_classification(f))
.filter(|f| include_external || f.is_user_code())
.collect()
}
}
#[derive(Debug, Default)]
pub struct PerlFrameClassifier {
user_paths: Vec<String>,
library_paths: Vec<String>,
}
impl PerlFrameClassifier {
#[must_use]
pub fn new() -> Self {
Self { user_paths: Vec::new(), library_paths: Vec::new() }
}
#[must_use]
pub fn with_user_path(mut self, path: impl Into<String>) -> Self {
self.user_paths.push(path.into());
self
}
#[must_use]
pub fn with_library_path(mut self, path: impl Into<String>) -> Self {
self.library_paths.push(path.into());
self
}
fn is_under_user_path(&self, path: &str) -> bool {
self.user_paths.iter().any(|user_path| path.starts_with(user_path))
}
fn is_under_library_path(&self, path: &str) -> bool {
self.library_paths.iter().any(|lib_path| path.starts_with(lib_path))
}
fn is_core_path(path: &str) -> bool {
let core_patterns = ["/perl/", "/perl5/", "/site_perl/", "/vendor_perl/", "/lib/perl5/"];
let core_packages = [
"strict.pm",
"warnings.pm",
"vars.pm",
"Exporter.pm",
"Carp.pm",
"constant.pm",
"overload.pm",
"AutoLoader.pm",
"base.pm",
"parent.pm",
"feature.pm",
"utf8.pm",
"encoding.pm",
"lib.pm",
];
for pattern in &core_patterns {
if path.contains(pattern) {
return true;
}
}
for module in &core_packages {
if path.ends_with(module) {
return true;
}
}
false
}
fn is_library_path(path: &str) -> bool {
let library_patterns =
["/local/lib/", "/vendor/", "/cpan/", "/.cpanm/", "/extlib/", "/fatlib/"];
for pattern in &library_patterns {
if path.contains(pattern) {
return true;
}
}
false
}
fn is_eval_source(path: &str) -> bool {
path.starts_with("(eval") || path.contains("(eval ")
}
}
impl FrameClassifier for PerlFrameClassifier {
fn classify(&self, frame: &StackFrame) -> FrameCategory {
let path = match frame.file_path() {
Some(p) => p,
None => return FrameCategory::Unknown,
};
if Self::is_eval_source(path) {
return FrameCategory::Eval;
}
if frame.source.as_ref().is_some_and(|s| s.is_eval()) {
return FrameCategory::Eval;
}
if self.is_under_user_path(path) {
return FrameCategory::User;
}
if self.is_under_library_path(path) {
return FrameCategory::Library;
}
if Self::is_core_path(path) {
return FrameCategory::Core;
}
if Self::is_library_path(path) {
return FrameCategory::Library;
}
FrameCategory::User
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stack::Source;
fn frame_with_path(path: &str) -> StackFrame {
StackFrame::new(1, "test", Some(Source::new(path)), 1)
}
#[test]
fn test_classify_user_code() {
let classifier = PerlFrameClassifier::new();
let frame = frame_with_path("/home/user/project/lib/MyApp/Module.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::User);
let frame = frame_with_path("./script.pl");
assert_eq!(classifier.classify(&frame), FrameCategory::User);
}
#[test]
fn test_classify_core_modules() {
let classifier = PerlFrameClassifier::new();
let frame = frame_with_path("/usr/lib/perl5/strict.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::Core);
let frame = frame_with_path("/usr/share/perl/5.30/Exporter.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::Core);
let frame = frame_with_path("/usr/lib/perl/5.30/warnings.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::Core);
}
#[test]
fn test_classify_library_code() {
let classifier = PerlFrameClassifier::new();
let frame = frame_with_path("/home/user/project/extlib/Moose.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::Library);
let frame = frame_with_path("/home/user/.cpanm/work/1234/Foo-1.0/lib/Foo.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::Library);
}
#[test]
fn test_classify_eval() {
let classifier = PerlFrameClassifier::new();
let frame = frame_with_path("(eval 42)");
assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
let frame = frame_with_path("(eval 10)[script.pl:5]");
assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
let mut frame = frame_with_path("/path/file.pm");
frame.source = Some(Source::new("/path/file.pm").with_origin("eval"));
assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
}
#[test]
fn test_classify_no_source() {
let classifier = PerlFrameClassifier::new();
let frame = StackFrame::new(1, "test", None, 1);
assert_eq!(classifier.classify(&frame), FrameCategory::Unknown);
}
#[test]
fn test_explicit_user_path() {
let classifier = PerlFrameClassifier::new().with_user_path("/my/project/");
let frame = frame_with_path("/my/project/local/lib/perl5/MyModule.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::User);
}
#[test]
fn test_explicit_library_path() {
let classifier = PerlFrameClassifier::new().with_library_path("/opt/mylibs/");
let frame = frame_with_path("/opt/mylibs/SomeModule.pm");
assert_eq!(classifier.classify(&frame), FrameCategory::Library);
}
#[test]
fn test_frame_category_presentation_hint() {
assert_eq!(FrameCategory::User.presentation_hint(), StackFramePresentationHint::Normal);
assert_eq!(FrameCategory::Library.presentation_hint(), StackFramePresentationHint::Subtle);
assert_eq!(FrameCategory::Core.presentation_hint(), StackFramePresentationHint::Subtle);
assert_eq!(FrameCategory::Eval.presentation_hint(), StackFramePresentationHint::Label);
}
#[test]
fn test_apply_classification() {
let classifier = PerlFrameClassifier::new();
let frame = frame_with_path("/usr/lib/perl5/strict.pm");
let classified = classifier.apply_classification(frame);
assert_eq!(classified.presentation_hint, Some(StackFramePresentationHint::Subtle));
}
#[test]
fn test_classify_all() {
let classifier = PerlFrameClassifier::new();
let frames = vec![
frame_with_path("/home/user/project/script.pl"),
frame_with_path("/usr/lib/perl5/strict.pm"),
frame_with_path("/home/user/project/lib/App.pm"),
];
let classified = classifier.classify_all(frames.clone(), true);
assert_eq!(classified.len(), 3);
let classified = classifier.classify_all(frames, false);
assert_eq!(classified.len(), 2);
}
#[test]
fn test_is_user_code() {
assert!(FrameCategory::User.is_user_code());
assert!(!FrameCategory::Library.is_user_code());
assert!(!FrameCategory::Core.is_user_code());
assert!(!FrameCategory::Eval.is_user_code());
}
#[test]
fn test_is_external() {
assert!(!FrameCategory::User.is_external());
assert!(FrameCategory::Library.is_external());
assert!(FrameCategory::Core.is_external());
assert!(!FrameCategory::Eval.is_external());
}
}