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]
indenter = "0.3.3"
thiserror = "1.0.26"
[dev-dependencies]
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.
```rust
```ignore
use thiserror::Error;
use thisdiagnostic::Diagnostic;
@ -40,24 +40,27 @@ pub enum MyDiagnostic {
)]
#[error("Tried to add a {bad_type} to a {good_type}")]
BadArithmetic {
src: PathBuf,
other_src: PathBuf,
// Regular error metadata for programmatic use.
good_type: Type,
bad_type: Type,
bad_var: Var,
#[span_source(src)]
#[label("This is a {bad_type}")]
bad_var_span: SourceSpan,
// Anything implementing the Source trait can be used as a source.
src: PathBuf,
other_src: String,
#[span_source(src)]
#[label("This is a {good_type}")]
// The context is the span of code that will be rendered.
// 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>,
#[span(other_src)]
#[label("{bad_var} is defined here")]
bad_var_definition_span: SourceSpan, // multiline span
},
#[error(transparent)]

View File

@ -1,7 +1,7 @@
/*!
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::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")]
pub use chain::*;
pub use error::*;
pub use protocol::*;
pub use reporter::*;
mod chain;
mod error;
mod source_impls;
mod protocol;
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::io::{self, Read};
use crate::MietteError;
/**
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.
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.
fn severity(&self) -> Severity;
@ -23,9 +30,9 @@ pub trait Diagnostic: std::error::Error + Send + Sync + 'static {
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.
fn details(&self) -> Option<&[DiagnosticDetail]> {
fn snippets(&self) -> Option<&[DiagnosticSnippet]> {
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 {
/// Get a `Read`er from a given [Source].
fn open(&self) -> io::Result<Box<dyn Read>>;
/// Read the bytes for a specific span from this Source.
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)]
pub struct DiagnosticDetail {
/// Explanation of this specific diagnostic detail.
pub struct DiagnosticSnippet {
/// Explanation of this specific diagnostic snippet.
pub message: Option<String>,
/// The "filename" for this diagnostic.
/// The "filename" for this snippet.
pub source_name: String,
/// A [Source] that can be used to read the actual text of a source.
pub source: Box<dyn Source>,
/// The primary [SourceSpan] where this diagnostic is located.
pub span: SourceSpan,
/// Additional [SourceSpan]s that can add secondary context.
pub other_spans: Option<Vec<SourceSpan>>,
pub context: SourceSpan,
/// Additional [SourceSpan]s that mark specific sections of the span, for
/// 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.
*/
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct SourceSpan {
/// A name for the thing this SourceSpan is actually pointing to.
pub label: String,
/// The start of the span.
pub start: SourceLocation,
/// The end of the span. Optional
pub end: Option<SourceLocation>,
pub start: SourceOffset,
/// The (exclusive) end of the span.
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 struct SourceLocation {
/// 0-indexed column of location.
pub column: usize,
/// 0-indexed line of location.
pub line: usize,
/// 0-indexed _character_ offset of location.
pub offset: usize,
pub type ByteOffset = usize;
/**
Newtype that represents the [ByteOffset] from the beginning of a [Source]
*/
#[derive(Clone, Copy, Debug)]
pub struct SourceOffset(ByteOffset);
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,
but largely meant to be an example.
*/
use std::fmt;
use indenter::indented;
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 {
fn render_detail(
&self,
f: &mut core::fmt::Formatter<'_>,
detail: &DiagnosticDetail,
) -> core::fmt::Result {
use core::fmt::Write as _;
write!(f, "\n[{}]", detail.source_name)?;
if let Some(msg) = &detail.message {
impl MietteReporter {
fn render_snippet(&self, f: &mut fmt::Formatter<'_>, snippet: &DiagnosticSnippet) -> fmt::Result {
use fmt::Write as _;
write!(f, "\n[{}]", snippet.source_name)?;
if let Some(msg) = &snippet.message {
write!(f, " {}:", msg)?;
}
writeln!(
indented(f),
"\n\n({}) @ line {}, col {} ",
detail.span.label,
detail.span.start.line + 1,
detail.span.start.column + 1
)?;
if let Some(other_spans) = &detail.other_spans {
for span in other_spans {
writeln!(
indented(f),
"\n{} @ line {}, col {} ",
span.label,
span.start.line + 1,
span.start.column + 1
)?;
writeln!(f)?;
writeln!(f)?;
let context_data = snippet
.source
.read_span(&snippet.context)
.map_err(|_| fmt::Error)?;
let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
let mut line = context_data.line();
let mut column = context_data.column();
let mut offset = snippet.context.start.offset();
let mut line_offset = offset;
let mut iter = context.chars().peekable();
let mut line_str = String::new();
let highlights = snippet.highlights.as_ref();
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(())
}
}
impl DiagnosticReporter for Reporter {
fn debug(
&self,
diagnostic: &(dyn Diagnostic),
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
use core::fmt::Write as _;
impl DiagnosticReporter for MietteReporter {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Write as _;
if f.alternate() {
return core::fmt::Debug::fmt(diagnostic, f);
return fmt::Debug::fmt(diagnostic, f);
}
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)?;
for detail in details {
self.render_detail(f, detail)?;
for snippet in snippets {
self.render_snippet(f, snippet)?;
}
}
@ -93,16 +141,13 @@ impl DiagnosticReporter for Reporter {
}
}
/// Literally what it says on the tin.
pub struct JokeReporter;
impl DiagnosticReporter for JokeReporter {
fn debug(
&self,
diagnostic: &(dyn Diagnostic),
f: &mut core::fmt::Formatter<'_>,
) -> core::fmt::Result {
fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
return core::fmt::Debug::fmt(diagnostic, f);
return fmt::Debug::fmt(diagnostic, f);
}
let sev = match diagnostic.severity() {
@ -124,8 +169,8 @@ impl DiagnosticReporter for JokeReporter {
f,
"miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
diagnostic.code(),
diagnostic.details().map(|details| {
details.iter().map(|detail| detail.message.clone()).collect::<Option<Vec<String>>>()
diagnostic.snippets().map(|snippets| {
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())
)?;

View File

@ -1,26 +1,82 @@
/*!
Default trait implementations for [Source].
*/
use std::fs::File;
use std::io::{Cursor, Read, Result};
use std::path::{Path, PathBuf};
use crate::{Source};
use crate::{MietteError, MietteSpanContents, Source, SourceSpan, SpanContents};
impl Source for String {
fn open(&self) -> Result<Box<dyn Read>> {
Ok(Box::new(Cursor::new(self.clone())))
fn read_span(&self, span: &SourceSpan) -> Result<Box<dyn SpanContents + '_>, MietteError> {
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 {
fn open(&self) -> Result<Box<dyn Read>> {
Ok(Box::new(File::open(self)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
impl Source for PathBuf {
fn open(&self) -> Result<Box<dyn Read>> {
Ok(Box::new(File::open(self)?))
#[test]
fn basic() -> Result<(), MietteError> {
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::{
Diagnostic, DiagnosticDetail, DiagnosticReporter, Reporter, Severity, SourceLocation,
SourceSpan,
Diagnostic, DiagnosticSnippet, DiagnosticReporter, MietteError, MietteReporter, Severity, SourceSpan,
};
use thiserror::Error;
#[derive(Error)]
#[error("oops!")]
struct MyBad {
details: Vec<DiagnosticDetail>,
details: Vec<DiagnosticSnippet>,
}
impl fmt::Debug for MyBad {
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?"])
}
fn details(&self) -> Option<&[DiagnosticDetail]> {
fn snippets(&self) -> Option<&[DiagnosticSnippet]> {
Some(&self.details)
}
}
#[test]
fn basic() -> io::Result<()> {
fn basic() -> Result<(), MietteError> {
let err = MyBad {
details: Vec::new(),
};
@ -50,26 +49,27 @@ fn basic() -> io::Result<()> {
}
#[test]
fn fancy() -> io::Result<()> {
fn fancy() -> Result<(), MietteError> {
let src = "source\n text\n here".to_string();
let err = MyBad {
details: vec![DiagnosticDetail {
details: vec![DiagnosticSnippet {
message: Some("This is the part that broke".into()),
source_name: "bad_file.rs".into(),
source: Box::new("source_text".to_string()),
other_spans: None,
span: SourceSpan {
label: "this thing here is bad".into(),
start: SourceLocation {
line: 0,
column: 0,
offset: 0,
},
end: None,
source: Box::new(src.clone()),
highlights: Some(vec![
("this bit here".into(), SourceSpan {
start: 9.into(),
end: 12.into(),
})
]),
context: SourceSpan {
start: 0.into(),
end: (src.len() - 1).into(),
},
}],
};
let out = format!("{:?}", err);
// 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(())
}