1use fancy_regex::Regex;
15use rich::{Console, ConsoleOptions, Renderable, Segment, Text};
16use std::sync::OnceLock;
17
18use crate::event::theme_style;
19use crate::hyperlink::Hyperlinker;
20
21pub const MAX_CAUSES: usize = 64;
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub enum Language {
29 Rust,
30 Python,
31 Java,
32 JavaScript,
33 Other(String),
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq)]
39pub struct Frame {
40 pub function: Option<String>,
42 pub path: Option<String>,
44 pub line: Option<usize>,
46 pub column: Option<usize>,
48 pub source: Option<String>,
50 pub metadata: Vec<(String, String)>,
52 pub library: bool,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub enum CauseKind {
60 CausedBy,
63 DuringHandling,
66}
67
68#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct StackTrace {
71 pub language: Language,
73 pub kind: Option<String>,
75 pub message: Option<String>,
77 pub frames: Vec<Frame>,
79 pub cause: Option<Box<StackTrace>>,
81 pub cause_kind: CauseKind,
83 pub omitted_causes: usize,
86 pub location: Option<Frame>,
89}
90
91impl StackTrace {
92 pub fn new(language: Language) -> Self {
94 StackTrace {
95 language,
96 kind: None,
97 message: None,
98 frames: Vec::new(),
99 cause: None,
100 cause_kind: CauseKind::CausedBy,
101 omitted_causes: 0,
102 location: None,
103 }
104 }
105
106 pub fn chain(&self) -> impl Iterator<Item = &StackTrace> {
108 std::iter::successors(Some(self), |trace| trace.cause.as_deref())
109 }
110
111 pub fn origin(&self) -> Option<&Frame> {
113 self.frames.iter().rev().find(|frame| !frame.library)
114 }
115
116 pub fn render_options(&self) -> StackTraceView<'_> {
118 StackTraceView {
119 trace: self,
120 linker: Hyperlinker::new(),
121 show_library: false,
122 }
123 }
124}
125
126impl Drop for StackTrace {
129 fn drop(&mut self) {
130 let mut next = self.cause.take();
131 while let Some(mut trace) = next {
132 next = trace.cause.take();
133 }
134 }
135}
136
137fn link_causes(mut traces: Vec<StackTrace>) -> Option<StackTrace> {
141 let omitted = traces.len().saturating_sub(MAX_CAUSES + 1);
142 traces.truncate(MAX_CAUSES + 1);
143 if let Some(last) = traces.last_mut() {
144 last.omitted_causes += omitted;
145 }
146 let mut result: Option<StackTrace> = None;
147 for mut trace in traces.into_iter().rev() {
148 trace.cause = result.take().map(Box::new);
149 result = Some(trace);
150 }
151 result
152}
153
154pub trait TraceParser: Send + Sync {
156 fn detect(&self, text: &str) -> bool;
158 fn parse(&self, text: &str) -> Option<StackTrace>;
160}
161
162pub struct Parsers {
164 parsers: Vec<Box<dyn TraceParser>>,
165}
166
167impl Default for Parsers {
168 fn default() -> Self {
169 Parsers {
170 parsers: vec![
171 Box::new(PythonParser),
172 Box::new(JavaParser),
173 Box::new(RustParser),
174 Box::new(JavaScriptParser),
175 ],
176 }
177 }
178}
179
180impl Parsers {
181 pub fn new() -> Self {
183 Parsers::default()
184 }
185
186 #[must_use]
188 pub fn with_parser(mut self, parser: impl TraceParser + 'static) -> Self {
189 self.parsers.insert(0, Box::new(parser));
190 self
191 }
192
193 pub fn parse(&self, text: &str) -> Option<StackTrace> {
195 self.parsers
196 .iter()
197 .find(|parser| parser.detect(text))
198 .and_then(|parser| parser.parse(text))
199 }
200}
201
202pub fn parse(text: &str) -> Option<StackTrace> {
204 Parsers::default().parse(text)
205}
206
207fn regex(cell: &'static OnceLock<Regex>, pattern: &str) -> &'static Regex {
208 cell.get_or_init(|| Regex::new(pattern).expect("valid trace pattern"))
209}
210
211fn number(value: Option<fancy_regex::Match<'_>>) -> Option<usize> {
212 value.and_then(|value| value.as_str().parse().ok())
213}
214
215fn kind_and_message(line: &str) -> (Option<String>, Option<String>) {
218 let line = line.trim();
219 match line.split_once(": ") {
220 Some((kind, message)) if !kind.contains(' ') => {
221 (Some(kind.to_string()), Some(message.to_string()))
222 }
223 _ if !line.contains(' ') && !line.is_empty() => (Some(line.to_string()), None),
224 _ => (None, Some(line.to_string()).filter(|line| !line.is_empty())),
225 }
226}
227
228pub struct RustParser;
230
231impl RustParser {
232 fn is_library(function: &str, path: Option<&str>) -> bool {
233 const CRATES: [&str; 6] = [
234 "std::",
235 "core::",
236 "alloc::",
237 "backtrace::",
238 "rust_begin_unwind",
239 "__rust",
240 ];
241 CRATES.iter().any(|prefix| {
242 function.starts_with(prefix) || function.starts_with(&format!("<{prefix}"))
243 }) || path.is_some_and(|path| {
244 path.starts_with("/rustc/")
245 || path.contains("/.cargo/registry/")
246 || path.contains("\\.cargo\\registry\\")
247 })
248 }
249}
250
251impl TraceParser for RustParser {
252 fn detect(&self, text: &str) -> bool {
253 static FRAME: OnceLock<Regex> = OnceLock::new();
254 text.contains("panicked at")
255 || regex(&FRAME, r"(?m)^\s*\d+: \S")
256 .is_match(text)
257 .unwrap_or(false)
258 }
259
260 fn parse(&self, text: &str) -> Option<StackTrace> {
261 static FRAME: OnceLock<Regex> = OnceLock::new();
262 static AT: OnceLock<Regex> = OnceLock::new();
263 static PANIC: OnceLock<Regex> = OnceLock::new();
264 static OLD_PANIC: OnceLock<Regex> = OnceLock::new();
265 let frame_re = regex(&FRAME, r"^\s*\d+:\s+(.+?)\s*$");
266 let at_re = regex(&AT, r"^\s+at\s+(.+?):(\d+)(?::(\d+))?\s*$");
267 let mut trace = StackTrace::new(Language::Rust);
268 let lines: Vec<&str> = text.lines().collect();
269
270 let panic_re = regex(&PANIC, r"panicked at (.+?):(\d+):(\d+):\s*$");
273 let old_re = regex(&OLD_PANIC, r"panicked at '(.*)', (.+?):(\d+):(\d+)");
274 let mut index = 0;
275 while index < lines.len() {
276 let line = lines[index];
277 if let Ok(Some(caps)) = old_re.captures(line) {
278 trace.kind = Some("panic".into());
279 trace.message = Some(caps[1].to_string());
280 trace.metadata_location(&caps[2], number(caps.get(3)), number(caps.get(4)));
281 } else if let Ok(Some(caps)) = panic_re.captures(line) {
282 trace.kind = Some("panic".into());
283 trace.metadata_location(&caps[1], number(caps.get(2)), number(caps.get(3)));
284 let mut message = Vec::new();
285 while index + 1 < lines.len()
286 && !lines[index + 1].starts_with("stack backtrace:")
287 && !lines[index + 1].starts_with("note:")
288 && !frame_re.is_match(lines[index + 1]).unwrap_or(false)
289 {
290 index += 1;
291 message.push(lines[index]);
292 }
293 trace.message = Some(message.join("\n")).filter(|message| !message.is_empty());
294 } else if let Ok(Some(caps)) = frame_re.captures(line) {
295 let mut function = caps[1].to_string();
296 if let Some(stripped) = function.rsplit_once("::h").filter(|(_, hash)| {
298 hash.len() == 16 && hash.chars().all(|c| c.is_ascii_hexdigit())
299 }) {
300 function = stripped.0.to_string();
301 }
302 let mut frame = Frame {
303 function: Some(function),
304 ..Frame::default()
305 };
306 let next = lines.get(index + 1).copied().unwrap_or("");
307 if let Ok(Some(at)) = at_re.captures(next) {
308 frame.path = at.get(1).map(|path| path.as_str().to_string());
309 frame.line = number(at.get(2));
310 frame.column = number(at.get(3));
311 index += 1;
312 }
313 frame.library = Self::is_library(
314 frame.function.as_deref().unwrap_or(""),
315 frame.path.as_deref(),
316 );
317 trace.frames.push(frame);
318 }
319 index += 1;
320 }
321 trace.frames.reverse();
323 (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
324 }
325}
326
327impl StackTrace {
328 fn metadata_location(&mut self, path: &str, line: Option<usize>, column: Option<usize>) {
330 self.location = Some(Frame {
331 path: Some(path.to_string()),
332 line,
333 column,
334 ..Frame::default()
335 });
336 }
337}
338
339pub struct PythonParser;
341
342const PY_CAUSE: &str = "The above exception was the direct cause of the following exception:";
343const PY_CONTEXT: &str = "During handling of the above exception, another exception occurred:";
344
345impl PythonParser {
346 fn block(text: &str) -> Option<StackTrace> {
347 static FILE: OnceLock<Regex> = OnceLock::new();
348 let file_re = regex(&FILE, r#"^\s*File "(.+?)", line (\d+)(?:, in (.+))?$"#);
349 let mut trace = StackTrace::new(Language::Python);
350 let mut last_line = None;
351 let lines: Vec<&str> = text.lines().collect();
352 let mut index = 0;
353 while index < lines.len() {
354 let line = lines[index];
355 if let Ok(Some(caps)) = file_re.captures(line) {
356 let path = caps[1].to_string();
357 let library = path.contains("site-packages")
358 || path.contains("/lib/python")
359 || path.starts_with('<');
360 let mut frame = Frame {
361 function: caps.get(3).map(|m| m.as_str().to_string()),
362 path: Some(path),
363 line: number(caps.get(2)),
364 library,
365 ..Frame::default()
366 };
367 while let Some(next) = lines.get(index + 1) {
369 let trimmed = next.trim();
370 if next.starts_with(" ")
371 && !trimmed.is_empty()
372 && !file_re.is_match(next).unwrap_or(false)
373 {
374 if trimmed.chars().all(|c| matches!(c, '^' | '~' | ' ')) {
375 index += 1;
376 continue;
377 }
378 if frame.source.is_none() {
379 frame.source = Some(trimmed.to_string());
380 }
381 index += 1;
382 } else {
383 break;
384 }
385 }
386 trace.frames.push(frame);
387 } else if !line.trim().is_empty()
388 && !line.starts_with("Traceback (most recent call last):")
389 && !line.starts_with(' ')
390 {
391 last_line = Some(index);
392 }
393 index += 1;
394 }
395 if let Some(last) = last_line {
396 let rest = lines[last..].join("\n");
398 let (kind, message) = kind_and_message(&rest);
399 trace.kind = kind;
400 trace.message = message;
401 }
402 (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
403 }
404}
405
406impl TraceParser for PythonParser {
407 fn detect(&self, text: &str) -> bool {
408 text.contains("Traceback (most recent call last):")
409 }
410
411 fn parse(&self, text: &str) -> Option<StackTrace> {
412 let mut blocks: Vec<(String, Option<CauseKind>)> = Vec::new();
414 let mut current = String::new();
415 let mut kind = None;
416 for line in text.lines() {
417 let relation = match line.trim() {
418 PY_CAUSE => Some(CauseKind::CausedBy),
419 PY_CONTEXT => Some(CauseKind::DuringHandling),
420 _ => None,
421 };
422 if let Some(relation) = relation {
423 blocks.push((std::mem::take(&mut current), kind));
424 kind = Some(relation);
425 } else {
426 current.push_str(line);
427 current.push('\n');
428 }
429 }
430 blocks.push((current, kind));
431 let mut chain: Vec<StackTrace> = Vec::new();
433 for (block, relation) in blocks {
434 let Some(mut trace) = Self::block(&block) else {
435 continue;
436 };
437 match relation {
438 Some(relation) if !chain.is_empty() => trace.cause_kind = relation,
439 _ => chain.clear(),
440 }
441 chain.push(trace);
442 }
443 chain.reverse();
444 link_causes(chain)
445 }
446}
447
448pub struct JavaParser;
450
451impl TraceParser for JavaParser {
452 fn detect(&self, text: &str) -> bool {
453 static AT: OnceLock<Regex> = OnceLock::new();
454 regex(&AT, r"(?m)^\s+at [\w$.<>]+\(.*\)\s*$")
455 .is_match(text)
456 .unwrap_or(false)
457 }
458
459 fn parse(&self, text: &str) -> Option<StackTrace> {
460 static AT: OnceLock<Regex> = OnceLock::new();
461 let at_re = regex(&AT, r"^\s+at (?:[\w.]+/)?([\w$.<>]+)\((.*)\)\s*$");
462 let mut traces: Vec<StackTrace> = Vec::new();
463 let mut header: Option<&str> = None;
464 for line in text.lines() {
465 if let Ok(Some(caps)) = at_re.captures(line) {
466 if traces.is_empty() {
468 let line = header.unwrap_or("").trim();
469 let line = line
470 .strip_prefix("Exception in thread ")
471 .and_then(|rest| rest.split_once("\" ").map(|(_, rest)| rest))
472 .unwrap_or(line);
473 let mut trace = StackTrace::new(Language::Java);
474 (trace.kind, trace.message) = kind_and_message(line);
475 traces.push(trace);
476 }
477 let Some(trace) = traces.last_mut() else {
478 continue;
479 };
480 let function = caps[1].to_string();
481 let location = &caps[2];
482 let mut frame = Frame {
483 library: ["java.", "javax.", "jdk.", "sun.", "com.sun.", "kotlin."]
484 .iter()
485 .any(|prefix| function.starts_with(prefix)),
486 function: Some(function),
487 ..Frame::default()
488 };
489 match location.rsplit_once(':') {
490 Some((file, line)) if line.parse::<usize>().is_ok() => {
491 frame.path = Some(file.to_string());
492 frame.line = line.parse().ok();
493 }
494 _ if location == "Native Method" => {
495 frame.metadata.push(("native".into(), "true".into()));
496 }
497 _ if location != "Unknown Source" && !location.is_empty() => {
498 frame.path = Some(location.to_string());
499 }
500 _ => {}
501 }
502 trace.frames.push(frame);
503 } else if let Some(rest) = line.trim_start().strip_prefix("Caused by: ") {
504 let mut trace = StackTrace::new(Language::Java);
505 (trace.kind, trace.message) = kind_and_message(rest);
506 traces.push(trace);
507 } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" more") {
508 if let Some(trace) = traces.last_mut() {
509 trace.frames.push(Frame {
510 metadata: vec![("elided".into(), line.trim().to_string())],
511 library: true,
512 ..Frame::default()
513 });
514 }
515 } else if traces.is_empty() && !line.trim().is_empty() {
516 header = Some(line);
517 }
518 }
519 for trace in &mut traces {
522 trace.frames.reverse();
523 }
524 link_causes(traces)
525 }
526}
527
528pub struct JavaScriptParser;
530
531impl TraceParser for JavaScriptParser {
532 fn detect(&self, text: &str) -> bool {
533 static AT: OnceLock<Regex> = OnceLock::new();
534 regex(&AT, r"(?m)^\s+at .*:\d+:\d+\)?(?: \{)?\s*$")
535 .is_match(text)
536 .unwrap_or(false)
537 }
538
539 fn parse(&self, text: &str) -> Option<StackTrace> {
540 static AT: OnceLock<Regex> = OnceLock::new();
541 let at_re = regex(
542 &AT,
543 r"^\s+at (?:(?:async )?(.+?) \()?(.+?):(\d+):(\d+)\)?(?: \{)?\s*$",
544 );
545 let mut traces: Vec<StackTrace> = Vec::new();
546 let mut header: Option<&str> = None;
547 for line in text.lines() {
548 if let Ok(Some(caps)) = at_re.captures(line) {
549 if traces.is_empty() {
552 let mut trace = StackTrace::new(Language::JavaScript);
553 (trace.kind, trace.message) = kind_and_message(header.unwrap_or(""));
554 traces.push(trace);
555 }
556 let Some(trace) = traces.last_mut() else {
557 continue;
558 };
559 let path = caps[2].to_string();
560 trace.frames.push(Frame {
561 library: path.starts_with("node:") || path.contains("node_modules"),
562 function: caps.get(1).map(|m| m.as_str().to_string()),
563 path: Some(path),
564 line: number(caps.get(3)),
565 column: number(caps.get(4)),
566 ..Frame::default()
567 });
568 } else if let Some(rest) = line.trim_start().strip_prefix("[cause]: ") {
569 let mut trace = StackTrace::new(Language::JavaScript);
570 (trace.kind, trace.message) = kind_and_message(rest.trim_end_matches(['{', ' ']));
571 traces.push(trace);
572 } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" ...") {
573 if let Some(trace) = traces.last_mut() {
574 trace.frames.push(Frame {
575 metadata: vec![("elided".into(), line.trim().to_string())],
576 library: true,
577 ..Frame::default()
578 });
579 }
580 } else if traces.is_empty() && !line.trim().is_empty() {
581 header = Some(line);
582 }
583 }
584 for trace in &mut traces {
585 trace.frames.reverse();
586 }
587 link_causes(traces)
588 }
589}
590
591pub fn capture(message: impl Into<String>) -> StackTrace {
595 let backtrace = std::backtrace::Backtrace::force_capture().to_string();
596 let mut trace = RustParser
597 .parse(&backtrace)
598 .unwrap_or_else(|| StackTrace::new(Language::Rust));
599 trace.kind = Some("panic".into());
600 trace.message = Some(message.into());
601 trace
602}
603
604pub fn panic_hook(
608 console: Console,
609) -> impl Fn(&std::panic::PanicHookInfo<'_>) + Send + Sync + 'static {
610 let console = std::sync::Mutex::new(console);
611 move |info| {
612 let payload = info.payload();
613 let message = payload
614 .downcast_ref::<&str>()
615 .map(|message| message.to_string())
616 .or_else(|| payload.downcast_ref::<String>().cloned())
617 .unwrap_or_else(|| "Box<dyn Any>".to_string());
618 let mut trace = capture(message);
619 if let Some(location) = info.location() {
620 trace.location = Some(Frame {
621 path: Some(location.file().to_string()),
622 line: Some(location.line() as usize),
623 column: Some(location.column() as usize),
624 ..Frame::default()
625 });
626 }
627 let console = console.lock().unwrap_or_else(|e| e.into_inner());
628 console.print(&trace.render_options());
629 }
630}
631
632fn clean(text: &str) -> String {
634 crate::sanitize_terminal_controls(text)
635}
636
637#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
639pub struct StackTraceView<'a> {
640 trace: &'a StackTrace,
641 linker: Hyperlinker,
642 show_library: bool,
643}
644
645impl StackTraceView<'_> {
646 pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
648 self.linker = linker;
649 self
650 }
651
652 pub fn show_library(mut self, show: bool) -> Self {
654 self.show_library = show;
655 self
656 }
657
658 fn append_chain(&self, console: &Console, text: &mut Text) {
662 let mut chain: Vec<&StackTrace> = Vec::new();
663 let mut omitted = 0usize;
664 for trace in self.trace.chain() {
665 if chain.len() <= MAX_CAUSES {
666 chain.push(trace);
667 } else {
668 omitted += 1;
669 }
670 }
671 if let Some(last) = chain.last() {
672 omitted += last.omitted_causes;
673 }
674 if omitted > 0 {
675 let label = if omitted == 1 { "cause" } else { "causes" };
676 text.append(
677 &format!("… {omitted} more {label}\n\n"),
678 Some(theme_style(console, "stacktrace.library", "dim").into()),
679 );
680 }
681 for (index, trace) in chain.iter().enumerate().rev() {
682 self.append_trace(console, text, trace);
683 let Some(caused) = index.checked_sub(1).map(|at| chain[at]) else {
684 continue;
685 };
686 let bridge = match caused.cause_kind {
687 CauseKind::CausedBy => "The error above caused the following error:",
688 CauseKind::DuringHandling => {
689 "The error below happened while handling the error above:"
690 }
691 };
692 text.append("\n", None);
693 text.append(
694 bridge,
695 Some(theme_style(console, "stacktrace.bridge", "italic dim").into()),
696 );
697 text.append("\n\n", None);
698 }
699 }
700
701 fn append_trace(&self, console: &Console, text: &mut Text, trace: &StackTrace) {
703 let dim = theme_style(console, "stacktrace.library", "dim");
704 let function_style = theme_style(console, "stacktrace.function", "green");
705 let location_style = theme_style(console, "stacktrace.location", "magenta");
706 let mut hidden = 0usize;
707 let flush_hidden = |text: &mut Text, hidden: &mut usize| {
708 if *hidden > 0 {
709 let label = if *hidden == 1 { "frame" } else { "frames" };
710 text.append(
711 &format!(" … {hidden} library {label}\n"),
712 Some(dim.clone().into()),
713 );
714 *hidden = 0;
715 }
716 };
717 for frame in &trace.frames {
718 if frame.library && !self.show_library {
719 hidden += 1;
720 continue;
721 }
722 flush_hidden(text, &mut hidden);
723 let base = if frame.library {
724 Some(dim.clone())
725 } else {
726 None
727 };
728 let name = frame.function.as_deref().unwrap_or("<unknown>");
729 let elided = frame.metadata.iter().find(|(key, _)| key == "elided");
730 text.append(" ", None);
731 match elided {
732 Some((_, label)) => text.append(&clean(label), Some(dim.clone().into())),
733 None => text.append(
734 &clean(name),
735 Some(base.clone().unwrap_or(function_style.clone()).into()),
736 ),
737 }
738 text.append("\n", None);
739 if let Some(path) = &frame.path {
740 text.append(" at ", base.clone().map(Into::into));
741 let location = self.linker.location(
742 path,
743 frame.line,
744 frame.column,
745 base.clone().unwrap_or(location_style.clone()),
746 );
747 *text = std::mem::take(text).append_text(&location);
748 text.append("\n", None);
749 }
750 if let Some(source) = &frame.source {
751 text.append(
752 &format!(" {}\n", clean(source)),
753 base.clone().map(Into::into),
754 );
755 }
756 }
757 flush_hidden(text, &mut hidden);
758 let error_style = theme_style(console, "stacktrace.error", "bold red");
759 let kind = trace.kind.as_deref().unwrap_or("error");
760 text.append(&clean(kind), Some(error_style.into()));
761 if let Some(message) = &trace.message {
762 text.append(": ", None);
763 text.append(&clean(message), None);
764 }
765 if let Some(location) = &trace.location
766 && let Some(path) = &location.path
767 {
768 text.append("\n at ", None);
769 let linked = self
770 .linker
771 .location(path, location.line, location.column, location_style);
772 *text = std::mem::take(text).append_text(&linked);
773 }
774 }
775}
776
777impl Renderable for StackTraceView<'_> {
778 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
779 let mut text = Text::new("");
780 self.append_chain(console, &mut text);
781 text.rich_render(console, options)
782 }
783}
784
785impl Renderable for StackTrace {
786 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
787 self.render_options().rich_render(console, options)
788 }
789}