From acfeb9c5b0e390c924194ee0363fc49fa8defbac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Tue, 17 Aug 2021 08:40:30 -0700 Subject: [PATCH] feat(protocol): new SourceSpans with labels --- README.md | 4 +- miette-derive/src/snippets.rs | 72 +++++------------------------------ src/protocol.rs | 62 +++++++++++++++++++++--------- src/reporter.rs | 22 ++++++----- src/source_impls.rs | 21 +++------- tests/derive.rs | 23 ++++++----- tests/reporter.rs | 18 ++------- 7 files changed, 89 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index bd0b598..a52267e 100644 --- a/README.md +++ b/README.md @@ -104,8 +104,8 @@ fn pretend_this_is_main() -> Result<(), MyBad> { Err(MyBad { src: Arc::new(src), filename: "bad_file.rs".into(), - snip: (0, (len - 1)).into(), - bad_bit: (9, 12).into(), + snip: (0, len).into(), + bad_bit: (9, 3).into(), }) } ``` diff --git a/miette-derive/src/snippets.rs b/miette-derive/src/snippets.rs index ded6a04..5d4e0b0 100644 --- a/miette-derive/src/snippets.rs +++ b/miette-derive/src/snippets.rs @@ -16,25 +16,21 @@ pub struct Snippets(Vec); struct Snippet { message: Option, highlights: Vec, - source_name: MemberOrString, source: syn::Member, snippet: syn::Member, } struct Highlight { highlight: syn::Member, - label: Option, } struct SnippetAttr { source: syn::Member, - source_name: MemberOrString, message: Option, } struct HighlightAttr { snippet: syn::Member, - label: Option, } enum MemberOrString { @@ -82,15 +78,8 @@ impl Parse for SnippetAttr { )) } }; - let src_name = iter - .next() - .ok_or_else(|| syn::Error::new(span, "Expected a source name."))?; let message = iter.next(); - Ok(SnippetAttr { - source, - source_name: src_name, - message, - }) + Ok(SnippetAttr { source, message }) } } @@ -107,8 +96,7 @@ impl Parse for HighlightAttr { "must be an identifier that refers to something with a #[snippet] attribute.", )), }; - let label = iter.next(); - Ok(HighlightAttr { snippet, label }) + Ok(HighlightAttr { snippet }) } } @@ -137,18 +125,13 @@ impl Snippets { span: field.span(), }) }; - let SnippetAttr { - source, - message, - source_name, - } = attr.parse_args::()?; + let SnippetAttr { source, message } = attr.parse_args::()?; // TODO: useful error when source refers to a field that doesn't exist. snippets.insert( snippet.clone(), Snippet { message, highlights: Vec::new(), - source_name, source, snippet, }, @@ -160,7 +143,7 @@ impl Snippets { for (i, field) in fields.iter().enumerate() { for attr in &field.attrs { if attr.path.is_ident("highlight") { - let HighlightAttr { snippet, label } = attr.parse_args::()?; + let HighlightAttr { snippet } = attr.parse_args::()?; if let Some(snippet) = snippets.get_mut(&snippet) { let member = if let Some(ident) = field.ident.clone() { syn::Member::Named(ident) @@ -170,10 +153,7 @@ impl Snippets { span: field.span(), }) }; - snippet.highlights.push(Highlight { - label, - highlight: member, - }); + snippet.highlights.push(Highlight { highlight: member }); } else { return Err(syn::Error::new(snippet.span(), "Highlight must refer to an existing field with a #[snippet(...)] attribute.")); } @@ -218,18 +198,6 @@ impl Snippets { source: self.#src_ident.clone(), }; - // Source name - let src_name = match &snippet.source_name { - MemberOrString::String(str) => { - quote! { - source_name: #str.into(), - } - } - MemberOrString::Member(member) => quote! { - source_name: self.#member.clone(), - }, - }; - // Context let context = &snippet.snippet; let context = quote! { @@ -238,9 +206,9 @@ impl Snippets { // Highlights let highlights = snippet.highlights.iter().map(|highlight| { - let Highlight { highlight, label } = highlight; + let Highlight { highlight } = highlight; quote! { - (#label.into(), self.#highlight.clone()) + self.#highlight.clone() } }); let highlights = quote! { @@ -253,7 +221,6 @@ impl Snippets { quote! { miette::DiagnosticSnippet { #msg - #src_name #src_ident #context #highlights @@ -314,26 +281,6 @@ impl Snippets { source: #src_ident.clone(), }; - // Source name - let src_name = match &snippet.source_name { - MemberOrString::String(str) => { - quote! { - source_name: #str.into(), - } - } - MemberOrString::Member(m) => { - let m = match m { - syn::Member::Named(id) => id.clone(), - syn::Member::Unnamed(syn::Index { index, .. }) => { - format_ident!("_{}", index) - } - }; - quote! { - source_name: #m.clone(), - } - } - }; - // Context let context = match &snippet.snippet { syn::Member::Named(id) => id.clone(), @@ -347,7 +294,7 @@ impl Snippets { // Highlights let highlights = snippet.highlights.iter().map(|highlight| { - let Highlight { highlight, label } = highlight; + let Highlight { highlight } = highlight; let m = match highlight { syn::Member::Named(id) => id.clone(), syn::Member::Unnamed(syn::Index { index, .. }) => { @@ -355,7 +302,7 @@ impl Snippets { } }; quote! { - (#label.into(), #m.clone()) + #m.clone() } }); let highlights = quote! { @@ -368,7 +315,6 @@ impl Snippets { quote! { miette::DiagnosticSnippet { #msg - #src_name #src_ident #context #highlights diff --git a/src/protocol.rs b/src/protocol.rs index d7ccf44..e1a7ad5 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -174,8 +174,6 @@ A snippet from a [Source] to be displayed with a message and possibly some highl pub struct DiagnosticSnippet { /// Explanation of this specific diagnostic snippet. pub message: Option, - /// 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: Arc, /// The primary [SourceSpan] where this diagnostic is located. @@ -183,7 +181,7 @@ pub struct DiagnosticSnippet { /// 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>, + pub highlights: Option>, } /** @@ -191,35 +189,65 @@ Span within a [Source] with an associated message. */ #[derive(Clone, Debug)] pub struct SourceSpan { + /// An optional label for this span. Rendered differently depending on + /// context. + label: Option, /// The start of the span. - pub start: SourceOffset, - /// The (exclusive) end of the span. - pub end: SourceOffset, + offset: SourceOffset, + /// The total length of the span. Think of this as an offset from `start`. + length: 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 new(start: SourceOffset, length: SourceOffset) -> Self { + Self { + label: None, + offset: start, + length, + } + } + + pub fn new_labeled(label: impl AsRef, start: SourceOffset, length: SourceOffset) -> Self { + Self { + label: Some(label.as_ref().into()), + offset: start, + length, + } + } + + pub fn offset(&self) -> usize { + self.offset.offset() + } + + pub fn label(&self) -> Option<&str> { + self.label.as_ref().map(|x| &x[..]) } pub fn len(&self) -> usize { - self.end.offset() - self.start.offset() + 1 + self.length.offset() } pub fn is_empty(&self) -> bool { - self.start.offset() == self.end.offset() + self.length.offset() == 0 } } impl From<(ByteOffset, ByteOffset)> for SourceSpan { - fn from((start, end): (ByteOffset, ByteOffset)) -> Self { + fn from((start, len): (ByteOffset, ByteOffset)) -> Self { Self { - start: start.into(), - end: end.into(), + label: None, + offset: start.into(), + length: len.into(), + } + } +} + +impl> From<(T, ByteOffset, ByteOffset)> for SourceSpan { + fn from((label, start, len): (T, ByteOffset, ByteOffset)) -> Self { + Self { + label: Some(label.as_ref().into()), + offset: start.into(), + length: len.into(), } } } diff --git a/src/reporter.rs b/src/reporter.rs index e531103..50213ca 100644 --- a/src/reporter.rs +++ b/src/reporter.rs @@ -23,7 +23,9 @@ impl MietteReporter { snippet: &DiagnosticSnippet, ) -> fmt::Result { use fmt::Write as _; - write!(f, "[{}]", snippet.source_name)?; + if let Some(source_name) = snippet.context.label() { + write!(f, "[{}]", source_name)?; + } if let Some(msg) = &snippet.message { write!(f, " {}:", msg)?; } @@ -36,7 +38,7 @@ impl MietteReporter { 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 offset = snippet.context.offset(); let mut line_offset = offset; let mut iter = context.chars().peekable(); let mut line_str = String::new(); @@ -71,20 +73,22 @@ impl MietteReporter { 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 { + for span in highlights { + if span.offset() >= line_offset && (span.offset() + span.len()) < offset { // Highlight only covers one line. write!(indented(f), "{: <2} | ", "⫶")?; write!( f, "{}{} ", - " ".repeat(span.start.offset() - line_offset), + " ".repeat(span.offset() - line_offset), "^".repeat(span.len()) )?; - writeln!(f, "{}", label)?; - } else if span.start.offset() < offset - && span.start.offset() >= line_offset - && span.end.offset() >= offset + if let Some(label) = span.label() { + writeln!(f, "{}", label)?; + } + } else if span.offset() < offset + && span.offset() >= line_offset + && (span.offset() + span.len()) >= offset { // Multiline highlight. todo!("Multiline highlights."); diff --git a/src/source_impls.rs b/src/source_impls.rs index 5b0e822..c42731e 100644 --- a/src/source_impls.rs +++ b/src/source_impls.rs @@ -10,7 +10,7 @@ impl Source for String { let mut start_column = 0usize; let mut iter = self.chars().peekable(); while let Some(char) = iter.next() { - if offset < span.start.offset() { + if offset < span.offset() { match char { '\r' => { if iter.next_if_eq(&'\n').is_some() { @@ -29,9 +29,9 @@ impl Source for String { } } - if offset >= span.end.offset() { + if offset >= span.offset() + span.len() - 1 { return Ok(Box::new(MietteSpanContents::new( - &self.as_bytes()[span.start.offset()..=span.end.offset()], + &self.as_bytes()[span.offset()..span.offset() + span.len()], start_line, start_column, ))); @@ -50,10 +50,7 @@ mod tests { #[test] fn basic() -> Result<(), MietteError> { let src = String::from("foo\n"); - let contents = src.read_span(&SourceSpan { - start: 0.into(), - end: 3.into(), - })?; + let contents = src.read_span(&(0, 4).into())?; assert_eq!("foo\n", std::str::from_utf8(contents.data()).unwrap()); Ok(()) } @@ -61,10 +58,7 @@ mod tests { #[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(), - })?; + let contents = src.read_span(&(4, 4).into())?; assert_eq!("bar\n", std::str::from_utf8(contents.data()).unwrap()); Ok(()) } @@ -72,10 +66,7 @@ mod tests { #[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(), - })?; + let contents = src.read_span(&(5, 5).into())?; assert_eq!("bar\r\n", std::str::from_utf8(contents.data()).unwrap()); Ok(()) } diff --git a/tests/derive.rs b/tests/derive.rs index a8d6df2..b379ef8 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -183,8 +183,8 @@ fn test_snippet_named_struct() { // / [my_snippet]: hi this is where the thing went wrong. // 1 | hello // 2 | world - #[snippet(src, "my_snippet.rs", "hi this is where the thing went wrong")] - snip: SourceSpan, + #[snippet(src, "hi this is where the thing went wrong")] + snip: SourceSpan, // Defines filename using `label` // "Highlights" are the specific highlights _inside_ the snippet. // These will be used to underline/point to specific sections of the @@ -196,9 +196,9 @@ fn test_snippet_named_struct() { // | ^^^^ ^^^^ - var 2 // | | // | var 1 - #[highlight(snip, "var 1")] - var1: SourceSpan, - #[highlight(snip, "var 2")] + #[highlight(snip)] + var1: SourceSpan, // label from SourceSpan is used, if any. + #[highlight(snip)] var2: SourceSpan, // Now with member source names @@ -216,15 +216,14 @@ fn test_snippet_unnamed_struct() { #[diagnostic(code(foo::bar::baz))] struct Foo( Arc, - #[snippet(0, "my_snippet.rs", "hi")] SourceSpan, - #[highlight(1, "var 1")] SourceSpan, - #[highlight(1, "var 2")] SourceSpan, + #[snippet(0, "hi")] SourceSpan, + #[highlight(1)] SourceSpan, + #[highlight(1)] SourceSpan, // referenced source name String, - String, - #[snippet(0, 4, 5)] SourceSpan, - #[highlight(6, "var 3")] SourceSpan, - #[highlight(6, "var 4")] SourceSpan, + #[snippet(0, 4)] SourceSpan, + #[highlight(5)] SourceSpan, + #[highlight(5)] SourceSpan, ); } diff --git a/tests/reporter.rs b/tests/reporter.rs index fa2c01c..eb4dab7 100644 --- a/tests/reporter.rs +++ b/tests/reporter.rs @@ -1,8 +1,6 @@ use std::{fmt, sync::Arc}; -use miette::{ - Diagnostic, DiagnosticReporter, DiagnosticSnippet, MietteError, MietteReporter, SourceSpan, -}; +use miette::{Diagnostic, DiagnosticReporter, DiagnosticSnippet, MietteError, MietteReporter}; use thiserror::Error; #[derive(Error)] @@ -51,19 +49,9 @@ fn fancy() -> Result<(), MietteError> { let err = MyBad { snippets: vec![DiagnosticSnippet { message: Some("This is the part that broke".into()), - source_name: "bad_file.rs".into(), source: Arc::new(src), - highlights: Some(vec![( - "this bit here".into(), - SourceSpan { - start: 9.into(), - end: 12.into(), - }, - )]), - context: SourceSpan { - start: 0.into(), - end: (len - 1).into(), - }, + highlights: Some(vec![("this bit here", 9, 4).into()]), + context: ("bad_file.rs", 0, len).into(), }], }; let out = format!("{:?}", err);