Skip to main content

perl_dap_stack/
classifier.rs

1//! Frame classifier for distinguishing user code from library code.
2//!
3//! This module provides utilities for classifying stack frames based on their
4//! source location, helping debuggers present relevant frames to users.
5
6use crate::{StackFrame, StackFramePresentationHint};
7
8/// Categories for stack frame classification.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FrameCategory {
11    /// User code (the developer's own code)
12    User,
13    /// Library code (third-party modules)
14    Library,
15    /// Core Perl code (built-in modules and internals)
16    Core,
17    /// Eval-generated code
18    Eval,
19    /// Unknown origin
20    Unknown,
21}
22
23impl FrameCategory {
24    /// Returns the appropriate presentation hint for this category.
25    #[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    /// Returns true if this category represents user-written code.
37    #[must_use]
38    pub fn is_user_code(&self) -> bool {
39        matches!(self, FrameCategory::User)
40    }
41
42    /// Returns true if this category represents external code.
43    #[must_use]
44    pub fn is_external(&self) -> bool {
45        matches!(self, FrameCategory::Library | FrameCategory::Core)
46    }
47}
48
49/// Trait for classifying stack frames.
50///
51/// Implementations determine whether a stack frame represents user code,
52/// library code, or core Perl internals.
53pub trait FrameClassifier {
54    /// Classifies a stack frame.
55    ///
56    /// # Arguments
57    ///
58    /// * `frame` - The frame to classify
59    ///
60    /// # Returns
61    ///
62    /// The classification category for the frame.
63    fn classify(&self, frame: &StackFrame) -> FrameCategory;
64
65    /// Applies classification to a frame, setting its presentation hint.
66    ///
67    /// # Arguments
68    ///
69    /// * `frame` - The frame to classify and update
70    ///
71    /// # Returns
72    ///
73    /// The frame with updated presentation hint.
74    fn apply_classification(&self, frame: StackFrame) -> StackFrame {
75        let category = self.classify(&frame);
76        frame.with_presentation_hint(category.presentation_hint())
77    }
78
79    /// Classifies and filters a list of frames.
80    ///
81    /// # Arguments
82    ///
83    /// * `frames` - The frames to classify
84    /// * `include_external` - Whether to include library/core frames
85    ///
86    /// # Returns
87    ///
88    /// Classified frames with appropriate presentation hints.
89    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/// Default Perl frame classifier.
99///
100/// This classifier uses path-based heuristics to determine frame categories:
101///
102/// - Core modules: Paths containing `/perl/`, `/perl5/`, or standard module names
103/// - Library code: Paths in common library directories (lib, vendor, local)
104/// - Eval code: Files named `(eval N)` or with eval origin
105/// - User code: Everything else (assumed to be project code)
106#[derive(Debug, Default)]
107pub struct PerlFrameClassifier {
108    /// Paths considered to be user code directories
109    user_paths: Vec<String>,
110    /// Paths considered to be library directories
111    library_paths: Vec<String>,
112}
113
114impl PerlFrameClassifier {
115    /// Creates a new classifier with default settings.
116    #[must_use]
117    pub fn new() -> Self {
118        Self { user_paths: Vec::new(), library_paths: Vec::new() }
119    }
120
121    /// Adds a path that should be considered user code.
122    ///
123    /// Files under this path will be classified as user code.
124    #[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    /// Adds a path that should be considered library code.
131    ///
132    /// Files under this path will be classified as library code.
133    #[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    /// Checks if a path is under any of the user paths.
140    fn is_under_user_path(&self, path: &str) -> bool {
141        self.user_paths.iter().any(|user_path| path.starts_with(user_path))
142    }
143
144    /// Checks if a path is under any of the library paths.
145    fn is_under_library_path(&self, path: &str) -> bool {
146        self.library_paths.iter().any(|lib_path| path.starts_with(lib_path))
147    }
148
149    /// Checks if a path looks like a Perl core module.
150    fn is_core_path(path: &str) -> bool {
151        // Common core module path patterns
152        let core_patterns = ["/perl/", "/perl5/", "/site_perl/", "/vendor_perl/", "/lib/perl5/"];
153
154        // Core module packages
155        let core_packages = [
156            "strict.pm",
157            "warnings.pm",
158            "vars.pm",
159            "Exporter.pm",
160            "Carp.pm",
161            "constant.pm",
162            "overload.pm",
163            "AutoLoader.pm",
164            "base.pm",
165            "parent.pm",
166            "feature.pm",
167            "utf8.pm",
168            "encoding.pm",
169            "lib.pm",
170        ];
171
172        // Check path patterns
173        for pattern in &core_patterns {
174            if path.contains(pattern) {
175                return true;
176            }
177        }
178
179        // Check if it's a known core module
180        for module in &core_packages {
181            if path.ends_with(module) {
182                return true;
183            }
184        }
185
186        false
187    }
188
189    /// Checks if a path looks like a library module.
190    fn is_library_path(path: &str) -> bool {
191        // Common library path patterns
192        let library_patterns =
193            ["/local/lib/", "/vendor/", "/cpan/", "/.cpanm/", "/extlib/", "/fatlib/"];
194
195        for pattern in &library_patterns {
196            if path.contains(pattern) {
197                return true;
198            }
199        }
200
201        false
202    }
203
204    /// Checks if a path looks like an eval source.
205    fn is_eval_source(path: &str) -> bool {
206        path.starts_with("(eval") || path.contains("(eval ")
207    }
208}
209
210impl FrameClassifier for PerlFrameClassifier {
211    fn classify(&self, frame: &StackFrame) -> FrameCategory {
212        // Check source
213        let path = match frame.file_path() {
214            Some(p) => p,
215            None => return FrameCategory::Unknown,
216        };
217
218        // Check for eval
219        if Self::is_eval_source(path) {
220            return FrameCategory::Eval;
221        }
222
223        // Also check source origin
224        if frame.source.as_ref().is_some_and(|s| s.is_eval()) {
225            return FrameCategory::Eval;
226        }
227
228        // Check explicit user paths first
229        if self.is_under_user_path(path) {
230            return FrameCategory::User;
231        }
232
233        // Check explicit library paths
234        if self.is_under_library_path(path) {
235            return FrameCategory::Library;
236        }
237
238        // Check for core modules
239        if Self::is_core_path(path) {
240            return FrameCategory::Core;
241        }
242
243        // Check for library modules
244        if Self::is_library_path(path) {
245            return FrameCategory::Library;
246        }
247
248        // Default to user code if we can't determine otherwise
249        // This is intentional: we want to show frames by default rather than hide them
250        FrameCategory::User
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257    use crate::Source;
258
259    fn frame_with_path(path: &str) -> StackFrame {
260        StackFrame::new(1, "test", Some(Source::new(path)), 1)
261    }
262
263    #[test]
264    fn test_classify_user_code() {
265        let classifier = PerlFrameClassifier::new();
266
267        // Regular project files should be user code
268        let frame = frame_with_path("/home/user/project/lib/MyApp/Module.pm");
269        assert_eq!(classifier.classify(&frame), FrameCategory::User);
270
271        let frame = frame_with_path("./script.pl");
272        assert_eq!(classifier.classify(&frame), FrameCategory::User);
273    }
274
275    #[test]
276    fn test_classify_core_modules() {
277        let classifier = PerlFrameClassifier::new();
278
279        let frame = frame_with_path("/usr/lib/perl5/strict.pm");
280        assert_eq!(classifier.classify(&frame), FrameCategory::Core);
281
282        let frame = frame_with_path("/usr/share/perl/5.30/Exporter.pm");
283        assert_eq!(classifier.classify(&frame), FrameCategory::Core);
284
285        let frame = frame_with_path("/usr/lib/perl/5.30/warnings.pm");
286        assert_eq!(classifier.classify(&frame), FrameCategory::Core);
287    }
288
289    #[test]
290    fn test_classify_library_code() {
291        let classifier = PerlFrameClassifier::new();
292
293        // Use paths that match library patterns but not core patterns
294        let frame = frame_with_path("/home/user/project/extlib/Moose.pm");
295        assert_eq!(classifier.classify(&frame), FrameCategory::Library);
296
297        let frame = frame_with_path("/home/user/.cpanm/work/1234/Foo-1.0/lib/Foo.pm");
298        assert_eq!(classifier.classify(&frame), FrameCategory::Library);
299    }
300
301    #[test]
302    fn test_classify_eval() {
303        let classifier = PerlFrameClassifier::new();
304
305        let frame = frame_with_path("(eval 42)");
306        assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
307
308        let frame = frame_with_path("(eval 10)[script.pl:5]");
309        assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
310
311        // Frame with eval origin
312        let mut frame = frame_with_path("/path/file.pm");
313        frame.source = Some(Source::new("/path/file.pm").with_origin("eval"));
314        assert_eq!(classifier.classify(&frame), FrameCategory::Eval);
315    }
316
317    #[test]
318    fn test_classify_no_source() {
319        let classifier = PerlFrameClassifier::new();
320
321        let frame = StackFrame::new(1, "test", None, 1);
322        assert_eq!(classifier.classify(&frame), FrameCategory::Unknown);
323    }
324
325    #[test]
326    fn test_explicit_user_path() {
327        let classifier = PerlFrameClassifier::new().with_user_path("/my/project/");
328
329        // Should be classified as user even if path looks like library
330        let frame = frame_with_path("/my/project/local/lib/perl5/MyModule.pm");
331        assert_eq!(classifier.classify(&frame), FrameCategory::User);
332    }
333
334    #[test]
335    fn test_explicit_library_path() {
336        let classifier = PerlFrameClassifier::new().with_library_path("/opt/mylibs/");
337
338        let frame = frame_with_path("/opt/mylibs/SomeModule.pm");
339        assert_eq!(classifier.classify(&frame), FrameCategory::Library);
340    }
341
342    #[test]
343    fn test_frame_category_presentation_hint() {
344        assert_eq!(FrameCategory::User.presentation_hint(), StackFramePresentationHint::Normal);
345        assert_eq!(FrameCategory::Library.presentation_hint(), StackFramePresentationHint::Subtle);
346        assert_eq!(FrameCategory::Core.presentation_hint(), StackFramePresentationHint::Subtle);
347        assert_eq!(FrameCategory::Eval.presentation_hint(), StackFramePresentationHint::Label);
348    }
349
350    #[test]
351    fn test_apply_classification() {
352        let classifier = PerlFrameClassifier::new();
353        let frame = frame_with_path("/usr/lib/perl5/strict.pm");
354
355        let classified = classifier.apply_classification(frame);
356        assert_eq!(classified.presentation_hint, Some(StackFramePresentationHint::Subtle));
357    }
358
359    #[test]
360    fn test_classify_all() {
361        let classifier = PerlFrameClassifier::new();
362
363        let frames = vec![
364            frame_with_path("/home/user/project/script.pl"),
365            frame_with_path("/usr/lib/perl5/strict.pm"),
366            frame_with_path("/home/user/project/lib/App.pm"),
367        ];
368
369        // With external frames
370        let classified = classifier.classify_all(frames.clone(), true);
371        assert_eq!(classified.len(), 3);
372
373        // Without external frames
374        let classified = classifier.classify_all(frames, false);
375        assert_eq!(classified.len(), 2);
376    }
377
378    #[test]
379    fn test_is_user_code() {
380        assert!(FrameCategory::User.is_user_code());
381        assert!(!FrameCategory::Library.is_user_code());
382        assert!(!FrameCategory::Core.is_user_code());
383        assert!(!FrameCategory::Eval.is_user_code());
384    }
385
386    #[test]
387    fn test_is_external() {
388        assert!(!FrameCategory::User.is_external());
389        assert!(FrameCategory::Library.is_external());
390        assert!(FrameCategory::Core.is_external());
391        assert!(!FrameCategory::Eval.is_external());
392    }
393}