feat(collection): add support for collection of labels (#341)

Fixes: https://github.com/zkat/miette/issues/315

Allow errors to have a number of labels determined at runtime.
An example of this is when the rust compiler labels all the arms of
a `match` expression when one of them has an incompatible type

To allow customization of the text for each label in a collection, add
support for using LabeledSpan in collections instead of just regular
spans
This commit is contained in:
Nahor 2024-02-15 18:14:04 -08:00 committed by GitHub
parent 6f09250cca
commit 03060245d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 537 additions and 66 deletions

View File

@ -49,6 +49,7 @@ libraries and such might not want.
- [... handler options](#-handler-options) - [... handler options](#-handler-options)
- [... dynamic diagnostics](#-dynamic-diagnostics) - [... dynamic diagnostics](#-dynamic-diagnostics)
- [... syntax highlighting](#-syntax-highlighting) - [... syntax highlighting](#-syntax-highlighting)
- [... collection of labels](#-collection-of-labels)
- [Acknowledgements](#acknowledgements) - [Acknowledgements](#acknowledgements)
- [License](#license) - [License](#license)
@ -671,6 +672,57 @@ trait to [`MietteHandlerOpts`] by calling the
[`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting) [`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
method. See the [`highlighters`] module docs for more details. method. See the [`highlighters`] module docs for more details.
#### ... collection of labels
When the number of labels is unknown, you can use a collection of `SourceSpan`
(or any type convertible into `SourceSpan`). For this, add the `collection`
parameter to `label` and use any type than can be iterated over for the field.
```rust
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyError {
#[label("main issue")]
primary_span: SourceSpan,
#[label(collection, "related to this")]
other_spans: Vec<Range<usize>>,
}
let report: miette::Report = MyError {
primary_span: (6, 9).into(),
other_spans: vec![19..26, 30..41],
}.into();
println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
```
A collection can also be of `LabeledSpan` if you want to have different text
for different labels. Labels with no text will use the one from the `label`
attribute
```rust
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyError {
#[label("main issue")]
primary_span: SourceSpan,
#[label(collection, "related to this")]
other_spans: Vec<LabeledSpan>, // LabeledSpan
}
let report: miette::Report = MyError {
primary_span: (6, 9).into(),
other_spans: vec![
LabeledSpan::new(None, 19, 7), // Use default text `related to this`
LabeledSpan::new(Some("and also this".to_string()), 30, 11), // Use specific text
],
}.into();
println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
```
### MSRV ### MSRV
This crate requires rustc 1.70.0 or later. This crate requires rustc 1.70.0 or later.

View File

@ -16,16 +16,23 @@ use crate::{
pub struct Labels(Vec<Label>); pub struct Labels(Vec<Label>);
#[derive(PartialEq, Eq)]
enum LabelType {
Default,
Primary,
Collection,
}
struct Label { struct Label {
label: Option<Display>, label: Option<Display>,
ty: syn::Type, ty: syn::Type,
span: syn::Member, span: syn::Member,
primary: bool, lbl_ty: LabelType,
} }
struct LabelAttr { struct LabelAttr {
label: Option<Display>, label: Option<Display>,
primary: bool, lbl_ty: LabelType,
} }
impl Parse for LabelAttr { impl Parse for LabelAttr {
@ -42,20 +49,24 @@ impl Parse for LabelAttr {
} }
}); });
let la = input.lookahead1(); let la = input.lookahead1();
let (primary, label) = if la.peek(syn::token::Paren) { let (lbl_ty, label) = if la.peek(syn::token::Paren) {
// #[label(primary?, "{}", x)] // #[label(primary?, "{}", x)]
let content; let content;
parenthesized!(content in input); parenthesized!(content in input);
let primary = if content.peek(syn::Ident) { let attr = match content.parse::<Option<syn::Ident>>()? {
let ident: syn::Ident = content.parse()?; Some(ident) if ident == "primary" => {
if ident != "primary" { let _ = content.parse::<Token![,]>();
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`.")); LabelType::Primary
} }
let _ = content.parse::<Token![,]>(); Some(ident) if ident == "collection" => {
true let _ = content.parse::<Token![,]>();
} else { LabelType::Collection
false }
Some(_) => {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
}
_ => LabelType::Default,
}; };
if content.peek(syn::LitStr) { if content.peek(syn::LitStr) {
@ -70,17 +81,17 @@ impl Parse for LabelAttr {
args, args,
has_bonus_display: false, has_bonus_display: false,
}; };
(primary, Some(display)) (attr, Some(display))
} else if !primary { } else if !content.is_empty() {
return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`.")); return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
} else { } else {
(primary, None) (attr, None)
} }
} else if la.peek(Token![=]) { } else if la.peek(Token![=]) {
// #[label = "blabla"] // #[label = "blabla"]
input.parse::<Token![=]>()?; input.parse::<Token![=]>()?;
( (
false, LabelType::Default,
Some(Display { Some(Display {
fmt: input.parse()?, fmt: input.parse()?,
args: TokenStream::new(), args: TokenStream::new(),
@ -88,9 +99,9 @@ impl Parse for LabelAttr {
}), }),
) )
} else { } else {
(false, None) (LabelType::Default, None)
}; };
Ok(LabelAttr { label, primary }) Ok(LabelAttr { label, lbl_ty })
} }
} }
@ -119,10 +130,14 @@ impl Labels {
}) })
}; };
use quote::ToTokens; use quote::ToTokens;
let LabelAttr { label, primary } = let LabelAttr { label, lbl_ty } =
syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?; syn::parse2::<LabelAttr>(attr.meta.to_token_stream())?;
if primary && labels.iter().any(|l: &Label| l.primary) { if lbl_ty == LabelType::Primary
&& labels
.iter()
.any(|l: &Label| l.lbl_ty == LabelType::Primary)
{
return Err(syn::Error::new( return Err(syn::Error::new(
field.span(), field.span(),
"Cannot have more than one primary label.", "Cannot have more than one primary label.",
@ -133,7 +148,7 @@ impl Labels {
label, label,
span, span,
ty: field.ty.clone(), ty: field.ty.clone(),
primary, lbl_ty,
}); });
} }
} }
@ -147,46 +162,82 @@ impl Labels {
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> { pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option<TokenStream> {
let (display_pat, display_members) = display_pat_members(fields); let (display_pat, display_members) = display_pat_members(fields);
let labels = self.0.iter().map(|highlight| { let labels_gen_var = quote! { labels };
let labels = self.0.iter().filter_map(|highlight| {
let Label { let Label {
span, span,
label, label,
ty, ty,
primary, lbl_ty,
} = highlight; } = highlight;
if *lbl_ty == LabelType::Collection {
return None;
}
let var = quote! { __miette_internal_var }; let var = quote! { __miette_internal_var };
let ctor = if *primary { let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! { std::option::Option::None }
};
let ctor = if *lbl_ty == LabelType::Primary {
quote! { miette::LabeledSpan::new_primary_with_span } quote! { miette::LabeledSpan::new_primary_with_span }
} else { } else {
quote! { miette::LabeledSpan::new_with_span } quote! { miette::LabeledSpan::new_with_span }
}; };
if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members); Some(quote! {
quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span) .map(|#var| #ctor(
.map(|#var| #ctor( #display,
std::option::Option::Some(format!(#fmt #args)), #var.clone(),
#var.clone(), ))
)) })
}
} else {
quote! {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
.map(|#var| #ctor(
std::option::Option::None,
#var.clone(),
))
}
}
}); });
let collections = self.0.iter().filter_map(|label| {
let Label {
span,
label,
ty,
lbl_ty,
} = label;
if *lbl_ty != LabelType::Collection {
return None;
}
let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! { std::option::Option::None }
};
Some(quote! {
let display = #display;
#labels_gen_var.extend(self.#span.iter().map(|label| {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(label)
.map(|span| {
use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
if #display.is_some() && labeled_span.label().is_none() {
labeled_span.set_label(#display)
}
labeled_span
})
}));
})
});
Some(quote! { Some(quote! {
#[allow(unused_variables)] #[allow(unused_variables)]
fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>> { fn labels(&self) -> std::option::Option<std::boxed::Box<dyn std::iter::Iterator<Item = miette::LabeledSpan> + '_>> {
use miette::macro_helpers::ToOption; use miette::macro_helpers::ToOption;
let Self #display_pat = self; let Self #display_pat = self;
std::option::Option::Some(Box::new(vec![
let mut #labels_gen_var = vec![
#(#labels),* #(#labels),*
].into_iter().filter(Option::is_some).map(Option::unwrap))) ];
#(#collections)*
std::option::Option::Some(Box::new(#labels_gen_var.into_iter().filter(Option::is_some).map(Option::unwrap)))
} }
}) })
} }
@ -198,8 +249,12 @@ impl Labels {
|ident, fields, DiagnosticConcreteArgs { labels, .. }| { |ident, fields, DiagnosticConcreteArgs { labels, .. }| {
let (display_pat, display_members) = display_pat_members(fields); let (display_pat, display_members) = display_pat_members(fields);
labels.as_ref().and_then(|labels| { labels.as_ref().and_then(|labels| {
let variant_labels = labels.0.iter().map(|label| { let labels_gen_var = quote! { labels };
let Label { span, label, ty, primary } = label; let variant_labels = labels.0.iter().filter_map(|label| {
let Label { span, label, ty, lbl_ty } = label;
if *lbl_ty == LabelType::Collection {
return None;
}
let field = match &span { let field = match &span {
syn::Member::Named(ident) => ident.clone(), syn::Member::Named(ident) => ident.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => { syn::Member::Unnamed(syn::Index { index, .. }) => {
@ -207,29 +262,57 @@ impl Labels {
} }
}; };
let var = quote! { __miette_internal_var }; let var = quote! { __miette_internal_var };
let ctor = if *primary { let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! { std::option::Option::None }
};
let ctor = if *lbl_ty == LabelType::Primary {
quote! { miette::LabeledSpan::new_primary_with_span } quote! { miette::LabeledSpan::new_primary_with_span }
} else { } else {
quote! { miette::LabeledSpan::new_with_span } quote! { miette::LabeledSpan::new_with_span }
}; };
if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members); Some(quote! {
quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field) .map(|#var| #ctor(
.map(|#var| #ctor( #display,
std::option::Option::Some(format!(#fmt #args)), #var.clone(),
#var.clone(), ))
)) })
} });
} else { let collections = labels.0.iter().filter_map(|label| {
quote! { let Label { span, label, ty, lbl_ty } = label;
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field) if *lbl_ty != LabelType::Collection {
.map(|#var| #ctor( return None;
std::option::Option::None,
#var.clone(),
))
}
} }
let field = match &span {
syn::Member::Named(ident) => ident.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
format_ident!("_{}", index)
}
};
let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
quote! { std::option::Option::None }
};
Some(quote! {
let display = #display;
#labels_gen_var.extend(#field.iter().map(|label| {
miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(label)
.map(|span| {
use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
if #display.is_some() && labeled_span.label().is_none() {
labeled_span.set_label(#display)
}
labeled_span
})
}));
})
}); });
let variant_name = ident.clone(); let variant_name = ident.clone();
match &fields { match &fields {
@ -237,9 +320,11 @@ impl Labels {
_ => Some(quote! { _ => Some(quote! {
Self::#variant_name #display_pat => { Self::#variant_name #display_pat => {
use miette::macro_helpers::ToOption; use miette::macro_helpers::ToOption;
std::option::Option::Some(std::boxed::Box::new(vec![ let mut #labels_gen_var = vec![
#(#variant_labels),* #(#variant_labels),*
].into_iter().filter(Option::is_some).map(Option::unwrap))) ];
#(#collections)*
std::option::Option::Some(std::boxed::Box::new(#labels_gen_var.into_iter().filter(Option::is_some).map(Option::unwrap)))
} }
}), }),
} }

View File

@ -1,3 +1,5 @@
use crate::protocol::{LabeledSpan, SourceSpan};
// Huge thanks to @jam1gamer for this hack: // Huge thanks to @jam1gamer for this hack:
// https://twitter.com/jam1garner/status/1515887996444323840 // https://twitter.com/jam1garner/status/1515887996444323840
@ -36,3 +38,24 @@ impl<T> ToOption for &OptionalWrapper<T> {
Some(value) Some(value)
} }
} }
#[doc(hidden)]
#[derive(Debug)]
pub struct ToLabelSpanWrapper {}
pub trait ToLabeledSpan<T> {
#[doc(hidden)]
fn to_labeled_span(span: T) -> LabeledSpan;
}
impl ToLabeledSpan<LabeledSpan> for ToLabelSpanWrapper {
fn to_labeled_span(span: LabeledSpan) -> LabeledSpan {
span
}
}
impl<T> ToLabeledSpan<T> for ToLabelSpanWrapper
where
T: Into<SourceSpan>,
{
fn to_labeled_span(span: T) -> LabeledSpan {
LabeledSpan::new_with_span(None, span.into())
}
}

View File

@ -293,6 +293,11 @@ impl LabeledSpan {
} }
} }
/// Change the text of the label
pub fn set_label(&mut self, label: Option<String>) {
self.label = label;
}
/// Makes a new label at specified span /// Makes a new label at specified span
/// ///
/// # Examples /// # Examples

View File

@ -0,0 +1,306 @@
use std::{
collections::{LinkedList, VecDeque},
ops::Range,
};
// Testing of the `diagnostic` attr used by derive(Diagnostic)
use miette::{Diagnostic, LabeledSpan, NamedSource, SourceSpan};
use thiserror::Error;
#[test]
fn attr_collection_in_enum() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
enum MyBad {
Only {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "and here")]
highlight2: Vec<SourceSpan>,
},
}
let src = "source\n text\n here".to_string();
let err = MyBad::Only {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: vec![(1, 2).into(), (3, 4).into()],
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_in_struct() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "and here")]
highlight2: Vec<SourceSpan>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: vec![(1, 2).into(), (3, 4).into()],
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_as_deque() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "and here")]
highlight2: VecDeque<SourceSpan>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: VecDeque::from([(1, 2).into(), (3, 4).into()]),
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_as_linked_list() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "and here")]
highlight2: LinkedList<SourceSpan>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: LinkedList::from([(1, 2).into(), (3, 4).into()]),
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_of_tuple() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "and here")]
highlight2: Vec<(usize, usize)>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: vec![(1, 2), (3, 4)],
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_of_range() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "and here")]
highlight2: Vec<Range<usize>>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: vec![1..3, 3..7],
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_of_labeled_span_in_struct() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "then there")]
highlight2: Vec<LabeledSpan>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: vec![
LabeledSpan::new_with_span(Some("continuing here".to_string()), (1, 2)),
LabeledSpan::new_with_span(None, (3, 4)),
],
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("continuing here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("then there".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_of_labeled_span_in_enum() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
enum MyBad {
Only {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "then there")]
highlight2: Vec<LabeledSpan>,
},
}
let src = "source\n text\n here".to_string();
let err = MyBad::Only {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: vec![
LabeledSpan::new_with_span(Some("continuing here".to_string()), (1, 2)),
LabeledSpan::new_with_span(None, (3, 4)),
],
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("continuing here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("then there".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
}
#[test]
fn attr_collection_multi() {
#[derive(Debug, Diagnostic, Error)]
#[error("oops!")]
struct MyBad {
#[source_code]
src: NamedSource<String>,
#[label("this bit here")]
highlight: SourceSpan,
#[label(collection, "and here")]
highlight2: Vec<SourceSpan>,
#[label(collection, "and there")]
highlight3: Vec<SourceSpan>,
}
let src = "source\n text\n here".to_string();
let err = MyBad {
src: NamedSource::new("bad_file.rs", src),
highlight: (9, 4).into(),
highlight2: vec![(1, 2).into(), (3, 4).into()],
highlight3: vec![(5, 6).into(), (7, 8).into()],
};
let mut label_iter = err.labels().unwrap();
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and there".into()), 5usize, 6usize);
assert_eq!(err_span, expectation);
let err_span = label_iter.next().unwrap();
let expectation = LabeledSpan::new(Some("and there".into()), 7usize, 8usize);
assert_eq!(err_span, expectation);
}