diff --git a/.gitignore b/.gitignore index 869df07..18d09e8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.idea/ /target Cargo.lock \ No newline at end of file diff --git a/examples/test_io.rs b/examples/test_io.rs new file mode 100644 index 0000000..2c1fe66 --- /dev/null +++ b/examples/test_io.rs @@ -0,0 +1,18 @@ +use ariadne::{CausedByFrame, Report, ReportKind, Source}; +use std::fs::File; +use std::ops::Range; + +fn main() { + let error = match File::open("file/that/must/not/exist.rs") { + Ok(_) => panic!("Oops! This should not happen."), + Err(e) => e, + }; + Report::>::build(ReportKind::Error, (), 34) + .with_message("File not found") + .with_help("Please check the path and try again") + .push_backtrace(CausedByFrame::io_error(error, "not-exist.rs")) + .push_backtrace(CausedByFrame::new("std/fs/file.rs").with_object("std::fs::File").with_position(123, 45)) + .finish() + .print(Source::from(include_str!("sample.tao"))) + .unwrap(); +} diff --git a/src/caused_by.rs b/src/caused_by.rs new file mode 100644 index 0000000..df5cba9 --- /dev/null +++ b/src/caused_by.rs @@ -0,0 +1,88 @@ +use std::path::{Path}; + +/// Configuration for display backtraces +pub struct CausedByConfig { + /// Show the backtrace with number labels + pub show_label_numbers: bool, + /// Number labels start at this index, `index0` vs `index1` + pub label_index_start: usize, +} + +/// A backtrace collection +pub struct CausedBy { + pub(crate) frames: Vec, +} + +/// A backtrace frame +#[derive(Debug)] +pub struct CausedByFrame { + /// File path buf + pub file: String, + /// Function name, or module path, e.g. `std::io::read_line` + pub object: Option, + /// Line Column + pub position: Option<(usize, usize)>, +} + +impl Default for CausedByConfig { + fn default() -> Self { + Self { + show_label_numbers: true, + label_index_start: 1, + } + } +} + +impl Default for CausedBy { + fn default() -> Self { + Self { frames: Vec::new() } + } +} + +impl CausedBy { + /// Add a frame to the backtrace + pub fn push_frame(&mut self, frame: CausedByFrame) { + self.frames.push(frame); + } + /// Clear all of the frames + pub fn clear(&mut self) { + self.frames.clear(); + } +} + +impl CausedByFrame { + /// Create a new backtrace frame + pub fn new(file: impl Into) -> Self { + let path = file.into(); + debug_assert!( + path.lines().count() == 1, + "File path must be in a single line" + ); + Self { + file: path, + object: None, + position: None, + } + } + /// Add an io error to the backtrace + pub fn io_error(error: std::io::Error, path: impl AsRef) -> Self { + let path = path.as_ref(); + let path = path.to_string_lossy().to_string(); + Self::new(path).with_object(error.to_string()) + } + /// Name to display for the object + pub fn with_object(mut self, object: impl Into) -> Self { + let name = object.into(); + debug_assert!( + name.lines().count() == 1, + "Object name must be in a single line" + ); + self.object = Some(name); + self + } + /// Where the object is located + pub fn with_position(mut self, line: usize, column: usize) -> Self { + self.position = Some((line, column)); + self + } +} diff --git a/src/lib.rs b/src/lib.rs index d2a92dd..2eee5db 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,10 @@ mod source; mod display; mod draw; mod write; +mod caused_by; pub use crate::{ + caused_by::{CausedBy, CausedByFrame, CausedByConfig}, source::{Line, Source, Cache, FileCache, FnCache, sources}, draw::{Fmt, ColorGenerator}, }; @@ -141,6 +143,7 @@ pub struct Report<'a, S: Span = Range> { help: Option, location: (::Owned, usize), labels: Vec>, + backtrace: CausedBy, config: Config, } @@ -155,6 +158,7 @@ impl Report<'_, S> { help: None, location: (src_id.into(), offset), labels: Vec::new(), + backtrace: CausedBy::default(), config: Config::default(), } } @@ -220,6 +224,7 @@ pub struct ReportBuilder<'a, S: Span> { help: Option, location: (::Owned, usize), labels: Vec>, + backtrace: CausedBy, config: Config, } @@ -273,7 +278,11 @@ impl<'a, S: Span> ReportBuilder<'a, S> { let config = &self.config; // This would not be necessary in Rust 2021 edition self.labels.extend(labels.into_iter().map(|mut label| { label.color = config.filter_color(label.color); label })); } - + /// add a new backtrace frame to the report + pub fn push_backtrace(mut self, trace: CausedByFrame) -> Self { + self.backtrace.push_frame(trace); + self + } /// Add a label to the report. pub fn with_label(mut self, label: Label) -> Self { self.add_label(label); @@ -302,6 +311,7 @@ impl<'a, S: Span> ReportBuilder<'a, S> { help: self.help, location: self.location, labels: self.labels, + backtrace: self.backtrace, config: self.config, } } diff --git a/src/write.rs b/src/write.rs index 70d0383..72f488b 100644 --- a/src/write.rs +++ b/src/write.rs @@ -1,6 +1,7 @@ use std::borrow::Borrow; use std::io; use std::ops::Range; +use crate::CausedByConfig; use super::draw::{self, StreamAwareFmt, StreamType}; use super::{Cache, CharSet, Label, LabelAttach, Report, ReportKind, Show, Span, Write}; @@ -777,6 +778,52 @@ impl Report<'_, S> { } } } + if !self.backtrace.frames.is_empty() { + let indent = if self.config.compact { " " } else { " " }; + write!(w, "\n")?; + write!(w, "{}\n", "Caused by:".fg(self.config.note_color(), s))?; + // TODO: make this configurable + let mut config = CausedByConfig::default(); + config.label_index_start = 0; + config.show_label_numbers = true; + if config.show_label_numbers { + let max = config.label_index_start + self.backtrace.frames.len(); + let max_width = max.to_string().len(); + // 0: object + // at path/to/file.rs:123:45 + for (index, frame) in self.backtrace.frames.iter().enumerate() { + match &frame.object { + Some(s) => { + writeln!(w, "{indent}{:width$}: {object}", index + config.label_index_start, width = max_width, object = s)?; + write!(w, "{indent}{:width$} at {path}", "", width = max_width, path = &frame.file)?; + } + None => { + write!(w, "{:width$}: {path}", index + config.label_index_start, width = max_width, path = &frame.file)?; + } + } + match &frame.position { + Some((line, col)) => writeln!(w, "({}:{})", line, col)?, + None => writeln!(w)? + } + } + } else { + for frame in &self.backtrace.frames { + match &frame.object { + Some(s) => { + writeln!(w, "{indent}{}", s)?; + write!(w, "{indent}{indent}at {}", &frame.file)?; + } + None => { + write!(w, "{}", &frame.file)?; + } + } + match &frame.position { + Some((line, col)) => writeln!(w, "({}:{})", line, col)?, + None => writeln!(w)? + } + } + } + } Ok(()) } }