perl_dap_stack/
classifier.rs1use crate::{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_user_path(&self, path: &str) -> bool {
141 self.user_paths.iter().any(|user_path| path.starts_with(user_path))
142 }
143
144 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 fn is_core_path(path: &str) -> bool {
151 let core_patterns = ["/perl/", "/perl5/", "/site_perl/", "/vendor_perl/", "/lib/perl5/"];
153
154 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 for pattern in &core_patterns {
174 if path.contains(pattern) {
175 return true;
176 }
177 }
178
179 for module in &core_packages {
181 if path.ends_with(module) {
182 return true;
183 }
184 }
185
186 false
187 }
188
189 fn is_library_path(path: &str) -> bool {
191 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 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 let path = match frame.file_path() {
214 Some(p) => p,
215 None => return FrameCategory::Unknown,
216 };
217
218 if Self::is_eval_source(path) {
220 return FrameCategory::Eval;
221 }
222
223 if frame.source.as_ref().is_some_and(|s| s.is_eval()) {
225 return FrameCategory::Eval;
226 }
227
228 if self.is_under_user_path(path) {
230 return FrameCategory::User;
231 }
232
233 if self.is_under_library_path(path) {
235 return FrameCategory::Library;
236 }
237
238 if Self::is_core_path(path) {
240 return FrameCategory::Core;
241 }
242
243 if Self::is_library_path(path) {
245 return FrameCategory::Library;
246 }
247
248 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 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 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 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 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 let classified = classifier.classify_all(frames.clone(), true);
371 assert_eq!(classified.len(), 3);
372
373 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}