feat(protocol) overhauled entire protocol to be based on byte offsets (#1)

Includes other improvements!

BREAKING CHANGE: Yeah this is pretty much a rewrite.
This commit is contained in:
Kat Marchán 2021-08-04 20:16:59 -07:00
parent 3b3a07bcb5
commit 6b51694733
No known key found for this signature in database
GPG Key ID: AEB529C08A3C7E9E
9 changed files with 337 additions and 125 deletions

View File

@ -12,6 +12,7 @@ edition = "2018"
[dependencies] [dependencies]
indenter = "0.3.3" indenter = "0.3.3"
thiserror = "1.0.26"
[dev-dependencies] [dev-dependencies]
thiserror = "1.0.26" thiserror = "1.0.26"

View File

@ -6,7 +6,7 @@ you FAIL miette? you fail her compiler like the unsafe C program? oh! oh! jail f
Here's an example of using something like `thisdiagnostic` to define Diagnostics declaratively. Here's an example of using something like `thisdiagnostic` to define Diagnostics declaratively.
```rust ```ignore
use thiserror::Error; use thiserror::Error;
use thisdiagnostic::Diagnostic; use thisdiagnostic::Diagnostic;
@ -40,24 +40,27 @@ pub enum MyDiagnostic {
)] )]
#[error("Tried to add a {bad_type} to a {good_type}")] #[error("Tried to add a {bad_type} to a {good_type}")]
BadArithmetic { BadArithmetic {
src: PathBuf, // Regular error metadata for programmatic use.
other_src: PathBuf,
good_type: Type, good_type: Type,
bad_type: Type, bad_type: Type,
bad_var: Var, bad_var: Var,
#[span_source(src)] // Anything implementing the Source trait can be used as a source.
#[label("This is a {bad_type}")] src: PathBuf,
bad_var_span: SourceSpan, other_src: String,
#[span_source(src)] // The context is the span of code that will be rendered.
#[label("This is a {good_type}")] // There can be multiple contexts in a Diagnostic.
#[context(src, "This region is where things went wrong.")]
ctx: SourceSpan,
// Highlights underline and label specific subsections of the context.
#[highlight(ctx, "This is a {bad_type}")]
bad_var_span: SourceSpan, // These can span multiple lines!
// They can be optional!
#[highlight(ctx, "This is a {good_type}")]
good_var_span: Option<SourceSpan>, good_var_span: Option<SourceSpan>,
#[span(other_src)]
#[label("{bad_var} is defined here")]
bad_var_definition_span: SourceSpan, // multiline span
}, },
#[error(transparent)] #[error(transparent)]

View File

@ -1,7 +1,7 @@
/*! /*!
Iterate over error `.source()` chains. Iterate over error `.source()` chains.
NOTE: This module is taken wholesale from https://crates.io/crates/eyre. NOTE: This module is taken wholesale from <https://crates.io/crates/eyre>.
*/ */
use std::error::Error as StdError; use std::error::Error as StdError;
use std::vec; use std::vec;

14
src/error.rs Normal file
View File

@ -0,0 +1,14 @@
pub use std::io;
pub use thiserror::Error;
/**
Error enum for miette. Used by certain operations in the protocol.
*/
#[derive(Debug, Error)]
pub enum MietteError {
#[error(transparent)]
IoError(#[from] io::Error),
#[error("The given offset is outside the bounds of its Source")]
OutOfBounds
}

View File

@ -1,10 +1,11 @@
#![doc = include_str!("../README.md")] #![doc = include_str!("../README.md")]
pub use chain::*; pub use error::*;
pub use protocol::*; pub use protocol::*;
pub use reporter::*; pub use reporter::*;
mod chain; mod chain;
mod error;
mod source_impls; mod source_impls;
mod protocol; mod protocol;
mod reporter; mod reporter;

View File

@ -1,5 +1,12 @@
/*!
This module defines the core of the miette protocol: a series of types and traits
that you can implement to get access to miette's (and related library's) full
reporting and such features.
*/
use std::fmt::Display; use std::fmt::Display;
use std::io::{self, Read};
use crate::MietteError;
/** /**
Adds rich metadata to your Error that can be used by [DiagnosticReporter] to print Adds rich metadata to your Error that can be used by [DiagnosticReporter] to print
@ -13,7 +20,7 @@ pub trait Diagnostic: std::error::Error + Send + Sync + 'static {
/// `E0123` or Enums will work just fine. /// `E0123` or Enums will work just fine.
fn code(&self) -> &(dyn Display + 'static); fn code(&self) -> &(dyn Display + 'static);
/// Diagnostic severity. This may be used by [Reporter]s to change the /// Diagnostic severity. This may be used by [DiagnosticReporter]s to change the
/// display format of this diagnostic. /// display format of this diagnostic.
fn severity(&self) -> Severity; fn severity(&self) -> Severity;
@ -23,9 +30,9 @@ pub trait Diagnostic: std::error::Error + Send + Sync + 'static {
None None
} }
/// Additional contextual details. This is typically used for adding /// Additional contextual snippets. This is typically used for adding
/// marked-up source file output the way compilers often do. /// marked-up source file output the way compilers often do.
fn details(&self) -> Option<&[DiagnosticDetail]> { fn snippets(&self) -> Option<&[DiagnosticSnippet]> {
None None
} }
} }
@ -69,52 +76,137 @@ pub enum Severity {
} }
/** /**
Represents a readable source of some sort: a source file, a String, etc. Represents a readable source of some sort.
This trait is able to support simple Source types like [String]s, as well
as more involved types like indexes into centralized `SourceMap`-like types,
file handles, and even network streams.
If you can read it, you can source it,
and it's not necessary to read the whole thing--meaning you should be able to
support Sources which are gigabytes or larger in size.
*/ */
pub trait Source: std::fmt::Debug + Send + Sync + 'static { pub trait Source: std::fmt::Debug + Send + Sync + 'static {
/// Get a `Read`er from a given [Source]. /// Read the bytes for a specific span from this Source.
fn open(&self) -> io::Result<Box<dyn Read>>; fn read_span<'a>(&'a self, span: &SourceSpan)
-> Result<Box<dyn SpanContents<'a> + '_>, MietteError>;
} }
/** /**
Details and additional context to be displayed. Contents of a [Source] covered by [SourceSpan].
Includes line and column information to optimize highlight calculations.
*/
pub trait SpanContents<'a> {
/// Reference to the data inside the associated span, in bytes.
fn data(&self) -> &[u8];
/// The 0-indexed line in the associated [Source] where the data begins.
fn line(&self) -> usize;
/// The 0-indexed column in the associated [Source] where the data begins,
/// relative to `line`.
fn column(&self) -> usize;
}
/**
Basic implementation of the [SpanContents] trait, for convenience.
*/
#[derive(Clone, Debug)]
pub struct MietteSpanContents<'a> {
/// Data from a [Source], in bytes.
data: &'a [u8],
// The 0-indexed line where the associated [SourceSpan] _starts_.
line: usize,
// The 0-indexed column where the associated [SourceSpan] _starts_.
column: usize,
}
impl<'a> MietteSpanContents<'a> {
/// Make a new [MietteSpanContents] object.
pub fn new(data: &'a [u8], line: usize, column: usize) -> MietteSpanContents<'a> {
MietteSpanContents { data, line, column }
}
}
impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
fn data(&self) -> &[u8] {
self.data
}
fn line(&self) -> usize {
self.line
}
fn column(&self) -> usize {
self.column
}
}
/**
A snippet from a [Source] to be displayed with a message and possibly some highlights.
*/ */
#[derive(Debug)] #[derive(Debug)]
pub struct DiagnosticDetail { pub struct DiagnosticSnippet {
/// Explanation of this specific diagnostic detail. /// Explanation of this specific diagnostic snippet.
pub message: Option<String>, pub message: Option<String>,
/// The "filename" for this diagnostic. /// The "filename" for this snippet.
pub source_name: String, pub source_name: String,
/// A [Source] that can be used to read the actual text of a source. /// A [Source] that can be used to read the actual text of a source.
pub source: Box<dyn Source>, pub source: Box<dyn Source>,
/// The primary [SourceSpan] where this diagnostic is located. /// The primary [SourceSpan] where this diagnostic is located.
pub span: SourceSpan, pub context: SourceSpan,
/// Additional [SourceSpan]s that can add secondary context. /// Additional [SourceSpan]s that mark specific sections of the span, for
pub other_spans: Option<Vec<SourceSpan>>, /// example, to underline specific text within the larger span. They're
/// paired with labels that should be applied to those sections.
pub highlights: Option<Vec<(String, SourceSpan)>>,
} }
/** /**
Span within a [Source] with an associated message. Span within a [Source] with an associated message.
*/ */
#[derive(Debug)] #[derive(Clone, Debug)]
pub struct SourceSpan { pub struct SourceSpan {
/// A name for the thing this SourceSpan is actually pointing to.
pub label: String,
/// The start of the span. /// The start of the span.
pub start: SourceLocation, pub start: SourceOffset,
/// The end of the span. Optional /// The (exclusive) end of the span.
pub end: Option<SourceLocation>, pub end: SourceOffset,
}
impl SourceSpan {
pub fn new(start: SourceOffset, end: SourceOffset) -> Self {
assert!(
start.offset() <= end.offset(),
"Starting offset must come before the end offset."
);
Self { start, end }
}
pub fn len(&self) -> usize {
self.end.offset() - self.start.offset() + 1
}
pub fn is_empty(&self) -> bool {
self.start.offset() == self.end.offset()
}
} }
/** /**
Specific location in a [SourceSpan] "Raw" type for the byte offset from the beginning of a [Source].
*/ */
#[derive(Debug)] pub type ByteOffset = usize;
pub struct SourceLocation {
/// 0-indexed column of location. /**
pub column: usize, Newtype that represents the [ByteOffset] from the beginning of a [Source]
/// 0-indexed line of location. */
pub line: usize, #[derive(Clone, Copy, Debug)]
/// 0-indexed _character_ offset of location. pub struct SourceOffset(ByteOffset);
pub offset: usize,
impl SourceOffset {
/// Actual byte offset.
pub fn offset(&self) -> ByteOffset {
self.0
}
}
impl From<ByteOffset> for SourceOffset {
fn from(bytes: ByteOffset) -> Self {
SourceOffset(bytes)
}
} }

View File

@ -2,56 +2,104 @@
Basic reporter for Diagnostics. Probably good enough for most use-cases, Basic reporter for Diagnostics. Probably good enough for most use-cases,
but largely meant to be an example. but largely meant to be an example.
*/ */
use std::fmt;
use indenter::indented; use indenter::indented;
use crate::chain::Chain; use crate::chain::Chain;
use crate::protocol::{Diagnostic, DiagnosticDetail, DiagnosticReporter, Severity}; use crate::protocol::{Diagnostic, DiagnosticSnippet, DiagnosticReporter, Severity};
pub struct Reporter; /**
Reference implementation of the [DiagnosticReporter] trait. This is generally
good enough for simple use-cases, but you might want to implement your own if
you want custom reporting for your tool or app.
*/
pub struct MietteReporter;
impl Reporter { impl MietteReporter {
fn render_detail( fn render_snippet(&self, f: &mut fmt::Formatter<'_>, snippet: &DiagnosticSnippet) -> fmt::Result {
&self, use fmt::Write as _;
f: &mut core::fmt::Formatter<'_>, write!(f, "\n[{}]", snippet.source_name)?;
detail: &DiagnosticDetail, if let Some(msg) = &snippet.message {
) -> core::fmt::Result {
use core::fmt::Write as _;
write!(f, "\n[{}]", detail.source_name)?;
if let Some(msg) = &detail.message {
write!(f, " {}:", msg)?; write!(f, " {}:", msg)?;
} }
writeln!( writeln!(f)?;
indented(f), writeln!(f)?;
"\n\n({}) @ line {}, col {} ", let context_data = snippet
detail.span.label, .source
detail.span.start.line + 1, .read_span(&snippet.context)
detail.span.start.column + 1 .map_err(|_| fmt::Error)?;
)?; let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
if let Some(other_spans) = &detail.other_spans { let mut line = context_data.line();
for span in other_spans { let mut column = context_data.column();
writeln!( let mut offset = snippet.context.start.offset();
indented(f), let mut line_offset = offset;
"\n{} @ line {}, col {} ", let mut iter = context.chars().peekable();
span.label, let mut line_str = String::new();
span.start.line + 1, let highlights = snippet.highlights.as_ref();
span.start.column + 1 while let Some(char) = iter.next() {
)?; offset += char.len_utf8();
match char {
'\r' => {
if iter.next_if_eq(&'\n').is_some() {
offset += 1;
line += 1;
column = 0;
} else {
line_str.push(char);
column += 1;
}
}
'\n' => {
line += 1;
column = 0;
}
_ => {
line_str.push(char);
column += 1;
}
}
if iter.peek().is_none() {
line += 1;
}
if column == 0 || iter.peek().is_none() {
writeln!(indented(f), "{: <2} | {}", line, line_str)?;
line_str.clear();
if let Some(highlights) = highlights {
for (label, span) in highlights {
if span.start.offset() >= line_offset && span.end.offset() < offset {
// Highlight only covers one line.
write!(indented(f), "{: <2} | ", "")?;
write!(
f,
"{}{} ",
" ".repeat(span.start.offset() - line_offset),
"^".repeat(span.len())
)?;
writeln!(f, "{}", label)?;
} else if span.start.offset() < offset
&& span.start.offset() >= line_offset
&& span.end.offset() >= offset
{
// Multiline highlight.
todo!("Multiline highlights.");
}
}
}
line_offset = offset;
} }
} }
Ok(()) Ok(())
} }
} }
impl DiagnosticReporter for Reporter { impl DiagnosticReporter for MietteReporter {
fn debug( fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
&self, use fmt::Write as _;
diagnostic: &(dyn Diagnostic),
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
use core::fmt::Write as _;
if f.alternate() { if f.alternate() {
return core::fmt::Debug::fmt(diagnostic, f); return fmt::Debug::fmt(diagnostic, f);
} }
let sev = match diagnostic.severity() { let sev = match diagnostic.severity() {
@ -75,10 +123,10 @@ impl DiagnosticReporter for Reporter {
} }
} }
if let Some(details) = diagnostic.details() { if let Some(snippets) = diagnostic.snippets() {
writeln!(f)?; writeln!(f)?;
for detail in details { for snippet in snippets {
self.render_detail(f, detail)?; self.render_snippet(f, snippet)?;
} }
} }
@ -93,16 +141,13 @@ impl DiagnosticReporter for Reporter {
} }
} }
/// Literally what it says on the tin.
pub struct JokeReporter; pub struct JokeReporter;
impl DiagnosticReporter for JokeReporter { impl DiagnosticReporter for JokeReporter {
fn debug( fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
&self,
diagnostic: &(dyn Diagnostic),
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
if f.alternate() { if f.alternate() {
return core::fmt::Debug::fmt(diagnostic, f); return fmt::Debug::fmt(diagnostic, f);
} }
let sev = match diagnostic.severity() { let sev = match diagnostic.severity() {
@ -124,8 +169,8 @@ impl DiagnosticReporter for JokeReporter {
f, f,
"miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!", "miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
diagnostic.code(), diagnostic.code(),
diagnostic.details().map(|details| { diagnostic.snippets().map(|snippets| {
details.iter().map(|detail| detail.message.clone()).collect::<Option<Vec<String>>>() snippets.iter().map(|snippet| snippet.message.clone()).collect::<Option<Vec<String>>>()
}).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into()) }).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into())
)?; )?;

View File

@ -1,26 +1,82 @@
/*! /*!
Default trait implementations for [Source]. Default trait implementations for [Source].
*/ */
use std::fs::File; use crate::{MietteError, MietteSpanContents, Source, SourceSpan, SpanContents};
use std::io::{Cursor, Read, Result};
use std::path::{Path, PathBuf};
use crate::{Source};
impl Source for String { impl Source for String {
fn open(&self) -> Result<Box<dyn Read>> { fn read_span(&self, span: &SourceSpan) -> Result<Box<dyn SpanContents + '_>, MietteError> {
Ok(Box::new(Cursor::new(self.clone()))) let mut offset = 0usize;
let mut start_line = 0usize;
let mut start_column = 0usize;
let mut iter = self.chars().peekable();
while let Some(char) = iter.next() {
if offset < span.start.offset() {
match char {
'\r' => {
if iter.next_if_eq(&'\n').is_some() {
offset += 1;
}
start_line += 1;
start_column = 0;
}
'\n' => {
start_line += 1;
start_column = 0;
}
_ => {
start_column += 1;
}
}
}
if offset >= span.end.offset() {
return Ok(Box::new(MietteSpanContents::new(
&self.as_bytes()[span.start.offset()..=span.end.offset()],
start_line,
start_column,
)));
}
offset += char.len_utf8();
}
Err(MietteError::OutOfBounds)
} }
} }
impl Source for Path { #[cfg(test)]
fn open(&self) -> Result<Box<dyn Read>> { mod tests {
Ok(Box::new(File::open(self)?)) use super::*;
}
}
impl Source for PathBuf { #[test]
fn open(&self) -> Result<Box<dyn Read>> { fn basic() -> Result<(), MietteError> {
Ok(Box::new(File::open(self)?)) let src = String::from("foo\n");
let contents = src.read_span(&SourceSpan {
start: 0.into(),
end: 3.into(),
})?;
assert_eq!("foo\n", std::str::from_utf8(contents.data()).unwrap());
Ok(())
}
#[test]
fn middle() -> Result<(), MietteError> {
let src = String::from("foo\nbar\nbaz\n");
let contents = src.read_span(&SourceSpan {
start: 4.into(),
end: 7.into(),
})?;
assert_eq!("bar\n", std::str::from_utf8(contents.data()).unwrap());
Ok(())
}
#[test]
fn with_crlf() -> Result<(), MietteError> {
let src = String::from("foo\r\nbar\r\nbaz\r\n");
let contents = src.read_span(&SourceSpan {
start: 5.into(),
end: 9.into(),
})?;
assert_eq!("bar\r\n", std::str::from_utf8(contents.data()).unwrap());
Ok(())
} }
} }

View File

@ -1,20 +1,19 @@
use std::{fmt, io}; use std::fmt;
use miette::{ use miette::{
Diagnostic, DiagnosticDetail, DiagnosticReporter, Reporter, Severity, SourceLocation, Diagnostic, DiagnosticSnippet, DiagnosticReporter, MietteError, MietteReporter, Severity, SourceSpan,
SourceSpan,
}; };
use thiserror::Error; use thiserror::Error;
#[derive(Error)] #[derive(Error)]
#[error("oops!")] #[error("oops!")]
struct MyBad { struct MyBad {
details: Vec<DiagnosticDetail>, details: Vec<DiagnosticSnippet>,
} }
impl fmt::Debug for MyBad { impl fmt::Debug for MyBad {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Reporter.debug(self, f) MietteReporter.debug(self, f)
} }
} }
@ -31,13 +30,13 @@ impl Diagnostic for MyBad {
Some(&["try doing it better next time?"]) Some(&["try doing it better next time?"])
} }
fn details(&self) -> Option<&[DiagnosticDetail]> { fn snippets(&self) -> Option<&[DiagnosticSnippet]> {
Some(&self.details) Some(&self.details)
} }
} }
#[test] #[test]
fn basic() -> io::Result<()> { fn basic() -> Result<(), MietteError> {
let err = MyBad { let err = MyBad {
details: Vec::new(), details: Vec::new(),
}; };
@ -50,26 +49,27 @@ fn basic() -> io::Result<()> {
} }
#[test] #[test]
fn fancy() -> io::Result<()> { fn fancy() -> Result<(), MietteError> {
let src = "source\n text\n here".to_string();
let err = MyBad { let err = MyBad {
details: vec![DiagnosticDetail { details: vec![DiagnosticSnippet {
message: Some("This is the part that broke".into()), message: Some("This is the part that broke".into()),
source_name: "bad_file.rs".into(), source_name: "bad_file.rs".into(),
source: Box::new("source_text".to_string()), source: Box::new(src.clone()),
other_spans: None, highlights: Some(vec![
span: SourceSpan { ("this bit here".into(), SourceSpan {
label: "this thing here is bad".into(), start: 9.into(),
start: SourceLocation { end: 12.into(),
line: 0, })
column: 0, ]),
offset: 0, context: SourceSpan {
}, start: 0.into(),
end: None, end: (src.len() - 1).into(),
}, },
}], }],
}; };
let out = format!("{:?}", err); let out = format!("{:?}", err);
// println!("{}", out); // println!("{}", out);
assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n (this thing here is bad) @ line 1, col 1 \n\n﹦try doing it better next time?\n".to_string(), out); assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 | source\n 2 | text\n ⫶ | ^^^^ this bit here\n 3 | here\n\n﹦try doing it better next time?\n".to_string(), out);
Ok(()) Ok(())
} }