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 pub fn with_parser(mut self, parser: impl TraceParser + 'static) -> Self {
188 self.parsers.insert(0, Box::new(parser));
189 self
190 }
191
192 pub fn parse(&self, text: &str) -> Option<StackTrace> {
194 self.parsers
195 .iter()
196 .find(|parser| parser.detect(text))
197 .and_then(|parser| parser.parse(text))
198 }
199}
200
201pub fn parse(text: &str) -> Option<StackTrace> {
203 Parsers::default().parse(text)
204}
205
206fn regex(cell: &'static OnceLock<Regex>, pattern: &str) -> &'static Regex {
207 cell.get_or_init(|| Regex::new(pattern).expect("valid trace pattern"))
208}
209
210fn number(value: Option<fancy_regex::Match<'_>>) -> Option<usize> {
211 value.and_then(|value| value.as_str().parse().ok())
212}
213
214fn kind_and_message(line: &str) -> (Option<String>, Option<String>) {
217 let line = line.trim();
218 match line.split_once(": ") {
219 Some((kind, message)) if !kind.contains(' ') => {
220 (Some(kind.to_string()), Some(message.to_string()))
221 }
222 _ if !line.contains(' ') && !line.is_empty() => (Some(line.to_string()), None),
223 _ => (None, Some(line.to_string()).filter(|line| !line.is_empty())),
224 }
225}
226
227pub struct RustParser;
229
230impl RustParser {
231 fn is_library(function: &str, path: Option<&str>) -> bool {
232 const CRATES: [&str; 6] = [
233 "std::",
234 "core::",
235 "alloc::",
236 "backtrace::",
237 "rust_begin_unwind",
238 "__rust",
239 ];
240 CRATES.iter().any(|prefix| {
241 function.starts_with(prefix) || function.starts_with(&format!("<{prefix}"))
242 }) || path.is_some_and(|path| {
243 path.starts_with("/rustc/")
244 || path.contains("/.cargo/registry/")
245 || path.contains("\\.cargo\\registry\\")
246 })
247 }
248}
249
250impl TraceParser for RustParser {
251 fn detect(&self, text: &str) -> bool {
252 static FRAME: OnceLock<Regex> = OnceLock::new();
253 text.contains("panicked at")
254 || regex(&FRAME, r"(?m)^\s*\d+: \S")
255 .is_match(text)
256 .unwrap_or(false)
257 }
258
259 fn parse(&self, text: &str) -> Option<StackTrace> {
260 static FRAME: OnceLock<Regex> = OnceLock::new();
261 static AT: OnceLock<Regex> = OnceLock::new();
262 static PANIC: OnceLock<Regex> = OnceLock::new();
263 static OLD_PANIC: OnceLock<Regex> = OnceLock::new();
264 let frame_re = regex(&FRAME, r"^\s*\d+:\s+(.+?)\s*$");
265 let at_re = regex(&AT, r"^\s+at\s+(.+?):(\d+)(?::(\d+))?\s*$");
266 let mut trace = StackTrace::new(Language::Rust);
267 let lines: Vec<&str> = text.lines().collect();
268
269 let panic_re = regex(&PANIC, r"panicked at (.+?):(\d+):(\d+):\s*$");
272 let old_re = regex(&OLD_PANIC, r"panicked at '(.*)', (.+?):(\d+):(\d+)");
273 let mut index = 0;
274 while index < lines.len() {
275 let line = lines[index];
276 if let Ok(Some(caps)) = old_re.captures(line) {
277 trace.kind = Some("panic".into());
278 trace.message = Some(caps[1].to_string());
279 trace.metadata_location(&caps[2], number(caps.get(3)), number(caps.get(4)));
280 } else if let Ok(Some(caps)) = panic_re.captures(line) {
281 trace.kind = Some("panic".into());
282 trace.metadata_location(&caps[1], number(caps.get(2)), number(caps.get(3)));
283 let mut message = Vec::new();
284 while index + 1 < lines.len()
285 && !lines[index + 1].starts_with("stack backtrace:")
286 && !lines[index + 1].starts_with("note:")
287 && !frame_re.is_match(lines[index + 1]).unwrap_or(false)
288 {
289 index += 1;
290 message.push(lines[index]);
291 }
292 trace.message = Some(message.join("\n")).filter(|message| !message.is_empty());
293 } else if let Ok(Some(caps)) = frame_re.captures(line) {
294 let mut function = caps[1].to_string();
295 if let Some(stripped) = function.rsplit_once("::h").filter(|(_, hash)| {
297 hash.len() == 16 && hash.chars().all(|c| c.is_ascii_hexdigit())
298 }) {
299 function = stripped.0.to_string();
300 }
301 let mut frame = Frame {
302 function: Some(function),
303 ..Frame::default()
304 };
305 let next = lines.get(index + 1).copied().unwrap_or("");
306 if let Ok(Some(at)) = at_re.captures(next) {
307 frame.path = at.get(1).map(|path| path.as_str().to_string());
308 frame.line = number(at.get(2));
309 frame.column = number(at.get(3));
310 index += 1;
311 }
312 frame.library = Self::is_library(
313 frame.function.as_deref().unwrap_or(""),
314 frame.path.as_deref(),
315 );
316 trace.frames.push(frame);
317 }
318 index += 1;
319 }
320 trace.frames.reverse();
322 (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
323 }
324}
325
326impl StackTrace {
327 fn metadata_location(&mut self, path: &str, line: Option<usize>, column: Option<usize>) {
329 self.location = Some(Frame {
330 path: Some(path.to_string()),
331 line,
332 column,
333 ..Frame::default()
334 });
335 }
336}
337
338pub struct PythonParser;
340
341const PY_CAUSE: &str = "The above exception was the direct cause of the following exception:";
342const PY_CONTEXT: &str = "During handling of the above exception, another exception occurred:";
343
344impl PythonParser {
345 fn block(text: &str) -> Option<StackTrace> {
346 static FILE: OnceLock<Regex> = OnceLock::new();
347 let file_re = regex(&FILE, r#"^\s*File "(.+?)", line (\d+)(?:, in (.+))?$"#);
348 let mut trace = StackTrace::new(Language::Python);
349 let mut last_line = None;
350 let lines: Vec<&str> = text.lines().collect();
351 let mut index = 0;
352 while index < lines.len() {
353 let line = lines[index];
354 if let Ok(Some(caps)) = file_re.captures(line) {
355 let path = caps[1].to_string();
356 let library = path.contains("site-packages")
357 || path.contains("/lib/python")
358 || path.starts_with('<');
359 let mut frame = Frame {
360 function: caps.get(3).map(|m| m.as_str().to_string()),
361 path: Some(path),
362 line: number(caps.get(2)),
363 library,
364 ..Frame::default()
365 };
366 while let Some(next) = lines.get(index + 1) {
368 let trimmed = next.trim();
369 if next.starts_with(" ")
370 && !trimmed.is_empty()
371 && !file_re.is_match(next).unwrap_or(false)
372 {
373 if trimmed.chars().all(|c| matches!(c, '^' | '~' | ' ')) {
374 index += 1;
375 continue;
376 }
377 if frame.source.is_none() {
378 frame.source = Some(trimmed.to_string());
379 }
380 index += 1;
381 } else {
382 break;
383 }
384 }
385 trace.frames.push(frame);
386 } else if !line.trim().is_empty()
387 && !line.starts_with("Traceback (most recent call last):")
388 && !line.starts_with(' ')
389 {
390 last_line = Some(index);
391 }
392 index += 1;
393 }
394 if let Some(last) = last_line {
395 let rest = lines[last..].join("\n");
397 let (kind, message) = kind_and_message(&rest);
398 trace.kind = kind;
399 trace.message = message;
400 }
401 (trace.kind.is_some() || !trace.frames.is_empty()).then_some(trace)
402 }
403}
404
405impl TraceParser for PythonParser {
406 fn detect(&self, text: &str) -> bool {
407 text.contains("Traceback (most recent call last):")
408 }
409
410 fn parse(&self, text: &str) -> Option<StackTrace> {
411 let mut blocks: Vec<(String, Option<CauseKind>)> = Vec::new();
413 let mut current = String::new();
414 let mut kind = None;
415 for line in text.lines() {
416 let relation = match line.trim() {
417 PY_CAUSE => Some(CauseKind::CausedBy),
418 PY_CONTEXT => Some(CauseKind::DuringHandling),
419 _ => None,
420 };
421 if let Some(relation) = relation {
422 blocks.push((std::mem::take(&mut current), kind));
423 kind = Some(relation);
424 } else {
425 current.push_str(line);
426 current.push('\n');
427 }
428 }
429 blocks.push((current, kind));
430 let mut chain: Vec<StackTrace> = Vec::new();
432 for (block, relation) in blocks {
433 let Some(mut trace) = Self::block(&block) else {
434 continue;
435 };
436 match relation {
437 Some(relation) if !chain.is_empty() => trace.cause_kind = relation,
438 _ => chain.clear(),
439 }
440 chain.push(trace);
441 }
442 chain.reverse();
443 link_causes(chain)
444 }
445}
446
447pub struct JavaParser;
449
450impl TraceParser for JavaParser {
451 fn detect(&self, text: &str) -> bool {
452 static AT: OnceLock<Regex> = OnceLock::new();
453 regex(&AT, r"(?m)^\s+at [\w$.<>]+\(.*\)\s*$")
454 .is_match(text)
455 .unwrap_or(false)
456 }
457
458 fn parse(&self, text: &str) -> Option<StackTrace> {
459 static AT: OnceLock<Regex> = OnceLock::new();
460 let at_re = regex(&AT, r"^\s+at (?:[\w.]+/)?([\w$.<>]+)\((.*)\)\s*$");
461 let mut traces: Vec<StackTrace> = Vec::new();
462 let mut header: Option<&str> = None;
463 for line in text.lines() {
464 if let Ok(Some(caps)) = at_re.captures(line) {
465 if traces.is_empty() {
467 let line = header.unwrap_or("").trim();
468 let line = line
469 .strip_prefix("Exception in thread ")
470 .and_then(|rest| rest.split_once("\" ").map(|(_, rest)| rest))
471 .unwrap_or(line);
472 let mut trace = StackTrace::new(Language::Java);
473 (trace.kind, trace.message) = kind_and_message(line);
474 traces.push(trace);
475 }
476 let Some(trace) = traces.last_mut() else {
477 continue;
478 };
479 let function = caps[1].to_string();
480 let location = &caps[2];
481 let mut frame = Frame {
482 library: ["java.", "javax.", "jdk.", "sun.", "com.sun.", "kotlin."]
483 .iter()
484 .any(|prefix| function.starts_with(prefix)),
485 function: Some(function),
486 ..Frame::default()
487 };
488 match location.rsplit_once(':') {
489 Some((file, line)) if line.parse::<usize>().is_ok() => {
490 frame.path = Some(file.to_string());
491 frame.line = line.parse().ok();
492 }
493 _ if location == "Native Method" => {
494 frame.metadata.push(("native".into(), "true".into()));
495 }
496 _ if location != "Unknown Source" && !location.is_empty() => {
497 frame.path = Some(location.to_string());
498 }
499 _ => {}
500 }
501 trace.frames.push(frame);
502 } else if let Some(rest) = line.trim_start().strip_prefix("Caused by: ") {
503 let mut trace = StackTrace::new(Language::Java);
504 (trace.kind, trace.message) = kind_and_message(rest);
505 traces.push(trace);
506 } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" more") {
507 if let Some(trace) = traces.last_mut() {
508 trace.frames.push(Frame {
509 metadata: vec![("elided".into(), line.trim().to_string())],
510 library: true,
511 ..Frame::default()
512 });
513 }
514 } else if traces.is_empty() && !line.trim().is_empty() {
515 header = Some(line);
516 }
517 }
518 for trace in &mut traces {
521 trace.frames.reverse();
522 }
523 link_causes(traces)
524 }
525}
526
527pub struct JavaScriptParser;
529
530impl TraceParser for JavaScriptParser {
531 fn detect(&self, text: &str) -> bool {
532 static AT: OnceLock<Regex> = OnceLock::new();
533 regex(&AT, r"(?m)^\s+at .*:\d+:\d+\)?(?: \{)?\s*$")
534 .is_match(text)
535 .unwrap_or(false)
536 }
537
538 fn parse(&self, text: &str) -> Option<StackTrace> {
539 static AT: OnceLock<Regex> = OnceLock::new();
540 let at_re = regex(
541 &AT,
542 r"^\s+at (?:(?:async )?(.+?) \()?(.+?):(\d+):(\d+)\)?(?: \{)?\s*$",
543 );
544 let mut traces: Vec<StackTrace> = Vec::new();
545 let mut header: Option<&str> = None;
546 for line in text.lines() {
547 if let Ok(Some(caps)) = at_re.captures(line) {
548 if traces.is_empty() {
551 let mut trace = StackTrace::new(Language::JavaScript);
552 (trace.kind, trace.message) = kind_and_message(header.unwrap_or(""));
553 traces.push(trace);
554 }
555 let Some(trace) = traces.last_mut() else {
556 continue;
557 };
558 let path = caps[2].to_string();
559 trace.frames.push(Frame {
560 library: path.starts_with("node:") || path.contains("node_modules"),
561 function: caps.get(1).map(|m| m.as_str().to_string()),
562 path: Some(path),
563 line: number(caps.get(3)),
564 column: number(caps.get(4)),
565 ..Frame::default()
566 });
567 } else if let Some(rest) = line.trim_start().strip_prefix("[cause]: ") {
568 let mut trace = StackTrace::new(Language::JavaScript);
569 (trace.kind, trace.message) = kind_and_message(rest.trim_end_matches(['{', ' ']));
570 traces.push(trace);
571 } else if line.trim_start().starts_with("... ") && line.trim_end().ends_with(" ...") {
572 if let Some(trace) = traces.last_mut() {
573 trace.frames.push(Frame {
574 metadata: vec![("elided".into(), line.trim().to_string())],
575 library: true,
576 ..Frame::default()
577 });
578 }
579 } else if traces.is_empty() && !line.trim().is_empty() {
580 header = Some(line);
581 }
582 }
583 for trace in &mut traces {
584 trace.frames.reverse();
585 }
586 link_causes(traces)
587 }
588}
589
590pub fn capture(message: impl Into<String>) -> StackTrace {
594 let backtrace = std::backtrace::Backtrace::force_capture().to_string();
595 let mut trace = RustParser
596 .parse(&backtrace)
597 .unwrap_or_else(|| StackTrace::new(Language::Rust));
598 trace.kind = Some("panic".into());
599 trace.message = Some(message.into());
600 trace
601}
602
603pub fn panic_hook(
607 console: Console,
608) -> impl Fn(&std::panic::PanicHookInfo<'_>) + Send + Sync + 'static {
609 let console = std::sync::Mutex::new(console);
610 move |info| {
611 let payload = info.payload();
612 let message = payload
613 .downcast_ref::<&str>()
614 .map(|message| message.to_string())
615 .or_else(|| payload.downcast_ref::<String>().cloned())
616 .unwrap_or_else(|| "Box<dyn Any>".to_string());
617 let mut trace = capture(message);
618 if let Some(location) = info.location() {
619 trace.location = Some(Frame {
620 path: Some(location.file().to_string()),
621 line: Some(location.line() as usize),
622 column: Some(location.column() as usize),
623 ..Frame::default()
624 });
625 }
626 let console = console.lock().unwrap_or_else(|e| e.into_inner());
627 console.print(&trace.render_options());
628 }
629}
630
631fn clean(text: &str) -> String {
633 crate::sanitize_terminal_controls(text)
634}
635
636pub struct StackTraceView<'a> {
638 trace: &'a StackTrace,
639 linker: Hyperlinker,
640 show_library: bool,
641}
642
643impl StackTraceView<'_> {
644 pub fn hyperlinker(mut self, linker: Hyperlinker) -> Self {
646 self.linker = linker;
647 self
648 }
649
650 pub fn show_library(mut self, show: bool) -> Self {
652 self.show_library = show;
653 self
654 }
655
656 fn append_chain(&self, console: &Console, text: &mut Text) {
660 let mut chain: Vec<&StackTrace> = Vec::new();
661 let mut omitted = 0usize;
662 for trace in self.trace.chain() {
663 if chain.len() <= MAX_CAUSES {
664 chain.push(trace);
665 } else {
666 omitted += 1;
667 }
668 }
669 if let Some(last) = chain.last() {
670 omitted += last.omitted_causes;
671 }
672 if omitted > 0 {
673 let label = if omitted == 1 { "cause" } else { "causes" };
674 text.append(
675 &format!("… {omitted} more {label}\n\n"),
676 Some(theme_style(console, "stacktrace.library", "dim").into()),
677 );
678 }
679 for (index, trace) in chain.iter().enumerate().rev() {
680 self.append_trace(console, text, trace);
681 let Some(caused) = index.checked_sub(1).map(|at| chain[at]) else {
682 continue;
683 };
684 let bridge = match caused.cause_kind {
685 CauseKind::CausedBy => "The error above caused the following error:",
686 CauseKind::DuringHandling => {
687 "The error below happened while handling the error above:"
688 }
689 };
690 text.append("\n", None);
691 text.append(
692 bridge,
693 Some(theme_style(console, "stacktrace.bridge", "italic dim").into()),
694 );
695 text.append("\n\n", None);
696 }
697 }
698
699 fn append_trace(&self, console: &Console, text: &mut Text, trace: &StackTrace) {
701 let dim = theme_style(console, "stacktrace.library", "dim");
702 let function_style = theme_style(console, "stacktrace.function", "green");
703 let location_style = theme_style(console, "stacktrace.location", "magenta");
704 let mut hidden = 0usize;
705 let flush_hidden = |text: &mut Text, hidden: &mut usize| {
706 if *hidden > 0 {
707 let label = if *hidden == 1 { "frame" } else { "frames" };
708 text.append(
709 &format!(" … {hidden} library {label}\n"),
710 Some(dim.clone().into()),
711 );
712 *hidden = 0;
713 }
714 };
715 for frame in &trace.frames {
716 if frame.library && !self.show_library {
717 hidden += 1;
718 continue;
719 }
720 flush_hidden(text, &mut hidden);
721 let base = if frame.library {
722 Some(dim.clone())
723 } else {
724 None
725 };
726 let name = frame.function.as_deref().unwrap_or("<unknown>");
727 let elided = frame.metadata.iter().find(|(key, _)| key == "elided");
728 text.append(" ", None);
729 match elided {
730 Some((_, label)) => text.append(&clean(label), Some(dim.clone().into())),
731 None => text.append(
732 &clean(name),
733 Some(base.clone().unwrap_or(function_style.clone()).into()),
734 ),
735 }
736 text.append("\n", None);
737 if let Some(path) = &frame.path {
738 text.append(" at ", base.clone().map(Into::into));
739 let location = self.linker.location(
740 path,
741 frame.line,
742 frame.column,
743 base.clone().unwrap_or(location_style.clone()),
744 );
745 *text = std::mem::take(text).append_text(&location);
746 text.append("\n", None);
747 }
748 if let Some(source) = &frame.source {
749 text.append(
750 &format!(" {}\n", clean(source)),
751 base.clone().map(Into::into),
752 );
753 }
754 }
755 flush_hidden(text, &mut hidden);
756 let error_style = theme_style(console, "stacktrace.error", "bold red");
757 let kind = trace.kind.as_deref().unwrap_or("error");
758 text.append(&clean(kind), Some(error_style.into()));
759 if let Some(message) = &trace.message {
760 text.append(": ", None);
761 text.append(&clean(message), None);
762 }
763 if let Some(location) = &trace.location {
764 if let Some(path) = &location.path {
765 text.append("\n at ", None);
766 let linked =
767 self.linker
768 .location(path, location.line, location.column, location_style);
769 *text = std::mem::take(text).append_text(&linked);
770 }
771 }
772 }
773}
774
775impl Renderable for StackTraceView<'_> {
776 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
777 let mut text = Text::new("");
778 self.append_chain(console, &mut text);
779 text.rich_render(console, options)
780 }
781}
782
783impl Renderable for StackTrace {
784 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
785 self.render_options().rich_render(console, options)
786 }
787}